Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(cloudtrail): better typed event selector apis #8097

Merged
merged 3 commits into from
May 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If objectPrefix is empty is it ok for the / to be appended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so, yes.

From the documentation -

"To log data events for all objects in all S3 buckets in your AWS account, specify the prefix as arn:aws:s3:::.

To log data events for all objects in an S3 bucket, specify the bucket and an empty object prefix such as arn:aws:s3:::bucket-1/. The trail logs data events for all objects in this S3 bucket.

To log data events for specific objects, specify the S3 bucket and object prefix such as arn:aws:s3:::bucket-1/example-images. The trail logs data events for objects in this S3 bucket that match the prefix."

}

/**
* 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