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
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const fs = require('fs');
const cdk = require('aws-cdk-lib/core');
const s3 = require('aws-cdk-lib/aws-s3');

const stackPrefix = process.env.STACK_NAME_PREFIX;
if (!stackPrefix) {
throw new Error('the STACK_NAME_PREFIX environment variable is required');
}

const SHARED_BUCKET_NAME = `${stackPrefix}-validate-online-shared-bucket`;

class SecurityPlugin {
constructor() {
this.name = 'SecurityPlugin';
this.version = '1.0.0';
}

validate(context) {
const violations = [];
for (const templatePath of context.templatePaths) {
const template = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
for (const [logicalId, resource] of Object.entries(template.Resources || {})) {
if (resource.Type === 'AWS::S3::Bucket') {
violations.push({
ruleName: 'no-public-buckets',
description: 'S3 Buckets must not be publicly accessible',
fix: 'Set PublicAccessBlockConfiguration on the bucket',
severity: 'error',
violatingResources: [{
resourceLogicalId: logicalId,
templatePath,
locations: [`/Resources/${logicalId}/Properties/PublicAccessBlockConfiguration`],
}],
});
}
}
}
return { success: violations.length === 0, violations };
}
}

const app = new cdk.App();
cdk.Validations.of(app).addPlugins(new SecurityPlugin());

// Valid stack — no offline or online errors
class ValidStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);
new cdk.CfnResource(this, 'WaitHandle', {
type: 'AWS::CloudFormation::WaitConditionHandle',
});
}
}

// Deployed stack — owns the bucket with the shared name
class DeployedStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);
new s3.Bucket(this, 'ExistingBucket', {
bucketName: SHARED_BUCKET_NAME,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}

// Conflicting stack — tries to create a bucket with the same name (early validation error)
class ConflictingStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);
new s3.Bucket(this, 'ConflictBucket', {
bucketName: SHARED_BUCKET_NAME,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}

// Combined stack — has BOTH offline (S3 bucket triggers SecurityPlugin)
// AND online errors (bucket name conflict caught by CFN early validation)
class CombinedStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);
new s3.Bucket(this, 'MyBucket', {
bucketName: SHARED_BUCKET_NAME,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}

new ValidStack(app, `${stackPrefix}-validate-online-valid`);
new DeployedStack(app, `${stackPrefix}-validate-online-deployed`);
new ConflictingStack(app, `${stackPrefix}-validate-online-conflicting`);
new CombinedStack(app, `${stackPrefix}-validate-online-combined`);

app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"app": "node app.js",
"versionReporting": false,
"context": {
"@aws-cdk/core:failSynthOnValidationErrors": false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk validate --no-online skips CloudFormation validation',
withSpecificFixture('validate-online-app', async (fixture) => {
// Deploy a stack that owns the bucket
await fixture.cdk(
['deploy', '--require-approval=never', fixture.fullStackName('validate-online-deployed')],
);

// Validate with --no-online: the bucket name conflict should NOT be caught
const output = await fixture.cdk(
['--unstable=validate', 'validate', '--no-online', fixture.fullStackName('validate-online-conflicting')],
{
allowErrExit: true,
},
);

expect(output).not.toContain('already exists');
expect(output).not.toContain('CloudFormation');

// Cleanup
await fixture.cdk(['destroy', '--force', fixture.fullStackName('validate-online-deployed')]);
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk validate --online catches bucket name conflict',
withSpecificFixture('validate-online-app', async (fixture) => {
// Deploy a stack that owns the bucket
await fixture.cdk(
['deploy', '--require-approval=never', fixture.fullStackName('validate-online-deployed')],
);

// Now validate a stack that tries to create the same bucket name
const output = await fixture.cdk(
['--unstable=validate', 'validate', '--online', fixture.fullStackName('validate-online-conflicting')],
{
allowErrExit: true,
},
);

expect(output).toContain('CloudFormation');
expect(output).toContain('already exists');

// Cleanup
await fixture.cdk(['destroy', '--force', fixture.fullStackName('validate-online-deployed')]);
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk validate --online reports both offline and online errors',
withSpecificFixture('validate-online-app', async (fixture) => {
// Deploy a stack that owns the bucket
await fixture.cdk(
['deploy', '--require-approval=never', fixture.fullStackName('validate-online-deployed')],
);

// Validate combined stack — has both a bucket (SecurityPlugin) and
// uses the same bucket name (CFN early validation conflict)
const output = await fixture.cdk(
['--unstable=validate', 'validate', '--online', fixture.fullStackName('validate-online-combined')],
{
allowErrExit: true,
},
);

// Offline: SecurityPlugin catches the S3 bucket
expect(output).toContain('S3 Buckets must not be publicly accessible');
expect(output).toContain('SecurityPlugin');

// Online: CloudFormation catches the bucket name conflict
expect(output).toContain('already exists');
expect(output).toContain('CloudFormation');

// Cleanup
await fixture.cdk(['destroy', '--force', fixture.fullStackName('validate-online-deployed')]);
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk validate --online passes for valid template',
withSpecificFixture('validate-online-app', async (fixture) => {
const output = await fixture.cdk(
['--unstable=validate', 'validate', '--online', fixture.fullStackName('validate-online-valid')],
);

expect(output).toContain('No problems found');
}),
);
11 changes: 11 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ export interface ValidateOptions {
* Select the stacks to validate
*/
readonly stacks?: StackSelector;

/**
* Submit templates to CloudFormation for early validation.
*
* Creates a non-executing change set per stack and reports any
* early validation errors (invalid resource types, property validation, name conflicts).
* Requires AWS credentials.
*
* @default true
*/
readonly online?: boolean;
}

/**
Expand Down
Loading
Loading