-
-
Notifications
You must be signed in to change notification settings - Fork 357
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update Macie support - CORE-387 (#459)
- Loading branch information
Showing
4 changed files
with
199 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters