diff --git a/eventbridge-cloudtrail-dataplane-cdk/README.md b/eventbridge-cloudtrail-dataplane-cdk/README.md new file mode 100644 index 000000000..86203c0f0 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/README.md @@ -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. + +## 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 + +## Deployment Instructions + +1. Clone and navigate to the pattern: + ``` + cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk + npm install + ``` +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 diff --git a/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts b/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts new file mode 100644 index 000000000..bde4d97d6 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts @@ -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'); diff --git a/eventbridge-cloudtrail-dataplane-cdk/cdk.json b/eventbridge-cloudtrail-dataplane-cdk/cdk.json new file mode 100644 index 000000000..a6700a2ff --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts" +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json b/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json new file mode 100644 index 000000000..d7850ad3a --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json @@ -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" + } + ] +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts new file mode 100644 index 000000000..c17d1ef98 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts @@ -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, + }); + + // CloudTrail trail with data events for EventBridge + const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', { + bucket: trailBucket, + trailName: 'eventbridge-dataplane-trail', + isMultiRegionTrail: false, + }); + + // Enable EventBridge data plane events logging + trail.addEventSelector(cloudtrail.DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']); + + // Lambda function to process CloudTrail events + 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, + }); + + // 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)); + + new cdk.CfnOutput(this, 'ProcessorFunctionName', { value: processor.functionName }); + new cdk.CfnOutput(this, 'TrailBucketName', { value: trailBucket.bucketName }); + new cdk.CfnOutput(this, 'RuleName', { value: rule.ruleName }); + } +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/package.json b/eventbridge-cloudtrail-dataplane-cdk/package.json new file mode 100644 index 000000000..8ea2424dd --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/package.json @@ -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" + } +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/src/index.js b/eventbridge-cloudtrail-dataplane-cdk/src/index.js new file mode 100644 index 000000000..98327e629 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/src/index.js @@ -0,0 +1,15 @@ +exports.handler = async (event) => { + const detail = event.detail || {}; + console.log(JSON.stringify({ + 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 }; +}; diff --git a/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json b/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json new file mode 100644 index 000000000..a4f77b1b2 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json @@ -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"] +} diff --git a/lambda-verified-permissions-cdk/README.md b/lambda-verified-permissions-cdk/README.md new file mode 100644 index 000000000..03a099869 --- /dev/null +++ b/lambda-verified-permissions-cdk/README.md @@ -0,0 +1,58 @@ +# Amazon Verified Permissions with AWS Lambda + +This pattern deploys a Lambda function that authorizes requests using Amazon Verified Permissions with Cedar policies for fine-grained access control. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-verified-permissions-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. + +## Requirements + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Node.js 22+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed + +## Architecture + +``` +┌──────────┐ ┌──────────────────┐ ┌─────────────────────────┐ +│ Client │────▶│ AWS Lambda │────▶│ Amazon Verified │ +│ │ │ (Authorizer) │ │ Permissions │ +└──────────┘ └──────────────────┘ │ (Cedar Policy Store) │ + └─────────────────────────┘ +``` + +## How it works + +1. Lambda receives an authorization request with user identity, action, and resource. +2. Lambda calls the Verified Permissions `IsAuthorized` API with the request context. +3. Cedar policies evaluate the request and return ALLOW or DENY. +4. The pattern includes two policies: admins can perform any action, readers can only read. + +## Deployment + +```bash +npm install +cdk deploy +``` + +## Testing + +```bash +python3 -c " +import boto3, json +client = boto3.client('lambda') +# Admin can delete (ALLOW) +r = client.invoke(FunctionName='', Payload=json.dumps({'body': json.dumps({'userId':'alice','role':'admin','action':'Delete','resourceId':'doc-1','classification':'confidential'})})) +print('Admin Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) +# Reader cannot delete (DENY) +r = client.invoke(FunctionName='', Payload=json.dumps({'body': json.dumps({'userId':'bob','role':'reader','action':'Delete','resourceId':'doc-2','classification':'public'})})) +print('Reader Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) +" +``` + +## Cleanup + +```bash +cdk destroy +``` diff --git a/lambda-verified-permissions-cdk/bin/app.ts b/lambda-verified-permissions-cdk/bin/app.ts new file mode 100644 index 000000000..a8eddd0c0 --- /dev/null +++ b/lambda-verified-permissions-cdk/bin/app.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { LambdaVerifiedPermissionsStack } from '../lib/lambda-verified-permissions-stack'; +const app = new cdk.App(); +new LambdaVerifiedPermissionsStack(app, 'LambdaVerifiedPermissionsStack'); diff --git a/lambda-verified-permissions-cdk/cdk.json b/lambda-verified-permissions-cdk/cdk.json new file mode 100644 index 000000000..bebf36838 --- /dev/null +++ b/lambda-verified-permissions-cdk/cdk.json @@ -0,0 +1 @@ +{"app":"npx ts-node --prefer-ts-exts bin/app.ts"} diff --git a/lambda-verified-permissions-cdk/example-pattern.json b/lambda-verified-permissions-cdk/example-pattern.json new file mode 100644 index 000000000..c3682ce2d --- /dev/null +++ b/lambda-verified-permissions-cdk/example-pattern.json @@ -0,0 +1 @@ +{"title":"Amazon Verified Permissions with AWS Lambda","description":"Deploy a Lambda function that authorizes requests using Amazon Verified Permissions Cedar policies.","language":"TypeScript","level":"300","framework":"CDK","introBox":{"headline":"How it works","text":["Lambda receives an authorization request and calls Amazon Verified Permissions IsAuthorized API with Cedar policies to make fine-grained access control decisions."]},"gitHub":{"template":{"repoURL":"https://github.com/aws-samples/serverless-patterns/tree/main/lambda-verified-permissions-cdk","templateURL":"serverless-patterns/lambda-verified-permissions-cdk","projectFolder":"lambda-verified-permissions-cdk"}},"resources":{"bullets":[{"text":"Amazon Verified Permissions","link":"https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html"}]},"deploy":{"text":["cdk deploy"],"commands":["npm install","cdk deploy"]},"testing":{"text":["Invoke the function URL with authorization request"]},"cleanup":{"text":["cdk destroy"],"commands":["cdk destroy"]},"authors":[{"name":"Nithin Chandran R","bio":"Technical Account Manager at AWS","linkedin":"nithin-chandran-r"}],"services":{"from":[{"service":"lambda"}],"to":[{"service":"verifiedpermissions"}]}} diff --git a/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts b/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts new file mode 100644 index 000000000..5f7e0e267 --- /dev/null +++ b/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts @@ -0,0 +1,71 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as verifiedpermissions from 'aws-cdk-lib/aws-verifiedpermissions'; +import { Construct } from 'constructs'; + +export class LambdaVerifiedPermissionsStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create policy store with Cedar schema + const policyStore = new verifiedpermissions.CfnPolicyStore(this, 'PolicyStore', { + validationSettings: { mode: 'STRICT' }, + schema: { + cedarJson: JSON.stringify({ + 'MyApp': { + entityTypes: { + User: { shape: { type: 'Record', attributes: { role: { type: 'String' } } } }, + Document: { shape: { type: 'Record', attributes: { owner: { type: 'String' }, classification: { type: 'String' } } } } + }, + actions: { + Read: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } }, + Write: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } }, + Delete: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } } + } + } + }) + } + }); + + // Create policies + new verifiedpermissions.CfnPolicy(this, 'AdminPolicy', { + policyStoreId: policyStore.attrPolicyStoreId, + definition: { + static: { + statement: 'permit(principal, action, resource) when { principal.role == "admin" };', + description: 'Admins can perform any action' + } + } + }); + + new verifiedpermissions.CfnPolicy(this, 'ReaderPolicy', { + policyStoreId: policyStore.attrPolicyStoreId, + definition: { + static: { + statement: 'permit(principal, action == MyApp::Action::"Read", resource) when { principal.role == "reader" };', + description: 'Readers can only read documents' + } + } + }); + + // Lambda authorizer function + const authFn = new lambda.Function(this, 'AuthorizerFn', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + environment: { POLICY_STORE_ID: policyStore.attrPolicyStoreId }, + timeout: cdk.Duration.seconds(10) + }); + + authFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['verifiedpermissions:IsAuthorized'], + resources: [`arn:aws:verifiedpermissions::${this.account}:policy-store/${policyStore.attrPolicyStoreId}`] + })); + + const fnUrl = authFn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.AWS_IAM }); + + new cdk.CfnOutput(this, 'FunctionUrl', { value: fnUrl.url }); + new cdk.CfnOutput(this, 'PolicyStoreId', { value: policyStore.attrPolicyStoreId }); + } +} diff --git a/lambda-verified-permissions-cdk/package.json b/lambda-verified-permissions-cdk/package.json new file mode 100644 index 000000000..9b89ff6e9 --- /dev/null +++ b/lambda-verified-permissions-cdk/package.json @@ -0,0 +1,14 @@ +{ + "name": "lambda-verified-permissions-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.js" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "typescript": "~5.4.0", + "@types/node": "^20.0.0" + } +} diff --git a/lambda-verified-permissions-cdk/src/index.js b/lambda-verified-permissions-cdk/src/index.js new file mode 100644 index 000000000..b0abc3096 --- /dev/null +++ b/lambda-verified-permissions-cdk/src/index.js @@ -0,0 +1,30 @@ +const { VerifiedPermissionsClient, IsAuthorizedCommand } = require('@aws-sdk/client-verifiedpermissions'); +const client = new VerifiedPermissionsClient(); + +exports.handler = async (event) => { + const body = JSON.parse(event.body || '{}'); + const { userId, role, action, resourceId, classification } = body; + + if (!userId || !role || !action || !resourceId) { + return { statusCode: 400, body: JSON.stringify({ error: 'Missing required fields: userId, role, action, resourceId' }) }; + } + + const params = { + policyStoreId: process.env.POLICY_STORE_ID, + principal: { entityType: 'MyApp::User', entityId: userId }, + action: { actionType: 'MyApp::Action', actionId: action }, + resource: { entityType: 'MyApp::Document', entityId: resourceId }, + entities: { + entityList: [ + { identifier: { entityType: 'MyApp::User', entityId: userId }, attributes: { role: { string: role } } }, + { identifier: { entityType: 'MyApp::Document', entityId: resourceId }, attributes: { owner: { string: userId }, classification: { string: classification || 'public' } } } + ] + } + }; + + const result = await client.send(new IsAuthorizedCommand(params)); + return { + statusCode: 200, + body: JSON.stringify({ decision: result.decision, userId, action, resourceId, role }) + }; +}; diff --git a/lambda-verified-permissions-cdk/tsconfig.json b/lambda-verified-permissions-cdk/tsconfig.json new file mode 100644 index 000000000..5a686fa68 --- /dev/null +++ b/lambda-verified-permissions-cdk/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"ES2020","module":"commonjs","lib":["es2020"],"declaration":true,"strict":true,"noImplicitAny":true,"strictNullChecks":true,"noEmit":false,"resolveJsonModule":true,"esModuleInterop":true,"outDir":"./build","rootDir":"."}}