From 2cdd37e4348b9f2e669f7acd408de3751cd36b83 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 19 May 2020 17:21:09 +0100 Subject: [PATCH 1/3] feat(cloudtrail): user specified log group Allow for users to set their own log group that CloudTrail must send events to. Expose a log group instance property that returns the user specified or auto-created log group. closes #6162 --- .../@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts | 32 +++++++++--- .../aws-cloudtrail/test/test.cloudtrail.ts | 51 +++++++++++++++++-- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 365b78b3f4087..57210356406c0 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -63,12 +63,19 @@ export interface TrailProps { readonly sendToCloudWatchLogs?: boolean; /** - * How long to retain logs in CloudWatchLogs. Ignored if sendToCloudWatchLogs is false + * How long to retain logs in CloudWatchLogs. + * Ignored if sendToCloudWatchLogs is false or if cloudWatchLogGroup is set. * * @default logs.RetentionDays.OneYear */ readonly cloudWatchLogsRetention?: logs.RetentionDays; + /** + * Log Group to which CloudTrail to push logs to. Ignored if sendToCloudWatchLogs is set to false. + * @default - a new log group is created and used. + */ + readonly cloudWatchLogGroup?: logs.ILogGroup; + /** The AWS Key Management Service (AWS KMS) key ID that you want to use to encrypt CloudTrail logs. * * @default - No encryption. @@ -154,6 +161,12 @@ export class Trail extends Resource { */ public readonly trailSnsTopicArn: string; + /** + * The CloudWatch log group to which CloudTrail events are sent. + * `undefined` if `sendToCloudWatchLogs` property is false. + */ + public readonly logGroup: logs.ILogGroup | undefined; + private s3bucket: s3.IBucket; private eventSelectors: EventSelector[] = []; @@ -183,19 +196,22 @@ export class Trail extends Resource { }, })); - 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, - }); + if (props.cloudWatchLogGroup) { + this.logGroup = props.cloudWatchLogGroup; + } else { + this.logGroup = new logs.LogGroup(this, 'LogGroup', { + retention: props.cloudWatchLogsRetention ?? logs.RetentionDays.ONE_YEAR, + }); + } logsRole = new iam.Role(this, 'LogsRole', { assumedBy: cloudTrailPrincipal }); logsRole.addToPolicy(new iam.PolicyStatement({ actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], - resources: [logGroup.attrArn], + resources: [this.logGroup.logGroupArn], })); } @@ -217,8 +233,8 @@ export class Trail extends Resource { kmsKeyId: props.kmsKey && props.kmsKey.keyArn, s3BucketName: this.s3bucket.bucketName, s3KeyPrefix: props.s3KeyPrefix, - cloudWatchLogsLogGroupArn: logGroup && logGroup.attrArn, - cloudWatchLogsRoleArn: logsRole && logsRole.roleArn, + cloudWatchLogsLogGroupArn: this.logGroup?.logGroupArn, + cloudWatchLogsRoleArn: logsRole?.roleArn, snsTopicName: props.snsTopic, eventSelectors: this.eventSelectors, }); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index 5138395d43be4..f6233d438b556 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -1,7 +1,7 @@ -import { expect, haveResource, not, SynthUtils } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, not, SynthUtils } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { RetentionDays } from '@aws-cdk/aws-logs'; +import { LogGroup, RetentionDays } from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -160,10 +160,12 @@ export = { 'with cloud watch logs': { 'enabled'(test: Test) { const stack = getTestStack(); - new Trail(stack, 'MyAmazingCloudTrail', { + const t = new Trail(stack, 'MyAmazingCloudTrail', { sendToCloudWatchLogs: true, }); + test.ok(t.logGroup); + test.deepEqual(stack.resolve(t.logGroup!.logGroupArn), { 'Fn::GetAtt': ['MyAmazingCloudTrailLogGroup2BE67F87', 'Arn'] }); expect(stack).to(haveResource('AWS::CloudTrail::Trail')); expect(stack).to(haveResource('AWS::S3::Bucket')); expect(stack).to(haveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties)); @@ -177,7 +179,7 @@ export = { Effect: 'Allow', Action: ['logs:PutLogEvents', 'logs:CreateLogStream'], Resource: { - 'Fn::GetAtt': ['MyAmazingCloudTrailLogGroupAAD65144', 'Arn'], + 'Fn::GetAtt': ['MyAmazingCloudTrailLogGroup2BE67F87', 'Arn'], }, }], }, @@ -188,6 +190,7 @@ export = { test.deepEqual(trail.DependsOn, [logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); test.done(); }, + 'enabled and custom retention'(test: Test) { const stack = getTestStack(); new Trail(stack, 'MyAmazingCloudTrail', { @@ -207,6 +210,46 @@ export = { test.deepEqual(trail.DependsOn, [logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); test.done(); }, + + 'enabled and with custom log group'(test: Test) { + const stack = getTestStack(); + const cloudWatchLogGroup = new LogGroup(stack, 'MyLogGroup', { + retention: RetentionDays.FIVE_DAYS, + }); + new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: RetentionDays.ONE_WEEK, + cloudWatchLogGroup, + }); + + expect(stack).to(haveResource('AWS::Logs::LogGroup', { + RetentionInDays: 5, + })); + + expect(stack).to(haveResource('AWS::CloudTrail::Trail', { + CloudWatchLogsLogGroupArn: stack.resolve(cloudWatchLogGroup.logGroupArn), + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Resource: stack.resolve(cloudWatchLogGroup.logGroupArn), + }], + }, + })); + test.done(); + }, + + 'disabled'(test: Test) { + const stack = getTestStack(); + const t = new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: false, + cloudWatchLogsRetention: RetentionDays.ONE_WEEK, + }); + test.equals(t.logGroup, undefined); + expect(stack).notTo(haveResource('AWS::Logs::LogGroup')); + test.done(); + }, }, 'with event selectors': { 'with default props'(test: Test) { From fbd40cb62d42e147074c7ffc9cdc2324af351a15 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 20 May 2020 10:13:55 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Shiv Lakshminarayan --- packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 57210356406c0..eb33cae7fa37b 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -66,7 +66,7 @@ export interface TrailProps { * How long to retain logs in CloudWatchLogs. * Ignored if sendToCloudWatchLogs is false or if cloudWatchLogGroup is set. * - * @default logs.RetentionDays.OneYear + * @default logs.RetentionDays.ONE_YEAR */ readonly cloudWatchLogsRetention?: logs.RetentionDays; @@ -165,7 +165,7 @@ export class Trail extends Resource { * The CloudWatch log group to which CloudTrail events are sent. * `undefined` if `sendToCloudWatchLogs` property is false. */ - public readonly logGroup: logs.ILogGroup | undefined; + public readonly logGroup?: logs.ILogGroup; private s3bucket: s3.IBucket; private eventSelectors: EventSelector[] = []; From 828682a01eb5e1a40b1e53ba6258fe7cbf0345a4 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 20 May 2020 11:08:12 +0100 Subject: [PATCH 3/3] readme --- packages/@aws-cdk/aws-cloudtrail/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index ff57b4961a740..c0c1577e4ca6c 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -51,7 +51,9 @@ const trail = new cloudtrail.Trail(this, 'CloudTrail', { ``` This creates the same setup as above - but also logs events to a created CloudWatch Log stream. -By default, the created log group has a retention period of 365 Days, but this is also configurable. +By default, the created log group has a retention period of 365 Days, but this is also configurable +via the `cloudWatchLogsRetention` property. If you would like to specify the log group explicitly, +use the `cloudwatchLogGroup` property. For using CloudTrail event selector to log specific S3 events, you can use the `CloudTrailProps` configuration object.