From 2289dcb0c9c3a9aaaf184dfa31427e406fb895b1 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:17:39 -0400 Subject: [PATCH] Add Bedrock invocation logging destinations (#12) Audit finding H-20: no audit trail of model I/O for seahaven-alex, which returns payments, invoices, WO/PO, and HR/SA8000 data. S3 bucket (Glacier at 90d, expire 365d) + CloudWatch log group (90d) + delivery role assumable only by bedrock.amazonaws.com scoped by SourceAccount/SourceArn. The account-level logging configuration has no CloudFormation resource type, so it is applied via CLI post-deploy (documented in the construct header) - same pattern as the Config recorder (INFRA-17). Cross-reviewed: no BLOCKs. Verified live: converse invocation logged to /aws/bedrock/model-invocations. --- lib/account-baseline-stack.ts | 7 +++ lib/bedrock-logging.ts | 108 ++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 lib/bedrock-logging.ts diff --git a/lib/account-baseline-stack.ts b/lib/account-baseline-stack.ts index 0333d9e..3cadd24 100644 --- a/lib/account-baseline-stack.ts +++ b/lib/account-baseline-stack.ts @@ -7,6 +7,7 @@ import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail"; import { Construct } from "constructs"; import { DetectiveControls } from "./detective-controls"; import { GovernanceToggles } from "./governance-toggles"; +import { BedrockLogging } from "./bedrock-logging"; import { CisMonitoring } from "./cis-monitoring"; import { FlowLogs } from "./flow-logs"; import { SesMonitoring } from "./ses-monitoring"; @@ -158,6 +159,12 @@ export class AccountBaselineStack extends cdk.Stack { // Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks. new AppWebAcl(this, "AppWebAcl"); + // ── Day 5 AI governance ── + // Bedrock model invocation logging destinations + delivery role (H-20). + // The account-level logging configuration itself has no CFN resource type; + // applied via CLI post-deploy (see lib/bedrock-logging.ts header). + new BedrockLogging(this, "BedrockLogging"); + cdk.Tags.of(this).add("Project", "account-baseline"); cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); cdk.Tags.of(this).add("Environment", "prod"); diff --git a/lib/bedrock-logging.ts b/lib/bedrock-logging.ts new file mode 100644 index 0000000..a37a429 --- /dev/null +++ b/lib/bedrock-logging.ts @@ -0,0 +1,108 @@ +import * as cdk from "aws-cdk-lib"; +import * as s3 from "aws-cdk-lib/aws-s3"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as logs from "aws-cdk-lib/aws-logs"; +import { Construct } from "constructs"; + +/** + * Destinations + delivery role for Bedrock model invocation logging (audit + * H-20). `seahaven-alex` is employee-facing and returns payments, invoices, + * WO/PO, and HR/SA8000 data — model I/O needs an audit trail. + * + * CloudFormation has no resource type for the logging configuration itself + * (account-level `PutModelInvocationLoggingConfiguration`), so — like the + * Config recorder (INFRA-17) — the toggle is applied via CLI after deploy: + * + * aws bedrock put-model-invocation-logging-configuration --logging-config '{ + * "cloudWatchConfig": { + * "logGroupName": "", + * "roleArn": "", + * "largeDataDeliveryS3Config": {"bucketName": "", "keyPrefix": "large-payloads"} + * }, + * "s3Config": {"bucketName": "", "keyPrefix": "invocation-logs"}, + * "textDataDeliveryEnabled": true, + * "imageDataDeliveryEnabled": true, + * "embeddingDataDeliveryEnabled": false + * }' + */ +export class BedrockLogging extends Construct { + public readonly bucket: s3.Bucket; + public readonly logGroup: logs.LogGroup; + public readonly deliveryRole: iam.Role; + + constructor(scope: Construct, id: string) { + super(scope, id); + + const stack = cdk.Stack.of(this); + + this.bucket = new s3.Bucket(this, "InvocationLogsBucket", { + bucketName: `seahaven-bedrock-invocation-logs-${stack.account}`, + encryption: s3.BucketEncryption.S3_MANAGED, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + enforceSSL: true, + versioned: false, + lifecycleRules: [ + { + id: "transition-and-expire", + transitions: [ + { + storageClass: s3.StorageClass.GLACIER, + transitionAfter: cdk.Duration.days(90), + }, + ], + expiration: cdk.Duration.days(365), + abortIncompleteMultipartUploadAfter: cdk.Duration.days(7), + }, + ], + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // Bedrock writes invocation logs to S3 directly via bucket policy — no role. + this.bucket.addToResourcePolicy( + new iam.PolicyStatement({ + sid: "AmazonBedrockLogsWrite", + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal("bedrock.amazonaws.com")], + actions: ["s3:PutObject"], + resources: [this.bucket.arnForObjects("*")], + conditions: { + StringEquals: { "aws:SourceAccount": stack.account }, + ArnLike: { + "aws:SourceArn": `arn:aws:bedrock:${stack.region}:${stack.account}:*`, + }, + }, + }), + ); + + this.logGroup = new logs.LogGroup(this, "InvocationLogGroup", { + logGroupName: "/aws/bedrock/model-invocations", + retention: logs.RetentionDays.THREE_MONTHS, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // CloudWatch delivery requires a role Bedrock can assume, scoped to this + // account/source and to the one log group. + this.deliveryRole = new iam.Role(this, "DeliveryRole", { + roleName: "seahaven-bedrock-invocation-logging", + assumedBy: new iam.ServicePrincipal("bedrock.amazonaws.com", { + conditions: { + StringEquals: { "aws:SourceAccount": stack.account }, + ArnLike: { + "aws:SourceArn": `arn:aws:bedrock:${stack.region}:${stack.account}:*`, + }, + }, + }), + }); + + this.deliveryRole.addToPolicy( + new iam.PolicyStatement({ + actions: ["logs:CreateLogStream", "logs:PutLogEvents"], + resources: [this.logGroup.logGroupArn, `${this.logGroup.logGroupArn}:log-stream:*`], + }), + ); + + new cdk.CfnOutput(this, "BedrockLogBucketName", { value: this.bucket.bucketName }); + new cdk.CfnOutput(this, "BedrockLogGroupName", { value: this.logGroup.logGroupName }); + new cdk.CfnOutput(this, "BedrockLoggingRoleArn", { value: this.deliveryRole.roleArn }); + } +}