Skip to content

Commit

Permalink
Implement support for Macie member accounts (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
zackproser committed Jul 18, 2022
1 parent 52b3e2e commit 127dad6
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The currently supported functionality includes:
- Inspecting and deleting all Customer managed keys from Key Management Service in an AWS account
- Inspecting and deleting all CloudWatch Log Groups in an AWS Account
- Inspecting and deleting all GuardDuty Detectors in an AWS Account
- Inspecting and deleting all Macie member accounts in an AWS account - as long as those accounts were created by Invitation - and not via AWS Organizations

### BEWARE!

Expand Down
15 changes: 15 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp

}
// End GuardDuty detectors
// Macie member accounts
macieAccounts := MacieMember{}
if IsNukeable(macieAccounts.ResourceName(), resourceTypes) {
accountIds, err := getAllMacieMemberAccounts(session, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
if len(accountIds) > 0 {
macieAccounts.AccountIds = accountIds
resourcesInRegion.Resources = append(resourcesInRegion.Resources, macieAccounts)
}

}
// End Macie member accounts

if len(resourcesInRegion.Resources) > 0 {
account.Resources[region] = resourcesInRegion
Expand Down Expand Up @@ -815,6 +829,7 @@ func ListResourceTypes() []string {
KmsCustomerKeys{}.ResourceName(),
CloudWatchLogGroups{}.ResourceName(),
GuardDuty{}.ResourceName(),
MacieMember{}.ResourceName(),
}
sort.Strings(resourceTypes)
return resourceTypes
Expand Down
86 changes: 86 additions & 0 deletions aws/macie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package aws

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

func getAllMacieMemberAccounts(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]string, error) {
svc := macie2.New(session)
stssvc := sts.New(session)

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 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)
}

currentAccountId := aws.StringValue(output.Account)

allMacieAccounts = append(allMacieAccounts, currentAccountId)
}
}

return allMacieAccounts, nil
}

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

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

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

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

if disassociateErr != nil {
return errors.WithStackTrace(disassociateErr)
}

_, err := svc.DisableMacie(&macie2.DisableMacieInput{})
if err != nil {
return errors.WithStackTrace(err)
}

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

return nil
}
59 changes: 59 additions & 0 deletions aws/macie_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package aws

import (
"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/config"
"github.com/gruntwork-io/cloud-nuke/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

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, time.Now(), config.Config{})
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)
}
29 changes: 29 additions & 0 deletions aws/macie_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package aws

import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gruntwork-io/go-commons/errors"
)

type MacieMember struct {
AccountIds []string
}

func (r MacieMember) ResourceName() string {
return "macie-member"
}

func (r MacieMember) ResourceIdentifiers() []string {
return r.AccountIds
}

func (r MacieMember) MaxBatchSize() int {
return 10
}

func (r MacieMember) Nuke(session *session.Session, identifiers []string) error {
if err := nukeAllMacieMemberAccounts(session, identifiers); err != nil {
return errors.WithStackTrace(err)
}
return nil
}
21 changes: 21 additions & 0 deletions util/get_current_account_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package util

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/gruntwork-io/go-commons/errors"
)

func GetCurrentAccountId(session *session.Session) (string, error) {
stssvc := sts.New(session)

input := &sts.GetCallerIdentityInput{}

output, err := stssvc.GetCallerIdentity(input)
if err != nil {
return "", errors.WithStackTrace(err)
}

return aws.StringValue(output.Account), nil
}

0 comments on commit 127dad6

Please sign in to comment.