Skip to content

Commit

Permalink
Refactor macie (#531)
Browse files Browse the repository at this point in the history
  • Loading branch information
james03160927 authored Jul 31, 2023
1 parent b8dc82c commit f56fe75
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 89 deletions.
2 changes: 1 addition & 1 deletion aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
71 changes: 30 additions & 41 deletions aws/macie.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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
}
Expand All @@ -95,42 +87,39 @@ 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",
})
}
}

// 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.")
Expand All @@ -139,34 +128,34 @@ 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",
})
}
}

// 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{
Expand All @@ -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)
Expand Down
120 changes: 79 additions & 41 deletions aws/macie_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 6 additions & 6 deletions aws/macie_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit f56fe75

Please sign in to comment.