Skip to content

Commit

Permalink
chore(cloudtrail): better typed event selector apis (#8097)
Browse files Browse the repository at this point in the history
## Commit Message
chore(cloudtrail): better typed event selector apis (#8097)

The event selector APIs now take strongly typed `IFunction` and
`IBucket` instead of a string that is expected to contain the ARN.

Additionally, add APIs to log all S3 data events and to log all Lambda
data events.

Change the type of `snsTopic` from `string` to `ITopic`.

BREAKING CHANGE: API signatures of `addS3EventSelectors` and
`addLambdaEventSelectors` have changed. Their parameters are now
strongly typed to accept `IBucket` and `IFunction` respectively.
* **cloudtrail:** `addS3EventSelectors` and `addLambdaEventSelectors`
can no longer be used to configure all S3 data events or all Lambda data
events. Two new APIs `logAllS3DataEvents()` and
`logAllLambdaDataEvents()` have been introduced to achieve this.
* **cloudtrail:** The property `snsTopic` is now of the type `ITopic`.
## End Commit Message

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Niranjan Jayakar authored May 27, 2020
1 parent 99e7330 commit 0028778
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 95 deletions.
13 changes: 6 additions & 7 deletions packages/@aws-cdk/aws-cloudtrail/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,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
Expand All @@ -90,7 +89,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]);
Expand Down
54 changes: 46 additions & 8 deletions packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -82,11 +84,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.
Expand All @@ -105,7 +107,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;
}

/**
Expand Down Expand Up @@ -252,7 +254,7 @@ export class Trail extends Resource {
s3KeyPrefix: props.s3KeyPrefix,
cloudWatchLogsLogGroupArn: this.logGroup?.logGroupArn,
cloudWatchLogsRoleArn: logsRole?.roleArn,
snsTopicName: props.snsTopic,
snsTopicName: props.snsTopic?.topicName,
eventSelectors: this.eventSelectors,
});

Expand Down Expand Up @@ -316,13 +318,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.
Expand All @@ -332,13 +345,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.
*
Expand Down Expand Up @@ -373,6 +397,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
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-cloudtrail/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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"
},
Expand Down
188 changes: 113 additions & 75 deletions packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -246,67 +246,113 @@ 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', () => {
const stack = getTestStack();

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', () => {
Expand All @@ -318,46 +364,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']);

expect(stack).toHaveResource('AWS::CloudTrail::Trail');
expect(stack).not.toHaveResource('AWS::Logs::LogGroup');
expect(stack).not.toHaveResource('AWS::IAM::Role');
cloudTrail.logAllLambdaDataEvents();

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' ],
}],
},
],
});
});
});
});
Expand Down
Loading

0 comments on commit 0028778

Please sign in to comment.