Skip to content

Commit

Permalink
Update Macie support - CORE-387 (#459)
Browse files Browse the repository at this point in the history
  • Loading branch information
arsci authored May 9, 2023
1 parent 89becac commit 60cca18
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 106 deletions.
2 changes: 1 addition & 1 deletion aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -1298,7 +1298,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 := getAllMacieMemberAccounts(cloudNukeSession)
accountIds, err := getMacie(cloudNukeSession, excludeAfter)
if err != nil {
ge := report.GeneralError{
Error: err,
Expand Down
198 changes: 138 additions & 60 deletions aws/macie.go
Original file line number Diff line number Diff line change
@@ -1,109 +1,187 @@
package aws

import (
goerror "errors"
"strings"
"time"

"github.com/gruntwork-io/cloud-nuke/telemetry"
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"

"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/aws/aws-sdk-go/service/sts"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/cloud-nuke/report"
"github.com/gruntwork-io/go-commons/errors"
)

// getAllMacieMemberAccounts 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 excludeAfter or configObj patterns, so we
// currently can only accept a session
func getAllMacieMemberAccounts(session *session.Session) ([]string, error) {
// GetMacieSession 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)
stssvc := sts.New(session)
var macieStatus []string

output, err := svc.GetMacieSession(&macie2.GetMacieSessionInput{})

allMacieAccounts := []string{}
output, err := svc.GetAdministratorAccount(&macie2.GetAdministratorAccountInput{})
if err != nil {
// There are several different errors that AWS may return when you attempt to call Macie operations on an account
// that doesn't yet have Macie enabled. For our purposes, this is fine, as we're only looking for those accounts and
// regions where Macie is enabled. Therefore, we ignore only these expected errors, and return any other error that might occur
var ade *macie2.AccessDeniedException
var rnfe *macie2.ResourceNotFoundException

switch {
case goerror.As(err, &ade):
logging.Logger.Debugf("Macie AccessDeniedException means macie is not enabled in account, so skipping")
return allMacieAccounts, nil
case goerror.As(err, &rnfe):
logging.Logger.Debugf("Macie ResourceNotFoundException means macie is not enabled in account, so skipping")
return allMacieAccounts, nil
default:
return allMacieAccounts, errors.WithStackTrace(err)
// 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
// enabled and return nil to indicate there are no resources to nuke
if strings.Contains(err.Error(), "Macie is not enabled") {
return nil, nil
}
return nil, errors.WithStackTrace(err)
}
// If the current account does have an Administrator account relationship, and it is enabled, then we consider this a macie member account
if output.Administrator != nil && output.Administrator.RelationshipStatus != nil {
if aws.StringValue(output.Administrator.RelationshipStatus) == macie2.RelationshipStatusEnabled {

input := &sts.GetCallerIdentityInput{}
output, err := stssvc.GetCallerIdentity(input)
if err != nil {
return allMacieAccounts, errors.WithStackTrace(err)
}
if shouldIncludeMacie(output, excludeAfter) {
macieStatus = append(macieStatus, *output.Status)
}

currentAccountId := aws.StringValue(output.Account)
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) {
var memberAccountIds []*string

// OnlyAssociated=false input parameter includes "pending" invite members
members, err := svc.ListMembers(&macie2.ListMembersInput{OnlyAssociated: aws.String("false")})
if err != nil {
return nil, errors.WithStackTrace(err)
}
for _, member := range members.Members {
memberAccountIds = append(memberAccountIds, member.AccountId)
}

allMacieAccounts = append(allMacieAccounts, currentAccountId)
for awsgo.StringValue(members.NextToken) != "" {
members, err = svc.ListMembers(&macie2.ListMembersInput{NextToken: members.NextToken})
if err != nil {
return nil, errors.WithStackTrace(err)
}
for _, member := range members.Members {
memberAccountIds = append(memberAccountIds, member.AccountId)
}
}
logging.Logger.Debugf("Found %d member accounts attached to macie", len(memberAccountIds))
return memberAccountIds, nil
}

func removeMacieMembers(svc *macie2.Macie2, memberAccountIds []*string) error {

// Member accounts must first be disassociated
for _, accountId := range memberAccountIds {
_, err := svc.DisassociateMember(&macie2.DisassociateMemberInput{Id: accountId})
if err != nil {
return err
}
logging.Logger.Debugf("%s member account disassociated", *accountId)

return allMacieAccounts, nil
// Once disassociated, member accounts can be deleted
_, err = svc.DeleteMember(&macie2.DeleteMemberInput{Id: accountId})
if err != nil {
return err
}
logging.Logger.Debugf("%s member account deleted", *accountId)
}
return nil
}

func nukeAllMacieMemberAccounts(session *session.Session, identifiers []string) error {
func nukeMacie(session *session.Session, identifier []string) error {
svc := macie2.New(session)
region := aws.StringValue(session.Config.Region)

if len(identifiers) == 0 {
logging.Logger.Debugf("No Macie member accounts to nuke in region %s", *session.Config.Region)
if len(identifier) == 0 {
logging.Logger.Debugf("No Macie member accounts to nuke in region %s", region)
return nil
}

logging.Logger.Debugf("Deleting Macie account membership and disabling Macie in %s", region)

for _, accountId := range identifiers {
_, disassociateErr := svc.DisassociateFromAdministratorAccount(&macie2.DisassociateFromAdministratorAccountInput{})

if disassociateErr != nil {
// Check for and remove any member accounts in Macie
// Macie cannot be disabled with active member accounts
memberAccountIds, err := getAllMacieMembers(svc)
if err != nil {
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Error finding macie member accounts",
}, map[string]interface{}{
"region": *svc.Config.Region,
"reason": "Error finding macie member accounts",
})
}
if err == nil && len(memberAccountIds) > 0 {
err = removeMacieMembers(svc, memberAccountIds)
if err != nil {
logging.Logger.Errorf("[Failed] Failed to remove members from macie")
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Error Nuking MACIE",
EventName: "Error removing members from macie",
}, map[string]interface{}{
"region": *session.Config.Region,
"region": *svc.Config.Region,
"reason": "Unable to remove members",
})
return errors.WithStackTrace(disassociateErr)
}
}

_, err := svc.DisableMacie(&macie2.DisableMacieInput{})

// Record status of this resource
e := report.Entry{
Identifier: accountId,
ResourceType: "Macie member account",
Error: err,
// Check for an administrator account
// Macie cannot be disabled with an active administrator account
adminAccount, err := svc.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.")
} else {
logging.Logger.Errorf("[Failed] Failed to check for administrator account")
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Error checking for administrator account in Macie",
}, map[string]interface{}{
"region": *svc.Config.Region,
"reason": "Unable to find admin account",
})
}
report.Record(e)
}

// Disassociate administrator account if it exists
if adminAccount.Administrator != nil {
_, err := svc.DisassociateFromAdministratorAccount(&macie2.DisassociateFromAdministratorAccountInput{})
if err != nil {
logging.Logger.Errorf("[Failed] Failed to disassociate from administrator account")
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Error Nuking MACIE",
EventName: "Error disassociating administrator account in Macie",
}, map[string]interface{}{
"region": *session.Config.Region,
"region": *svc.Config.Region,
"reason": "Unable to disassociate admin account",
})
return errors.WithStackTrace(err)
}

logging.Logger.Debugf("[OK] Macie account association for accountId %s deleted in %s", accountId, region)
}

// Disable Macie
_, err = svc.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,
"reason": "Error Nuking MACIE",
})
e := report.Entry{
Identifier: aws.StringValue(&identifier[0]),
ResourceType: "Macie",
Error: err,
}
report.Record(e)
} else {
logging.Logger.Debugf("[OK] Macie disabled in %s", *svc.Config.Region)
e := report.Entry{
Identifier: *svc.Config.Region,
ResourceType: "Macie",
}
report.Record(e)
}
return nil
}
103 changes: 59 additions & 44 deletions aws/macie_test.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,61 @@
package aws

//func TestListMacieAccounts(t *testing.T) {
// // Currently we hardcode to region us-east-1, because this is where our "standing" test invite exists
// region := "us-east-1"
// session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
// require.NoError(t, err)
//
// accountId, err := util.GetCurrentAccountId(session)
// require.NoError(t, err)
//
// acceptTestInvite(t, session)
// // Clean up after test by deleting the macie account association
// defer nukeAllMacieMemberAccounts(session, []string{accountId})
//
// retrievedAccountIds, lookupErr := getAllMacieMemberAccounts(session)
// require.NoError(t, lookupErr)
//
// assert.Contains(t, retrievedAccountIds, accountId)
//}

// Macie is not very conducive to programmatic testing. In order to make this test work, we maintain a standing invite
// from our phxdevops test account to our nuclear-wasteland account. We can continuously "nuke" our membership because
// Macie supports a member account *that was invited* to remove its own association at any time. Meanwhile, disassociating
// in this manner does not destroy or invalidate the original invitation, which allows us to to continually re-accept it
// from our nuclear-wasteland account (where cloud-nuke tests are run), just so that we can nuke it again
//
// Macie is also regional, so for the purposes of cost-savings and lower admin overhead, we're initially only testing this
// in the one hardcoded region - us-east-1
//
// The other reason we only test in us-east-1 is to avoid conflict with our Macie test in the CIS service catalog, which uses
// these same two accounts for similar purposes, but in EU regions.
// See: https://github.com/gruntwork-io/terraform-aws-cis-service-catalog/blob/master/test/security/macie_test.go
//func acceptTestInvite(t *testing.T, session *session.Session) {
// svc := macie2.New(session)
//
// // Accept the "standing" invite from our other test account to become a Macie member account
// // This works because Macie invites don't expire or get deleted when you disassociate your member account following an invitation
// acceptInviteInput := &macie2.AcceptInvitationInput{
// AdministratorAccountId: aws.String("353720269506"), // sandbox
// InvitationId: aws.String("18c0febb89142640f07ba497b19bac8e"), // "standing" test invite ID
// }
//
// _, acceptInviteErr := svc.AcceptInvitation(acceptInviteInput)
// require.NoError(t, acceptInviteErr)
//}
import (
"strings"
"testing"
"time"

"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/telemetry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// 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.

func TestMacie(t *testing.T) {
telemetry.InitTelemetry("cloud-nuke", "", "")
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)
logging.Logger.Infof("Region: %s", region)

awsSession, err := session.NewSession(&aws.Config{Region: aws.String(region)})
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")
}

macieEnabled, err := getMacie(awsSession, time.Now())
require.NoError(t, err)

logging.Logger.Infof("Nuking Macie")
require.NoError(t, nukeMacie(awsSession, macieEnabled))

macieEnabled, err = getMacie(awsSession, time.Now())
require.NoError(t, err)
assert.Empty(t, macieEnabled)
}
2 changes: 1 addition & 1 deletion aws/macie_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (r MacieMember) MaxBatchSize() int {
}

func (r MacieMember) Nuke(session *session.Session, identifiers []string) error {
if err := nukeAllMacieMemberAccounts(session, identifiers); err != nil {
if err := nukeMacie(session, identifiers); err != nil {
return errors.WithStackTrace(err)
}
return nil
Expand Down

0 comments on commit 60cca18

Please sign in to comment.