diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index ff57b4961a740..ad1b4ecae13fb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -64,13 +64,12 @@ const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); // Adds an event selector to the bucket magic-bucket. // By default, this includes management events and all operations (Read + Write) -trail.addS3EventSelector(["arn:aws:s3:::magic-bucket/"]); +trail.logAllS3DataEvents(); -// Adds an event selector to the bucket foo, with a specific configuration -trail.addS3EventSelector(["arn:aws:s3:::foo/"], { - includeManagementEvents: false, - readWriteType: ReadWriteType.ALL, -}); +// Adds an event selector to the bucket foo +trail.addS3EventSelector([{ + bucket: fooBucket // 'fooBucket' is of type s3.IBucket +}]); ``` For using CloudTrail event selector to log events about Lambda @@ -88,7 +87,7 @@ const lambdaFunction = new lambda.Function(stack, 'AnAmazingFunction', { }); // Add an event selector to log data events for all functions in the account. -trail.addLambdaEventSelector(["arn:aws:lambda"]); +trail.logAllLambdaDataEvents(); // Add an event selector to log data events for the provided Lambda functions. trail.addLambdaEventSelector([lambdaFunction.functionArn]); diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 365b78b3f4087..b42c49983befe 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -1,8 +1,10 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; import { Construct, Resource, Stack } from '@aws-cdk/core'; import { CfnTrail } from './cloudtrail.generated'; @@ -75,11 +77,11 @@ export interface TrailProps { */ readonly kmsKey?: kms.IKey; - /** The name of an Amazon SNS topic that is notified when new log files are published. + /** SNS topic that is notified when new log files are published. * * @default - No notifications. */ - readonly snsTopic?: string; // TODO: fix to use L2 SNS + readonly snsTopic?: sns.ITopic; /** * The name of the trail. We recoomend customers do not set an explicit name. @@ -98,7 +100,7 @@ export interface TrailProps { * * @default - if not supplied a bucket will be created with all the correct permisions */ - readonly bucket?: s3.IBucket + readonly bucket?: s3.IBucket; } /** @@ -219,7 +221,7 @@ export class Trail extends Resource { s3KeyPrefix: props.s3KeyPrefix, cloudWatchLogsLogGroupArn: logGroup && logGroup.attrArn, cloudWatchLogsRoleArn: logsRole && logsRole.roleArn, - snsTopicName: props.snsTopic, + snsTopicName: props.snsTopic?.topicName, eventSelectors: this.eventSelectors, }); @@ -283,13 +285,24 @@ export class Trail extends Resource { * Data events: These events provide insight into the resource operations performed on or within a resource. * These are also known as data plane operations. * - * @param dataResourceValues the list of data resource ARNs to include in logging (maximum 250 entries). + * @param handlers the list of lambda function handlers whose data events should be logged (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addLambdaEventSelector(dataResourceValues: string[], options: AddEventSelectorOptions = {}) { + public addLambdaEventSelector(handlers: lambda.IFunction[], options: AddEventSelectorOptions = {}) { + if (handlers.length === 0) { return; } + const dataResourceValues = handlers.map((h) => h.functionArn); return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, dataResourceValues, options); } + /** + * Log all Lamda data events for all lambda functions the account. + * @see https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html + * @default false + */ + public logAllLambdaDataEvents(options: AddEventSelectorOptions = {}) { + return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, [ 'arn:aws:lambda' ], options); + } + /** * When an event occurs in your account, CloudTrail evaluates whether the event matches the settings for your trails. * Only events that match your trail settings are delivered to your Amazon S3 bucket and Amazon CloudWatch Logs log group. @@ -299,13 +312,24 @@ export class Trail extends Resource { * Data events: These events provide insight into the resource operations performed on or within a resource. * These are also known as data plane operations. * - * @param dataResourceValues the list of data resource ARNs to include in logging (maximum 250 entries). + * @param s3Selector the list of S3 bucket with optional prefix to include in logging (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addS3EventSelector(dataResourceValues: string[], options: AddEventSelectorOptions = {}) { + public addS3EventSelector(s3Selector: S3EventSelector[], options: AddEventSelectorOptions = {}) { + if (s3Selector.length === 0) { return; } + const dataResourceValues = s3Selector.map((sel) => `${sel.bucket.bucketArn}/${sel.objectPrefix ?? ''}`); return this.addEventSelector(DataResourceType.S3_OBJECT, dataResourceValues, options); } + /** + * Log all S3 data events for all objects for all buckets in the account. + * @see https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html + * @default false + */ + public logAllS3DataEvents(options: AddEventSelectorOptions = {}) { + return this.addEventSelector(DataResourceType.S3_OBJECT, [ 'arn:aws:s3:::' ], options); + } + /** * Create an event rule for when an event is recorded by any Trail in the account. * @@ -343,6 +367,20 @@ export interface AddEventSelectorOptions { readonly includeManagementEvents?: boolean; } +/** + * Selecting an S3 bucket and an optional prefix to be logged for data events. + */ +export interface S3EventSelector { + /** S3 bucket */ + readonly bucket: s3.IBucket; + + /** + * Data events for objects whose key matches this prefix will be logged. + * @default - all objects + */ + readonly objectPrefix?: string; +} + /** * Resource type for a data event */ diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 94cbbd4c7990c..9df625d069add 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -79,6 +79,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -90,6 +91,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index 3a16cfbbd06a5..d69413336efe4 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -1,4 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert'; +import { ABSENT, SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -208,54 +208,98 @@ describe('cloudtrail', () => { }); describe('with event selectors', () => { - test('with default props', () => { + test('all s3 events', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addS3EventSelector(['arn:aws:s3:::']); + cloudTrail.logAllS3DataEvents(); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [ 'arn:aws:s3:::' ], + }], + IncludeManagementEvents: ABSENT, + ReadWriteType: ABSENT, + }, + ], + }); + }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toBeUndefined(); - expect(selector.IncludeManagementEvents).toBeUndefined(); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::S3::Object'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual('arn:aws:s3:::'); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + test('specific s3 buckets and objects', () => { + const stack = getTestStack(); + const bucket = new s3.Bucket(stack, 'testBucket', { bucketName: 'test-bucket' }); + + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.addS3EventSelector([{ bucket }]); + cloudTrail.addS3EventSelector([{ + bucket, + objectPrefix: 'prefix-1/prefix-2', + }]); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [{ + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': [ 'testBucketDF4D7D1A', 'Arn' ]}, + '/', + ], + ], + }], + }], + }, + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [{ + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': [ 'testBucketDF4D7D1A', 'Arn' ]}, + '/prefix-1/prefix-2', + ], + ], + }], + }], + }, + ], + }); + }); + + test('no s3 event selector when list is empty', () => { + const stack = getTestStack(); + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.addS3EventSelector([]); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [], + }); }); test('with hand-specified props', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addS3EventSelector(['arn:aws:s3:::'], { includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); + cloudTrail.logAllS3DataEvents({ includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toEqual('ReadOnly'); - expect(selector.IncludeManagementEvents).toEqual(false); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::S3::Object'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual('arn:aws:s3:::'); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [ 'arn:aws:s3:::' ], + }], + IncludeManagementEvents: false, + ReadWriteType: 'ReadOnly', + }, + ], + }); }); test('with management event', () => { @@ -263,12 +307,14 @@ describe('cloudtrail', () => { new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WRITE_ONLY }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toEqual('WriteOnly'); - expect(selector.IncludeManagementEvents).toEqual(true); - expect(selector.DataResources).toEqual(undefined); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + IncludeManagementEvents: true, + ReadWriteType: 'WriteOnly', + }, + ], + }); }); test('for Lambda function data event', () => { @@ -280,46 +326,38 @@ describe('cloudtrail', () => { }); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addLambdaEventSelector([lambdaFunction.functionArn]); - - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::Lambda::Function'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + cloudTrail.addLambdaEventSelector([lambdaFunction]); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toBeUndefined(); - expect(selector.IncludeManagementEvents).toBeUndefined(); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::Lambda::Function'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual({ 'Fn::GetAtt': [ 'LambdaFunctionBF21E41F', 'Arn' ] }); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::Lambda::Function', + Values: [{ + 'Fn::GetAtt': [ 'LambdaFunctionBF21E41F', 'Arn' ], + }], + }], + }, + ], + }); }); test('for all Lambda function data events', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addLambdaEventSelector(['arn:aws:lambda']); + cloudTrail.logAllLambdaDataEvents(); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toBeUndefined(); - expect(selector.IncludeManagementEvents).toBeUndefined(); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::Lambda::Function'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual('arn:aws:lambda'); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::Lambda::Function', + Values: [ 'arn:aws:lambda' ], + }], + }, + ], + }); }); }); }); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts index fa57b1f2caf05..ad8614b3c1564 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts @@ -37,7 +37,7 @@ Trailbucket.addToResourcePolicy(new iam.PolicyStatement({ const trail = new cloudtrail.Trail(stack, 'Trail', {bucket: Trailbucket}); -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -trail.addS3EventSelector([bucket.arnForObjects('')]); +trail.addLambdaEventSelector([lambdaFunction]); +trail.addS3EventSelector([{bucket}]); app.synth(); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts index bee7fc432d6ed..5f53f4efeb0fa 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts @@ -14,7 +14,7 @@ const lambdaFunction = new lambda.Function(stack, 'LambdaFunction', { }); const trail = new cloudtrail.Trail(stack, 'Trail'); -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -trail.addS3EventSelector([bucket.arnForObjects('')]); +trail.addLambdaEventSelector([lambdaFunction]); +trail.addS3EventSelector([{bucket}]); app.synth();