Skip to content

Commit

Permalink
Implement support for configservice recorders and rules (#405)
Browse files Browse the repository at this point in the history
  • Loading branch information
zackproser authored Feb 6, 2023
1 parent f8f53d0 commit 50592c3
Show file tree
Hide file tree
Showing 13 changed files with 664 additions and 16 deletions.
8 changes: 6 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ jobs:
--exclude-resource-type iam-policy \
--exclude-resource-type vpc \
--exclude-resource-type kmscustomerkeys \
--exclude-resource-type ecr
--exclude-resource-type ecr \
--exclude-resource-type config-recorders \
--exclude-resource-type config-rules
no_output_timeout: 1h
nuke_sandbox:
<<: *defaults
Expand All @@ -71,7 +73,9 @@ jobs:
--exclude-resource-type iam-policy \
--exclude-resource-type vpc \
--exclude-resource-type kmscustomerkeys \
--exclude-resource-type ecr
--exclude-resource-type ecr \
--exclude-resource-type config-recorders \
--exclude-resource-type config-rules
no_output_timeout: 1h
deploy:
<<: *defaults
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ The currently supported functionality includes:
- Inspecting and deleting all SNS Topics in an AWS account
- Inspecting and deleting all CloudTrail Trails in an AWS account
- Inspecting and deleting all ECR Repositories in an AWS account
- Inspecting and deleting all Config service recorders in an AWS account
- Inspecting and deleting all Config service rules in an AWS account

### BEWARE!

Expand Down Expand Up @@ -533,6 +535,8 @@ To find out what we options are supported in the config file today, consult this
| ecr | none | ✅ | none | none |
| rds (+neptune and documentdb) | none | ✅ | none | none |
| lt | none | ✅ | none | none |
| config-recorders | none | ✅ | none | none |
| config-rules | none | ✅ | none | none |
| ... (more to come) | none | none | none | none |


Expand Down
63 changes: 55 additions & 8 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,14 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
bucketNamesPerRegion, ok := resourcesCache["S3"]

if !ok {
bucketNamesPerRegion, err = getAllS3Buckets(cloudNukeSession, excludeAfter, targetRegions, "", s3Buckets.MaxConcurrentGetSize(), configObj)
bucketNamesPerRegion, err = getAllS3Buckets(
cloudNukeSession,
excludeAfter,
targetRegions,
"",
s3Buckets.MaxConcurrentGetSize(),
configObj,
)
if err != nil {
ge := report.GeneralError{
Error: err,
Expand Down Expand Up @@ -913,7 +920,6 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
if len(keys) > 0 {
customerKeys.KeyAliases = aliases
customerKeys.KeyIds = awsgo.StringValueSlice(keys)

resourcesInRegion.Resources = append(resourcesInRegion.Resources, customerKeys)
}

Expand All @@ -936,7 +942,6 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
guardDutyDetectors.detectorIds = detectors
resourcesInRegion.Resources = append(resourcesInRegion.Resources, guardDutyDetectors)
}

}
// End GuardDuty detectors

Expand Down Expand Up @@ -1113,6 +1118,44 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
// End ECR Repositories

// Config Service Rules
configServiceRules := ConfigServiceRule{}
if IsNukeable(configServiceRules.ResourceName(), resourceTypes) {
configServiceRuleNames, err := getAllConfigRules(cloudNukeSession, excludeAfter, configObj)
if err != nil {
ge := report.GeneralError{
Error: err,
Description: "Unable to retrieve Config service rules",
ResourceType: configServiceRules.ResourceName(),
}
report.RecordError(ge)
}
if len(configServiceRuleNames) > 0 {
configServiceRules.RuleNames = configServiceRuleNames
resourcesInRegion.Resources = append(resourcesInRegion.Resources, configServiceRules)
}
}
// End Config service rules

// Config Service recorders
configServiceRecorders := ConfigServiceRecorders{}
if IsNukeable(configServiceRecorders.ResourceName(), resourceTypes) {
configServiceRecorderNames, err := getAllConfigRecorders(cloudNukeSession, excludeAfter, configObj)
if err != nil {
ge := report.GeneralError{
Error: err,
Description: "Unable to retrieve Config service recorders",
ResourceType: configServiceRecorders.ResourceName(),
}
report.RecordError(ge)
}
if len(configServiceRecorderNames) > 0 {
configServiceRecorders.RecorderNames = configServiceRecorderNames
resourcesInRegion.Resources = append(resourcesInRegion.Resources, configServiceRecorders)
}
}
// End Config service recorders

if len(resourcesInRegion.Resources) > 0 {
account.Resources[region] = resourcesInRegion
}
Expand Down Expand Up @@ -1153,7 +1196,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
// End IAM Users

//IAM Groups
// IAM Groups
iamGroups := IAMGroups{}
if IsNukeable(iamGroups.ResourceName(), resourceTypes) {
groupNames, err := getAllIamGroups(session, excludeAfter, configObj)
Expand All @@ -1165,9 +1208,9 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
globalResources.Resources = append(globalResources.Resources, iamGroups)
}
}
//END IAM Groups
// END IAM Groups

//IAM Policies
// IAM Policies
iamPolicies := IAMPolicies{}
if IsNukeable(iamPolicies.ResourceName(), resourceTypes) {
policyArns, err := getAllLocalIamPolicies(session, excludeAfter, configObj)
Expand All @@ -1179,7 +1222,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
globalResources.Resources = append(globalResources.Resources, iamPolicies)
}
}
//End IAM Policies
// End IAM Policies

// IAM OpenID Connect Providers
oidcProviders := OIDCProviders{}
Expand Down Expand Up @@ -1279,6 +1322,8 @@ func ListResourceTypes() []string {
EC2KeyPairs{}.ResourceName(),
ECR{}.ResourceName(),
LaunchTemplates{}.ResourceName(),
ConfigServiceRule{}.ResourceName(),
ConfigServiceRecorders{}.ResourceName(),
}
sort.Strings(resourceTypes)
return resourceTypes
Expand Down Expand Up @@ -1314,7 +1359,9 @@ func nukeAllResourcesInRegion(account *AwsAccountResources, region string, sessi
if err := resources.Nuke(session, batch); err != nil {
// TODO: Figure out actual error type
if strings.Contains(err.Error(), "RequestLimitExceeded") {
logging.Logger.Debug("Request limit reached. Waiting 1 minute before making new requests")
logging.Logger.Debug(
"Request limit reached. Waiting 1 minute before making new requests",
)
time.Sleep(1 * time.Minute)
continue
}
Expand Down
83 changes: 83 additions & 0 deletions aws/config_recorder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package aws

import (
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/configservice"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/cloud-nuke/report"
"github.com/gruntwork-io/go-commons/errors"
)

func getAllConfigRecorders(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]string, error) {
svc := configservice.New(session)

configRecorderNames := []string{}

param := &configservice.DescribeConfigurationRecordersInput{}

output, err := svc.DescribeConfigurationRecorders(param)
if err != nil {
return []string{}, errors.WithStackTrace(err)
}

for _, configRecorder := range output.ConfigurationRecorders {
if shouldIncludeConfigRecorder(configRecorder, excludeAfter, configObj) {
configRecorderNames = append(configRecorderNames, aws.StringValue(configRecorder.Name))
}
}

return configRecorderNames, nil
}

func shouldIncludeConfigRecorder(configRecorder *configservice.ConfigurationRecorder, excludeAfter time.Time, configObj config.Config) bool {
if configRecorder == nil {
return false
}

return config.ShouldInclude(
aws.StringValue(configRecorder.Name),
configObj.ConfigServiceRecorder.IncludeRule.NamesRegExp,
configObj.ConfigServiceRecorder.ExcludeRule.NamesRegExp,
)
}

func nukeAllConfigRecorders(session *session.Session, configRecorderNames []string) error {
svc := configservice.New(session)

if len(configRecorderNames) == 0 {
logging.Logger.Debugf("No Config recorders to nuke in region %s", *session.Config.Region)
return nil
}

var deletedNames []*string

for _, configRecorderName := range configRecorderNames {
params := &configservice.DeleteConfigurationRecorderInput{
ConfigurationRecorderName: aws.String(configRecorderName),
}

_, err := svc.DeleteConfigurationRecorder(params)

// Record status of this resource
e := report.Entry{
Identifier: configRecorderName,
ResourceType: "Config Recorder",
Error: err,
}
report.Record(e)

if err != nil {
logging.Logger.Debugf("[Failed] %s", err)
} else {
deletedNames = append(deletedNames, aws.String(configRecorderName))
logging.Logger.Debugf("Deleted Config Recorder: %s", configRecorderName)
}
}

logging.Logger.Debugf("[OK] %d Config Recorders deleted in %s", len(deletedNames), *session.Config.Region)
return nil
}
89 changes: 89 additions & 0 deletions aws/config_recorder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package aws

import (
"testing"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/configservice"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestListConfigRecorders(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

// You can only have one configuration recorder per region, so instead of our usual
// create and list pattern, we'll just ensure there is a recorder in our target region,
// creating one if necessary, and then that we can see that config recorder returned by
// getAllConfigRecorders
configRecorderName := ensureConfigurationRecorderExistsInRegion(t, region)

configRecorderNames, lookupErr := getAllConfigRecorders(session, time.Now(), config.Config{})
require.NoError(t, lookupErr)
require.NotEmpty(t, configRecorderNames)

// Sanity check that we got back a recorder
assert.Equal(t, configRecorderNames[0], configRecorderName)
}

func TestNukeConfigRecorderOne(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

configRecorderName := ensureConfigurationRecorderExistsInRegion(t, region)

defer deleteConfigRecorder(t, region, configRecorderName, false)

require.NoError(
t,
nukeAllConfigRecorders(session, []string{configRecorderName}),
)

assertConfigRecordersDeleted(t, region)
}

// Test helpers

func deleteConfigRecorder(t *testing.T, region string, configRecorderName string, checkErr bool) {
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

configService := configservice.New(session)

param := &configservice.DeleteConfigurationRecorderInput{
ConfigurationRecorderName: aws.String(configRecorderName),
}

_, deleteErr := configService.DeleteConfigurationRecorder(param)
if checkErr {
require.NoError(t, deleteErr)
}
}

func assertConfigRecordersDeleted(t *testing.T, region string) {
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

svc := configservice.New(session)

param := &configservice.DescribeConfigurationRecordersInput{}

resp, err := svc.DescribeConfigurationRecorders(param)
require.NoError(t, err)

require.Empty(t, resp.ConfigurationRecorders)
}
29 changes: 29 additions & 0 deletions aws/config_recorder_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package aws

import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gruntwork-io/go-commons/errors"
)

type ConfigServiceRecorders struct {
RecorderNames []string
}

func (u ConfigServiceRecorders) ResourceName() string {
return "config-recorders"
}

func (u ConfigServiceRecorders) ResourceIdentifiers() []string {
return u.RecorderNames
}

func (u ConfigServiceRecorders) MaxBatchSize() int {
return 50
}

func (u ConfigServiceRecorders) Nuke(session *session.Session, configServiceRecorderNames []string) error {
if err := nukeAllConfigRecorders(session, configServiceRecorderNames); err != nil {
return errors.WithStackTrace(err)
}
return nil
}
Loading

0 comments on commit 50592c3

Please sign in to comment.