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,83 @@
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');
}

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',
});
}
}

// Invalid stack (online only) — CFN rejects the resource type
class OnlineInvalidStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);
new cdk.CfnResource(this, 'BadResource', {
type: 'AWS::Fake::DoesNotExist',
properties: { SomeProperty: 'value' },
});
}
}

// Combined stack — has BOTH offline (S3 bucket triggers SecurityPlugin)
// AND online errors (invalid resource type rejected by CFN)
class CombinedStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);
new s3.Bucket(this, 'MyBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
new cdk.CfnResource(this, 'BadResource', {
type: 'AWS::Fake::DoesNotExist',
properties: { SomeProperty: 'value' },
});
}
}

new ValidStack(app, `${stackPrefix}-validate-online-valid`);
new OnlineInvalidStack(app, `${stackPrefix}-validate-online-invalid`);
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,14 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk validate --no-online skips CloudFormation validation',
withSpecificFixture('validate-online-app', async (fixture) => {
const output = await fixture.cdk(
['--unstable=validate', 'validate', '--no-online', fixture.fullStackName('validate-online-invalid')],
);

// With --no-online, the invalid resource type should NOT be caught
expect(output).not.toContain('AWS::Fake::DoesNotExist');
expect(output).not.toContain('CloudFormation');
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk validate --online catches invalid resource type',
withSpecificFixture('validate-online-app', async (fixture) => {
const output = await fixture.cdk(
['--unstable=validate', 'validate', '--online', fixture.fullStackName('validate-online-invalid')],
{
allowErrExit: true,
},
);

expect(output).toContain('CloudFormation');
expect(output).toContain('AWS::Fake::DoesNotExist');
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { integTest, withSpecificFixture } from '../../../lib';

integTest(
'cdk validate --online reports both offline and online errors',
withSpecificFixture('validate-online-app', async (fixture) => {
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 rejects the fake resource type
expect(output).toContain('AWS::Fake::DoesNotExist');
expect(output).toContain('CloudFormation');
}),
);
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 violations 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