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
1 change: 0 additions & 1 deletion examples/basic-usage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env node
import 'source-map-support/register';
import { App, Stack } from 'aws-cdk-lib';

import { SecureBucket, SecurityLevel } from '../src';
Expand Down
1 change: 0 additions & 1 deletion examples/blueprints-usage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env node
import 'source-map-support/register';
import { App, Stack, PropertyInjectors } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';

Expand Down
20 changes: 20 additions & 0 deletions src/resources/s3-bucket/secure-bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ export class SecureBucket extends Construct {
/** The underlying hardened bucket. */
public readonly bucket: Bucket;

/** The bucket's ARN. Convenience pass-through for `this.bucket.bucketArn`. */
public get bucketArn(): string {
return this.bucket.bucketArn;
}

/** The bucket's name. Convenience pass-through for `this.bucket.bucketName`. */
public get bucketName(): string {
return this.bucket.bucketName;
}

/** The bucket's domain name. Convenience pass-through for `this.bucket.bucketDomainName`. */
public get bucketDomainName(): string {
return this.bucket.bucketDomainName;
}

/** The bucket's regional domain name. Convenience pass-through for `this.bucket.bucketRegionalDomainName`. */
public get bucketRegionalDomainName(): string {
return this.bucket.bucketRegionalDomainName;
}

constructor(scope: Construct, id: string, props: SecureBucketProps = {}) {
super(scope, id);

Expand Down
84 changes: 84 additions & 0 deletions test/resources/s3-bucket/fields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { RemovalPolicy } from 'aws-cdk-lib';

import {
secureMaxVersioned,
secureMaxRemovalPolicy,
} from '../../../src/resources/s3-bucket/fields';

describe('fields: secureMaxVersioned', () => {
it('should enable versioning when the tier value is true', () => {
expect(secureMaxVersioned(true, undefined)).toBe(true);
expect(secureMaxVersioned(true, false)).toBe(true);
expect(secureMaxVersioned(true, true)).toBe(true);
});

it('should never weaken an enabled tier value to false', () => {
expect(secureMaxVersioned(true, false)).toBe(true);
});

it('should adopt an incoming true value when the tier value is false', () => {
expect(secureMaxVersioned(false, true)).toBe(true);
});

it('should stay disabled when both values are false/undefined', () => {
expect(secureMaxVersioned(false, undefined)).toBe(false);
expect(secureMaxVersioned(false, false)).toBe(false);
});

it('should treat truthy/incoming coercion correctly', () => {
expect(secureMaxVersioned(false, true)).toBe(true);
expect(secureMaxVersioned(true, false)).toBe(true);
});
});

describe('fields: secureMaxRemovalPolicy', () => {
it('should return the tier value when incoming is undefined', () => {
expect(secureMaxRemovalPolicy(RemovalPolicy.RETAIN, undefined)).toBe(RemovalPolicy.RETAIN);
expect(secureMaxRemovalPolicy(RemovalPolicy.DESTROY, undefined)).toBe(RemovalPolicy.DESTROY);
});

it('should never weaken a RETAIN tier', () => {
expect(secureMaxRemovalPolicy(RemovalPolicy.RETAIN, RemovalPolicy.DESTROY)).toBe(
RemovalPolicy.RETAIN
);
expect(secureMaxRemovalPolicy(RemovalPolicy.RETAIN, RemovalPolicy.SNAPSHOT)).toBe(
RemovalPolicy.RETAIN
);
expect(secureMaxRemovalPolicy(RemovalPolicy.RETAIN, RemovalPolicy.RETAIN)).toBe(
RemovalPolicy.RETAIN
);
});

it('should adopt an incoming policy that is at least as strong as the tier', () => {
expect(secureMaxRemovalPolicy(RemovalPolicy.DESTROY, RemovalPolicy.SNAPSHOT)).toBe(
RemovalPolicy.SNAPSHOT
);
expect(secureMaxRemovalPolicy(RemovalPolicy.DESTROY, RemovalPolicy.RETAIN)).toBe(
RemovalPolicy.RETAIN
);
expect(secureMaxRemovalPolicy(RemovalPolicy.SNAPSHOT, RemovalPolicy.RETAIN)).toBe(
RemovalPolicy.RETAIN
);
});

it('should keep the weaker tier value when incoming is weaker', () => {
expect(secureMaxRemovalPolicy(RemovalPolicy.SNAPSHOT, RemovalPolicy.DESTROY)).toBe(
RemovalPolicy.SNAPSHOT
);
expect(secureMaxRemovalPolicy(RemovalPolicy.DESTROY, RemovalPolicy.DESTROY)).toBe(
RemovalPolicy.DESTROY
);
});

it('should rank SNAPSHOT between DESTROY and RETAIN', () => {
expect(secureMaxRemovalPolicy(RemovalPolicy.SNAPSHOT, RemovalPolicy.SNAPSHOT)).toBe(
RemovalPolicy.SNAPSHOT
);
expect(secureMaxRemovalPolicy(RemovalPolicy.DESTROY, RemovalPolicy.SNAPSHOT)).toBe(
RemovalPolicy.SNAPSHOT
);
expect(secureMaxRemovalPolicy(RemovalPolicy.SNAPSHOT, RemovalPolicy.RETAIN)).toBe(
RemovalPolicy.RETAIN
);
});
});
23 changes: 23 additions & 0 deletions test/resources/s3-bucket/secure-bucket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
});

describe('default configuration (HIGH)', () => {
it('should create a bucket with CIS-critical settings enforced', () => {

Check warning on line 15 in test/resources/s3-bucket/secure-bucket.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

Test has no assertions
new SecureBucket(stack, 'TestBucket');

const template = Template.fromStack(stack);
Expand All @@ -35,7 +35,7 @@
});
});

it('should enforce SSL via a bucket policy', () => {

Check warning on line 38 in test/resources/s3-bucket/secure-bucket.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

Test has no assertions
new SecureBucket(stack, 'TestBucket');

const template = Template.fromStack(stack);
Expand Down Expand Up @@ -81,12 +81,12 @@
);
expect(mainBucket).toBeDefined();
// tier-variable: versioning off at LOW
expect(mainBucket!['Properties'].VersioningConfiguration).toBeUndefined();

Check warning on line 84 in test/resources/s3-bucket/secure-bucket.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

Forbidden non-null assertion
// CIS-critical: encryption still enforced at LOW
expect(mainBucket!['Properties'].BucketEncryption).toBeDefined();

Check warning on line 86 in test/resources/s3-bucket/secure-bucket.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

Forbidden non-null assertion
});

it('LOW does not provision an access-log bucket', () => {

Check warning on line 89 in test/resources/s3-bucket/secure-bucket.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

Test has no assertions
new SecureBucket(stack, 'TestBucket', { securityLevel: SecurityLevel.LOW });

const template = Template.fromStack(stack);
Expand All @@ -95,7 +95,7 @@
});

describe('custom configuration', () => {
it('should allow overriding bucket name', () => {

Check warning on line 98 in test/resources/s3-bucket/secure-bucket.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

Test has no assertions
new SecureBucket(stack, 'TestBucket', { bucketName: 'my-custom-bucket-name' });

const template = Template.fromStack(stack);
Expand All @@ -111,7 +111,7 @@
b => b['Properties']?.OwnershipControls && !b['Properties']?.AccessControl
);
expect(mainBucket).toBeDefined();
expect(mainBucket!['Properties'].VersioningConfiguration).toBeUndefined();

Check warning on line 114 in test/resources/s3-bucket/secure-bucket.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

Forbidden non-null assertion
});

it('should not be able to disable encryption (CIS-critical)', () => {
Expand Down Expand Up @@ -184,4 +184,27 @@
expect(secureBucket.bucket.node.id).toBe('Bucket');
});
});

describe('attribute pass-through getters', () => {
it('exposes bucketArn and bucketName directly on the construct', () => {
const bucket = new SecureBucket(stack, 'TestBucket', {
bucketName: 'my-secure-bucket',
});

expect(bucket.bucketArn).toBeDefined();
expect(bucket.bucketArn).toBe(bucket.bucket.bucketArn);
expect(bucket.bucketName).toBe(bucket.bucket.bucketName);
expect(bucket.bucketDomainName).toBe(bucket.bucket.bucketDomainName);
expect(bucket.bucketRegionalDomainName).toBe(bucket.bucket.bucketRegionalDomainName);
});

it('resolves getters to the same values as the wrapped bucket', () => {
const bucket = new SecureBucket(stack, 'TestBucket', {
bucketName: 'direct-access',
});

expect(bucket.bucketArn).toBe(bucket.bucket.bucketArn);
expect(bucket.bucketName).toBe(bucket.bucket.bucketName);
});
});
});
Loading