From 59fb321edbee024b99e8a3dd387991c21f87cd3b Mon Sep 17 00:00:00 2001 From: Kai Nguyen Date: Mon, 16 Mar 2026 21:22:31 +1100 Subject: [PATCH 1/3] MI-312: Add default aspect for S3 and DynamoDB --- packages/cdk-aspects/index.ts | 2 + packages/cdk-aspects/lib/defaults/dynamodb.ts | 108 ++++++++++++++++++ .../cdk-aspects/lib/defaults/s3-bucket.ts | 91 +++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 packages/cdk-aspects/lib/defaults/dynamodb.ts create mode 100644 packages/cdk-aspects/lib/defaults/s3-bucket.ts diff --git a/packages/cdk-aspects/index.ts b/packages/cdk-aspects/index.ts index 1acda703..50639441 100644 --- a/packages/cdk-aspects/index.ts +++ b/packages/cdk-aspects/index.ts @@ -1,5 +1,7 @@ +export * from "./lib/defaults/dynamodb"; export * from "./lib/defaults/log-group"; export * from "./lib/defaults/nodejs-function"; +export * from "./lib/defaults/s3-bucket"; export * from "./lib/defaults/step-functions"; export * from "./lib/lambda-sfn-versioning"; export * from "./lib/microservice-checks"; diff --git a/packages/cdk-aspects/lib/defaults/dynamodb.ts b/packages/cdk-aspects/lib/defaults/dynamodb.ts new file mode 100644 index 00000000..7d9c05a8 --- /dev/null +++ b/packages/cdk-aspects/lib/defaults/dynamodb.ts @@ -0,0 +1,108 @@ +import { RemovalPolicy, type IAspect } from "aws-cdk-lib"; +import { CfnTable, Table, type TableProps } from "aws-cdk-lib/aws-dynamodb"; +import { RetentionDays } from "aws-cdk-lib/aws-logs"; +import { IConstruct } from "constructs"; + +interface Config { + duration: "SHORT" | "MEDIUM" | "LONG"; +} + +/** + * Aspect that automatically applies configuration-aware defaults to DynamoDB Tables + * + * Visits all constructs in the scope and automatically applies configuration-specific + * removal policies and point-in-time recovery settings to DynamoDB tables. + * Different configurations balance between cost optimization and data retention needs. + * + * @example + * ```typescript + * // Apply configuration-specific defaults to all tables + * Aspects.of(app).add(new DynamoDbDefaultsAspect({ duration: 'SHORT' })); + * + * // Tables automatically inherit configuration defaults + * new Table(stack, 'MyTable', { + * // point-in-time recovery and removal policy applied automatically + * }); + * ``` + * + * @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.Table.html + */ +export class DynamoDbDefaultsAspect implements IAspect { + private readonly defaultProps: TableProps; + + /** + * Creates a new DynamoDbDefaultsAspect + * + * @param config - Configuration identifier used to select appropriate defaults. + */ + constructor(config: Config) { + this.defaultProps = this.retentionProperties(config.duration); + } + + /** + * Get duration-specific DynamoDB table properties + * + * @param duration - The duration to get the table properties for + * @returns The table properties for the duration + */ + private retentionProperties( + duration: "SHORT" | "MEDIUM" | "LONG" + ): TableProps { + switch (duration) { + case "SHORT": + return { + pointInTimeRecoverySpecification: { + pointInTimeRecoveryEnabled: false, + }, + removalPolicy: RemovalPolicy.DESTROY, + }; + case "MEDIUM": + return { + pointInTimeRecoverySpecification: { + pointInTimeRecoveryEnabled: true, + recoveryPeriodInDays: RetentionDays.ONE_MONTH, + }, + removalPolicy: RemovalPolicy.DESTROY, + }; + default: + return { + pointInTimeRecoverySpecification: { + pointInTimeRecoveryEnabled: true, + recoveryPeriodInDays: RetentionDays.THREE_MONTHS, + }, + removalPolicy: RemovalPolicy.RETAIN, + }; + } + } + + /** + * Visits a construct and applies configuration-appropriate defaults + * + * Applies configuration-specific point-in-time recovery and removal policies + * to tables that don't already have these properties explicitly set. + * + * @param node - The construct to potentially modify + */ + visit(node: IConstruct): void { + if (node instanceof Table) { + const { pointInTimeRecoverySpecification, removalPolicy } = + this.defaultProps; + + if (removalPolicy) { + node.applyRemovalPolicy(removalPolicy); + } + + if (pointInTimeRecoverySpecification !== undefined) { + const cfnTable = node.node.defaultChild as CfnTable; + if ( + cfnTable && + cfnTable.pointInTimeRecoverySpecification === undefined + ) { + cfnTable.pointInTimeRecoverySpecification = { + ...pointInTimeRecoverySpecification, + }; + } + } + } + } +} diff --git a/packages/cdk-aspects/lib/defaults/s3-bucket.ts b/packages/cdk-aspects/lib/defaults/s3-bucket.ts new file mode 100644 index 00000000..d5536217 --- /dev/null +++ b/packages/cdk-aspects/lib/defaults/s3-bucket.ts @@ -0,0 +1,91 @@ +import { Duration, RemovalPolicy, type IAspect } from "aws-cdk-lib"; +import { Bucket, CfnBucket, type BucketProps } from "aws-cdk-lib/aws-s3"; +import { IConstruct } from "constructs"; + +interface Config { + duration: "SHORT" | "MEDIUM" | "LONG"; +} + +/** + * Aspect that automatically applies configuration-aware defaults to S3 Buckets + * + * Visits all constructs in the scope and automatically applies configuration-specific + * lifecycle and removal policies to S3 buckets. Different configurations balance + * between cost optimization and data retention needs. + * + * @example + * ```typescript + * // Apply configuration-specific defaults to all buckets + * Aspects.of(app).add(new S3DefaultsAspect({ autoDelete: true, duration: 'SHORT' })); + * + * // Buckets automatically inherit configuration defaults + * new Bucket(stack, 'MyBucket', { + * // lifecycle and removal policy applied automatically + * }); + * ``` + * + * @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html + */ +export class S3DefaultsAspect implements IAspect { + private readonly defaultProps: BucketProps; + + /** + * Creates a new S3DefaultsAspect + * + * @param config - Configuration identifier used to select appropriate defaults. + */ + constructor(config: Config) { + const props = this.retentionProperties(config.duration); + this.defaultProps = { ...props }; + } + + /** + * Get duration-specific object expiration + * + * @param duration - The duration to get the expiration for + * @returns The expiration Duration, or undefined for LONG retention + */ + private retentionProperties(duration: "SHORT" | "MEDIUM" | "LONG") { + switch (duration) { + case "SHORT": + return { + lifecycleRules: [{ expiration: Duration.days(30) }], + removalPolicy: RemovalPolicy.DESTROY, + }; + case "MEDIUM": + return { + lifecycleRules: [{ expiration: Duration.days(90) }], + removalPolicy: RemovalPolicy.DESTROY, + }; + default: + return { + lifecycleRules: [], + removalPolicy: RemovalPolicy.RETAIN, + }; + } + } + + /** + * Visits a construct and applies configuration-appropriate defaults + * + * Applies a removal policy and lifecycle rules to buckets that don't + * already have a lifecycle configuration explicitly set. + * + * @param node - The construct to potentially modify + */ + visit(node: IConstruct): void { + if (node instanceof Bucket) { + const { lifecycleRules, removalPolicy } = this.defaultProps; + if (removalPolicy) { + node.applyRemovalPolicy(removalPolicy); + } + + if (lifecycleRules?.length) { + const cfnBucket = node.node.defaultChild as CfnBucket; + if (cfnBucket && cfnBucket.lifecycleConfiguration === undefined) { + lifecycleRules.forEach(rule => node.addLifecycleRule(rule)); + } + } + } + } +} From 91347d62e048c23ae85f657e97c1dd357c1b2a70 Mon Sep 17 00:00:00 2001 From: Kai Nguyen Date: Mon, 16 Mar 2026 21:32:04 +1100 Subject: [PATCH 2/3] MI-312: Added changeset config --- .changeset/late-papers-float.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/late-papers-float.md diff --git a/.changeset/late-papers-float.md b/.changeset/late-papers-float.md new file mode 100644 index 00000000..029d687f --- /dev/null +++ b/.changeset/late-papers-float.md @@ -0,0 +1,5 @@ +--- +"@aligent/cdk-aspects": minor +--- + +Add default aspects for S3 and DynamoDB resources to enforce secure configuration defaults From 288a59cbfdb3800d2d3032abe6d695caf852496c Mon Sep 17 00:00:00 2001 From: Kai Nguyen Date: Thu, 19 Mar 2026 12:16:06 +1100 Subject: [PATCH 3/3] MI-312: Update DynamoDB default configuration --- packages/cdk-aspects/lib/defaults/dynamodb.ts | 96 +++++++++++++------ 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/packages/cdk-aspects/lib/defaults/dynamodb.ts b/packages/cdk-aspects/lib/defaults/dynamodb.ts index 7d9c05a8..af280920 100644 --- a/packages/cdk-aspects/lib/defaults/dynamodb.ts +++ b/packages/cdk-aspects/lib/defaults/dynamodb.ts @@ -1,6 +1,10 @@ import { RemovalPolicy, type IAspect } from "aws-cdk-lib"; -import { CfnTable, Table, type TableProps } from "aws-cdk-lib/aws-dynamodb"; -import { RetentionDays } from "aws-cdk-lib/aws-logs"; +import { + BillingMode, + CfnTable, + Table, + type TableProps, +} from "aws-cdk-lib/aws-dynamodb"; import { IConstruct } from "constructs"; interface Config { @@ -51,57 +55,95 @@ export class DynamoDbDefaultsAspect implements IAspect { switch (duration) { case "SHORT": return { - pointInTimeRecoverySpecification: { - pointInTimeRecoveryEnabled: false, - }, + billingMode: BillingMode.PROVISIONED, + readCapacity: 1, + writeCapacity: 1, removalPolicy: RemovalPolicy.DESTROY, }; case "MEDIUM": return { - pointInTimeRecoverySpecification: { - pointInTimeRecoveryEnabled: true, - recoveryPeriodInDays: RetentionDays.ONE_MONTH, - }, + billingMode: BillingMode.PAY_PER_REQUEST, + maxReadRequestUnits: 100, + maxWriteRequestUnits: 100, removalPolicy: RemovalPolicy.DESTROY, }; default: return { - pointInTimeRecoverySpecification: { - pointInTimeRecoveryEnabled: true, - recoveryPeriodInDays: RetentionDays.THREE_MONTHS, - }, + billingMode: BillingMode.PAY_PER_REQUEST, removalPolicy: RemovalPolicy.RETAIN, }; } } + private isProvisionedThroughputConfigured() { + const { billingMode, readCapacity, writeCapacity } = this.defaultProps; + + return ( + billingMode === BillingMode.PROVISIONED && + readCapacity !== undefined && + writeCapacity !== undefined + ); + } + + private isOnDemandThroughputConfigured() { + const { billingMode, maxReadRequestUnits, maxWriteRequestUnits } = + this.defaultProps; + + return ( + billingMode === BillingMode.PAY_PER_REQUEST && + maxReadRequestUnits !== undefined && + maxWriteRequestUnits !== undefined + ); + } + /** * Visits a construct and applies configuration-appropriate defaults * - * Applies configuration-specific point-in-time recovery and removal policies - * to tables that don't already have these properties explicitly set. + * Applies configuration-specific billing mode, throughput, point-in-time recovery, + * and removal policies to tables that don't already have these properties explicitly set. * * @param node - The construct to potentially modify */ visit(node: IConstruct): void { if (node instanceof Table) { - const { pointInTimeRecoverySpecification, removalPolicy } = - this.defaultProps; + const { + billingMode, + readCapacity, + writeCapacity, + maxReadRequestUnits, + maxWriteRequestUnits, + removalPolicy, + } = this.defaultProps; if (removalPolicy) { node.applyRemovalPolicy(removalPolicy); } - if (pointInTimeRecoverySpecification !== undefined) { - const cfnTable = node.node.defaultChild as CfnTable; - if ( - cfnTable && - cfnTable.pointInTimeRecoverySpecification === undefined - ) { - cfnTable.pointInTimeRecoverySpecification = { - ...pointInTimeRecoverySpecification, - }; - } + const cfnTable = node.node.defaultChild as CfnTable; + if (!cfnTable) return; + + if (cfnTable.billingMode === undefined) { + cfnTable.billingMode = billingMode; + } + + if ( + cfnTable.provisionedThroughput === undefined && + this.isProvisionedThroughputConfigured() + ) { + cfnTable.provisionedThroughput = { + readCapacityUnits: readCapacity!, + writeCapacityUnits: writeCapacity!, + }; + } + + if ( + cfnTable.onDemandThroughput === undefined && + this.isOnDemandThroughputConfigured() + ) { + cfnTable.onDemandThroughput = { + maxReadRequestUnits, + maxWriteRequestUnits, + }; } } }