Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
58 changes: 58 additions & 0 deletions eventbridge-cloudtrail-dataplane-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Amazon EventBridge Data Plane Logging with AWS CloudTrail

This pattern enables CloudTrail data plane logging for Amazon EventBridge and triggers a Lambda function when PutEvents API calls are detected, providing security and operational visibility into event bus activity.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/eventbridge-cloudtrail-dataplane-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommendation: Mention that CloudTrail data events are billed separately and link pricing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a callout box in the README noting CloudTrail data events are billed separately, with a link to the pricing page.


## Requirements

* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed
* [Node.js](https://nodejs.org/en/download/) installed

@parikhudit parikhudit Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Specify Node.js 18+ or appropriate in Requirements (function uses Node.js 20.x)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated to Node.js 20+.


## Deployment Instructions

1. Clone and navigate to the pattern:
```
cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk
npm install
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add cdk bootstrap for first time users

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a cdk bootstrap step in the deployment instructions.

2. Deploy:
```
cdk deploy
```

## How it works

- A CloudTrail trail is created with data event logging enabled
- EventBridge data plane API calls (PutEvents) are now logged to CloudTrail (new May 2026 feature)
- An EventBridge rule captures these CloudTrail events matching `aws.events` source with `PutEvents` event name
- A Lambda function processes the events, logging the caller identity, source IP, event bus, and entry count
- This enables security teams to audit who is putting events to which bus

## Testing

```bash
# Put a test event to the default event bus
aws events put-events --entries '[{"Source":"test.app","DetailType":"TestEvent","Detail":"{\"key\":\"value\"}"}]'

# Check Lambda logs (allow ~5 minutes for CloudTrail delivery)
aws logs tail /aws/lambda/$(aws cloudformation describe-stacks \
--stack-name EventbridgeCloudtrailDataplaneStack \
--query 'Stacks[0].Outputs[?OutputKey==`ProcessorFunctionName`].OutputValue' --output text) \
--follow
```

## Cleanup

```
cdk destroy
```

---

Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
7 changes: 7 additions & 0 deletions eventbridge-cloudtrail-dataplane-cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { EventbridgeCloudtrailDataplaneStack } from '../lib/eventbridge-cloudtrail-dataplane-stack';

const app = new cdk.App();
new EventbridgeCloudtrailDataplaneStack(app, 'EventbridgeCloudtrailDataplaneStack');
3 changes: 3 additions & 0 deletions eventbridge-cloudtrail-dataplane-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts"
}
40 changes: 40 additions & 0 deletions eventbridge-cloudtrail-dataplane-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"title": "Amazon EventBridge Data Plane Logging with AWS CloudTrail",
"description": "Monitor EventBridge PutEvents API calls using CloudTrail data plane logging with Lambda alerting for security and operational visibility.",
"language": "TypeScript",
"level": "300",
"framework": "CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern enables CloudTrail data plane logging for Amazon EventBridge (launched May 2026).",
"CloudTrail captures PutEvents API calls and delivers them as events to EventBridge.",
"An EventBridge rule matches these CloudTrail events and triggers a Lambda function for alerting.",
"This provides visibility into who is putting events, from where, and how many — essential for security auditing."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-cloudtrail-dataplane-cdk",
"templateURL": "serverless-patterns/eventbridge-cloudtrail-dataplane-cdk",
"projectFolder": "eventbridge-cloudtrail-dataplane-cdk",
"templateFile": "lib/eventbridge-cloudtrail-dataplane-stack.ts"
}
},
"resources": {
"bullets": [
{ "text": "EventBridge Data Plane CloudTrail Logging", "link": "https://aws.amazon.com/about-aws/whats-new/2026/05/amazon-eventbridge-data-aws-cloudtrail/" },
{ "text": "CloudTrail Data Events", "link": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html" }
]
},
"deploy": { "text": ["cdk deploy"] },
"testing": { "text": ["See the README for testing instructions."] },
"cleanup": { "text": ["cdk destroy"] },
"authors": [
{
"name": "Nithin Chandran R",
"bio": "Technical Account Manager at AWS, passionate about serverless and AI/ML.",
"linkedin": "nithin-chandran-r"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class EventbridgeCloudtrailDataplaneStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

// S3 bucket for CloudTrail logs
const trailBucket = new s3.Bucket(this, 'TrailBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
enforceSSL: true,
Comment on lines +26 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The bucket holding CloudTrail logs has no serverAccessLogsBucket configured. For an audit-trail bucket, S3 server access logging adds a second-tier audit record of who accessed/modified the audit logs themselves. This is a defense-in-depth recommendation rather than a hard requirement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a dedicated accessLogsBucket with serverAccessLogsBucket pointing to it.

});
Comment on lines +26 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Set explicit encryption and blockPublicAccess on the trail bucket. This bucket holds CloudTrail audit logs, so best to be explicit even though Amazon S3 now defaults to SSE-S3 at the API.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — added encryption: S3_MANAGED and blockPublicAccess: BLOCK_ALL to both buckets.


// CloudTrail trail with data events for EventBridge
const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', {
bucket: trailBucket,
trailName: 'eventbridge-dataplane-trail',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please drop the hardcoded trailName.
Although trailName: 'eventbridge-dataplane-trail' is unique per account/region. A second deployment of the stack (e.g. to test a change in a different stack name) could fail with Trail already exists. For a sample, the simplest fix is to remove the property and let CDK auto-generate a unique name. If a stable name is desired, expose it as a CfnParameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed — letting CloudFormation generate the name now.

isMultiRegionTrail: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

isMultiRegionTrail: false misses cross-region PutEvents. For an audit/security pattern, the trail should capture all regions of the account. Console-created trails are multi-region by default. Suggest isMultiRegionTrail: true. Also you may want to call out in README that EventBridge rules are region-local, multi-region detection requires deploying the rule in each region or fanning in via cross-region buses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair point, flipped to isMultiRegionTrail: true.

});

// Enable EventBridge data plane events logging
trail.addEventSelector(cloudtrail.DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']);

// Lambda function to process CloudTrail events
Comment on lines +37 to +61

@parikhudit parikhudit Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wrong CloudTrail data event resource type; this trail does not capture EventBridge PutEvents. addEventSelector(DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']) adds a basic event selector that logs Lambda Invoke data events for every Lambda function in the account/region. EventBridge PutEvents data events require an advanced event selector with resources.type = AWS::Events::EventBus, per the EventBridge / CloudTrail integration docs. The CDK L2 DataResourceType enum only exposes LAMBDA_FUNCTION and S3_OBJECT, so we need CfnTrail with AdvancedEventSelectors. Kindly check and confirm.

@parikhudit parikhudit Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd also suggest a quick smoke-test command in the README that asserts the Lambda actually logged a record.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After fixing above, you may want to drop or update the "allow ~5 minutes" note in the README test step, as CloudTrail data events would not typically take that long to reach EventBridge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — you're right, LAMBDA_FUNCTION doesn't capture EventBridge PutEvents at all. Switched to AWS::Events::EventBus via advanced event selectors (the CDK L2 addEventSelector doesn't support this resource type yet, so I used the CfnTrail escape hatch).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea — I'll add a quick aws events put-events + aws logs tail assertion to the Testing section so folks can verify end-to-end without guessing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right — data events via advanced selectors hit EventBridge much faster than the old management-event path. I'll update to ~30 seconds and add the smoke-test command so it's verifiable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea -- added a CLI verification command that checks the CloudTrail log group for recent events.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the testing section to reference the CLI check instead of the wait-and-hope approach.

const processor = new lambda.Function(this, 'EventProcessor', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('src'),
timeout: cdk.Duration.seconds(10),
loggingFormat: lambda.LoggingFormat.JSON,
Comment on lines +64 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The processor function has no logRetention (or pre-created logGroup with retention) configured. The auto-created log group /aws/lambda/ will retain logs forever, accruing CloudWatch Logs storage cost. So either set retention or give a callout in README and wherever applicable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added logRetention: ONE_WEEK. Keeps costs down for a demo pattern.

});
Comment on lines +62 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Configure onFailure and retryAttempts on the async Lambda. Even with the EventBridge-side DLQ, function-level destinations give clearer signal for code-level failures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — set retryAttempts: 2 and onFailure: new SqsDestination(dlq) on the Lambda.


// EventBridge rule to capture EventBridge PutEvents API calls from CloudTrail
const rule = new events.Rule(this, 'DataPlaneRule', {
eventPattern: {
source: ['aws.events'],
detailType: ['AWS API Call via CloudTrail'],
detail: {
eventSource: ['events.amazonaws.com'],
eventName: ['PutEvents'],
},
},
});

rule.addTarget(new targets.LambdaFunction(processor));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add a DLQ to the EventBridge target. Without deadLetterQueue on the LambdaFunction target, failed invocations would end up in the void after EventBridge's default retries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a shared SQS DLQ for the EventBridge target. Failed deliveries go there with 14-day retention.


new cdk.CfnOutput(this, 'ProcessorFunctionName', { value: processor.functionName });
new cdk.CfnOutput(this, 'TrailBucketName', { value: trailBucket.bucketName });
new cdk.CfnOutput(this, 'RuleName', { value: rule.ruleName });
}
}
16 changes: 16 additions & 0 deletions eventbridge-cloudtrail-dataplane-cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "eventbridge-cloudtrail-dataplane-cdk",
"version": "1.0.0",
"bin": { "app": "bin/app.ts" },
"scripts": { "build": "tsc", "cdk": "cdk" },
"dependencies": {
"aws-cdk-lib": "^2.180.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
},
"devDependencies": {
"typescript": "~5.4.0",
"ts-node": "^10.9.0",
"@types/node": "^20.0.0"
}
}
15 changes: 15 additions & 0 deletions eventbridge-cloudtrail-dataplane-cdk/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
exports.handler = async (event) => {
const detail = event.detail || {};
console.log(JSON.stringify({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

usingconsole.log(JSON.stringify(...)) here, on Node.js 20 with LoggingFormat.JSON you can use context.logger.info({...}) directly. The return { statusCode: 200 } would be unused for async EventBridge invocations

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Switched to console.info — with loggingFormat: JSON on the Lambda, CloudWatch will pick up the structured fields automatically.

message: 'EventBridge data plane API call detected',
eventName: detail.eventName,
eventSource: detail.eventSource,
sourceIPAddress: detail.sourceIPAddress,
userAgent: detail.userAgent,
userIdentity: detail.userIdentity?.arn,
eventBusName: detail.requestParameters?.entries?.[0]?.eventBusName || 'default',
entryCount: detail.requestParameters?.entries?.length || 0,
eventTime: detail.eventTime,
}));
return { statusCode: 200 };
};
8 changes: 8 additions & 0 deletions eventbridge-cloudtrail-dataplane-cdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES2020", "module": "commonjs", "lib": ["es2020"],
"declaration": true, "strict": true, "outDir": "build",
"rootDir": ".", "skipLibCheck": true, "forceConsistentCasingInFileNames": true
},
"exclude": ["node_modules", "build"]
}