From 1115c9b8448bda8bb7069290a6ca35b65e21b8ae Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Thu, 29 Sep 2022 13:09:38 -0500 Subject: [PATCH 1/8] 116 - WIP implement scaffolding for iam_groups --- README.md | 1 + aws/aws.go | 6 +++++ aws/iam_group.go | 58 ++++++++++++++++++++++++++++++++++++++++++ aws/iam_group_test.go | 33 ++++++++++++++++++++++++ aws/iam_group_types.go | 37 +++++++++++++++++++++++++++ config/config.go | 1 + 6 files changed, 136 insertions(+) create mode 100644 aws/iam_group.go create mode 100644 aws/iam_group_test.go create mode 100644 aws/iam_group_types.go diff --git a/README.md b/README.md index 9a15b4b8..9c16cf03 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The currently supported functionality includes: - Deleting VPCs in an AWS Account (except for default VPCs which is handled by the dedicated `defaults-aws` subcommand) - Inspecting and deleting all IAM users in an AWS account - Inspecting and deleting all IAM roles in an AWS account +- Inspecting and deleting all IAM groups in an AWS account - Inspecting and deleting all Secrets Manager Secrets in an AWS account - Inspecting and deleting all NAT Gateways in an AWS account - Inspecting and deleting all IAM Access Analyzers in an AWS account diff --git a/aws/aws.go b/aws/aws.go index 1f9ed386..3f08f234 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -851,6 +851,11 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp } // End IAM Users + //IAM Groups + //TODO Search through existig iam groups for resources to nuke + //TODO Append the nukeable ones to the global list of resources + //END IAM Groups + // IAM OpenID Connect Providers oidcProviders := OIDCProviders{} if IsNukeable(oidcProviders.ResourceName(), resourceTypes) { @@ -913,6 +918,7 @@ func ListResourceTypes() []string { S3Buckets{}.ResourceName(), IAMUsers{}.ResourceName(), IAMRoles{}.ResourceName(), + IAMGroups{}.ResourceName(), SecretsManagerSecrets{}.ResourceName(), NatGateways{}.ResourceName(), OpenSearchDomains{}.ResourceName(), diff --git a/aws/iam_group.go b/aws/iam_group.go new file mode 100644 index 00000000..a7695fed --- /dev/null +++ b/aws/iam_group.go @@ -0,0 +1,58 @@ +package aws + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/gruntwork-io/cloud-nuke/config" +) + +func getAllIamGroups(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { + //TODO implement + return nil, nil +} + +//nukeAllIamGroups - delete all IAM Roles +func nukeAllIamGroups(session *session.Session, groupNames []*string) error { + //TODO implement + return nil +} + +//deleteIamGroupAsync - Asynchronosly remove an IAM group from AWS along with any sub-items +func deleteIamGroupAsync() { + //TODO implement +} + +//TODO do I need shouldInclude function? + +//deleteIamGroup - removes an IAM group from AWS (ensure group is empty before calling?) +func deleteIamGroup(svc *iam.IAM, groupName *string) error { + return nil +} + +//Not sure if needed +func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configObj config.Config) bool { + return false +} + +//TODO delete policy functions belong here eventually but out of scope for trial + +//Custom Errors +type TooManyIamGroupErr struct{} + +func (err TooManyIamGroupErr) Error() string { + return "Too many IAM Groups requested at once" +} + +//Sanity Check + +// 1. aws.go lists all the resources +// 2. I'd be adding something to the global resources that checks for any nukeable groups +// 3. If not dry run, aws.go calls the .Nuke() function on each resource +// 4. As far as I can tell (excluding potentially policies) there are no pre-requisites for removing +// an iam group. Any users would get cleaned up by existing functionality and order shouldn't matter +// 5. I'll add IAMGroups to the config.go file which may get us config file support for free, not 100% sure + +//IAM GROUP TYPES +// Implement AwsResources interface diff --git a/aws/iam_group_test.go b/aws/iam_group_test.go new file mode 100644 index 00000000..d6affd2b --- /dev/null +++ b/aws/iam_group_test.go @@ -0,0 +1,33 @@ +package aws + +import ( + "testing" + "time" + + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListIamGroups(t *testing.T) { + t.Parallel() + + region, err := getRandomRegion() + require.NoError(t, err) + + session, err := session.NewSession(&awsgo.Config{ + Region: awsgo.String(region), + }) + require.NoError(t, err) + + groupNames, err := getAllIamGroups(session, time.Now(), config.Config{}) + require.NoError(t, err) + assert.NotEmpty(t, groupNames) //TODO based on iam test ask if needs preconfiguration to have at least one group +} + +//TODO implement create a new testing group, with users? +func createTestGroup(t *testing.T, session *session.Session, name string) error { + return nil +} diff --git a/aws/iam_group_types.go b/aws/iam_group_types.go new file mode 100644 index 00000000..733e061e --- /dev/null +++ b/aws/iam_group_types.go @@ -0,0 +1,37 @@ +package aws + +import ( + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/go-commons/errors" +) + +//IAMGroups - represents all IAMGroups on the AWS Account +type IAMGroups struct { + GroupNames []string +} + +//ResourceName - the simple name of the AWS resource +func (u IAMGroups) ResourceName() string { + return "iam-group" +} + +// ResourceIdentifiers - The IAM GroupNames +func (g IAMGroups) ResourceIdentifiers() []string { + return g.GroupNames +} + +// Tentative batch size to ensure AWS doesn't throttle +// There's a global max of 500 groups so it shouldn't take long either way +func (g IAMGroups) MaxBatchSize() int { + return 80 +} + +// Nuke - Destroy every group in this collection +func (g IAMGroups) Nuke(session *session.Session, identifiers []string) error { + if err := nukeAllIamGroups(session, awsgo.StringSlice(identifiers)); err != nil { + return errors.WithStackTrace(err) + } + + return nil +} diff --git a/config/config.go b/config/config.go index a7e6e5ab..68f91891 100644 --- a/config/config.go +++ b/config/config.go @@ -12,6 +12,7 @@ import ( type Config struct { S3 ResourceType `yaml:"s3"` IAMUsers ResourceType `yaml:"IAMUsers"` + IAMGroups ResourceType `yaml:"IAMGroups"` //TODO update the test IAMRoles ResourceType `yaml:"IAMRoles"` SecretsManagerSecrets ResourceType `yaml:"SecretsManager"` NatGateway ResourceType `yaml:"NatGateway"` From b894315b20393e4a95eee06fa92880b2ed9ce9fb Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Thu, 29 Sep 2022 14:21:52 -0500 Subject: [PATCH 2/8] 116 - WIP - write tests for iam_group --- aws/iam_group.go | 73 +++++++++++++++++++++++++++++++++++-------- aws/iam_group_test.go | 39 +++++++++++++++++++++-- 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/aws/iam_group.go b/aws/iam_group.go index a7695fed..6381da3f 100644 --- a/aws/iam_group.go +++ b/aws/iam_group.go @@ -3,37 +3,84 @@ 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/iam" "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/cloud-nuke/logging" + "github.com/gruntwork-io/gruntwork-cli/errors" ) func getAllIamGroups(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { - //TODO implement - return nil, nil + svc := iam.New(session) + + allIamGroups := []*string{} + err := svc.ListGroupsPages( + &iam.ListGroupsInput{}, + func(page *iam.ListGroupsOutput, lastPage bool) bool { + for _, iamGroup := range page.Groups { + if shouldIncludeIamGroup(iamGroup, excludeAfter, configObj) { + allIamGroups = append(allIamGroups, iamGroup.GroupName) + } + } + return !lastPage + }, + ) + if err != nil { + return nil, errors.WithStackTrace(err) + } + return allIamGroups, nil } -//nukeAllIamGroups - delete all IAM Roles +//nukeAllIamGroups - delete all IAM Roles. Caller is responsible for pagination (no more than 100/request) func nukeAllIamGroups(session *session.Session, groupNames []*string) error { - //TODO implement - return nil -} + region := aws.StringValue(session.Config.Region) //Since this is a global resource this can be any random region + svc := iam.New(session) + + if len(groupNames) == 0 { + logging.Logger.Info("No IAM Groups to nuke") + return nil + } + + //Probably not required since pagination is handled by the caller + if len(groupNames) > 100 { + logging.Logger.Errorf("Nuking too many IAM Groups at once (100): Halting to avoid rate limits") + return TooManyIamGroupErr{} + } + + //No bulk delete exists, do it with goroutines + //TODO -//deleteIamGroupAsync - Asynchronosly remove an IAM group from AWS along with any sub-items -func deleteIamGroupAsync() { //TODO implement + return nil } -//TODO do I need shouldInclude function? - -//deleteIamGroup - removes an IAM group from AWS (ensure group is empty before calling?) +//deleteIamGroup - removes an IAM group from AWS func deleteIamGroup(svc *iam.IAM, groupName *string) error { + _, err := svc.DeleteGroup(&iam.DeleteGroupInput{ + GroupName: groupName, + }) + if err != nil { + return errors.WithStackTrace(err) + } return nil } -//Not sure if needed +//check if iam group should be included based on config rules (RegExp and Exclude After) func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configObj config.Config) bool { - return false + if iamGroup == nil { + return false + } + + if excludeAfter.Before(*iamGroup.CreateDate) { + return false + } + + return config.ShouldInclude( + aws.StringValue(iamGroup.GroupName), + configObj.IAMGroups.IncludeRule.NamesRegExp, + configObj.IAMGroups.ExcludeRule.NamesRegExp, + ) } //TODO delete policy functions belong here eventually but out of scope for trial diff --git a/aws/iam_group_test.go b/aws/iam_group_test.go index d6affd2b..dbf55076 100644 --- a/aws/iam_group_test.go +++ b/aws/iam_group_test.go @@ -1,16 +1,20 @@ package aws import ( + "fmt" "testing" "time" awsgo "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/cloud-nuke/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +//Test that we can list IAM groups in an AWS account func TestListIamGroups(t *testing.T) { t.Parallel() @@ -24,10 +28,41 @@ func TestListIamGroups(t *testing.T) { groupNames, err := getAllIamGroups(session, time.Now(), config.Config{}) require.NoError(t, err) - assert.NotEmpty(t, groupNames) //TODO based on iam test ask if needs preconfiguration to have at least one group + assert.NotEmpty(t, groupNames) } -//TODO implement create a new testing group, with users? +//TODO could create empty and full IAM groups? + +//Creates an empty IAM group for testing func createTestGroup(t *testing.T, session *session.Session, name string) error { + svc := iam.New(session) + + groupInput := &iam.CreateGroupInput{ + GroupName: awsgo.String(name), + } + + group, err := svc.CreateGroup(groupInput) + fmt.Println(group.Group.Arn) + require.NoError(t, err) return nil } + +//Test that we can nuke iam groups. +func TestNukeIamGroups(t *testing.T) { + t.Parallel() + + region, err := getRandomRegion() + require.NoError(t, err) + + session, err := session.NewSession(&awsgo.Config{ + Region: awsgo.String(region), + }) + require.NoError(t, err) + + name := "cloud-nuke-test" + util.UniqueID() + err = createTestGroup(t, session, name) + require.NoError(t, err) + + err = nukeAllIamGroups(session, []*string{&name}) + require.NoError(t, err) +} From b57e837036c9adf3c8e1e3650d257b24e0f2394c Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Thu, 29 Sep 2022 14:47:25 -0500 Subject: [PATCH 3/8] 116 - WIP - implement iam_group.go FNs --- aws/iam_group.go | 54 +++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/aws/iam_group.go b/aws/iam_group.go index 6381da3f..d7e14278 100644 --- a/aws/iam_group.go +++ b/aws/iam_group.go @@ -1,6 +1,7 @@ package aws import ( + "sync" "time" "github.com/aws/aws-sdk-go/aws" @@ -9,6 +10,7 @@ import ( "github.com/gruntwork-io/cloud-nuke/config" "github.com/gruntwork-io/cloud-nuke/logging" "github.com/gruntwork-io/gruntwork-cli/errors" + "github.com/hashicorp/go-multierror" ) func getAllIamGroups(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { @@ -49,21 +51,43 @@ func nukeAllIamGroups(session *session.Session, groupNames []*string) error { } //No bulk delete exists, do it with goroutines - //TODO + logging.Logger.Info("Deleting all IAM Groups") + wg := new(sync.WaitGroup) + wg.Add(len(groupNames)) + errChans := make([]chan error, len(groupNames)) + for i, groupName := range groupNames { + errChans[i] = make(chan error, 1) + go deleteIamGroupAsync(wg, errChans[i], svc, groupName) + } + wg.Wait() + + //Collapse the errors down to one + var allErrs *multierror.Error + for _, errChan := range errChans { + if err := <-errChan; err != nil { + allErrs = multierror.Append(allErrs, err) + logging.Logger.Errorf("[Failed] %s", err) + } + } + finalErr := allErrs.ErrorOrNil() + if finalErr != nil { + return errors.WithStackTrace(finalErr) + } - //TODO implement + //Print Successful deletions + for _, groupName := range groupNames { + logging.Logger.Infof("[OK] IAM Group %s was deleted in %s", aws.StringValue(groupName), region) + } return nil } -//deleteIamGroup - removes an IAM group from AWS -func deleteIamGroup(svc *iam.IAM, groupName *string) error { +//deleteIamGroup - removes an IAM group from AWS, designed to run as a goroutine +func deleteIamGroupAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, groupName *string) { + defer wg.Done() _, err := svc.DeleteGroup(&iam.DeleteGroupInput{ GroupName: groupName, }) - if err != nil { - return errors.WithStackTrace(err) - } - return nil + errChan <- err } //check if iam group should be included based on config rules (RegExp and Exclude After) @@ -83,7 +107,7 @@ func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configOb ) } -//TODO delete policy functions belong here eventually but out of scope for trial +//TODO delete policy functions belong here eventually but out of scope for now //Custom Errors type TooManyIamGroupErr struct{} @@ -91,15 +115,3 @@ type TooManyIamGroupErr struct{} func (err TooManyIamGroupErr) Error() string { return "Too many IAM Groups requested at once" } - -//Sanity Check - -// 1. aws.go lists all the resources -// 2. I'd be adding something to the global resources that checks for any nukeable groups -// 3. If not dry run, aws.go calls the .Nuke() function on each resource -// 4. As far as I can tell (excluding potentially policies) there are no pre-requisites for removing -// an iam group. Any users would get cleaned up by existing functionality and order shouldn't matter -// 5. I'll add IAMGroups to the config.go file which may get us config file support for free, not 100% sure - -//IAM GROUP TYPES -// Implement AwsResources interface From 7ad78cd3ed1934d1d61fc8fe418dd6580ff2ba04 Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Thu, 29 Sep 2022 15:17:40 -0500 Subject: [PATCH 4/8] 116 - IAM Group removal Detach users from groups before attempting to remove the group --- aws/iam_group.go | 28 ++++++++++++++++--- aws/iam_group_test.go | 64 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/aws/iam_group.go b/aws/iam_group.go index d7e14278..81a50dde 100644 --- a/aws/iam_group.go +++ b/aws/iam_group.go @@ -84,10 +84,32 @@ func nukeAllIamGroups(session *session.Session, groupNames []*string) error { //deleteIamGroup - removes an IAM group from AWS, designed to run as a goroutine func deleteIamGroupAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, groupName *string) { defer wg.Done() - _, err := svc.DeleteGroup(&iam.DeleteGroupInput{ + var multierr *multierror.Error + + //Remove any users from the group + //TODO make this threaded + getGroupInput := &iam.GetGroupInput{ + GroupName: groupName, + } + grp, err := svc.GetGroup(getGroupInput) + for _, user := range grp.Users { + unlinkUserInput := &iam.RemoveUserFromGroupInput{ + UserName: user.UserName, + GroupName: groupName, + } + _, err := svc.RemoveUserFromGroup(unlinkUserInput) + if err != nil { + multierr = multierror.Append(multierr, err) + } + } + + _, err = svc.DeleteGroup(&iam.DeleteGroupInput{ GroupName: groupName, }) - errChan <- err + if err != nil { + multierr = multierror.Append(multierr, err) + } + errChan <- multierr.ErrorOrNil() } //check if iam group should be included based on config rules (RegExp and Exclude After) @@ -107,8 +129,6 @@ func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configOb ) } -//TODO delete policy functions belong here eventually but out of scope for now - //Custom Errors type TooManyIamGroupErr struct{} diff --git a/aws/iam_group_test.go b/aws/iam_group_test.go index dbf55076..1b249095 100644 --- a/aws/iam_group_test.go +++ b/aws/iam_group_test.go @@ -31,10 +31,8 @@ func TestListIamGroups(t *testing.T) { assert.NotEmpty(t, groupNames) } -//TODO could create empty and full IAM groups? - //Creates an empty IAM group for testing -func createTestGroup(t *testing.T, session *session.Session, name string) error { +func createEmptyTestGroup(t *testing.T, session *session.Session, name string) error { svc := iam.New(session) groupInput := &iam.CreateGroupInput{ @@ -47,6 +45,36 @@ func createTestGroup(t *testing.T, session *session.Session, name string) error return nil } +func createNonEmptyTestGroup(t *testing.T, session *session.Session, groupName string, userName string) error { + svc := iam.New(session) + + //Create User + userInput := &iam.CreateUserInput{ + UserName: awsgo.String(userName), + } + + _, err := svc.CreateUser(userInput) + require.NoError(t, err) + + //Create Group + groupInput := &iam.CreateGroupInput{ + GroupName: awsgo.String(groupName), + } + + _, err = svc.CreateGroup(groupInput) + require.NoError(t, err) + + //Add user to Group + userGroupLinkInput := &iam.AddUserToGroupInput{ + GroupName: awsgo.String(groupName), + UserName: awsgo.String(userName), + } + _, err = svc.AddUserToGroup(userGroupLinkInput) + require.NoError(t, err) + + return nil +} + //Test that we can nuke iam groups. func TestNukeIamGroups(t *testing.T) { t.Parallel() @@ -59,10 +87,34 @@ func TestNukeIamGroups(t *testing.T) { }) require.NoError(t, err) - name := "cloud-nuke-test" + util.UniqueID() - err = createTestGroup(t, session, name) + //Create test entities + emptyName := "cloud-nuke-test" + util.UniqueID() + err = createEmptyTestGroup(t, session, emptyName) + require.NoError(t, err) + + nonEmptyName := "cloud-nuke-test" + util.UniqueID() + userName := "cloud-nuke-test" + util.UniqueID() + err = createNonEmptyTestGroup(t, session, nonEmptyName, userName) + require.NoError(t, err) + + //Assert test entities exist + groupNames, err := getAllIamGroups(session, time.Now(), config.Config{}) + require.NoError(t, err) + assert.Contains(t, awsgo.StringValueSlice(groupNames), nonEmptyName) + assert.Contains(t, awsgo.StringValueSlice(groupNames), emptyName) + + //Nuke test entities + err = nukeAllIamGroups(session, []*string{&emptyName, &nonEmptyName}) require.NoError(t, err) - err = nukeAllIamGroups(session, []*string{&name}) + err = nukeAllIamUsers(session, []*string{&userName}) require.NoError(t, err) + + //Assert test entites don't exist anymore + groupNames, err = getAllIamGroups(session, time.Now(), config.Config{}) + require.NoError(t, err) + assert.NotContains(t, awsgo.StringValueSlice(groupNames), nonEmptyName) + assert.NotContains(t, awsgo.StringValueSlice(groupNames), emptyName) } + +//TODO could test filtered nuke if time From d25df76fcc6266b372ccb96942497ab0d8b54d43 Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Thu, 29 Sep 2022 15:34:59 -0500 Subject: [PATCH 5/8] 116 - Add IAM Groups to the main aws.go --- aws/aws.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/aws/aws.go b/aws/aws.go index 3f08f234..d3bf247e 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -852,8 +852,17 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp // End IAM Users //IAM Groups - //TODO Search through existig iam groups for resources to nuke - //TODO Append the nukeable ones to the global list of resources + iamGroups := IAMGroups{} + if IsNukeable(iamGroups.ResourceName(), resourceTypes) { + groupNames, err := getAllIamGroups(session, excludeAfter, configObj) + if err != nil { + return nil, errors.WithStackTrace(err) + } + if len(groupNames) > 0 { + iamGroups.GroupNames = awsgo.StringValueSlice(groupNames) + globalResources.Resources = append(globalResources.Resources, iamGroups) + } + } //END IAM Groups // IAM OpenID Connect Providers From cf5c39f5ead240348321d8b107d2a6e29965b399 Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Sat, 1 Oct 2022 11:06:03 -0500 Subject: [PATCH 6/8] 116 - detach policies before removing iam group --- aws/iam_group.go | 23 +++++++++++++-- aws/iam_group_test.go | 67 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/aws/iam_group.go b/aws/iam_group.go index 81a50dde..e1d7ebb3 100644 --- a/aws/iam_group.go +++ b/aws/iam_group.go @@ -87,7 +87,6 @@ func deleteIamGroupAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, g var multierr *multierror.Error //Remove any users from the group - //TODO make this threaded getGroupInput := &iam.GetGroupInput{ GroupName: groupName, } @@ -103,6 +102,26 @@ func deleteIamGroupAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, g } } + //Detach any policies on the group + allPolicies := []*string{} + err = svc.ListAttachedGroupPoliciesPages(&iam.ListAttachedGroupPoliciesInput{GroupName: groupName}, + func(page *iam.ListAttachedGroupPoliciesOutput, lastPage bool) bool { + for _, iamPolicy := range page.AttachedPolicies { + allPolicies = append(allPolicies, iamPolicy.PolicyArn) + } + return !lastPage + }, + ) + + for _, policy := range allPolicies { + unlinkPolicyInput := &iam.DetachGroupPolicyInput{ + GroupName: groupName, + PolicyArn: policy, + } + _, err = svc.DetachGroupPolicy(unlinkPolicyInput) + } + + //Delete the group _, err = svc.DeleteGroup(&iam.DeleteGroupInput{ GroupName: groupName, }) @@ -118,7 +137,7 @@ func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configOb return false } - if excludeAfter.Before(*iamGroup.CreateDate) { + if excludeAfter.Before(aws.TimeValue(iamGroup.CreateDate)) { return false } diff --git a/aws/iam_group_test.go b/aws/iam_group_test.go index 1b249095..1d3c4c96 100644 --- a/aws/iam_group_test.go +++ b/aws/iam_group_test.go @@ -45,7 +45,14 @@ func createEmptyTestGroup(t *testing.T, session *session.Session, name string) e return nil } -func createNonEmptyTestGroup(t *testing.T, session *session.Session, groupName string, userName string) error { +//Stores information for cleanup +type groupInfo struct { + GroupName *string + UserName *string + PolicyArn *string +} + +func createNonEmptyTestGroup(t *testing.T, session *session.Session, groupName string, userName string) (*groupInfo, error) { svc := iam.New(session) //Create User @@ -56,6 +63,24 @@ func createNonEmptyTestGroup(t *testing.T, session *session.Session, groupName s _, err := svc.CreateUser(userInput) require.NoError(t, err) + //Create Policy + policyOutput, err := svc.CreatePolicy(&iam.CreatePolicyInput{ + PolicyDocument: awsgo.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": "ec2:DescribeInstances", + "Resource": "*" + } + ] + }`), + PolicyName: awsgo.String("policy-" + groupName), + Description: awsgo.String("Policy created by cloud-nuke tests - Should be deleted"), + }) + require.NoError(t, err) + //Create Group groupInput := &iam.CreateGroupInput{ GroupName: awsgo.String(groupName), @@ -72,6 +97,38 @@ func createNonEmptyTestGroup(t *testing.T, session *session.Session, groupName s _, err = svc.AddUserToGroup(userGroupLinkInput) require.NoError(t, err) + //Add policy to Group + + groupPolicyInput := &iam.AttachGroupPolicyInput{ + PolicyArn: policyOutput.Policy.Arn, + GroupName: awsgo.String(groupName), + } + _, err = svc.AttachGroupPolicy(groupPolicyInput) + + info := &groupInfo{ + GroupName: &groupName, + PolicyArn: policyOutput.Policy.Arn, + UserName: &userName, + } + + return info, nil +} + +func deleteGroupExtraResources(session *session.Session, info *groupInfo) error { + svc := iam.New(session) + _, err := svc.DeletePolicy(&iam.DeletePolicyInput{ + PolicyArn: info.PolicyArn, + }) + if err != nil { + return err + } + + _, err = svc.DeleteUser(&iam.DeleteUserInput{ + UserName: info.UserName, + }) + if err != nil { + return err + } return nil } @@ -94,7 +151,8 @@ func TestNukeIamGroups(t *testing.T) { nonEmptyName := "cloud-nuke-test" + util.UniqueID() userName := "cloud-nuke-test" + util.UniqueID() - err = createNonEmptyTestGroup(t, session, nonEmptyName, userName) + info, err := createNonEmptyTestGroup(t, session, nonEmptyName, userName) + defer deleteGroupExtraResources(session, info) require.NoError(t, err) //Assert test entities exist @@ -107,14 +165,9 @@ func TestNukeIamGroups(t *testing.T) { err = nukeAllIamGroups(session, []*string{&emptyName, &nonEmptyName}) require.NoError(t, err) - err = nukeAllIamUsers(session, []*string{&userName}) - require.NoError(t, err) - //Assert test entites don't exist anymore groupNames, err = getAllIamGroups(session, time.Now(), config.Config{}) require.NoError(t, err) assert.NotContains(t, awsgo.StringValueSlice(groupNames), nonEmptyName) assert.NotContains(t, awsgo.StringValueSlice(groupNames), emptyName) } - -//TODO could test filtered nuke if time From c75b348b9c33dc6e83ae9d5993a1eb5729feda91 Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Thu, 27 Oct 2022 09:55:23 -0500 Subject: [PATCH 7/8] 116 - Add iam policy removal --- README.md | 66 +++++++------ aws/aws.go | 15 +++ aws/iam_group.go | 17 ++-- aws/iam_group_test.go | 40 ++++---- aws/iam_policy.go | 203 ++++++++++++++++++++++++++++++++++++++ aws/iam_policy_test.go | 209 ++++++++++++++++++++++++++++++++++++++++ aws/iam_policy_types.go | 36 +++++++ config/config.go | 3 +- 8 files changed, 525 insertions(+), 64 deletions(-) create mode 100644 aws/iam_policy.go create mode 100644 aws/iam_policy_test.go create mode 100644 aws/iam_policy_types.go diff --git a/README.md b/README.md index 9c16cf03..d802f670 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ The currently supported functionality includes: - Inspecting and deleting all IAM users in an AWS account - Inspecting and deleting all IAM roles in an AWS account - Inspecting and deleting all IAM groups in an AWS account +- Inspecting and deleting all customer managed IAM policies in an AWS account - Inspecting and deleting all Secrets Manager Secrets in an AWS account - Inspecting and deleting all NAT Gateways in an AWS account - Inspecting and deleting all IAM Access Analyzers in an AWS account @@ -463,38 +464,39 @@ Be careful when nuking and append the `--dry-run` option if you're unsure. Even To find out what we options are supported in the config file today, consult this table. Resource types at the top level of the file that are supported are listed here. -| resource type | names | names_regex | tags | tags_regex | -|--------------------|-------|-------------|------|------------| -| s3 | none | ✅ | none | none | -| iam | none | ✅ | none | none | -| ecsserv | none | ✅ | none | none | -| ecscluster | none | ✅ | none | none | -| secretsmanager | none | ✅ | none | none | -| nat-gateway | none | ✅ | none | none | -| accessanalyzer | none | ✅ | none | none | -| dynamodb | none | ✅ | none | none | -| ebs | none | ✅ | none | none | -| lambda | none | ✅ | none | none | -| elbv2 | none | ✅ | none | none | -| ecs | none | ✅ | none | none | -| elasticache | none | ✅ | none | none | -| vpc | none | ✅ | none | none | -| oidcprovider | none | ✅ | none | none | -| cloudwatch-loggroup | none | ✅ | none | none | -| kmscustomerkeys | none | ✅ | none | none | -| asg | none | ✅ | none | none | -| lc | none | ✅ | none | none | -| eip | none | ✅ | none | none | -| ec2 | none | ✅ | none | none | -| apigateway | none | ✅ | none | none | -| apigatewayv2 | none | ✅ | none | none | -| eks | none | ✅ | none | none | -| kinesis-stream | none | ✅ | none | none | -| efs | none | ✅ | none | none | -| acmpca | none | none | none | none | -| iam role | none | none | none | none | -| sagemaker-notebook-instances| none| ✅ | none | none | -| ... (more to come) | none | none | none | none | +| resource type | names | names_regex | tags | tags_regex | +|------------------------------|-------|-------------|------|------------| +| s3 | none | ✅ | none | none | +| iam user | none | ✅ | none | none | +| ecsserv | none | ✅ | none | none | +| ecscluster | none | ✅ | none | none | +| secretsmanager | none | ✅ | none | none | +| nat-gateway | none | ✅ | none | none | +| accessanalyzer | none | ✅ | none | none | +| dynamodb | none | ✅ | none | none | +| ebs | none | ✅ | none | none | +| lambda | none | ✅ | none | none | +| elbv2 | none | ✅ | none | none | +| ecs | none | ✅ | none | none | +| elasticache | none | ✅ | none | none | +| vpc | none | ✅ | none | none | +| oidcprovider | none | ✅ | none | none | +| cloudwatch-loggroup | none | ✅ | none | none | +| kmscustomerkeys | none | ✅ | none | none | +| asg | none | ✅ | none | none | +| lc | none | ✅ | none | none | +| eip | none | ✅ | none | none | +| ec2 | none | ✅ | none | none | +| apigateway | none | ✅ | none | none | +| apigatewayv2 | none | ✅ | none | none | +| eks | none | ✅ | none | none | +| kinesis-stream | none | ✅ | none | none | +| efs | none | ✅ | none | none | +| acmpca | none | none | none | none | +| iam role | none | ✅ | none | none | +| iam policy | none | ✅ | none | none | +| sagemaker-notebook-instances | none | ✅ | none | none | +| ... (more to come) | none | none | none | none | ### Log level diff --git a/aws/aws.go b/aws/aws.go index d3bf247e..d31774f0 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -865,6 +865,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp } //END IAM Groups + //IAM Policies + iamPolicies := IAMPolicies{} + if IsNukeable(iamPolicies.ResourceName(), resourceTypes) { + policyArns, err := getAllLocalIamPolicies(session, excludeAfter, configObj) + if err != nil { + return nil, errors.WithStackTrace(err) + } + if len(policyArns) > 0 { + iamPolicies.PolicyArns = awsgo.StringValueSlice(policyArns) + globalResources.Resources = append(globalResources.Resources, iamPolicies) + } + } + //End IAM Policies + // IAM OpenID Connect Providers oidcProviders := OIDCProviders{} if IsNukeable(oidcProviders.ResourceName(), resourceTypes) { @@ -928,6 +942,7 @@ func ListResourceTypes() []string { IAMUsers{}.ResourceName(), IAMRoles{}.ResourceName(), IAMGroups{}.ResourceName(), + IAMPolicies{}.ResourceName(), SecretsManagerSecrets{}.ResourceName(), NatGateways{}.ResourceName(), OpenSearchDomains{}.ResourceName(), diff --git a/aws/iam_group.go b/aws/iam_group.go index e1d7ebb3..9cdb6b27 100644 --- a/aws/iam_group.go +++ b/aws/iam_group.go @@ -16,7 +16,7 @@ import ( func getAllIamGroups(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { svc := iam.New(session) - allIamGroups := []*string{} + var allIamGroups []*string err := svc.ListGroupsPages( &iam.ListGroupsInput{}, func(page *iam.ListGroupsOutput, lastPage bool) bool { @@ -34,9 +34,8 @@ func getAllIamGroups(session *session.Session, excludeAfter time.Time, configObj return allIamGroups, nil } -//nukeAllIamGroups - delete all IAM Roles. Caller is responsible for pagination (no more than 100/request) +// nukeAllIamGroups - delete all IAM groups. Caller is responsible for pagination (no more than 100/request) func nukeAllIamGroups(session *session.Session, groupNames []*string) error { - region := aws.StringValue(session.Config.Region) //Since this is a global resource this can be any random region svc := iam.New(session) if len(groupNames) == 0 { @@ -74,14 +73,10 @@ func nukeAllIamGroups(session *session.Session, groupNames []*string) error { return errors.WithStackTrace(finalErr) } - //Print Successful deletions - for _, groupName := range groupNames { - logging.Logger.Infof("[OK] IAM Group %s was deleted in %s", aws.StringValue(groupName), region) - } return nil } -//deleteIamGroup - removes an IAM group from AWS, designed to run as a goroutine +// deleteIamGroup - removes an IAM group from AWS, designed to run as a goroutine func deleteIamGroupAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, groupName *string) { defer wg.Done() var multierr *multierror.Error @@ -127,11 +122,13 @@ func deleteIamGroupAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, g }) if err != nil { multierr = multierror.Append(multierr, err) + } else { + logging.Logger.Infof("[OK] IAM Group %s was deleted in global", aws.StringValue(groupName)) } errChan <- multierr.ErrorOrNil() } -//check if iam group should be included based on config rules (RegExp and Exclude After) +// check if iam group should be included based on config rules (RegExp and Exclude After) func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configObj config.Config) bool { if iamGroup == nil { return false @@ -148,7 +145,7 @@ func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configOb ) } -//Custom Errors +// TooManyIamGroupErr Custom Errors type TooManyIamGroupErr struct{} func (err TooManyIamGroupErr) Error() string { diff --git a/aws/iam_group_test.go b/aws/iam_group_test.go index 1d3c4c96..471b1bbf 100644 --- a/aws/iam_group_test.go +++ b/aws/iam_group_test.go @@ -1,7 +1,7 @@ package aws import ( - "fmt" + "github.com/gruntwork-io/cloud-nuke/logging" "testing" "time" @@ -14,24 +14,24 @@ import ( "github.com/stretchr/testify/require" ) -//Test that we can list IAM groups in an AWS account +// Test that we can list IAM groups in an AWS account func TestListIamGroups(t *testing.T) { t.Parallel() region, err := getRandomRegion() require.NoError(t, err) - session, err := session.NewSession(&awsgo.Config{ + localSession, err := session.NewSession(&awsgo.Config{ Region: awsgo.String(region), }) require.NoError(t, err) - groupNames, err := getAllIamGroups(session, time.Now(), config.Config{}) + groupNames, err := getAllIamGroups(localSession, time.Now(), config.Config{}) require.NoError(t, err) assert.NotEmpty(t, groupNames) } -//Creates an empty IAM group for testing +// Creates an empty IAM group for testing func createEmptyTestGroup(t *testing.T, session *session.Session, name string) error { svc := iam.New(session) @@ -39,13 +39,12 @@ func createEmptyTestGroup(t *testing.T, session *session.Session, name string) e GroupName: awsgo.String(name), } - group, err := svc.CreateGroup(groupInput) - fmt.Println(group.Group.Arn) + _, err := svc.CreateGroup(groupInput) require.NoError(t, err) return nil } -//Stores information for cleanup +// Stores information for cleanup type groupInfo struct { GroupName *string UserName *string @@ -114,59 +113,58 @@ func createNonEmptyTestGroup(t *testing.T, session *session.Session, groupName s return info, nil } -func deleteGroupExtraResources(session *session.Session, info *groupInfo) error { +func deleteGroupExtraResources(session *session.Session, info *groupInfo) { svc := iam.New(session) _, err := svc.DeletePolicy(&iam.DeletePolicyInput{ PolicyArn: info.PolicyArn, }) if err != nil { - return err + logging.Logger.Errorf("Unable to delete test policy: %s", *info.PolicyArn) } _, err = svc.DeleteUser(&iam.DeleteUserInput{ UserName: info.UserName, }) if err != nil { - return err + logging.Logger.Errorf("Unable to delete test user: %s", *info.UserName) } - return nil } -//Test that we can nuke iam groups. +// Test that we can nuke iam groups. func TestNukeIamGroups(t *testing.T) { t.Parallel() region, err := getRandomRegion() require.NoError(t, err) - session, err := session.NewSession(&awsgo.Config{ + localSession, err := session.NewSession(&awsgo.Config{ Region: awsgo.String(region), }) require.NoError(t, err) //Create test entities emptyName := "cloud-nuke-test" + util.UniqueID() - err = createEmptyTestGroup(t, session, emptyName) + err = createEmptyTestGroup(t, localSession, emptyName) require.NoError(t, err) nonEmptyName := "cloud-nuke-test" + util.UniqueID() userName := "cloud-nuke-test" + util.UniqueID() - info, err := createNonEmptyTestGroup(t, session, nonEmptyName, userName) - defer deleteGroupExtraResources(session, info) + info, err := createNonEmptyTestGroup(t, localSession, nonEmptyName, userName) + defer deleteGroupExtraResources(localSession, info) require.NoError(t, err) //Assert test entities exist - groupNames, err := getAllIamGroups(session, time.Now(), config.Config{}) + groupNames, err := getAllIamGroups(localSession, time.Now(), config.Config{}) require.NoError(t, err) assert.Contains(t, awsgo.StringValueSlice(groupNames), nonEmptyName) assert.Contains(t, awsgo.StringValueSlice(groupNames), emptyName) //Nuke test entities - err = nukeAllIamGroups(session, []*string{&emptyName, &nonEmptyName}) + err = nukeAllIamGroups(localSession, []*string{&emptyName, &nonEmptyName}) require.NoError(t, err) - //Assert test entites don't exist anymore - groupNames, err = getAllIamGroups(session, time.Now(), config.Config{}) + //Assert test entities don't exist anymore + groupNames, err = getAllIamGroups(localSession, time.Now(), config.Config{}) require.NoError(t, err) assert.NotContains(t, awsgo.StringValueSlice(groupNames), nonEmptyName) assert.NotContains(t, awsgo.StringValueSlice(groupNames), emptyName) diff --git a/aws/iam_policy.go b/aws/iam_policy.go new file mode 100644 index 00000000..3fc176c6 --- /dev/null +++ b/aws/iam_policy.go @@ -0,0 +1,203 @@ +package aws + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/cloud-nuke/logging" + "github.com/gruntwork-io/go-commons/errors" + "github.com/hashicorp/go-multierror" + "sync" + "time" +) + +// Returns the ARN of all customer managed policies +func getAllLocalIamPolicies(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { + svc := iam.New(session) + + var allIamPolicies []*string + + err := svc.ListPoliciesPages( + &iam.ListPoliciesInput{Scope: aws.String(iam.PolicyScopeTypeLocal)}, + func(page *iam.ListPoliciesOutput, lastPage bool) bool { + for _, policy := range page.Policies { + if shouldIncludeIamPolicy(policy, excludeAfter, configObj) { + allIamPolicies = append(allIamPolicies, policy.Arn) + } + } + return !lastPage + }, + ) + + if err != nil { + return nil, errors.WithStackTrace(err) + } + + return allIamPolicies, nil +} + +// Delete all iam customer managed policies. Caller is responsible for pagination (no more than 100/request) +func nukeAllIamPolicies(session *session.Session, policyArns []*string) error { + svc := iam.New(session) + + if len(policyArns) == 0 { + logging.Logger.Info("No IAM Policies to nuke") + } + + //Probably not required since pagination is handled by the caller + if len(policyArns) > 100 { + logging.Logger.Errorf("Nuking too many IAM Policies at once (100): Halting to avoid rate limits") + return TooManyIamPolicyErr{} + } + + //No Bulk Delete exists, do it with goroutines + logging.Logger.Info("Deleting all IAM Policies") + wg := new(sync.WaitGroup) + wg.Add(len(policyArns)) + errChans := make([]chan error, len(policyArns)) + for i, arn := range policyArns { + errChans[i] = make(chan error, 1) + go deleteIamPolicyAsync(wg, errChans[i], svc, arn) + } + wg.Wait() + + //Collapse the errors down to one + var allErrs *multierror.Error + for _, errChan := range errChans { + if err := <-errChan; err != nil { + allErrs = multierror.Append(allErrs, err) + logging.Logger.Errorf("[Failed] %s", err) + } + } + finalErr := allErrs.ErrorOrNil() + if finalErr != nil { + return errors.WithStackTrace(finalErr) + } + + return nil +} + +// Removes an IAM Policy from AWS, designed to run as a goroutine +func deleteIamPolicyAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, policyArn *string) { + defer wg.Done() + var multierr *multierror.Error + + //Detach any entities the policy is attached to + err := detachPolicyEntities(svc, policyArn) + if err != nil { + multierr = multierror.Append(multierr, err) + } + + //Get Old Policy Versions + var versionsToRemove []*string + err = svc.ListPolicyVersionsPages(&iam.ListPolicyVersionsInput{PolicyArn: policyArn}, + func(page *iam.ListPolicyVersionsOutput, lastPage bool) bool { + for _, policyVersion := range page.Versions { + if !*policyVersion.IsDefaultVersion { + versionsToRemove = append(versionsToRemove, policyVersion.VersionId) + } + } + return !lastPage + }) + if err != nil { + multierr = multierror.Append(multierr, err) + } + + //Delete old policy versions + for _, versionId := range versionsToRemove { + _, err = svc.DeletePolicyVersion(&iam.DeletePolicyVersionInput{VersionId: versionId, PolicyArn: policyArn}) + if err != nil { + multierr = multierror.Append(multierr, err) + } + } + //Delete the policy + _, err = svc.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: policyArn}) + if err != nil { + multierr = multierror.Append(multierr, err) + } else { + logging.Logger.Infof("[OK] IAM Policy %s was deleted in global", aws.StringValue(policyArn)) + } + + errChan <- multierr.ErrorOrNil() +} + +func detachPolicyEntities(svc *iam.IAM, policyArn *string) error { + var allPolicyGroups []*string + var allPolicyRoles []*string + var allPolicyUsers []*string + err := svc.ListEntitiesForPolicyPages(&iam.ListEntitiesForPolicyInput{PolicyArn: policyArn}, + func(page *iam.ListEntitiesForPolicyOutput, lastPage bool) bool { + for _, group := range page.PolicyGroups { + allPolicyGroups = append(allPolicyGroups, group.GroupName) + } + for _, role := range page.PolicyRoles { + allPolicyRoles = append(allPolicyRoles, role.RoleName) + } + for _, user := range page.PolicyUsers { + allPolicyUsers = append(allPolicyUsers, user.UserName) + } + return !lastPage + }, + ) + if err != nil { + return err + } + //Detach policy from any users + for _, userName := range allPolicyUsers { + detachUserInput := &iam.DetachUserPolicyInput{ + UserName: userName, + PolicyArn: policyArn, + } + _, err = svc.DetachUserPolicy(detachUserInput) + if err != nil { + return err + } + } + //Detach policy from any groups + for _, groupName := range allPolicyGroups { + detachGroupInput := &iam.DetachGroupPolicyInput{ + GroupName: groupName, + PolicyArn: policyArn, + } + _, err = svc.DetachGroupPolicy(detachGroupInput) + if err != nil { + return err + } + } + //Detach policy from any roles + for _, roleName := range allPolicyRoles { + detachRoleInput := &iam.DetachRolePolicyInput{ + RoleName: roleName, + PolicyArn: policyArn, + } + _, err = svc.DetachRolePolicy(detachRoleInput) + if err != nil { + return err + } + } + return err +} + +func shouldIncludeIamPolicy(iamPolicy *iam.Policy, excludeAfter time.Time, configObj config.Config) bool { + if iamPolicy == nil { + return false + } + + if excludeAfter.Before(aws.TimeValue(iamPolicy.CreateDate)) { + return false + } + + return config.ShouldInclude( + aws.StringValue(iamPolicy.PolicyName), + configObj.IAMPolicies.IncludeRule.NamesRegExp, + configObj.IAMPolicies.ExcludeRule.NamesRegExp, + ) +} + +// TooManyIamPolicyErr Custom Errors +type TooManyIamPolicyErr struct{} + +func (err TooManyIamPolicyErr) Error() string { + return "Too many IAM Groups requested at once" +} diff --git a/aws/iam_policy_test.go b/aws/iam_policy_test.go new file mode 100644 index 00000000..e961f84b --- /dev/null +++ b/aws/iam_policy_test.go @@ -0,0 +1,209 @@ +package aws + +import ( + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/cloud-nuke/logging" + "github.com/gruntwork-io/cloud-nuke/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +// Stores info for cleanup +type entityInfo struct { + PolicyArn *string + UserName *string + GroupName *string + RoleName *string +} + +const fakePolicy string = `{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": "elasticmapreduce:*", + "Resource": "*", + "Condition": { + "BoolIfExists": { + "aws:MultiFactorAuthPresent": "true" + } + } + } + ] + }` + +func TestListIamPolicies(t *testing.T) { + t.Parallel() + region, err := getRandomRegion() + require.NoError(t, err) + + localSession, err := session.NewSession(&awsgo.Config{ + Region: awsgo.String(region), + }) + require.NoError(t, err) + + policyArns, err := getAllLocalIamPolicies(localSession, time.Now(), config.Config{}) + require.NoError(t, err) + assert.NotEmpty(t, policyArns) +} + +func createPolicyWithNoEntities(t *testing.T, session *session.Session, name string) (string, error) { + svc := iam.New(session) + doc := awsgo.String(fakePolicy) + policy, err := svc.CreatePolicy(&iam.CreatePolicyInput{PolicyName: awsgo.String(name), PolicyDocument: doc}) + require.NoError(t, err) + return *policy.Policy.Arn, nil +} + +func createPolicyWithEntities(t *testing.T, session *session.Session, name string) (*entityInfo, error) { + policyArn, err := createPolicyWithNoEntities(t, session, name) + svc := iam.New(session) + if err != nil { + return nil, err + } + + //Create version + versionInput := &iam.CreatePolicyVersionInput{ + PolicyArn: awsgo.String(policyArn), + PolicyDocument: awsgo.String(fakePolicy), + } + _, err = svc.CreatePolicyVersion(versionInput) + if err != nil { + return nil, err + } + //Create User and link + userName := awsgo.String("test-user-" + util.UniqueID()) + _, err = svc.CreateUser(&iam.CreateUserInput{UserName: userName}) + if err != nil { + return nil, err + } + + userPolicyLink := &iam.AttachUserPolicyInput{ + UserName: userName, + PolicyArn: awsgo.String(policyArn), + } + _, err = svc.AttachUserPolicy(userPolicyLink) + if err != nil { + return nil, err + } + + //Create role and link + roleName := awsgo.String("test-role-" + util.UniqueID()) + roleInput := &iam.CreateRoleInput{ + RoleName: roleName, + AssumeRolePolicyDocument: awsgo.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "sts:AssumeRole" + ], + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + } + } + ] + }`), + } + _, err = svc.CreateRole(roleInput) + if err != nil { + return nil, err + } + + rolePolicyLink := &iam.AttachRolePolicyInput{RoleName: roleName, PolicyArn: awsgo.String(policyArn)} + _, err = svc.AttachRolePolicy(rolePolicyLink) + if err != nil { + return nil, err + } + + //Create group and link + groupName := awsgo.String("test-group-" + util.UniqueID()) + groupInput := &iam.CreateGroupInput{GroupName: groupName} + _, err = svc.CreateGroup(groupInput) + if err != nil { + return nil, err + } + + groupPolicyLink := &iam.AttachGroupPolicyInput{GroupName: groupName, PolicyArn: awsgo.String(policyArn)} + _, err = svc.AttachGroupPolicy(groupPolicyLink) + if err != nil { + return nil, err + } + + return &entityInfo{ + PolicyArn: &policyArn, + UserName: userName, + RoleName: roleName, + GroupName: groupName, + }, nil +} + +func deletePolicyExtraResources(session *session.Session, entityInfo *entityInfo) { + svc := iam.New(session) + //Delete User + _, err := svc.DeleteUser(&iam.DeleteUserInput{UserName: entityInfo.UserName}) + if err != nil { + logging.Logger.Errorf("Unable to delete test user: %s", *entityInfo.UserName) + } + + //Delete Role + _, err = svc.DeleteRole(&iam.DeleteRoleInput{RoleName: entityInfo.RoleName}) + if err != nil { + logging.Logger.Errorf("Unable to delete test role: %s", *entityInfo.RoleName) + } + + //Delete Group + _, err = svc.DeleteGroup(&iam.DeleteGroupInput{GroupName: entityInfo.GroupName}) + if err != nil { + logging.Logger.Errorf("Unable to delete test group: %s", *entityInfo.GroupName) + } +} + +func TestNukeIamPolicies(t *testing.T) { + t.Parallel() + + region, err := getRandomRegion() + require.NoError(t, err) + + localSession, err := session.NewSession(&awsgo.Config{ + Region: awsgo.String(region), + }) + require.NoError(t, err) + + //Create test entities + emptyName := "cloud-nuke-test" + util.UniqueID() + var emptyPolicyArn string + emptyPolicyArn, err = createPolicyWithNoEntities(t, localSession, emptyName) + require.NoError(t, err) + + nonEmptyName := "cloud-nuke-test" + util.UniqueID() + entities, err := createPolicyWithEntities(t, localSession, nonEmptyName) + defer deletePolicyExtraResources(localSession, entities) + require.NoError(t, err) + + //Assert test entities exist + var policyArns []*string + policyArns, err = getAllLocalIamPolicies(localSession, time.Now(), config.Config{}) + require.NoError(t, err) + assert.Contains(t, awsgo.StringValueSlice(policyArns), emptyPolicyArn) + assert.Contains(t, awsgo.StringValueSlice(policyArns), *entities.PolicyArn) + + //Nuke test entities + err = nukeAllIamPolicies(localSession, []*string{&emptyPolicyArn, entities.PolicyArn}) + require.NoError(t, err) + + //Assert test entities don't exist anymore + policyArns, err = getAllLocalIamPolicies(localSession, time.Now(), config.Config{}) + require.NoError(t, err) + assert.NotContains(t, awsgo.StringValueSlice(policyArns), emptyPolicyArn) + assert.NotContains(t, awsgo.StringValueSlice(policyArns), *entities.PolicyArn) +} diff --git a/aws/iam_policy_types.go b/aws/iam_policy_types.go new file mode 100644 index 00000000..19128f43 --- /dev/null +++ b/aws/iam_policy_types.go @@ -0,0 +1,36 @@ +package aws + +import ( + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/go-commons/errors" +) + +// IAMPolicies - represents all IAM Policies on the AWS account +type IAMPolicies struct { + PolicyArns []string +} + +// ResourceName - the simple name of the AWS resource +func (p IAMPolicies) ResourceName() string { + return "iam-policy" +} + +// ResourceIdentifiers - The IAM GroupNames +func (p IAMPolicies) ResourceIdentifiers() []string { + return p.PolicyArns +} + +// MaxBatchSize Tentative batch size to ensure AWS doesn't throttle +func (p IAMPolicies) MaxBatchSize() int { + return 80 +} + +// Nuke - Destroy every group in this collection +func (p IAMPolicies) Nuke(session *session.Session, identifiers []string) error { + if err := nukeAllIamPolicies(session, awsgo.StringSlice(identifiers)); err != nil { + return errors.WithStackTrace(err) + } + + return nil +} diff --git a/config/config.go b/config/config.go index 68f91891..9113fd76 100644 --- a/config/config.go +++ b/config/config.go @@ -12,7 +12,8 @@ import ( type Config struct { S3 ResourceType `yaml:"s3"` IAMUsers ResourceType `yaml:"IAMUsers"` - IAMGroups ResourceType `yaml:"IAMGroups"` //TODO update the test + IAMGroups ResourceType `yaml:"IAMGroups"` + IAMPolicies ResourceType `yaml:"IAMPolicies"` IAMRoles ResourceType `yaml:"IAMRoles"` SecretsManagerSecrets ResourceType `yaml:"SecretsManager"` NatGateway ResourceType `yaml:"NatGateway"` From 72862d0b7c1d54c7991d61bf93dd312cff9a41e6 Mon Sep 17 00:00:00 2001 From: Andrew Ellison Date: Mon, 31 Oct 2022 09:48:19 -0500 Subject: [PATCH 8/8] 116 - PR fix for broken test config --- config/config_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config_test.go b/config/config_test.go index 9cf455eb..54602e10 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -40,6 +40,8 @@ func emptyConfig() *Config { ResourceType{FilterRule{}, FilterRule{}}, ResourceType{FilterRule{}, FilterRule{}}, ResourceType{FilterRule{}, FilterRule{}}, + ResourceType{FilterRule{}, FilterRule{}}, + ResourceType{FilterRule{}, FilterRule{}}, } }