Skip to content

Commit

Permalink
feat(cloudtrail): accept existing S3 bucket (#3680)
Browse files Browse the repository at this point in the history
Make CloudTrail accept existing S3 bucket to write to.

Fixes #3651.
  • Loading branch information
slipdexic authored and rix0rrr committed Aug 22, 2019
1 parent 210ed8f commit c2d6847
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 17 deletions.
48 changes: 31 additions & 17 deletions packages/@aws-cdk/aws-cloudtrail/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export interface TrailProps {
* @default - No prefix.
*/
readonly s3KeyPrefix?: string;

/** The Amazon S3 bucket
*
* @default - if not supplied a bucket will be created with all the correct permisions
*/
readonly bucket?: s3.IBucket
}

export enum ReadWriteType {
Expand All @@ -109,6 +115,10 @@ export enum ReadWriteType {
*
* const cloudTrail = new CloudTrail(this, 'MyTrail');
*
* NOTE the above example creates an UNENCRYPTED bucket by default,
* If you are required to use an Encrypted bucket you can supply a preconfigured bucket
* via TrailProps
*
*/
export class Trail extends Resource {

Expand All @@ -122,33 +132,36 @@ export class Trail extends Resource {
*/
public readonly trailSnsTopicArn: string;

private s3bucket: s3.IBucket;
private eventSelectors: EventSelector[] = [];

constructor(scope: Construct, id: string, props: TrailProps = {}) {
super(scope, id, {
physicalName: props.trailName,
});

const s3bucket = new s3.Bucket(this, 'S3', {encryption: s3.BucketEncryption.UNENCRYPTED});
const cloudTrailPrincipal = new iam.ServicePrincipal("cloudtrail.amazonaws.com");

s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [s3bucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [s3bucket.arnForObjects(`AWSLogs/${Stack.of(this).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));
this.s3bucket = props.bucket || new s3.Bucket(this, 'S3', {encryption: s3.BucketEncryption.UNENCRYPTED});

this.s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [this.s3bucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

this.s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [this.s3bucket.arnForObjects(`AWSLogs/${Stack.of(this).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));

let logGroup: logs.CfnLogGroup | undefined;
let logsRole: iam.IRole | undefined;

if (props.sendToCloudWatchLogs) {
logGroup = new logs.CfnLogGroup(this, "LogGroup", {
retentionInDays: props.cloudWatchLogsRetention || logs.RetentionDays.ONE_YEAR
Expand All @@ -161,6 +174,7 @@ export class Trail extends Resource {
resources: [logGroup.attrArn],
}));
}

if (props.managementEvents) {
const managementEvent = {
includeManagementEvents: true,
Expand All @@ -177,7 +191,7 @@ export class Trail extends Resource {
includeGlobalServiceEvents: props.includeGlobalServiceEvents == null ? true : props.includeGlobalServiceEvents,
trailName: this.physicalName,
kmsKeyId: props.kmsKey && props.kmsKey.keyArn,
s3BucketName: s3bucket.bucketName,
s3BucketName: this.s3bucket.bucketName,
s3KeyPrefix: props.s3KeyPrefix,
cloudWatchLogsLogGroupArn: logGroup && logGroup.attrArn,
cloudWatchLogsRoleArn: logsRole && logsRole.roleArn,
Expand All @@ -192,7 +206,7 @@ export class Trail extends Resource {
});
this.trailSnsTopicArn = trail.attrSnsTopicArn;

const s3BucketPolicy = s3bucket.node.findChild("Policy").node.findChild("Resource") as s3.CfnBucketPolicy;
const s3BucketPolicy = this.s3bucket.node.findChild("Policy").node.findChild("Resource") as s3.CfnBucketPolicy;
trail.node.addDependency(s3BucketPolicy);

// If props.sendToCloudWatchLogs is set to true then the trail needs to depend on the created logsRole
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"Resources": {
"Bucket83908E77": {
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"S3486F821D": {
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain"
},
"S3Policy2E4AA1D6": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "S3486F821D"
},
"PolicyDocument": {
"Statement": [
{
"Action": "s3:GetBucketAcl",
"Effect": "Allow",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Resource": {
"Fn::GetAtt": [
"S3486F821D",
"Arn"
]
}
},
{
"Action": "s3:PutObject",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
}
},
"Effect": "Allow",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Resource": {
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"S3486F821D",
"Arn"
]
},
"/AWSLogs/",
{
"Ref": "AWS::AccountId"
},
"/*"
]
]
}
}
],
"Version": "2012-10-17"
}
}
},
"Trail022F0CF2": {
"Type": "AWS::CloudTrail::Trail",
"Properties": {
"IsLogging": true,
"S3BucketName": {
"Ref": "S3486F821D"
},
"EnableLogFileValidation": true,
"EventSelectors": [
{
"DataResources": [
{
"Type": "AWS::S3::Object",
"Values": [
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Bucket83908E77",
"Arn"
]
},
"/"
]
]
}
]
}
]
}
],
"IncludeGlobalServiceEvents": true,
"IsMultiRegionTrail": true
},
"DependsOn": [
"S3Policy2E4AA1D6"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import iam = require('@aws-cdk/aws-iam');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/core');
import { Stack } from '@aws-cdk/core';

import cloudtrail = require('../lib');

const app = new cdk.App();
const stack = new cdk.Stack(app, 'integ-cloudtrail');

const bucket = new s3.Bucket(stack, 'Bucket', { removalPolicy: cdk.RemovalPolicy.DESTROY });

// using exctecy the same code as inside the cloudtrail class to produce the supplied bucket and policy
const cloudTrailPrincipal = new iam.ServicePrincipal("cloudtrail.amazonaws.com");

const Trailbucket = new s3.Bucket(stack, 'S3', {encryption: s3.BucketEncryption.UNENCRYPTED});

Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.arnForObjects(`AWSLogs/${Stack.of(stack).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));

const trail = new cloudtrail.Trail(stack, 'Trail', {bucket: Trailbucket});

trail.addS3EventSelector([bucket.arnForObjects('')]);

app.synth();
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect, haveResource, not, SynthUtils } from '@aws-cdk/assert';
import iam = require('@aws-cdk/aws-iam');
import { RetentionDays } from '@aws-cdk/aws-logs';
import s3 = require('@aws-cdk/aws-s3');
import { Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
import { ReadWriteType, Trail } from '../lib';
Expand Down Expand Up @@ -67,6 +69,33 @@ export = {
test.deepEqual(trail.DependsOn, ['MyAmazingCloudTrailS3Policy39C120B0']);
test.done();
},
'with s3bucket'(test: Test) {
const stack = getTestStack();
const Trailbucket = new s3.Bucket(stack, 'S3');
const cloudTrailPrincipal = new iam.ServicePrincipal("cloudtrail.amazonaws.com");
Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.arnForObjects(`AWSLogs/${Stack.of(stack).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));

new Trail(stack, 'Trail', {bucket: Trailbucket});

expect(stack).to(haveResource("AWS::CloudTrail::Trail"));
expect(stack).to(haveResource("AWS::S3::Bucket"));
expect(stack).to(haveResource("AWS::S3::BucketPolicy"));
expect(stack).to(not(haveResource("AWS::Logs::LogGroup")));
test.done();
},
'with cloud watch logs': {
'enabled'(test: Test) {
const stack = getTestStack();
Expand Down

0 comments on commit c2d6847

Please sign in to comment.