Skip to content

Commit

Permalink
Support CloudTrail (#358)
Browse files Browse the repository at this point in the history
  • Loading branch information
zackproser authored Sep 29, 2022
1 parent 6322be8 commit b6cdebe
Show file tree
Hide file tree
Showing 13 changed files with 468 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The currently supported functionality includes:
- Inspecting and deleting all API Gateways (v1 and v2) in an AWS account
- Inspecting and deleting all Elastic FileSystems (efs) in an AWS account
- Inspecting and deleting all SNS Topics in an AWS account
- Inspecting and deleting all CloudTrail Trails in an AWS account

### BEWARE!

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

// Cloudtrail Trails
cloudtrailTrails := CloudtrailTrail{}
if IsNukeable(cloudtrailTrails.ResourceName(), resourceTypes) {
cloudtrailArns, err := getAllCloudtrailTrails(cloudNukeSession, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
if len(cloudtrailArns) > 0 {
cloudtrailTrails.Arns = awsgo.StringValueSlice(cloudtrailArns)
resourcesInRegion.Resources = append(resourcesInRegion.Resources, cloudtrailTrails)
}
}
// End Cloudtrail Trails

if len(resourcesInRegion.Resources) > 0 {
account.Resources[region] = resourcesInRegion
}
Expand Down Expand Up @@ -932,6 +946,7 @@ func ListResourceTypes() []string {
ApiGatewayV2{}.ResourceName(),
ElasticFileSystem{}.ResourceName(),
SNSTopic{}.ResourceName(),
CloudtrailTrail{}.ResourceName(),
}
sort.Strings(resourceTypes)
return resourceTypes
Expand Down
78 changes: 78 additions & 0 deletions aws/cloudtrail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package aws

import (
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudtrail"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/go-commons/errors"
)

func getAllCloudtrailTrails(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
svc := cloudtrail.New(session)

param := &cloudtrail.ListTrailsInput{}

trailIds := []*string{}

paginator := func(output *cloudtrail.ListTrailsOutput, lastPage bool) bool {
for _, trailInfo := range output.Trails {
if shouldIncludeCloudtrailTrail(trailInfo, configObj) {
trailIds = append(trailIds, trailInfo.TrailARN)
}
}
return !lastPage
}

err := svc.ListTrailsPages(param, paginator)
if err != nil {
return trailIds, errors.WithStackTrace(err)
}

return trailIds, nil
}

func shouldIncludeCloudtrailTrail(trail *cloudtrail.TrailInfo, configObj config.Config) bool {
if trail == nil {
return false
}

return config.ShouldInclude(
aws.StringValue(trail.Name),
configObj.CloudtrailTrail.IncludeRule.NamesRegExp,
configObj.CloudtrailTrail.ExcludeRule.NamesRegExp,
)
}

func nukeAllCloudTrailTrails(session *session.Session, arns []*string) error {
svc := cloudtrail.New(session)

if len(arns) == 0 {
logging.Logger.Infof("No Cloudtrail Trails to nuke in region %s", *session.Config.Region)
return nil
}

logging.Logger.Infof("Deleting all Cloudtrail Trails in region %s", *session.Config.Region)
var deletedArns []*string

for _, arn := range arns {
params := &cloudtrail.DeleteTrailInput{
Name: arn,
}

_, err := svc.DeleteTrail(params)
if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
} else {
deletedArns = append(deletedArns, arn)
logging.Logger.Infof("Deleted Cloudtrail Trail: %s", aws.StringValue(arn))
}
}

logging.Logger.Infof("[OK] %d Cloudtrail Trail deleted in %s", len(deletedArns), *session.Config.Region)

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

import (
"fmt"
"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/cloudtrail"
"github.com/aws/aws-sdk-go/service/s3"
"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/cloud-nuke/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestListCloudTrailTrails(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

trailArn := createCloudTrailTrail(t, region)
defer deleteCloudTrailTrail(t, region, trailArn, false)

trailArns, err := getAllCloudtrailTrails(session, time.Now(), config.Config{})
require.NoError(t, err)
assert.Contains(t, aws.StringValueSlice(trailArns), aws.StringValue(trailArn))
}

func deleteCloudTrailTrail(t *testing.T, region string, trailARN *string, checkErr bool) {
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

cloudtrailSvc := cloudtrail.New(session)

param := &cloudtrail.DeleteTrailInput{
Name: trailARN,
}

_, deleteErr := cloudtrailSvc.DeleteTrail(param)
if checkErr {
require.NoError(t, deleteErr)
}
}

func createCloudTrailTrail(t *testing.T, region string) *string {
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

cloudtrailSvc := cloudtrail.New(session)
s3Svc := s3.New(session)
stsSvc := sts.New(session)

name := strings.ToLower(fmt.Sprintf("cloud-nuke-test-%s-%s", util.UniqueID(), util.UniqueID()))

logging.Logger.Debugf("Bucket: %s - creating", name)

_, bucketCreateErr := s3Svc.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(name),
})

require.NoError(t, bucketCreateErr)

waitErr := s3Svc.WaitUntilBucketExists(
&s3.HeadBucketInput{
Bucket: aws.String(name),
},
)

require.NoError(t, waitErr)

// Create and attach the expected S3 bucket policy that CloudTrail requires
policyJson := `
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSCloudTrailAclCheck20150319",
"Effect": "Allow",
"Principal": {"Service": "cloudtrail.amazonaws.com"},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::%s",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudtrail:%s:%s:trail/%s"
}
}
},
{
"Sid": "AWSCloudTrailWrite20150319",
"Effect": "Allow",
"Principal": {"Service": "cloudtrail.amazonaws.com"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::%s/AWSLogs/%s/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control",
"aws:SourceArn": "arn:aws:cloudtrail:%s:%s:trail/%s"
}
}
}
]
}
`

// Look up the current account ID so that we can interpolate it in the S3 bucket policy
callerIdInput := &sts.GetCallerIdentityInput{}

result, err := stsSvc.GetCallerIdentity(callerIdInput)

require.NoError(t, err)

renderedJson := fmt.Sprintf(
policyJson,
name,
region,
aws.StringValue(result.Account),
name,
name,
aws.StringValue(result.Account),
region,
aws.StringValue(result.Account),
name,
)

_, err = s3Svc.PutBucketPolicy(&s3.PutBucketPolicyInput{
Bucket: aws.String(name),
Policy: aws.String(strings.TrimSpace(renderedJson)),
})

require.NoError(t, err)

// Add an arbitrary sleep to account for eventual consistency
time.Sleep(15 * time.Second)

param := &cloudtrail.CreateTrailInput{
Name: aws.String(name),
S3BucketName: aws.String(name),
}

output, createTrailErr := cloudtrailSvc.CreateTrail(param)
require.NoError(t, createTrailErr)

return output.TrailARN
}

func TestNukeCloudTrailOne(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

trailArn := createCloudTrailTrail(t, region)
defer deleteCloudTrailTrail(t, region, trailArn, false)

identifiers := []*string{trailArn}

require.NoError(
t,
nukeAllCloudTrailTrails(session, identifiers),
)

assertCloudTrailTrailsDeleted(t, region, identifiers)
}

func TestNukeCloudTrailTrailMoreThanOne(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

trailArns := []*string{}
for i := 0; i < 3; i++ {
// We ignore errors in the delete call here, because it is intended to be a stop gap in case there is a bug in nuke.
trailArn := createCloudTrailTrail(t, region)
defer deleteCloudTrailTrail(t, region, trailArn, false)
trailArns = append(trailArns, trailArn)
}

require.NoError(
t,
nukeAllCloudTrailTrails(session, trailArns),
)

assertCloudTrailTrailsDeleted(t, region, trailArns)
}

func assertCloudTrailTrailsDeleted(t *testing.T, region string, identifiers []*string) {
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)
svc := cloudtrail.New(session)

resp, err := svc.DescribeTrails(&cloudtrail.DescribeTrailsInput{
TrailNameList: identifiers,
})
require.NoError(t, err)
if len(resp.TrailList) > 0 {
t.Fatalf("At least one of the following CloudTrail Trails was not deleted: %+v\n", aws.StringValueSlice(identifiers))
}
}
35 changes: 35 additions & 0 deletions aws/cloudtrail_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package aws

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

// CloudWatchLogGroup - represents all ec2 instances
type CloudtrailTrail struct {
Arns []string
}

// ResourceName - the simple name of the aws resource
func (ct CloudtrailTrail) ResourceName() string {
return "cloudtrail"
}

// ResourceIdentifiers - The instance ids of the ec2 instances
func (ct CloudtrailTrail) ResourceIdentifiers() []string {
return ct.Arns
}

func (ct CloudtrailTrail) MaxBatchSize() int {
return 50
}

// Nuke - nuke 'em all!!!
func (ct CloudtrailTrail) Nuke(session *session.Session, identifiers []string) error {
if err := nukeAllCloudTrailTrails(session, awsgo.StringSlice(identifiers)); err != nil {
return errors.WithStackTrace(err)
}

return nil
}
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Config struct {
APIGateway ResourceType `yaml:"APIGateway"`
APIGatewayV2 ResourceType `yaml:"APIGatewayV2"`
ElasticFileSystem ResourceType `yaml:"ElasticFileSystem"`
CloudtrailTrail ResourceType `yaml:"CloudtrailTrail"`
}

type ResourceType struct {
Expand Down
Loading

0 comments on commit b6cdebe

Please sign in to comment.