diff --git a/.circleci/config.yml b/.circleci/config.yml index d189cfe9..6a4c10a2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 @@ -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 diff --git a/README.md b/README.md index 2ec28f32..639c6cdc 100644 --- a/README.md +++ b/README.md @@ -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! @@ -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 | diff --git a/aws/aws.go b/aws/aws.go index 771a0ed3..f5ed7dfc 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -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, @@ -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) } @@ -936,7 +942,6 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp guardDutyDetectors.detectorIds = detectors resourcesInRegion.Resources = append(resourcesInRegion.Resources, guardDutyDetectors) } - } // End GuardDuty detectors @@ -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 } @@ -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) @@ -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) @@ -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{} @@ -1279,6 +1322,8 @@ func ListResourceTypes() []string { EC2KeyPairs{}.ResourceName(), ECR{}.ResourceName(), LaunchTemplates{}.ResourceName(), + ConfigServiceRule{}.ResourceName(), + ConfigServiceRecorders{}.ResourceName(), } sort.Strings(resourceTypes) return resourceTypes @@ -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 } diff --git a/aws/config_recorder.go b/aws/config_recorder.go new file mode 100644 index 00000000..80cadeb6 --- /dev/null +++ b/aws/config_recorder.go @@ -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 +} diff --git a/aws/config_recorder_test.go b/aws/config_recorder_test.go new file mode 100644 index 00000000..7a99424e --- /dev/null +++ b/aws/config_recorder_test.go @@ -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) +} diff --git a/aws/config_recorder_types.go b/aws/config_recorder_types.go new file mode 100644 index 00000000..de7adc9c --- /dev/null +++ b/aws/config_recorder_types.go @@ -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 +} diff --git a/aws/config_service.go b/aws/config_service.go new file mode 100644 index 00000000..122e1d83 --- /dev/null +++ b/aws/config_service.go @@ -0,0 +1,86 @@ +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 getAllConfigRules(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]string, error) { + svc := configservice.New(session) + + configRuleNames := []string{} + + paginator := func(output *configservice.DescribeConfigRulesOutput, lastPage bool) bool { + for _, configRule := range output.ConfigRules { + if shouldIncludeConfigRule(configRule, excludeAfter, configObj) { + configRuleNames = append(configRuleNames, aws.StringValue(configRule.ConfigRuleName)) + } + } + return !lastPage + } + + // Pass an empty config rules input, to signify we want all config rules returned + param := &configservice.DescribeConfigRulesInput{} + + err := svc.DescribeConfigRulesPages(param, paginator) + if err != nil { + return nil, errors.WithStackTrace(err) + } + + return configRuleNames, nil +} + +func shouldIncludeConfigRule(configRule *configservice.ConfigRule, excludeAfter time.Time, configObj config.Config) bool { + if configRule == nil { + return false + } + + return config.ShouldInclude( + aws.StringValue(configRule.ConfigRuleName), + configObj.ConfigServiceRule.IncludeRule.NamesRegExp, + configObj.ConfigServiceRule.ExcludeRule.NamesRegExp, + ) +} + +func nukeAllConfigServiceRules(session *session.Session, configRuleNames []string) error { + svc := configservice.New(session) + + if len(configRuleNames) == 0 { + logging.Logger.Debugf("No Config service rules to nuke in region %s", *session.Config.Region) + } + + var deletedConfigRuleNames []*string + + for _, configRuleName := range configRuleNames { + params := &configservice.DeleteConfigRuleInput{ + ConfigRuleName: aws.String(configRuleName), + } + _, err := svc.DeleteConfigRule(params) + + // Record status of this resource + e := report.Entry{ + Identifier: configRuleName, + ResourceType: "Config service rule", + Error: err, + } + report.Record(e) + + if err != nil { + logging.Logger.Debugf("[Failed] %s", err) + } else { + deletedConfigRuleNames = append(deletedConfigRuleNames, aws.String(configRuleName)) + logging.Logger.Debugf("Deleted Config service rule: %s", configRuleName) + } + } + + logging.Logger.Debugf("[OK] %d Config service rules deleted in %s", len(deletedConfigRuleNames), *session.Config.Region) + + return nil +} diff --git a/aws/config_service_test.go b/aws/config_service_test.go new file mode 100644 index 00000000..076861f9 --- /dev/null +++ b/aws/config_service_test.go @@ -0,0 +1,279 @@ +package aws + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/configservice" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/cloud-nuke/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListConfigServiceRules(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) + + configRuleName := createConfigServiceRule(t, region) + defer deleteConfigServiceRule(t, region, configRuleName, false) + + configServiceRuleNames, err := getAllConfigRules(session, time.Now(), config.Config{}) + require.NoError(t, err) + assert.Contains(t, configServiceRuleNames, configRuleName) +} + +func TestNukeConfigServiceRuleOne(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) + + configRuleName := createConfigServiceRule(t, region) + defer deleteConfigServiceRule(t, region, configRuleName, false) + + configServiceRuleNames := []string{configRuleName} + + require.NoError( + t, + nukeAllConfigServiceRules(session, configServiceRuleNames), + ) + + assertConfigServiceRulesDeleted(t, region, configServiceRuleNames) +} + +func TestNukeConfigServiceRuleMoreThanOne(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) + + configServiceRuleNames := []string{} + for i := 0; i < 3; i++ { + configServiceRuleName := createConfigServiceRule(t, region) + defer deleteConfigServiceRule(t, region, configServiceRuleName, false) + configServiceRuleNames = append(configServiceRuleNames, configServiceRuleName) + } + + require.NoError( + t, + nukeAllConfigServiceRules(session, configServiceRuleNames), + ) + + assertConfigServiceRulesDeleted(t, region, configServiceRuleNames) +} + +// Test helpers + +// ensureConfigurationRecorderExistsInRegion is a convenience method to be used during testing +// since you cannot create a custom configuration rule unless you already have a configuration +// recorder in the target region +func ensureConfigurationRecorderExistsInRegion(t *testing.T, region string) string { + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + + configService := configservice.New(session) + + param := &configservice.DescribeConfigurationRecordersInput{} + + output, lookupErr := configService.DescribeConfigurationRecorders(param) + require.NoError(t, lookupErr) + + // If we have no recorders in the region, create one - otherwise we have no more work to do + if len(output.ConfigurationRecorders) == 0 { + configRecorderName := createConfigurationRecorder(t, region) + require.NotEqual(t, "", configRecorderName) + return configRecorderName + } + + return aws.StringValue(output.ConfigurationRecorders[0].Name) +} + +// getAccountId contacts STS to retrieve the ID of the AWS account the tests are +// being run against. This is necessary because we need to "fake" an IAM role, +// but the fake IAM role must contain the correct account ID to avoid a cross-account +// validation error +func getAccountId(t *testing.T, session *session.Session) string { + stsService := sts.New(session) + stsOutput, stsErr := stsService.GetCallerIdentity(&sts.GetCallerIdentityInput{}) + require.NoError(t, stsErr) + + return aws.StringValue(stsOutput.Account) +} + +// scaffoldTestConfigRecorder is a convenience method that generates a custom config service rule +// suitable for use in testing +func scaffoldTestConfigRecorder(t *testing.T, session *session.Session) *configservice.ConfigurationRecorder { + testConfigurationRecorderName := "default" + + accountId := getAccountId(t, session) + // Create the test Configuration recorder + testConfigurationRecorder := &configservice.ConfigurationRecorder{ + Name: aws.String(testConfigurationRecorderName), + RecordingGroup: &configservice.RecordingGroup{ + AllSupported: aws.Bool(false), + IncludeGlobalResourceTypes: aws.Bool(false), + ResourceTypes: []*string{ + aws.String("AWS::Lambda::Function"), + }, + }, + // As the AWS documentation for config service notes, this RoleARN is not required by + // the API model, but it IS required by the AWS API, meaning you must pass a value + // We can "fake" this IAM role, in the sense that it doesn't actually need to exist, + // However, the AWS API will return an error if you don't supply the correct account + // number in the role ARN. Therefore, we need to look up the current account's ID dynamically + RoleARN: aws.String(fmt.Sprintf("arn:aws:iam::%s:role/S3Access", accountId)), + } + return testConfigurationRecorder +} + +func createConfigurationRecorder(t *testing.T, region string) string { + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + + configService := configservice.New(session) + + testConfigRecorder := scaffoldTestConfigRecorder(t, session) + + param := &configservice.PutConfigurationRecorderInput{ + ConfigurationRecorder: testConfigRecorder, + } + + // The PutConfigurationRecorderOutput is an empty struct + _, putErr := configService.PutConfigurationRecorder(param) + require.NoError(t, putErr) + + return aws.StringValue(testConfigRecorder.Name) +} + +// scaffoldConfigServiceRule is a convenience method that generates an essentially nonsensical custom config rule +// It is intended for use during testing +func scaffoldConfigServiceRule() *configservice.ConfigRule { + configServiceRuleName := strings.ToLower(fmt.Sprintf("cloud-nuke-test-%s-%s", util.UniqueID(), util.UniqueID())) + + configRulePolicyText := ` +# This rule checks if point in time recovery (PITR) is enabled on active Amazon DynamoDB tables +let status = ['ACTIVE'] + +rule tableisactive when + resourceType == "AWS::DynamoDB::Table" { + configuration.tableStatus == %status +} + +rule checkcompliance when + resourceType == "AWS::DynamoDB::Table" + tableisactive { + let pitr = supplementaryConfiguration.ContinuousBackupsDescription.pointInTimeRecoveryDescription.pointInTimeRecoveryStatus + %pitr == "ENABLED" +} + ` + sourceDetail := &configservice.SourceDetail{ + EventSource: aws.String("aws.config"), + MessageType: aws.String("ConfigurationItemChangeNotification"), + } + + configRuleSource := &configservice.Source{ + CustomPolicyDetails: &configservice.CustomPolicyDetails{ + PolicyRuntime: aws.String("guard-2.x.x"), + PolicyText: aws.String(configRulePolicyText), + }, + Owner: aws.String("CUSTOM_POLICY"), + SourceDetails: []*configservice.SourceDetail{ + sourceDetail, + }, + SourceIdentifier: aws.String("cloud-nuke-test"), + } + + return &configservice.ConfigRule{ + ConfigRuleName: aws.String(configServiceRuleName), + Source: configRuleSource, + } +} + +// createConfigServiceRule creates a custom AWS config service rule +func createConfigServiceRule(t *testing.T, region string) string { + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + + // We must first ensure there is a configuration recorder in the target region + // Otherwise, the attempt to create the custom configuration rule will fail + ensureConfigurationRecorderExistsInRegion(t, region) + + configService := configservice.New(session) + + testConfigServiceRule := scaffoldConfigServiceRule() + + param := &configservice.PutConfigRuleInput{ + ConfigRule: testConfigServiceRule, + } + + // PutConfigRuleOutput is an empty struct, so we just check for an error + _, createConfigServiceRuleErr := configService.PutConfigRule(param) + require.NoError(t, createConfigServiceRuleErr) + + return *testConfigServiceRule.ConfigRuleName +} + +func deleteConfigServiceRule(t *testing.T, region string, configServiceRuleName string, checkErr bool) { + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + + configService := configservice.New(session) + + param := &configservice.DeleteConfigRuleInput{ + ConfigRuleName: aws.String(configServiceRuleName), + } + + _, deleteErr := configService.DeleteConfigRule(param) + if checkErr { + require.NoError(t, deleteErr) + } +} + +func assertConfigServiceRulesDeleted(t *testing.T, region string, configServiceRuleNames []string) { + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + + svc := configservice.New(session) + + param := &configservice.DescribeConfigRulesInput{ + ConfigRuleNames: aws.StringSlice(configServiceRuleNames), + } + + resp, err := svc.DescribeConfigRules(param) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case configservice.ErrCodeNoSuchConfigRuleException: + t.Log("Ignoring Config service rule not found error in test lookup") + default: + require.NoError(t, err) + if len(resp.ConfigRules) > 0 { + configServiceRuleNames := []string{} + // Unpack config service rules that failed to delete + for _, configRule := range resp.ConfigRules { + configServiceRuleNames = append(configServiceRuleNames, aws.StringValue(configRule.ConfigRuleName)) + } + t.Fatalf("At least one of the following Config service rules was not deleted: %+v\n", aws.StringSlice(configServiceRuleNames)) + } + } + } + } +} diff --git a/aws/config_service_types.go b/aws/config_service_types.go new file mode 100644 index 00000000..c6e70966 --- /dev/null +++ b/aws/config_service_types.go @@ -0,0 +1,29 @@ +package aws + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/go-commons/errors" +) + +type ConfigServiceRule struct { + RuleNames []string +} + +func (c ConfigServiceRule) ResourceName() string { + return "config-rules" +} + +func (c ConfigServiceRule) ResourceIdentifiers() []string { + return c.RuleNames +} + +func (c ConfigServiceRule) MaxBatchSize() int { + return 200 +} + +func (c ConfigServiceRule) Nuke(session *session.Session, identifiers []string) error { + if err := nukeAllConfigServiceRules(session, identifiers); err != nil { + return errors.WithStackTrace(err) + } + return nil +} diff --git a/aws/ecr.go b/aws/ecr.go index 0b4dc9b9..5dea2fee 100644 --- a/aws/ecr.go +++ b/aws/ecr.go @@ -31,7 +31,6 @@ func getAllECRRepositories(session *session.Session, excludeAfter time.Time, con err := svc.DescribeRepositoriesPages(param, paginator) if err != nil { return nil, errors.WithStackTrace(err) - } return repositoryNames, nil @@ -53,7 +52,6 @@ func shouldIncludeECRRepository(repository *ecr.Repository, excludeAfter time.Ti configObj.ECRRepository.IncludeRule.NamesRegExp, configObj.ECRRepository.ExcludeRule.NamesRegExp, ) - } func nukeAllECRRepositories(session *session.Session, repositoryNames []string) error { @@ -94,5 +92,4 @@ func nukeAllECRRepositories(session *session.Session, repositoryNames []string) logging.Logger.Debugf("[OK] %d ECR Repositories deleted in %s", len(deletedNames), *session.Config.Region) return nil - } diff --git a/aws/ecr_test.go b/aws/ecr_test.go index 0dbb4697..4ee9830b 100644 --- a/aws/ecr_test.go +++ b/aws/ecr_test.go @@ -68,7 +68,6 @@ func createECRRepository(t *testing.T, region string) *string { require.NoError(t, createRepositoryErr) return output.Repository.RepositoryName - } func TestNukeECRRepositoryOne(t *testing.T) { @@ -127,7 +126,6 @@ func assertECRRepositoriesDeleted(t *testing.T, region string, repositoryNames [ } resp, err := svc.DescribeRepositories(param) - if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { @@ -137,7 +135,6 @@ func assertECRRepositoriesDeleted(t *testing.T, region string, repositoryNames [ default: require.NoError(t, err) if len(resp.Repositories) > 0 { - t.Logf("Repository: %+v\n", resp.Repositories) t.Fatalf("At least one of the following ECR Repositories was not deleted: %+v\n", aws.StringValueSlice(repositoryNames)) } } diff --git a/config/config.go b/config/config.go index 8be804a2..7d17de7c 100644 --- a/config/config.go +++ b/config/config.go @@ -47,6 +47,8 @@ type Config struct { ECRRepository ResourceType `yaml:"ECRRepository"` DBInstances ResourceType `yaml:"DBInstances"` LaunchTemplate ResourceType `yaml:"LaunchTemplate"` + ConfigServiceRule ResourceType `yaml:"ConfigServiceRule"` + ConfigServiceRecorder ResourceType `yaml:"ConfigServiceRecorder"` } type ResourceType struct { diff --git a/config/config_test.go b/config/config_test.go index 35cdeeb8..394d3497 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -48,6 +48,8 @@ func emptyConfig() *Config { ResourceType{FilterRule{}, FilterRule{}}, ResourceType{FilterRule{}, FilterRule{}}, ResourceType{FilterRule{}, FilterRule{}}, + ResourceType{FilterRule{}, FilterRule{}}, + ResourceType{FilterRule{}, FilterRule{}}, } }