Skip to content

Commit b6cdebe

Browse files
authored
Support CloudTrail (#358)
1 parent 6322be8 commit b6cdebe

13 files changed

+468
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The currently supported functionality includes:
4949
- Inspecting and deleting all API Gateways (v1 and v2) in an AWS account
5050
- Inspecting and deleting all Elastic FileSystems (efs) in an AWS account
5151
- Inspecting and deleting all SNS Topics in an AWS account
52+
- Inspecting and deleting all CloudTrail Trails in an AWS account
5253

5354
### BEWARE!
5455

aws/aws.go

+15
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
816816
}
817817
// End SNS Topics
818818

819+
// Cloudtrail Trails
820+
cloudtrailTrails := CloudtrailTrail{}
821+
if IsNukeable(cloudtrailTrails.ResourceName(), resourceTypes) {
822+
cloudtrailArns, err := getAllCloudtrailTrails(cloudNukeSession, excludeAfter, configObj)
823+
if err != nil {
824+
return nil, errors.WithStackTrace(err)
825+
}
826+
if len(cloudtrailArns) > 0 {
827+
cloudtrailTrails.Arns = awsgo.StringValueSlice(cloudtrailArns)
828+
resourcesInRegion.Resources = append(resourcesInRegion.Resources, cloudtrailTrails)
829+
}
830+
}
831+
// End Cloudtrail Trails
832+
819833
if len(resourcesInRegion.Resources) > 0 {
820834
account.Resources[region] = resourcesInRegion
821835
}
@@ -932,6 +946,7 @@ func ListResourceTypes() []string {
932946
ApiGatewayV2{}.ResourceName(),
933947
ElasticFileSystem{}.ResourceName(),
934948
SNSTopic{}.ResourceName(),
949+
CloudtrailTrail{}.ResourceName(),
935950
}
936951
sort.Strings(resourceTypes)
937952
return resourceTypes

aws/cloudtrail.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package aws
2+
3+
import (
4+
"time"
5+
6+
"github.com/aws/aws-sdk-go/aws"
7+
"github.com/aws/aws-sdk-go/aws/session"
8+
"github.com/aws/aws-sdk-go/service/cloudtrail"
9+
"github.com/gruntwork-io/cloud-nuke/config"
10+
"github.com/gruntwork-io/cloud-nuke/logging"
11+
"github.com/gruntwork-io/go-commons/errors"
12+
)
13+
14+
func getAllCloudtrailTrails(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
15+
svc := cloudtrail.New(session)
16+
17+
param := &cloudtrail.ListTrailsInput{}
18+
19+
trailIds := []*string{}
20+
21+
paginator := func(output *cloudtrail.ListTrailsOutput, lastPage bool) bool {
22+
for _, trailInfo := range output.Trails {
23+
if shouldIncludeCloudtrailTrail(trailInfo, configObj) {
24+
trailIds = append(trailIds, trailInfo.TrailARN)
25+
}
26+
}
27+
return !lastPage
28+
}
29+
30+
err := svc.ListTrailsPages(param, paginator)
31+
if err != nil {
32+
return trailIds, errors.WithStackTrace(err)
33+
}
34+
35+
return trailIds, nil
36+
}
37+
38+
func shouldIncludeCloudtrailTrail(trail *cloudtrail.TrailInfo, configObj config.Config) bool {
39+
if trail == nil {
40+
return false
41+
}
42+
43+
return config.ShouldInclude(
44+
aws.StringValue(trail.Name),
45+
configObj.CloudtrailTrail.IncludeRule.NamesRegExp,
46+
configObj.CloudtrailTrail.ExcludeRule.NamesRegExp,
47+
)
48+
}
49+
50+
func nukeAllCloudTrailTrails(session *session.Session, arns []*string) error {
51+
svc := cloudtrail.New(session)
52+
53+
if len(arns) == 0 {
54+
logging.Logger.Infof("No Cloudtrail Trails to nuke in region %s", *session.Config.Region)
55+
return nil
56+
}
57+
58+
logging.Logger.Infof("Deleting all Cloudtrail Trails in region %s", *session.Config.Region)
59+
var deletedArns []*string
60+
61+
for _, arn := range arns {
62+
params := &cloudtrail.DeleteTrailInput{
63+
Name: arn,
64+
}
65+
66+
_, err := svc.DeleteTrail(params)
67+
if err != nil {
68+
logging.Logger.Errorf("[Failed] %s", err)
69+
} else {
70+
deletedArns = append(deletedArns, arn)
71+
logging.Logger.Infof("Deleted Cloudtrail Trail: %s", aws.StringValue(arn))
72+
}
73+
}
74+
75+
logging.Logger.Infof("[OK] %d Cloudtrail Trail deleted in %s", len(deletedArns), *session.Config.Region)
76+
77+
return nil
78+
}

aws/cloudtrail_test.go

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package aws
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
"github.com/aws/aws-sdk-go/aws"
10+
"github.com/aws/aws-sdk-go/aws/session"
11+
"github.com/aws/aws-sdk-go/service/cloudtrail"
12+
"github.com/aws/aws-sdk-go/service/s3"
13+
"github.com/aws/aws-sdk-go/service/sts"
14+
"github.com/gruntwork-io/cloud-nuke/config"
15+
"github.com/gruntwork-io/cloud-nuke/logging"
16+
"github.com/gruntwork-io/cloud-nuke/util"
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
func TestListCloudTrailTrails(t *testing.T) {
22+
t.Parallel()
23+
24+
region, err := getRandomRegion()
25+
require.NoError(t, err)
26+
27+
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
28+
require.NoError(t, err)
29+
30+
trailArn := createCloudTrailTrail(t, region)
31+
defer deleteCloudTrailTrail(t, region, trailArn, false)
32+
33+
trailArns, err := getAllCloudtrailTrails(session, time.Now(), config.Config{})
34+
require.NoError(t, err)
35+
assert.Contains(t, aws.StringValueSlice(trailArns), aws.StringValue(trailArn))
36+
}
37+
38+
func deleteCloudTrailTrail(t *testing.T, region string, trailARN *string, checkErr bool) {
39+
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
40+
require.NoError(t, err)
41+
42+
cloudtrailSvc := cloudtrail.New(session)
43+
44+
param := &cloudtrail.DeleteTrailInput{
45+
Name: trailARN,
46+
}
47+
48+
_, deleteErr := cloudtrailSvc.DeleteTrail(param)
49+
if checkErr {
50+
require.NoError(t, deleteErr)
51+
}
52+
}
53+
54+
func createCloudTrailTrail(t *testing.T, region string) *string {
55+
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
56+
require.NoError(t, err)
57+
58+
cloudtrailSvc := cloudtrail.New(session)
59+
s3Svc := s3.New(session)
60+
stsSvc := sts.New(session)
61+
62+
name := strings.ToLower(fmt.Sprintf("cloud-nuke-test-%s-%s", util.UniqueID(), util.UniqueID()))
63+
64+
logging.Logger.Debugf("Bucket: %s - creating", name)
65+
66+
_, bucketCreateErr := s3Svc.CreateBucket(&s3.CreateBucketInput{
67+
Bucket: aws.String(name),
68+
})
69+
70+
require.NoError(t, bucketCreateErr)
71+
72+
waitErr := s3Svc.WaitUntilBucketExists(
73+
&s3.HeadBucketInput{
74+
Bucket: aws.String(name),
75+
},
76+
)
77+
78+
require.NoError(t, waitErr)
79+
80+
// Create and attach the expected S3 bucket policy that CloudTrail requires
81+
policyJson := `
82+
{
83+
"Version": "2012-10-17",
84+
"Statement": [
85+
{
86+
"Sid": "AWSCloudTrailAclCheck20150319",
87+
"Effect": "Allow",
88+
"Principal": {"Service": "cloudtrail.amazonaws.com"},
89+
"Action": "s3:GetBucketAcl",
90+
"Resource": "arn:aws:s3:::%s",
91+
"Condition": {
92+
"StringEquals": {
93+
"aws:SourceArn": "arn:aws:cloudtrail:%s:%s:trail/%s"
94+
}
95+
}
96+
},
97+
{
98+
"Sid": "AWSCloudTrailWrite20150319",
99+
"Effect": "Allow",
100+
"Principal": {"Service": "cloudtrail.amazonaws.com"},
101+
"Action": "s3:PutObject",
102+
"Resource": "arn:aws:s3:::%s/AWSLogs/%s/*",
103+
"Condition": {
104+
"StringEquals": {
105+
"s3:x-amz-acl": "bucket-owner-full-control",
106+
"aws:SourceArn": "arn:aws:cloudtrail:%s:%s:trail/%s"
107+
}
108+
}
109+
}
110+
]
111+
}
112+
`
113+
114+
// Look up the current account ID so that we can interpolate it in the S3 bucket policy
115+
callerIdInput := &sts.GetCallerIdentityInput{}
116+
117+
result, err := stsSvc.GetCallerIdentity(callerIdInput)
118+
119+
require.NoError(t, err)
120+
121+
renderedJson := fmt.Sprintf(
122+
policyJson,
123+
name,
124+
region,
125+
aws.StringValue(result.Account),
126+
name,
127+
name,
128+
aws.StringValue(result.Account),
129+
region,
130+
aws.StringValue(result.Account),
131+
name,
132+
)
133+
134+
_, err = s3Svc.PutBucketPolicy(&s3.PutBucketPolicyInput{
135+
Bucket: aws.String(name),
136+
Policy: aws.String(strings.TrimSpace(renderedJson)),
137+
})
138+
139+
require.NoError(t, err)
140+
141+
// Add an arbitrary sleep to account for eventual consistency
142+
time.Sleep(15 * time.Second)
143+
144+
param := &cloudtrail.CreateTrailInput{
145+
Name: aws.String(name),
146+
S3BucketName: aws.String(name),
147+
}
148+
149+
output, createTrailErr := cloudtrailSvc.CreateTrail(param)
150+
require.NoError(t, createTrailErr)
151+
152+
return output.TrailARN
153+
}
154+
155+
func TestNukeCloudTrailOne(t *testing.T) {
156+
t.Parallel()
157+
158+
region, err := getRandomRegion()
159+
require.NoError(t, err)
160+
161+
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
162+
require.NoError(t, err)
163+
164+
trailArn := createCloudTrailTrail(t, region)
165+
defer deleteCloudTrailTrail(t, region, trailArn, false)
166+
167+
identifiers := []*string{trailArn}
168+
169+
require.NoError(
170+
t,
171+
nukeAllCloudTrailTrails(session, identifiers),
172+
)
173+
174+
assertCloudTrailTrailsDeleted(t, region, identifiers)
175+
}
176+
177+
func TestNukeCloudTrailTrailMoreThanOne(t *testing.T) {
178+
t.Parallel()
179+
180+
region, err := getRandomRegion()
181+
require.NoError(t, err)
182+
183+
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
184+
require.NoError(t, err)
185+
186+
trailArns := []*string{}
187+
for i := 0; i < 3; i++ {
188+
// 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.
189+
trailArn := createCloudTrailTrail(t, region)
190+
defer deleteCloudTrailTrail(t, region, trailArn, false)
191+
trailArns = append(trailArns, trailArn)
192+
}
193+
194+
require.NoError(
195+
t,
196+
nukeAllCloudTrailTrails(session, trailArns),
197+
)
198+
199+
assertCloudTrailTrailsDeleted(t, region, trailArns)
200+
}
201+
202+
func assertCloudTrailTrailsDeleted(t *testing.T, region string, identifiers []*string) {
203+
session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
204+
require.NoError(t, err)
205+
svc := cloudtrail.New(session)
206+
207+
resp, err := svc.DescribeTrails(&cloudtrail.DescribeTrailsInput{
208+
TrailNameList: identifiers,
209+
})
210+
require.NoError(t, err)
211+
if len(resp.TrailList) > 0 {
212+
t.Fatalf("At least one of the following CloudTrail Trails was not deleted: %+v\n", aws.StringValueSlice(identifiers))
213+
}
214+
}

aws/cloudtrail_types.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package aws
2+
3+
import (
4+
awsgo "github.com/aws/aws-sdk-go/aws"
5+
"github.com/aws/aws-sdk-go/aws/session"
6+
"github.com/gruntwork-io/go-commons/errors"
7+
)
8+
9+
// CloudWatchLogGroup - represents all ec2 instances
10+
type CloudtrailTrail struct {
11+
Arns []string
12+
}
13+
14+
// ResourceName - the simple name of the aws resource
15+
func (ct CloudtrailTrail) ResourceName() string {
16+
return "cloudtrail"
17+
}
18+
19+
// ResourceIdentifiers - The instance ids of the ec2 instances
20+
func (ct CloudtrailTrail) ResourceIdentifiers() []string {
21+
return ct.Arns
22+
}
23+
24+
func (ct CloudtrailTrail) MaxBatchSize() int {
25+
return 50
26+
}
27+
28+
// Nuke - nuke 'em all!!!
29+
func (ct CloudtrailTrail) Nuke(session *session.Session, identifiers []string) error {
30+
if err := nukeAllCloudTrailTrails(session, awsgo.StringSlice(identifiers)); err != nil {
31+
return errors.WithStackTrace(err)
32+
}
33+
34+
return nil
35+
}

config/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Config struct {
3939
APIGateway ResourceType `yaml:"APIGateway"`
4040
APIGatewayV2 ResourceType `yaml:"APIGatewayV2"`
4141
ElasticFileSystem ResourceType `yaml:"ElasticFileSystem"`
42+
CloudtrailTrail ResourceType `yaml:"CloudtrailTrail"`
4243
}
4344

4445
type ResourceType struct {

0 commit comments

Comments
 (0)