Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/late-papers-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aligent/cdk-aspects": minor
---

Add default aspects for S3 and DynamoDB resources to enforce secure configuration defaults
2 changes: 2 additions & 0 deletions packages/cdk-aspects/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
150 changes: 150 additions & 0 deletions packages/cdk-aspects/lib/defaults/dynamodb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { RemovalPolicy, type IAspect } from "aws-cdk-lib";
import {
BillingMode,
CfnTable,
Table,
type TableProps,
} from "aws-cdk-lib/aws-dynamodb";
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 {
billingMode: BillingMode.PROVISIONED,
readCapacity: 1,
writeCapacity: 1,
removalPolicy: RemovalPolicy.DESTROY,
};
case "MEDIUM":
return {
billingMode: BillingMode.PAY_PER_REQUEST,
maxReadRequestUnits: 100,
maxWriteRequestUnits: 100,
removalPolicy: RemovalPolicy.DESTROY,
};
default:
return {
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 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 {
billingMode,
readCapacity,
writeCapacity,
maxReadRequestUnits,
maxWriteRequestUnits,
removalPolicy,
} = this.defaultProps;

if (removalPolicy) {
node.applyRemovalPolicy(removalPolicy);
}

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,
};
}
}
}
}
91 changes: 91 additions & 0 deletions packages/cdk-aspects/lib/defaults/s3-bucket.ts
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
}
}