Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for Macie member accounts #323

Merged
merged 15 commits into from
Jul 18, 2022
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 @@ -731,6 +731,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp

}
// End GuardDuty detectors
// Macie member accounts
macieAccounts := Macie{}
zackproser marked this conversation as resolved.
Show resolved Hide resolved
if IsNukeable(macieAccounts.ResourceName(), resourceTypes) {
accountIds, err := getAllMacieAccounts(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 @@ -829,6 +843,7 @@ func ListResourceTypes() []string {
KmsCustomerKeys{}.ResourceName(),
CloudWatchLogGroups{}.ResourceName(),
GuardDuty{}.ResourceName(),
Macie{}.ResourceName(),
}
sort.Strings(resourceTypes)
return resourceTypes
Expand Down
87 changes: 87 additions & 0 deletions aws/macie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 getAllMacieAccounts(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]string, error) {
zackproser marked this conversation as resolved.
Show resolved Hide resolved
svc := macie2.New(session)
stssvc := sts.New(session)

allMacieAccounts := []string{}
output, err := svc.GetAdministratorAccount(&macie2.GetAdministratorAccountInput{})
// 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)
}
}
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 errror 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")
case goerror.As(err, &rnfe):
logging.Logger.Debugf("Macie ResourceNotFoundException means macie is not enabled in account, so skipping")
default:
return allMacieAccounts, errors.WithStackTrace(err)
}
zackproser marked this conversation as resolved.
Show resolved Hide resolved
}
return allMacieAccounts, nil
}

func nukeAllMacieAccounts(session *session.Session, identifiers []string) error {
zackproser marked this conversation as resolved.
Show resolved Hide resolved
svc := macie2.New(session)
region := aws.StringValue(session.Config.Region)

disassociatedAccountIds := []string{}

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 in %s", region)
zackproser marked this conversation as resolved.
Show resolved Hide resolved

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)
disassociatedAccountIds = append(disassociatedAccountIds, accountId)
zackproser marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}
55 changes: 55 additions & 0 deletions aws/macie_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 nukeAllMacieAccounts(session, []string{accountId})

retrievedAccountIds, lookupErr := getAllMacieAccounts(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, diassociating
zackproser marked this conversation as resolved.
Show resolved Hide resolved
// 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
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("087285199408"), // phxdevops
InvitationId: aws.String("28c0eacd402dd97cbf8a0c14b6cc3237"), // "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 Macie struct {
AccountIds []string
}

func (r Macie) ResourceName() string {
return "macie"
zackproser marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

func (r Macie) Nuke(session *session.Session, identifiers []string) error {
if err := nukeAllMacieAccounts(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
}