diff --git a/aws/aws.go b/aws/aws.go index 977e0fa3..6f2f9836 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -1497,7 +1497,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp if IsNukeable(macieAccounts.ResourceName(), resourceTypes) { start := time.Now() // Unfortunately, the Macie API doesn't provide the metadata information we'd need to implement the excludeAfter or configObj patterns - accountIds, err := getMacie(cloudNukeSession, excludeAfter) + accountIds, err := macieAccounts.getAll(configObj) if err != nil { ge := report.GeneralError{ Error: err, diff --git a/aws/macie.go b/aws/macie.go index 147eafcf..c36627ab 100644 --- a/aws/macie.go +++ b/aws/macie.go @@ -1,31 +1,26 @@ package aws import ( - "strings" - "time" - + "github.com/gruntwork-io/cloud-nuke/config" "github.com/gruntwork-io/cloud-nuke/telemetry" commonTelemetry "github.com/gruntwork-io/go-commons/telemetry" + "strings" "github.com/aws/aws-sdk-go/aws" awsgo "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/macie2" "github.com/gruntwork-io/cloud-nuke/logging" "github.com/gruntwork-io/cloud-nuke/report" "github.com/gruntwork-io/go-commons/errors" ) -// GetMacieSession will find and return any Macie accounts that were created via accepting an invite from another AWS Account +// getAll will find and return any Macie accounts that were created via accepting an invite from another AWS Account // Unfortunately, the Macie API doesn't provide the metadata information we'd need to implement the configObj pattern, so we // currently can only accept a session and excludeAfter - -func getMacie(session *session.Session, excludeAfter time.Time) ([]string, error) { - svc := macie2.New(session) +func (mm MacieMember) getAll(configObj config.Config) ([]string, error) { var macieStatus []string - output, err := svc.GetMacieSession(&macie2.GetMacieSessionInput{}) - + output, err := mm.Client.GetMacieSession(&macie2.GetMacieSessionInput{}) if err != nil { // If Macie is not enabled when we call GetMacieSession, we get back an error // so we should ignore the error if it's just telling us the account/region is not @@ -36,25 +31,22 @@ func getMacie(session *session.Session, excludeAfter time.Time) ([]string, error return nil, errors.WithStackTrace(err) } - if shouldIncludeMacie(output, excludeAfter) { + // Note: there's no identifier for the Macie resource, so we just insert random elements to the return array + // to follow the similar framework as other resources. + if configObj.MacieMember.ShouldInclude(config.ResourceValue{ + Time: output.CreatedAt, + }) { macieStatus = append(macieStatus, *output.Status) } return macieStatus, nil } -func shouldIncludeMacie(macie *macie2.GetMacieSessionOutput, excludeAfter time.Time) bool { - if excludeAfter.Before(*macie.UpdatedAt) { - return false - } - return true -} - -func getAllMacieMembers(svc *macie2.Macie2) ([]*string, error) { +func (mm MacieMember) getAllMacieMembers() ([]*string, error) { var memberAccountIds []*string // OnlyAssociated=false input parameter includes "pending" invite members - members, err := svc.ListMembers(&macie2.ListMembersInput{OnlyAssociated: aws.String("false")}) + members, err := mm.Client.ListMembers(&macie2.ListMembersInput{OnlyAssociated: aws.String("false")}) if err != nil { return nil, errors.WithStackTrace(err) } @@ -63,7 +55,7 @@ func getAllMacieMembers(svc *macie2.Macie2) ([]*string, error) { } for awsgo.StringValue(members.NextToken) != "" { - members, err = svc.ListMembers(&macie2.ListMembersInput{NextToken: members.NextToken}) + members, err = mm.Client.ListMembers(&macie2.ListMembersInput{NextToken: members.NextToken}) if err != nil { return nil, errors.WithStackTrace(err) } @@ -75,18 +67,18 @@ func getAllMacieMembers(svc *macie2.Macie2) ([]*string, error) { return memberAccountIds, nil } -func removeMacieMembers(svc *macie2.Macie2, memberAccountIds []*string) error { +func (mm MacieMember) removeMacieMembers(memberAccountIds []*string) error { // Member accounts must first be disassociated for _, accountId := range memberAccountIds { - _, err := svc.DisassociateMember(&macie2.DisassociateMemberInput{Id: accountId}) + _, err := mm.Client.DisassociateMember(&macie2.DisassociateMemberInput{Id: accountId}) if err != nil { return err } logging.Logger.Debugf("%s member account disassociated", *accountId) // Once disassociated, member accounts can be deleted - _, err = svc.DeleteMember(&macie2.DeleteMemberInput{Id: accountId}) + _, err = mm.Client.DeleteMember(&macie2.DeleteMemberInput{Id: accountId}) if err != nil { return err } @@ -95,34 +87,31 @@ func removeMacieMembers(svc *macie2.Macie2, memberAccountIds []*string) error { return nil } -func nukeMacie(session *session.Session, identifier []string) error { - svc := macie2.New(session) - region := aws.StringValue(session.Config.Region) - +func (mm MacieMember) nukeAll(identifier []string) error { if len(identifier) == 0 { - logging.Logger.Debugf("No Macie member accounts to nuke in region %s", region) + logging.Logger.Debugf("No Macie member accounts to nuke in region %s", mm.Region) return nil } // Check for and remove any member accounts in Macie // Macie cannot be disabled with active member accounts - memberAccountIds, err := getAllMacieMembers(svc) + memberAccountIds, err := mm.getAllMacieMembers() if err != nil { telemetry.TrackEvent(commonTelemetry.EventContext{ EventName: "Error finding macie member accounts", }, map[string]interface{}{ - "region": *svc.Config.Region, + "region": mm.Region, "reason": "Error finding macie member accounts", }) } if err == nil && len(memberAccountIds) > 0 { - err = removeMacieMembers(svc, memberAccountIds) + err = mm.removeMacieMembers(memberAccountIds) if err != nil { logging.Logger.Errorf("[Failed] Failed to remove members from macie") telemetry.TrackEvent(commonTelemetry.EventContext{ EventName: "Error removing members from macie", }, map[string]interface{}{ - "region": *svc.Config.Region, + "region": mm.Region, "reason": "Unable to remove members", }) } @@ -130,7 +119,7 @@ func nukeMacie(session *session.Session, identifier []string) error { // Check for an administrator account // Macie cannot be disabled with an active administrator account - adminAccount, err := svc.GetAdministratorAccount(&macie2.GetAdministratorAccountInput{}) + adminAccount, err := mm.Client.GetAdministratorAccount(&macie2.GetAdministratorAccountInput{}) if err != nil { if strings.Contains(err.Error(), "there isn't a delegated Macie administrator") { logging.Logger.Debugf("No delegated Macie administrator found to remove.") @@ -139,7 +128,7 @@ func nukeMacie(session *session.Session, identifier []string) error { telemetry.TrackEvent(commonTelemetry.EventContext{ EventName: "Error checking for administrator account in Macie", }, map[string]interface{}{ - "region": *svc.Config.Region, + "region": mm.Region, "reason": "Unable to find admin account", }) } @@ -147,26 +136,26 @@ func nukeMacie(session *session.Session, identifier []string) error { // Disassociate administrator account if it exists if adminAccount.Administrator != nil { - _, err := svc.DisassociateFromAdministratorAccount(&macie2.DisassociateFromAdministratorAccountInput{}) + _, err := mm.Client.DisassociateFromAdministratorAccount(&macie2.DisassociateFromAdministratorAccountInput{}) if err != nil { logging.Logger.Errorf("[Failed] Failed to disassociate from administrator account") telemetry.TrackEvent(commonTelemetry.EventContext{ EventName: "Error disassociating administrator account in Macie", }, map[string]interface{}{ - "region": *svc.Config.Region, + "region": mm.Region, "reason": "Unable to disassociate admin account", }) } } // Disable Macie - _, err = svc.DisableMacie(&macie2.DisableMacieInput{}) + _, err = mm.Client.DisableMacie(&macie2.DisableMacieInput{}) if err != nil { logging.Logger.Errorf("[Failed] Failed to disable macie.") telemetry.TrackEvent(commonTelemetry.EventContext{ EventName: "Error Nuking MACIE", }, map[string]interface{}{ - "region": *svc.Config.Region, + "region": mm.Region, "reason": "Error Nuking MACIE", }) e := report.Entry{ @@ -176,9 +165,9 @@ func nukeMacie(session *session.Session, identifier []string) error { } report.Record(e) } else { - logging.Logger.Debugf("[OK] Macie disabled in %s", *svc.Config.Region) + logging.Logger.Debugf("[OK] Macie disabled in %s", mm.Region) e := report.Entry{ - Identifier: *svc.Config.Region, + Identifier: mm.Region, ResourceType: "Macie", } report.Record(e) diff --git a/aws/macie_test.go b/aws/macie_test.go index 4571e1e5..69ee340a 100644 --- a/aws/macie_test.go +++ b/aws/macie_test.go @@ -1,61 +1,99 @@ package aws import ( - "strings" - "testing" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" + awsgo "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/macie2" - "github.com/gruntwork-io/cloud-nuke/logging" + "github.com/aws/aws-sdk-go/service/macie2/macie2iface" + "github.com/gruntwork-io/cloud-nuke/config" "github.com/gruntwork-io/cloud-nuke/telemetry" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "testing" + "time" ) -// Macie tests are limited to testing the ability to find and disable basic Macie -// features. The functionality of cloud-nuke disassociating/deleting members and -// disassociating administrator accounts requires the use of multiple AWS accounts and the -// ability to send and accept invitations within those accounts. +type mockedMacie struct { + macie2iface.Macie2API + GetMacieSessionOutput macie2.GetMacieSessionOutput + ListMacieMembersOutput macie2.ListMembersOutput + DisassociateMemberOutput macie2.DisassociateMemberOutput + DeleteMemberOutput macie2.DeleteMemberOutput + GetAdministratorAccountOutput macie2.GetAdministratorAccountOutput + DisassociateFromAdministratorAccountOutput macie2.DisassociateFromAdministratorAccountOutput + DisableMacieOutput macie2.DisableMacieOutput +} + +func (m mockedMacie) GetMacieSession(input *macie2.GetMacieSessionInput) (*macie2.GetMacieSessionOutput, error) { + return &m.GetMacieSessionOutput, nil +} + +func (m mockedMacie) ListMembers(input *macie2.ListMembersInput) (*macie2.ListMembersOutput, error) { + return &m.ListMacieMembersOutput, nil +} + +func (m mockedMacie) DisassociateMember(input *macie2.DisassociateMemberInput) (*macie2.DisassociateMemberOutput, error) { + return &m.DisassociateMemberOutput, nil +} + +func (m mockedMacie) DeleteMember(input *macie2.DeleteMemberInput) (*macie2.DeleteMemberOutput, error) { + return &m.DeleteMemberOutput, nil +} + +func (m mockedMacie) GetAdministratorAccount(input *macie2.GetAdministratorAccountInput) (*macie2.GetAdministratorAccountOutput, error) { + return &m.GetAdministratorAccountOutput, nil +} + +func (m mockedMacie) DisassociateFromAdministratorAccount(input *macie2.DisassociateFromAdministratorAccountInput) (*macie2.DisassociateFromAdministratorAccountOutput, error) { + return &m.DisassociateFromAdministratorAccountOutput, nil +} -func TestMacie(t *testing.T) { +func (m mockedMacie) DisableMacie(input *macie2.DisableMacieInput) (*macie2.DisableMacieOutput, error) { + return &m.DisableMacieOutput, nil +} + +func TestMacie_GetAll(t *testing.T) { telemetry.InitTelemetry("cloud-nuke", "") t.Parallel() - region, err := getRandomRegion() - require.NoError(t, err) - logging.Logger.Infof("Region: %s", region) + now := time.Now() + mm := MacieMember{ + Client: mockedMacie{ + GetMacieSessionOutput: macie2.GetMacieSessionOutput{ + Status: awsgo.String("ENABLED"), + CreatedAt: awsgo.Time(now), + }, + }, + } - awsSession, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + _, err := mm.getAll(config.Config{}) require.NoError(t, err) +} - svc := macie2.New(awsSession) - - // Check if Macie is enabled - _, err = svc.GetMacieSession(&macie2.GetMacieSessionInput{}) - - if err != nil { - // GetMacieSession throws an error if Macie is not enabled - if strings.Contains(err.Error(), "Macie is not enabled") { - logging.Logger.Infof("Macie not enabled.") - logging.Logger.Infof("Enabling Macie") - _, err := svc.EnableMacie(&macie2.EnableMacieInput{}) - require.NoError(t, err) - } else { - require.NoError(t, err) - } - } else { - logging.Logger.Infof("Macie already enabled") - } +func TestMacie_NukeAll(t *testing.T) { + telemetry.InitTelemetry("cloud-nuke", "") + t.Parallel() - macieEnabled, err := getMacie(awsSession, time.Now()) - require.NoError(t, err) + mm := MacieMember{ + Client: mockedMacie{ + ListMacieMembersOutput: macie2.ListMembersOutput{ + Members: []*macie2.Member{ + { + AccountId: awsgo.String("123456789012"), + }, + }, + }, - logging.Logger.Infof("Nuking Macie") - require.NoError(t, nukeMacie(awsSession, macieEnabled)) + DisassociateMemberOutput: macie2.DisassociateMemberOutput{}, + DeleteMemberOutput: macie2.DeleteMemberOutput{}, + GetAdministratorAccountOutput: macie2.GetAdministratorAccountOutput{ + Administrator: &macie2.Invitation{ + AccountId: awsgo.String("123456789012"), + }, + }, + DisassociateFromAdministratorAccountOutput: macie2.DisassociateFromAdministratorAccountOutput{}, + DisableMacieOutput: macie2.DisableMacieOutput{}, + }, + } - macieEnabled, err = getMacie(awsSession, time.Now()) + err := mm.nukeAll([]string{"enabled"}) require.NoError(t, err) - assert.Empty(t, macieEnabled) } diff --git a/aws/macie_types.go b/aws/macie_types.go index 71e9847d..bb28a762 100644 --- a/aws/macie_types.go +++ b/aws/macie_types.go @@ -12,20 +12,20 @@ type MacieMember struct { AccountIds []string } -func (r MacieMember) ResourceName() string { +func (mm MacieMember) ResourceName() string { return "macie-member" } -func (r MacieMember) ResourceIdentifiers() []string { - return r.AccountIds +func (mm MacieMember) ResourceIdentifiers() []string { + return mm.AccountIds } -func (r MacieMember) MaxBatchSize() int { +func (mm MacieMember) MaxBatchSize() int { return 10 } -func (r MacieMember) Nuke(session *session.Session, identifiers []string) error { - if err := nukeMacie(session, identifiers); err != nil { +func (mm MacieMember) Nuke(session *session.Session, identifiers []string) error { + if err := mm.nukeAll(identifiers); err != nil { return errors.WithStackTrace(err) } return nil