From 5388c8897efd0dc3f7c3e51aeec0e9aa6f60bb55 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:51:22 -0400 Subject: [PATCH 1/6] feat(core): ship default CloudFormation validation rules in Rego The CloudFormationValidatePlugin already accepts custom Rego rules via its `regoRules` prop, but the auto-registered default instance passed none. This loads CDK-authored `.rego` rules from `core/lib/validation/rules/` into the default plugin instance so they run on every synth. The first rule set (`gamelift-fleet.rego`) ports the aws-gamelift-alpha BuildFleet L2 validations (name/description length, location and ingress-rule counts, non-negative location capacity) to template-level checks on `AWS::GameLift::Fleet`. Unlike the L2 constructor checks, these also cover templates produced via L1 constructs, escape hatches, and CfnInclude, and they evaluate post-synth so token-valued properties are already resolved. Findings surface as warnings and do not fail synth unless the app opts into `@aws-cdk/core:validateAgainstDefaultRules`, matching the existing plugin rollout posture. --- .../cloudformation-validate-plugin.ts | 24 ++- .../lib/validation/rules/gamelift-fleet.rego | 77 +++++++++ .../validation/default-rego-rules.test.ts | 163 ++++++++++++++++++ 3 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego create mode 100644 packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts diff --git a/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts b/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts index 9f7f7b364a713..4db8e4f5ff6f6 100644 --- a/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts +++ b/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts @@ -1,3 +1,5 @@ +import * as fs from 'fs'; +import * as path from 'path'; import { RegoEngine, TemplateFile, version } from '@aws/cloudformation-validate'; import type { Engine, EngineConfig, RuleInfo, Severity } from '@aws/cloudformation-validate'; import type { PolicyValidationPluginReport, PolicyViolatingResource } from './report'; @@ -66,7 +68,9 @@ export class CloudFormationValidatePlugin implements IPolicyValidationPlugin { */ public static _singletonInstance() { if (!CloudFormationValidatePlugin._instance) { - CloudFormationValidatePlugin._instance = new CloudFormationValidatePlugin(); + CloudFormationValidatePlugin._instance = new CloudFormationValidatePlugin({ + regoRules: defaultRegoRules(), + }); } return CloudFormationValidatePlugin._instance; } @@ -177,6 +181,24 @@ function mapSeverity(severity: Severity): string { } } +/** + * CDK-authored default Rego rules, shipped with aws-cdk-lib. + * + * These are ports of L2 construct validations to template-level rules, so the + * same checks also apply to templates produced via L1 constructs, escape + * hatches, or `CfnInclude`. See `rules/` next to this file. + */ +function defaultRegoRules(): ValidationRuleSource[] { + const rulesDir = path.join(__dirname, 'rules'); + return fs.readdirSync(rulesDir) + .filter((f) => f.endsWith('.rego')) + .sort() + .map((f) => ({ + name: f, + content: fs.readFileSync(path.join(rulesDir, f), 'utf-8'), + })); +} + // Rules that the engine will report but we want to ignore because CDK creates // the violation and customers don't control it. const IGNORE_RULES = new Set([ diff --git a/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego new file mode 100644 index 0000000000000..ae8446380f733 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego @@ -0,0 +1,77 @@ +package cdk_rules.gamelift + +import rego.v1 + +# Port of the aws-gamelift-alpha BuildFleet L2 construct validations +# (packages/@aws-cdk/aws-gamelift-alpha/lib/build-fleet.ts) to Rego, so the +# same checks apply to any template — L1 escape hatches, CfnInclude, raw +# CloudFormation — not only apps that use the L2 construct. + +# Fleet name can not be longer than 1024 characters +violation contains v if { + some name in resources_of_type("AWS::GameLift::Fleet") + fleet_name := resolve(name, "Properties.Name") + is_string(fleet_name) + count(fleet_name) > 1024 + v := make_diag_at( + "CDK-GameLift-001", "ERROR", name, + "Properties.Name", + sprintf("Fleet name can not be longer than 1024 characters but has %d characters", [count(fleet_name)]), + ) +} + +# Fleet description can not be longer than 1024 characters +violation contains v if { + some name in resources_of_type("AWS::GameLift::Fleet") + description := resolve(name, "Properties.Description") + is_string(description) + count(description) > 1024 + v := make_diag_at( + "CDK-GameLift-002", "ERROR", name, + "Properties.Description", + sprintf("Fleet description can not be longer than 1024 characters but has %d characters", [count(description)]), + ) +} + +# No more than 100 locations (home region + 99 remote) are allowed per fleet +violation contains v if { + some name in resources_of_type("AWS::GameLift::Fleet") + locations := resolve(name, "Properties.Locations") + is_array(locations) + count(locations) > 100 + v := make_diag_at( + "CDK-GameLift-003", "ERROR", name, + "Properties.Locations", + sprintf("No more than 100 locations are allowed per fleet, given %d", [count(locations)]), + ) +} + +# No more than 50 ingress rules are allowed per fleet +violation contains v if { + some name in resources_of_type("AWS::GameLift::Fleet") + permissions := resolve(name, "Properties.EC2InboundPermissions") + is_array(permissions) + count(permissions) > 50 + v := make_diag_at( + "CDK-GameLift-004", "ERROR", name, + "Properties.EC2InboundPermissions", + sprintf("No more than 50 ingress rules are allowed per fleet, given %d", [count(permissions)]), + ) +} + +# Location capacity: DesiredEC2Instances, MinSize and MaxSize cannot be negative +violation contains v if { + some name in resources_of_type("AWS::GameLift::Fleet") + locations := resolve(name, "Properties.Locations") + is_array(locations) + some i, location in locations + some field in ["DesiredEC2Instances", "MinSize", "MaxSize"] + value := location.LocationCapacity[field] + is_number(value) + value < 0 + v := make_diag_at( + "CDK-GameLift-005", "ERROR", name, + sprintf("Properties.Locations.%d.LocationCapacity.%s", [i, field]), + sprintf("%s for the Fleet cannot be lower than 0, given %v", [field, value]), + ) +} diff --git a/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts new file mode 100644 index 0000000000000..894d74798aaa6 --- /dev/null +++ b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts @@ -0,0 +1,163 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type { PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '../../../cx-api'; +import * as core from '../../lib'; + +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { return true; }); + jest.spyOn(console, 'log').mockImplementation(() => { return true; }); + process.exitCode = undefined; +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +const originalContextJson = process.env.CDK_CONTEXT_JSON; + +beforeAll(() => { + // These tests validate rule behavior through the report — strict mode would mask + // the signals by throwing before tests can assert on the report contents. + process.env.CDK_CONTEXT_JSON = JSON.stringify({ + ...JSON.parse(originalContextJson ?? '{}'), + '@aws-cdk/core:strictCfnValidateErrors': false, + }); +}); + +afterAll(() => { + jest.resetAllMocks(); + process.env.CDK_CONTEXT_JSON = originalContextJson; +}); + +describe('default GameLift fleet rules', () => { + test('CDK-GameLift-004 fires for a fleet with more than 50 ingress rules', () => { + const app = new core.App({ + context: { + [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, + }, + }); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + Name: 'my-fleet', + BuildId: 'build-1234', + EC2InstanceType: 'c5.large', + EC2InboundPermissions: Array.from({ length: 60 }, (_, i) => ({ + FromPort: 1000 + i, + ToPort: 1000 + i, + IpRange: '10.0.0.0/24', + Protocol: 'TCP', + })), + }, + }); + + const report = loadValidationReport(app.synth()); + const violations = pluginViolations(report); + + expect(violations).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-004', + description: expect.stringContaining('No more than 50 ingress rules are allowed per fleet, given 60'), + })); + }); + + test('CDK-GameLift-005 fires for a fleet location with negative capacity', () => { + const app = new core.App({ + context: { + [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, + }, + }); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + Name: 'my-fleet', + BuildId: 'build-1234', + EC2InstanceType: 'c5.large', + Locations: [{ + Location: 'us-east-1', + LocationCapacity: { DesiredEC2Instances: 1, MinSize: -2, MaxSize: 3 }, + }], + }, + }); + + const report = loadValidationReport(app.synth()); + const violations = pluginViolations(report); + + expect(violations).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-005', + description: expect.stringContaining('MinSize for the Fleet cannot be lower than 0, given -2'), + })); + }); + + test('no GameLift findings for a compliant fleet', () => { + const app = new core.App({ + context: { + [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, + }, + }); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + Name: 'my-fleet', + BuildId: 'build-1234', + EC2InstanceType: 'c5.large', + EC2InboundPermissions: [{ FromPort: 7777, ToPort: 7777, IpRange: '10.0.0.0/24', Protocol: 'TCP' }], + Locations: [{ + Location: 'us-east-1', + LocationCapacity: { DesiredEC2Instances: 1, MinSize: 0, MaxSize: 3 }, + }], + }, + }); + + const report = loadValidationReport(app.synth()); + const violations = pluginViolations(report); + + expect(violations.filter((v) => v.ruleName.startsWith('CDK-GameLift'))).toEqual([]); + }); + + test('all five GameLift rules are wired into the report', () => { + const app = new core.App({ + context: { + [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, + }, + }); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + Name: 'x'.repeat(1025), + Description: 'y'.repeat(1025), + BuildId: 'build-1234', + EC2InstanceType: 'c5.large', + EC2InboundPermissions: Array.from({ length: 60 }, (_, i) => ({ + FromPort: 1000 + i, ToPort: 1000 + i, IpRange: '10.0.0.0/24', Protocol: 'TCP', + })), + Locations: Array.from({ length: 101 }, (_, i) => ({ + Location: `loc-${i}`, + LocationCapacity: { DesiredEC2Instances: 1, MinSize: -1, MaxSize: 3 }, + })), + }, + }); + + const report = loadValidationReport(app.synth()); + const ruleNames = new Set(pluginViolations(report).map((v) => v.ruleName)); + + for (const id of ['CDK-GameLift-001', 'CDK-GameLift-002', 'CDK-GameLift-003', 'CDK-GameLift-004', 'CDK-GameLift-005']) { + expect(ruleNames).toContain(id); + } + }); +}); + +function loadValidationReport(asm: cxapi.CloudAssembly) { + const p = path.join(asm.directory, 'validation-report.json'); + return JSON.parse(fs.readFileSync(p, { encoding: 'utf-8' })) as PolicyValidationReportJson; +} + +function pluginViolations(report: PolicyValidationReportJson) { + return report.pluginReports + .filter((r) => r.pluginName === 'CloudFormation Validate') + .flatMap((r) => r.violations); +} From 07a815359516417d669f7af8dc52699410d3ac9b Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:22:29 -0400 Subject: [PATCH 2/6] feat(core): replace schema-covered Rego rules with cross-field invariants The first version of the GameLift rules duplicated checks the engine's built-in schema rules already report (F3031-F3034 cover string lengths, item counts, per-field minimums and patterns). Replace them with five cross-field invariants the resource schema cannot express: - CDK-GameLift-001: ingress rule FromPort must not exceed ToPort - CDK-GameLift-002: location capacity MinSize must not exceed MaxSize - CDK-GameLift-003: DesiredEC2Instances must lie within [MinSize, MaxSize] - CDK-GameLift-004: SIMPLE-routing alias must not set a terminal Message - CDK-GameLift-005: TERMINAL-routing alias must not reference a fleet The alias rules port the "terminal message or fleet, not both" check from aws-gamelift-alpha's Alias construct; the fleet rules add API-enforced invariants that today only fail at deploy time. --- .../lib/validation/rules/gamelift-fleet.rego | 99 +++++++---- .../validation/default-rego-rules.test.ts | 167 ++++++++++-------- 2 files changed, 155 insertions(+), 111 deletions(-) diff --git a/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego index ae8446380f733..5609dbe8d088a 100644 --- a/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego +++ b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego @@ -2,76 +2,99 @@ package cdk_rules.gamelift import rego.v1 -# Port of the aws-gamelift-alpha BuildFleet L2 construct validations -# (packages/@aws-cdk/aws-gamelift-alpha/lib/build-fleet.ts) to Rego, so the -# same checks apply to any template — L1 escape hatches, CfnInclude, raw -# CloudFormation — not only apps that use the L2 construct. +# Cross-field GameLift invariants ported from the aws-gamelift-alpha L2 +# constructs (build-fleet.ts, fleet-base.ts, alias.ts). Only checks the +# CloudFormation resource schema cannot express are included here — schema +# limits (lengths, item counts, per-field ranges, patterns) are already +# covered by the engine's built-in rules (F3031-F3034). +# +# Because these run on the synthesized template, they also cover L1 +# constructs, escape hatches, and CfnInclude, and token-valued properties +# are already resolved. -# Fleet name can not be longer than 1024 characters +# An ingress rule's port range must not be inverted (FromPort <= ToPort) violation contains v if { some name in resources_of_type("AWS::GameLift::Fleet") - fleet_name := resolve(name, "Properties.Name") - is_string(fleet_name) - count(fleet_name) > 1024 + permissions := resolve(name, "Properties.EC2InboundPermissions") + is_array(permissions) + some i, permission in permissions + from_port := permission.FromPort + to_port := permission.ToPort + is_number(from_port) + is_number(to_port) + from_port > to_port v := make_diag_at( "CDK-GameLift-001", "ERROR", name, - "Properties.Name", - sprintf("Fleet name can not be longer than 1024 characters but has %d characters", [count(fleet_name)]), + sprintf("Properties.EC2InboundPermissions.%d.FromPort", [i]), + sprintf("Ingress rule port range is inverted: FromPort %v is greater than ToPort %v", [from_port, to_port]), ) } -# Fleet description can not be longer than 1024 characters +# A location's capacity must satisfy MinSize <= MaxSize violation contains v if { some name in resources_of_type("AWS::GameLift::Fleet") - description := resolve(name, "Properties.Description") - is_string(description) - count(description) > 1024 + locations := resolve(name, "Properties.Locations") + is_array(locations) + some i, location in locations + min_size := location.LocationCapacity.MinSize + max_size := location.LocationCapacity.MaxSize + is_number(min_size) + is_number(max_size) + min_size > max_size v := make_diag_at( "CDK-GameLift-002", "ERROR", name, - "Properties.Description", - sprintf("Fleet description can not be longer than 1024 characters but has %d characters", [count(description)]), + sprintf("Properties.Locations.%d.LocationCapacity.MinSize", [i]), + sprintf("Location capacity MinSize %v is greater than MaxSize %v", [min_size, max_size]), ) } -# No more than 100 locations (home region + 99 remote) are allowed per fleet +# A location's DesiredEC2Instances must lie within [MinSize, MaxSize] violation contains v if { some name in resources_of_type("AWS::GameLift::Fleet") locations := resolve(name, "Properties.Locations") is_array(locations) - count(locations) > 100 + some i, location in locations + desired := location.LocationCapacity.DesiredEC2Instances + min_size := location.LocationCapacity.MinSize + max_size := location.LocationCapacity.MaxSize + is_number(desired) + is_number(min_size) + is_number(max_size) + min_size <= max_size # avoid double-reporting on top of CDK-GameLift-002 + outside_range(desired, min_size, max_size) v := make_diag_at( "CDK-GameLift-003", "ERROR", name, - "Properties.Locations", - sprintf("No more than 100 locations are allowed per fleet, given %d", [count(locations)]), + sprintf("Properties.Locations.%d.LocationCapacity.DesiredEC2Instances", [i]), + sprintf("Location capacity DesiredEC2Instances %v is outside the range [MinSize %v, MaxSize %v]", [desired, min_size, max_size]), ) } -# No more than 50 ingress rules are allowed per fleet +outside_range(value, lower, upper) if value < lower + +outside_range(value, lower, upper) if value > upper + +# An alias with SIMPLE routing must not carry a terminal Message violation contains v if { - some name in resources_of_type("AWS::GameLift::Fleet") - permissions := resolve(name, "Properties.EC2InboundPermissions") - is_array(permissions) - count(permissions) > 50 + some name, res in input.resources + res.resourceType == "AWS::GameLift::Alias" + res.properties.RoutingStrategy.Type == "SIMPLE" + res.properties.RoutingStrategy.Message v := make_diag_at( "CDK-GameLift-004", "ERROR", name, - "Properties.EC2InboundPermissions", - sprintf("No more than 50 ingress rules are allowed per fleet, given %d", [count(permissions)]), + "Properties.RoutingStrategy.Message", + "Alias with SIMPLE routing must not set a terminal Message; either route to a fleet or set a terminal message, not both", ) } -# Location capacity: DesiredEC2Instances, MinSize and MaxSize cannot be negative +# An alias with TERMINAL routing must not point at a fleet violation contains v if { - some name in resources_of_type("AWS::GameLift::Fleet") - locations := resolve(name, "Properties.Locations") - is_array(locations) - some i, location in locations - some field in ["DesiredEC2Instances", "MinSize", "MaxSize"] - value := location.LocationCapacity[field] - is_number(value) - value < 0 + some name, res in input.resources + res.resourceType == "AWS::GameLift::Alias" + res.properties.RoutingStrategy.Type == "TERMINAL" + res.properties.RoutingStrategy.FleetId v := make_diag_at( "CDK-GameLift-005", "ERROR", name, - sprintf("Properties.Locations.%d.LocationCapacity.%s", [i, field]), - sprintf("%s for the Fleet cannot be lower than 0, given %v", [field, value]), + "Properties.RoutingStrategy.FleetId", + "Alias with TERMINAL routing must not reference a fleet; either route to a fleet or set a terminal message, not both", ) } diff --git a/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts index 894d74798aaa6..f58734e73983b 100644 --- a/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts +++ b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts @@ -30,127 +30,148 @@ afterAll(() => { process.env.CDK_CONTEXT_JSON = originalContextJson; }); -describe('default GameLift fleet rules', () => { - test('CDK-GameLift-004 fires for a fleet with more than 50 ingress rules', () => { - const app = new core.App({ - context: { - [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, - }, - }); +describe('default GameLift rules', () => { + test('CDK-GameLift-001 fires for an inverted ingress port range', () => { + const app = testApp(); const stack = new core.Stack(app, 'TestStack'); new core.CfnResource(stack, 'MyFleet', { type: 'AWS::GameLift::Fleet', properties: { - Name: 'my-fleet', - BuildId: 'build-1234', - EC2InstanceType: 'c5.large', - EC2InboundPermissions: Array.from({ length: 60 }, (_, i) => ({ - FromPort: 1000 + i, - ToPort: 1000 + i, - IpRange: '10.0.0.0/24', - Protocol: 'TCP', - })), + ...FLEET_BASE, + EC2InboundPermissions: [ + { FromPort: 9000, ToPort: 80, IpRange: '10.0.0.0/24', Protocol: 'TCP' }, + ], }, }); - const report = loadValidationReport(app.synth()); - const violations = pluginViolations(report); - - expect(violations).toContainEqual(expect.objectContaining({ - ruleName: 'CDK-GameLift-004', - description: expect.stringContaining('No more than 50 ingress rules are allowed per fleet, given 60'), + expect(pluginViolations(loadValidationReport(app.synth()))).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-001', + description: expect.stringContaining('FromPort 9000 is greater than ToPort 80'), })); }); - test('CDK-GameLift-005 fires for a fleet location with negative capacity', () => { - const app = new core.App({ - context: { - [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, - }, - }); + test('CDK-GameLift-002 fires when a location capacity MinSize exceeds MaxSize', () => { + const app = testApp(); const stack = new core.Stack(app, 'TestStack'); new core.CfnResource(stack, 'MyFleet', { type: 'AWS::GameLift::Fleet', properties: { - Name: 'my-fleet', - BuildId: 'build-1234', - EC2InstanceType: 'c5.large', + ...FLEET_BASE, Locations: [{ Location: 'us-east-1', - LocationCapacity: { DesiredEC2Instances: 1, MinSize: -2, MaxSize: 3 }, + LocationCapacity: { DesiredEC2Instances: 5, MinSize: 10, MaxSize: 2 }, }], }, }); - const report = loadValidationReport(app.synth()); - const violations = pluginViolations(report); - - expect(violations).toContainEqual(expect.objectContaining({ - ruleName: 'CDK-GameLift-005', - description: expect.stringContaining('MinSize for the Fleet cannot be lower than 0, given -2'), + expect(pluginViolations(loadValidationReport(app.synth()))).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-002', + description: expect.stringContaining('MinSize 10 is greater than MaxSize 2'), })); }); - test('no GameLift findings for a compliant fleet', () => { - const app = new core.App({ - context: { - [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, - }, - }); + test('CDK-GameLift-003 fires when DesiredEC2Instances is outside [MinSize, MaxSize]', () => { + const app = testApp(); const stack = new core.Stack(app, 'TestStack'); new core.CfnResource(stack, 'MyFleet', { type: 'AWS::GameLift::Fleet', properties: { - Name: 'my-fleet', - BuildId: 'build-1234', - EC2InstanceType: 'c5.large', - EC2InboundPermissions: [{ FromPort: 7777, ToPort: 7777, IpRange: '10.0.0.0/24', Protocol: 'TCP' }], + ...FLEET_BASE, Locations: [{ Location: 'us-east-1', - LocationCapacity: { DesiredEC2Instances: 1, MinSize: 0, MaxSize: 3 }, + LocationCapacity: { DesiredEC2Instances: 50, MinSize: 1, MaxSize: 10 }, }], }, }); - const report = loadValidationReport(app.synth()); - const violations = pluginViolations(report); + expect(pluginViolations(loadValidationReport(app.synth()))).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-003', + description: expect.stringContaining('DesiredEC2Instances 50 is outside the range'), + })); + }); - expect(violations.filter((v) => v.ruleName.startsWith('CDK-GameLift'))).toEqual([]); + test('CDK-GameLift-004 fires for an alias with SIMPLE routing and a terminal message', () => { + const app = testApp(); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyAlias', { + type: 'AWS::GameLift::Alias', + properties: { + Name: 'my-alias', + RoutingStrategy: { Type: 'SIMPLE', FleetId: 'fleet-1234', Message: 'goodbye' }, + }, + }); + + expect(pluginViolations(loadValidationReport(app.synth()))).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-004', + })); }); - test('all five GameLift rules are wired into the report', () => { - const app = new core.App({ - context: { - [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, + test('CDK-GameLift-005 fires for an alias with TERMINAL routing and a fleet', () => { + const app = testApp(); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyAlias', { + type: 'AWS::GameLift::Alias', + properties: { + Name: 'my-alias', + RoutingStrategy: { Type: 'TERMINAL', Message: 'goodbye', FleetId: 'fleet-1234' }, }, }); + + expect(pluginViolations(loadValidationReport(app.synth()))).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-005', + })); + }); + + test('no GameLift findings for compliant resources', () => { + const app = testApp(); const stack = new core.Stack(app, 'TestStack'); new core.CfnResource(stack, 'MyFleet', { type: 'AWS::GameLift::Fleet', properties: { - Name: 'x'.repeat(1025), - Description: 'y'.repeat(1025), - BuildId: 'build-1234', - EC2InstanceType: 'c5.large', - EC2InboundPermissions: Array.from({ length: 60 }, (_, i) => ({ - FromPort: 1000 + i, ToPort: 1000 + i, IpRange: '10.0.0.0/24', Protocol: 'TCP', - })), - Locations: Array.from({ length: 101 }, (_, i) => ({ - Location: `loc-${i}`, - LocationCapacity: { DesiredEC2Instances: 1, MinSize: -1, MaxSize: 3 }, - })), + ...FLEET_BASE, + EC2InboundPermissions: [ + { FromPort: 7777, ToPort: 7777, IpRange: '10.0.0.0/24', Protocol: 'TCP' }, + ], + Locations: [{ + Location: 'us-east-1', + LocationCapacity: { DesiredEC2Instances: 5, MinSize: 1, MaxSize: 10 }, + }], + }, + }); + new core.CfnResource(stack, 'SimpleAlias', { + type: 'AWS::GameLift::Alias', + properties: { + Name: 'simple-alias', + RoutingStrategy: { Type: 'SIMPLE', FleetId: 'fleet-1234' }, + }, + }); + new core.CfnResource(stack, 'TerminalAlias', { + type: 'AWS::GameLift::Alias', + properties: { + Name: 'terminal-alias', + RoutingStrategy: { Type: 'TERMINAL', Message: 'goodbye' }, }, }); - const report = loadValidationReport(app.synth()); - const ruleNames = new Set(pluginViolations(report).map((v) => v.ruleName)); - - for (const id of ['CDK-GameLift-001', 'CDK-GameLift-002', 'CDK-GameLift-003', 'CDK-GameLift-004', 'CDK-GameLift-005']) { - expect(ruleNames).toContain(id); - } + const violations = pluginViolations(loadValidationReport(app.synth())); + expect(violations.filter((v) => v.ruleName.startsWith('CDK-GameLift'))).toEqual([]); }); }); +const FLEET_BASE = { + Name: 'my-fleet', + BuildId: 'build-1234', + EC2InstanceType: 'c5.large', +}; + +function testApp() { + return new core.App({ + context: { + [cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false, + }, + }); +} + function loadValidationReport(asm: cxapi.CloudAssembly) { const p = path.join(asm.directory, 'validation-report.json'); return JSON.parse(fs.readFileSync(p, { encoding: 'utf-8' })) as PolicyValidationReportJson; From 13de0874d6e3721b38ca8133d7faf0c607e86f51 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:55:36 -0400 Subject: [PATCH 3/6] test(core): add integ test for default Rego validation rules The stack deploys a compliant TERMINAL-routing GameLift alias and carries a violating alias (TERMINAL routing plus FleetId, CDK-GameLift-005) behind a never-true condition: the default Rego rules evaluate the full template regardless of conditions, so the finding is captured in the snapshot's validation-report.json, while CloudFormation never creates the invalid resource the GameLift API would reject. Deploying proves the default warning posture does not block deployment. --- .../DefaultRegoRulesStack.assets.json | 20 + .../DefaultRegoRulesStack.metadata.json | 32 + .../DefaultRegoRulesStack.template.json | 68 ++ ...efaultTestDeployAssertCC8E34BF.assets.json | 20 + ...aultTestDeployAssertCC8E34BF.metadata.json | 14 + ...aultTestDeployAssertCC8E34BF.template.json | 36 + .../cdk.out | 1 + .../integ.json | 14 + .../manifest.json | 620 ++++++++++++++++++ .../tree.json | 1 + .../validation-report.json | 62 ++ .../core/test/integ.default-rego-rules.ts | 45 ++ 12 files changed, 933 insertions(+) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.metadata.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.metadata.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/validation-report.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.assets.json new file mode 100644 index 0000000000000..f2925c9eb3f91 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.assets.json @@ -0,0 +1,20 @@ +{ + "version": "54.0.0", + "files": { + "f4371de82ae61e2f52ed21921b18783e453650f76390222b010e57fca3d6321e": { + "displayName": "DefaultRegoRulesStack Template", + "source": { + "path": "DefaultRegoRulesStack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region-98ea9860": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "f4371de82ae61e2f52ed21921b18783e453650f76390222b010e57fca3d6321e.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.metadata.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.metadata.json new file mode 100644 index 0000000000000..243d715ea5efb --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.metadata.json @@ -0,0 +1,32 @@ +{ + "/DefaultRegoRulesStack/CompliantAlias": [ + { + "type": "aws:cdk:logicalId", + "data": "CompliantAlias" + } + ], + "/DefaultRegoRulesStack/NeverTrue": [ + { + "type": "aws:cdk:logicalId", + "data": "NeverTrue" + } + ], + "/DefaultRegoRulesStack/ViolatingAlias": [ + { + "type": "aws:cdk:logicalId", + "data": "ViolatingAlias" + } + ], + "/DefaultRegoRulesStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/DefaultRegoRulesStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.template.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.template.json new file mode 100644 index 0000000000000..d634106a8e8e6 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesStack.template.json @@ -0,0 +1,68 @@ +{ + "Resources": { + "CompliantAlias": { + "Type": "AWS::GameLift::Alias", + "Properties": { + "Name": "default-rego-rules-compliant", + "RoutingStrategy": { + "Message": "server offline for maintenance", + "Type": "TERMINAL" + } + } + }, + "ViolatingAlias": { + "Type": "AWS::GameLift::Alias", + "Properties": { + "Name": "default-rego-rules-violating", + "RoutingStrategy": { + "FleetId": "fleet-11111111-2222-3333-4444-555555555555", + "Message": "goodbye", + "Type": "TERMINAL" + } + }, + "Condition": "NeverTrue" + } + }, + "Conditions": { + "NeverTrue": { + "Fn::Equals": [ + "true", + "false" + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets.json new file mode 100644 index 0000000000000..9b542ea9c49fb --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets.json @@ -0,0 +1,20 @@ +{ + "version": "54.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "displayName": "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF Template", + "source": { + "path": "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region-d8d86b35": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.metadata.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.metadata.json new file mode 100644 index 0000000000000..a48d81e3391fc --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.metadata.json @@ -0,0 +1,14 @@ +{ + "/DefaultRegoRulesTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/DefaultRegoRulesTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.template.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/cdk.out new file mode 100644 index 0000000000000..433ef06634165 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"54.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/integ.json new file mode 100644 index 0000000000000..0e1924b11d178 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/integ.json @@ -0,0 +1,14 @@ +{ + "version": "54.0.0", + "testCases": { + "DefaultRegoRulesTest/DefaultTest": { + "stacks": [ + "DefaultRegoRulesStack" + ], + "assertionStack": "DefaultRegoRulesTest/DefaultTest/DeployAssert", + "assertionStackName": "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF" + } + }, + "enableLookups": true, + "minimumCliVersion": "2.1131.0" +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/manifest.json new file mode 100644 index 0000000000000..8fd878cd111b4 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/manifest.json @@ -0,0 +1,620 @@ +{ + "version": "54.0.0", + "artifacts": { + "DefaultRegoRulesStack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "DefaultRegoRulesStack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "DefaultRegoRulesStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "DefaultRegoRulesStack.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/f4371de82ae61e2f52ed21921b18783e453650f76390222b010e57fca3d6321e.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "DefaultRegoRulesStack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "DefaultRegoRulesStack.assets" + ], + "additionalMetadataFile": "DefaultRegoRulesStack.metadata.json", + "displayName": "DefaultRegoRulesStack" + }, + "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.assets" + ], + "additionalMetadataFile": "DefaultRegoRulesTestDefaultTestDeployAssertCC8E34BF.metadata.json", + "displayName": "DefaultRegoRulesTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-cdk-lib/feature-flag-report": { + "type": "cdk:feature-flag-report", + "properties": { + "module": "aws-cdk-lib", + "flags": { + "@aws-cdk/aws-signer:signingProfileNamePassedToCfn": { + "userValue": true, + "recommendedValue": true, + "explanation": "Pass signingProfileName to CfnSigningProfile" + }, + "@aws-cdk/core:newStyleStackSynthesis": { + "recommendedValue": true, + "explanation": "Switch to new stack synthesis method which enables CI/CD", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:stackRelativeExports": { + "recommendedValue": true, + "explanation": "Name exports based on the construct paths relative to the stack, rather than the global construct path", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": { + "userValue": true, + "recommendedValue": true, + "explanation": "Disable implicit openListener when custom security groups are provided" + }, + "@aws-cdk/aws-rds:lowercaseDbIdentifier": { + "recommendedValue": true, + "explanation": "Force lowercasing of RDS Cluster names in CDK", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": { + "recommendedValue": true, + "explanation": "Allow adding/removing multiple UsagePlanKeys independently", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-lambda:recognizeVersionProps": { + "recommendedValue": true, + "explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-lambda:recognizeLayerVersion": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`." + }, + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": { + "recommendedValue": true, + "explanation": "Enable this feature flag to have cloudfront distributions use the security policy TLSv1.2_2021 by default.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:checkSecretUsage": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this flag to make it impossible to accidentally use SecretValues in unsafe locations" + }, + "@aws-cdk/core:target-partitions": { + "recommendedValue": [ + "aws", + "aws-cn" + ], + "explanation": "What regions to include in lookup tables of environment agnostic stacks" + }, + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": { + "userValue": true, + "recommendedValue": true, + "explanation": "ECS extensions will automatically add an `awslogs` driver if no logging is specified" + }, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature flag to have Launch Templates generated by the `InstanceRequireImdsv2Aspect` use unique names." + }, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": { + "userValue": true, + "recommendedValue": true, + "explanation": "ARN format used by ECS. In the new ARN format, the cluster name is part of the resource ID." + }, + "@aws-cdk/aws-iam:minimizePolicies": { + "userValue": true, + "recommendedValue": true, + "explanation": "Minimize IAM policies by combining Statements" + }, + "@aws-cdk/core:validateSnapshotRemovalPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "Error on snapshot removal policies on resources that do not support it." + }, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Generate key aliases that include the stack name" + }, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature flag to create an S3 bucket policy by default in cases where an AWS service would automatically create the Policy if one does not exist." + }, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": { + "userValue": true, + "recommendedValue": true, + "explanation": "Restrict KMS key policy for encrypted Queues a bit more" + }, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": { + "userValue": true, + "recommendedValue": true, + "explanation": "Make default CloudWatch Role behavior safe for multiple API Gateways in one environment" + }, + "@aws-cdk/core:enablePartitionLiterals": { + "userValue": true, + "recommendedValue": true, + "explanation": "Make ARNs concrete if AWS partition is known" + }, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": { + "userValue": true, + "recommendedValue": true, + "explanation": "Event Rules may only push to encrypted SQS queues in the same account" + }, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": { + "userValue": true, + "recommendedValue": true, + "explanation": "Avoid setting the \"ECS\" deployment controller when adding a circuit breaker" + }, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature to create default policy names for imported roles that depend on the stack the role is in." + }, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use S3 Bucket Policy instead of ACLs for Server Access Logging" + }, + "@aws-cdk/aws-route53-patters:useCertificate": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use the official `Certificate` resource instead of `DnsValidatedCertificate`" + }, + "@aws-cdk/customresources:installLatestAwsSdkDefault": { + "userValue": false, + "recommendedValue": false, + "explanation": "Whether to install the latest SDK by default in AwsCustomResource" + }, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use unique resource name for Database Proxy" + }, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": { + "userValue": true, + "recommendedValue": true, + "explanation": "Remove CloudWatch alarms from deployment group" + }, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": { + "userValue": true, + "recommendedValue": true, + "explanation": "Include authorizer configuration in the calculation of the API deployment logical ID." + }, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": { + "userValue": true, + "recommendedValue": true, + "explanation": "Define user data for a launch template by default when a machine image is provided." + }, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": { + "userValue": true, + "recommendedValue": true, + "explanation": "SecretTargetAttachments uses the ResourcePolicy of the attached Secret." + }, + "@aws-cdk/aws-redshift:columnId": { + "userValue": true, + "recommendedValue": true, + "explanation": "Whether to use an ID to track Redshift column changes" + }, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable AmazonEMRServicePolicy_v2 managed policies" + }, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": { + "userValue": true, + "recommendedValue": true, + "explanation": "Restrict access to the VPC default security group" + }, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": { + "userValue": true, + "recommendedValue": true, + "explanation": "Generate a unique id for each RequestValidator added to a method" + }, + "@aws-cdk/aws-kms:aliasNameRef": { + "userValue": true, + "recommendedValue": true, + "explanation": "KMS Alias name and keyArn will have implicit reference to KMS Key" + }, + "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable grant methods on Aliases imported by name to use kms:ResourceAliases condition" + }, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": { + "userValue": true, + "recommendedValue": true, + "explanation": "Generate a launch template when creating an AutoScalingGroup" + }, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": { + "userValue": true, + "recommendedValue": true, + "explanation": "Include the stack prefix in the stack name generation process" + }, + "@aws-cdk/aws-efs:denyAnonymousAccess": { + "userValue": true, + "recommendedValue": true, + "explanation": "EFS denies anonymous clients accesses" + }, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables support for Multi-AZ with Standby deployment for opensearch domains" + }, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default" + }, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, mount targets will have a stable logicalId that is linked to the associated subnet." + }, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, a scope of InstanceParameterGroup for AuroraClusterInstance with each parameters will change." + }, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, will always use the arn for identifiers for CfnSourceApiAssociation in the GraphqlApi construct rather than id." + }, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, creating an RDS database cluster from a snapshot will only render credentials for snapshot credentials." + }, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the CodeCommit source action is using the default branch name 'main'." + }, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the logical ID of a Lambda permission for a Lambda action includes an alarm ID." + }, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables Pipeline to set the default value for crossAccountKeys to false." + }, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables Pipeline to set the default pipeline type to V2." + }, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, IAM Policy created from KMS key grant will reduce the resource scope to this key only." + }, + "@aws-cdk/pipelines:reduceAssetRoleTrustScope": { + "recommendedValue": true, + "explanation": "Remove the root account principal from PipelineAssetsFileRole trust policy", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-eks:nodegroupNameAttribute": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, nodegroupName attribute of the provisioned EKS NodeGroup will not have the cluster name prefix." + }, + "@aws-cdk/aws-eks:useNativeOidcProvider": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, EKS V2 clusters will use the native OIDC provider resource AWS::IAM::OIDCProvider instead of creating the OIDCProvider with a custom resource (iam.OpenIDConnectProvider)." + }, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the default volume type of the EBS volume will be GP3" + }, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, remove default deployment alarm settings" + }, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": { + "userValue": false, + "recommendedValue": false, + "explanation": "When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default" + }, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": { + "userValue": false, + "recommendedValue": false, + "explanation": "When enabled, Adding notifications to a bucket in the current stack will not remove notification from imported stack." + }, + "@aws-cdk/aws-stepfunctions-tasks:useNewS3UriParametersForBedrockInvokeModelTask": { + "recommendedValue": true, + "explanation": "When enabled, use new props for S3 URI field in task definition of state machine for bedrock invoke model.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:explicitStackTags": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, stack tags need to be assigned explicitly on a Stack." + }, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration" + }, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled will allow you to specify a resource policy per replica, and not copy the source table policy to all replicas" + }, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, initOptions.timeout and resourceSignalTimeout values will be summed together." + }, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, a Lambda authorizer Permission created when using GraphqlApi will be properly scoped with a SourceArn." + }, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the value of property `instanceResourceId` in construct `DatabaseInstanceReadReplica` will be set to the correct value which is `DbiResourceId` instead of currently `DbInstanceArn`" + }, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CFN templates added with `cfn-include` will error if the template contains Resource Update or Create policies with CFN Intrinsics that include non-primitive values." + }, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, both `@aws-sdk` and `@smithy` packages will be excluded from the Lambda Node.js 18.x runtime to prevent version mismatches in bundled applications." + }, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the resource of IAM Run Ecs policy generated by SFN EcsRunTask will reference the definition, instead of constructing ARN." + }, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the BastionHost construct will use the latest Amazon Linux 2023 AMI, instead of Amazon Linux 2." + }, + "@aws-cdk/core:aspectStabilization": { + "recommendedValue": true, + "explanation": "When enabled, a stabilization loop will be run when invoking Aspects during synthesis.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, use a new method for DNS Name of user pool domain target without creating a custom resource." + }, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the default security group ingress rules will allow IPv6 ingress from anywhere" + }, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the default behaviour of OIDC provider will reject unauthorized connections" + }, + "@aws-cdk/core:enableAdditionalMetadataCollection": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CDK will expand the scope of usage data collected to better inform CDK development and improve communication for security concerns and emerging issues." + }, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": { + "userValue": false, + "recommendedValue": false, + "explanation": "[Deprecated] When enabled, Lambda will create new inline policies with AddToRolePolicy instead of adding to the Default Policy Statement" + }, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CDK will automatically generate a unique role name that is used for s3 object replication." + }, + "@aws-cdk/pipelines:reduceStageRoleTrustScope": { + "recommendedValue": true, + "explanation": "Remove the root account principal from Stage addActions trust policy", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-events:requireEventBusPolicySid": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, grantPutEventsTo() will use resource policies with Statement IDs for service principals." + }, + "@aws-cdk/core:aspectPrioritiesMutating": { + "userValue": true, + "recommendedValue": true, + "explanation": "When set to true, Aspects added by the construct library on your behalf will be given a priority of MUTATING." + }, + "@aws-cdk/aws-dynamodb:retainTableReplica": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, table replica will be default to the removal policy of source table unless specified otherwise." + }, + "@aws-cdk/cognito:logUserPoolClientSecretValue": { + "recommendedValue": false, + "explanation": "When disabled, the value of the user pool client secret will not be logged in the custom resource lambda function logs." + }, + "@aws-cdk/pipelines:reduceCrossAccountActionRoleTrustScope": { + "recommendedValue": true, + "explanation": "When enabled, scopes down the trust policy for the cross-account action role", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the resultWriterV2 property of DistributedMap will be used insted of resultWriter" + }, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": { + "userValue": true, + "recommendedValue": true, + "explanation": "Add an S3 trust policy to a KMS key resource policy for SNS subscriptions." + }, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the EgressOnlyGateway resource is only created if private subnets are defined in the dual-stack VPC." + }, + "@aws-cdk/aws-ec2-alpha:useResourceIdForVpcV2Migration": { + "recommendedValue": false, + "explanation": "When enabled, use resource IDs for VPC V2 migration" + }, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, setting any combination of options for BlockPublicAccess will automatically set true for any options not defined." + }, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CDK creates and manages loggroup for the lambda function" + }, + "@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, Network Load Balancer will be created with a security group by default." + }, + "@aws-cdk/aws-stepfunctions-tasks:httpInvokeDynamicJsonPathEndpoint": { + "recommendedValue": true, + "explanation": "When enabled, allows using a dynamic apiEndpoint with JSONPath format in HttpInvoke tasks.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-ecs-patterns:uniqueTargetGroupId": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, ECS patterns will generate unique target group IDs to prevent conflicts during load balancer replacement" + }, + "@aws-cdk/aws-route53-patterns:useDistribution": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use the `Distribution` resource instead of `CloudFrontWebDistribution`" + }, + "@aws-cdk/aws-cloudfront:defaultFunctionRuntimeV2_0": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use cloudfront-js-2.0 as the default runtime for CloudFront Functions" + }, + "@aws-cdk/aws-elasticloadbalancingv2:usePostQuantumTlsPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, HTTPS/TLS listeners use post-quantum TLS policy by default" + }, + "@aws-cdk/core:automaticL1Traits": { + "recommendedValue": true, + "explanation": "Automatically use the default L1 traits for L1 constructs`", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-batch:defaultToAL2023": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use AL2023 as the default imageType for EC2 Batch compute environments instead of the deprecated AL2" + }, + "@aws-cdk/aws-eks:defaultToAL2023": { + "recommendedValue": true, + "explanation": "Use AL2023 as the default AMI type for EKS managed node groups using non-GPU instance types instead of the deprecated AL2" + }, + "@aws-cdk/core:annotationsInValidationReport": { + "recommendedValue": true, + "explanation": "Include construct annotations (warnings and errors) in the policy validation report" + }, + "@aws-cdk/core:defaultCrossStackReferences": { + "recommendedValue": "weak", + "explanation": "Controls whether cross-region stack references are strong, weak, or both", + "unconfiguredBehavesLike": { + "v2": "strong" + } + }, + "@aws-cdk/core:validateAgainstDefaultRules": { + "recommendedValue": true, + "explanation": "Treat CloudFormation Validate findings as errors" + } + } + } + } + }, + "minimumCliVersion": "2.1131.0" +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/tree.json new file mode 100644 index 0000000000000..93b25fbc08661 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/tree.json @@ -0,0 +1 @@ +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"},"children":{"DefaultRegoRulesStack":{"id":"DefaultRegoRulesStack","path":"DefaultRegoRulesStack","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"CompliantAlias":{"id":"CompliantAlias","path":"DefaultRegoRulesStack/CompliantAlias","constructInfo":{"fqn":"aws-cdk-lib.aws_gamelift.CfnAlias","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::GameLift::Alias","aws:cdk:cloudformation:logicalId":"CompliantAlias","aws:cdk:cloudformation:props":{"name":"default-rego-rules-compliant","routingStrategy":{"type":"TERMINAL","message":"server offline for maintenance"}}}},"NeverTrue":{"id":"NeverTrue","path":"DefaultRegoRulesStack/NeverTrue","constructInfo":{"fqn":"aws-cdk-lib.CfnCondition","version":"0.0.0"}},"ViolatingAlias":{"id":"ViolatingAlias","path":"DefaultRegoRulesStack/ViolatingAlias","constructInfo":{"fqn":"aws-cdk-lib.aws_gamelift.CfnAlias","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::GameLift::Alias","aws:cdk:cloudformation:logicalId":"ViolatingAlias","aws:cdk:cloudformation:props":{"name":"default-rego-rules-violating","routingStrategy":{"type":"TERMINAL","message":"goodbye"}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"DefaultRegoRulesStack/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"DefaultRegoRulesStack/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}},"DefaultRegoRulesTest":{"id":"DefaultRegoRulesTest","path":"DefaultRegoRulesTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"DefaultRegoRulesTest/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"DefaultRegoRulesTest/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"}},"DeployAssert":{"id":"DeployAssert","path":"DefaultRegoRulesTest/DefaultTest/DeployAssert","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"DefaultRegoRulesTest/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"DefaultRegoRulesTest/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"}}}}} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/validation-report.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/validation-report.json new file mode 100644 index 0000000000000..37dc42164be29 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.js.snapshot/validation-report.json @@ -0,0 +1,62 @@ +{ + "version": "54.0.0", + "title": "Validation Report", + "pluginReports": [ + { + "pluginName": "CloudFormation Validate", + "pluginVersion": "1.5.1", + "conclusion": "success", + "violations": [ + { + "ruleName": "CDK-GameLift-005", + "description": "RoutingStrategy.FleetId: Alias with TERMINAL routing must not reference a fleet; either route to a fleet or set a terminal message, not both", + "severity": "warning", + "violatingConstructs": [ + { + "constructPath": "DefaultRegoRulesStack/ViolatingAlias", + "constructFqn": "aws-cdk-lib.aws_gamelift.CfnAlias", + "libraryVersion": "0.0.0", + "cloudFormationResource": { + "templatePath": "DefaultRegoRulesStack.template.json", + "logicalId": "ViolatingAlias", + "propertyPaths": [ + "Properties.RoutingStrategy.FleetId" + ] + } + } + ] + }, + { + "ruleName": "W8003", + "description": "Fn::Equals in condition 'NeverTrue' will always return False", + "severity": "warning", + "ruleMetadata": { + "category": "Best Practice" + }, + "violatingConstructs": [ + { + "constructPath": "DefaultRegoRulesStack", + "constructFqn": "aws-cdk-lib.Stack", + "libraryVersion": "0.0.0" + } + ] + }, + { + "ruleName": "F0001", + "description": "Resources section must exist and be non-empty", + "severity": "warning", + "ruleMetadata": { + "category": "Structure" + }, + "violatingConstructs": [ + { + "constructPath": "DefaultRegoRulesTest/DefaultTest/DeployAssert", + "constructFqn": "aws-cdk-lib.Stack", + "libraryVersion": "0.0.0" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts new file mode 100644 index 0000000000000..a770c07a20fef --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts @@ -0,0 +1,45 @@ +import * as cdk from 'aws-cdk-lib'; +import * as gamelift from 'aws-cdk-lib/aws-gamelift'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +/** + * Exercises the CDK-authored default Rego rules that ship with the + * CloudFormationValidatePlugin (core/lib/validation/rules/). + * + * The stack contains a compliant TERMINAL-routing GameLift alias (deployable + * without a fleet, no cost) and a violating alias that both routes to a fleet + * and carries a terminal message (CDK-GameLift-005). The violating alias sits + * behind a never-true condition: the default Rego rules evaluate the full + * template regardless of conditions, so the finding is still reported at + * synth, but CloudFormation never creates the invalid resource — which the + * GameLift API would reject. + * + * With the default warning posture the violation does not block deployment; + * the snapshot captures the violating template so a regression in default + * rule loading or evaluation shows up as a snapshot diff. + */ +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'DefaultRegoRulesStack'); + +new gamelift.CfnAlias(stack, 'CompliantAlias', { + name: 'default-rego-rules-compliant', + routingStrategy: { type: 'TERMINAL', message: 'server offline for maintenance' }, +}); + +const neverTrue = new cdk.CfnCondition(stack, 'NeverTrue', { + expression: cdk.Fn.conditionEquals('true', 'false'), +}); + +const violating = new gamelift.CfnAlias(stack, 'ViolatingAlias', { + name: 'default-rego-rules-violating', + routingStrategy: { type: 'TERMINAL', message: 'goodbye' }, +}); +violating.cfnOptions.condition = neverTrue; +// Inject the contradictory FleetId via escape hatch — the L2/L1 props would +// not produce this shape, which is exactly the gap the default rule covers. +violating.addPropertyOverride('RoutingStrategy.FleetId', 'fleet-11111111-2222-3333-4444-555555555555'); + +new IntegTest(app, 'DefaultRegoRulesTest', { + testCases: [stack], +}); From 160c8a673b7a293ff623a069e4f128d3b383d678 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:11:34 -0400 Subject: [PATCH 4/6] feat(core): merge default Rego rules into explicitly registered plugins An explicitly registered CloudFormationValidatePlugin replaces the auto-registered default instance, so passing custom rules would silently drop the CDK default rules. The constructor now merges the default rules with any user-supplied regoRules; a new includeDefaultRules prop (default true) opts out entirely. Also documents the default rules in the README (rule table, severity posture, suppression by ID) and adds tests covering the merge, the opt-out, and suppression of a default rule finding via Validations.of(scope).acknowledge(). --- packages/aws-cdk-lib/README.md | 40 ++++++++++ .../cloudformation-validate-plugin.ts | 24 ++++-- .../validation/default-rego-rules.test.ts | 78 +++++++++++++++++++ 3 files changed, 137 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk-lib/README.md b/packages/aws-cdk-lib/README.md index 0db2e369d0b35..b27bea5f855a3 100644 --- a/packages/aws-cdk-lib/README.md +++ b/packages/aws-cdk-lib/README.md @@ -1759,6 +1759,46 @@ Validations.of(app).addPlugins(new CloudFormationValidatePlugin({ })); ``` +An explicitly registered `CloudFormationValidatePlugin` replaces the +auto-registered default instance. Your custom rules are evaluated *in +addition to* the CDK default rules described below; pass +`includeDefaultRules: false` to opt out of the default rules entirely. + +#### CDK default rules + +In addition to the built-in rule set of the validation engine, the CDK ships +its own default rules, written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/). +These check cross-field invariants that the CloudFormation resource schemas +cannot express — mistakes that would otherwise pass template validation and +only fail at the service API during deployment. Because they run on the +synthesized template, they also cover resources defined through L1 constructs, +escape hatches, and `CfnInclude`, which construct-level (L2) validation cannot +see. + +The current rules cover Amazon GameLift resources: + +| Rule ID | Checks | +|---------|--------| +| `CDK-GameLift-001` | A fleet ingress rule's port range is not inverted (`FromPort` ≤ `ToPort`) | +| `CDK-GameLift-002` | A fleet location's capacity satisfies `MinSize` ≤ `MaxSize` | +| `CDK-GameLift-003` | A fleet location's `DesiredEC2Instances` lies within `[MinSize, MaxSize]` | +| `CDK-GameLift-004` | An alias with `SIMPLE` routing does not carry a terminal `Message` | +| `CDK-GameLift-005` | An alias with `TERMINAL` routing does not reference a fleet | + +Like all findings of this plugin, violations are reported as warnings unless +the `@aws-cdk/core:validateAgainstDefaultRules` context key is set to `true`, +in which case they become errors and fail synthesis. Individual rules can be +suppressed by their ID, using the same mechanism shown above: + +```ts fixture=validation-plugin +const app = new App(); + +Validations.of(app).acknowledge({ + id: 'CloudFormation-Validate::CDK-GameLift-001', + reason: 'This template is never deployed as-is; ports are rewritten downstream', +}); +``` + ### Additional plugins You can also add custom plugins like [cdk-nag](https://github.com/cdklabs/cdk-nag) and diff --git a/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts b/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts index 4db8e4f5ff6f6..0aa1f9ec4659e 100644 --- a/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts +++ b/packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts @@ -46,6 +46,18 @@ export interface CloudFormationValidatePluginProps { * @default - no guard rules */ readonly guardRules?: ValidationRuleSource[]; + + /** + * Whether to evaluate the default Rego rules that ship with the CDK. + * + * Registering a `CloudFormationValidatePlugin` explicitly replaces the + * auto-registered default instance, so without this flag adding custom + * rules would silently drop the CDK default rules. Individual default + * rules can be suppressed by ID via `Validations.of(scope).acknowledge()`. + * + * @default true + */ + readonly includeDefaultRules?: boolean; } /** @@ -68,9 +80,7 @@ export class CloudFormationValidatePlugin implements IPolicyValidationPlugin { */ public static _singletonInstance() { if (!CloudFormationValidatePlugin._instance) { - CloudFormationValidatePlugin._instance = new CloudFormationValidatePlugin({ - regoRules: defaultRegoRules(), - }); + CloudFormationValidatePlugin._instance = new CloudFormationValidatePlugin(); } return CloudFormationValidatePlugin._instance; } @@ -83,8 +93,12 @@ export class CloudFormationValidatePlugin implements IPolicyValidationPlugin { constructor(props: CloudFormationValidatePluginProps = {}) { const config: EngineConfig = {}; - if (props.regoRules) { - config.customRules = props.regoRules; + const regoRules = [ + ...(props.includeDefaultRules ?? true) ? defaultRegoRules() : [], + ...props.regoRules ?? [], + ]; + if (regoRules.length > 0) { + config.customRules = regoRules; } if (props.guardRules) { config.guardRules = props.guardRules; diff --git a/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts index f58734e73983b..e5c9351cd9162 100644 --- a/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts +++ b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts @@ -156,6 +156,84 @@ describe('default GameLift rules', () => { const violations = pluginViolations(loadValidationReport(app.synth())); expect(violations.filter((v) => v.ruleName.startsWith('CDK-GameLift'))).toEqual([]); }); + + test('an explicitly registered plugin with custom rules still evaluates the default rules', () => { + const app = testApp(); + core.Validations.of(app).addPlugins(new core.CloudFormationValidatePlugin({ + regoRules: [{ + name: 'my-custom.rego', + content: [ + 'package my_custom', + 'import rego.v1', + 'violation contains v if {', + ' some name in resources_of_type("AWS::GameLift::Alias")', + ' v := make_diag("MY-CUSTOM-001", "WARN", name, "custom rule fired")', + '}', + ].join('\n'), + }], + })); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyAlias', { + type: 'AWS::GameLift::Alias', + properties: { + Name: 'my-alias', + RoutingStrategy: { Type: 'SIMPLE', FleetId: 'fleet-1234', Message: 'goodbye' }, + }, + }); + + const violations = pluginViolations(loadValidationReport(app.synth())); + + // Both the user's custom rule and the CDK default rule must fire + expect(violations).toContainEqual(expect.objectContaining({ ruleName: 'MY-CUSTOM-001' })); + expect(violations).toContainEqual(expect.objectContaining({ ruleName: 'CDK-GameLift-004' })); + }); + + test('default rule findings can be suppressed via acknowledge()', () => { + const app = testApp(); + core.Validations.of(app).acknowledge({ + id: 'CloudFormation-Validate::CDK-GameLift-004', + reason: 'testing suppression of a default rule', + }); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyAlias', { + type: 'AWS::GameLift::Alias', + properties: { + Name: 'my-alias', + RoutingStrategy: { Type: 'SIMPLE', FleetId: 'fleet-1234', Message: 'goodbye' }, + }, + }); + + const report = loadValidationReport(app.synth()); + const violations = pluginViolations(report); + + // The finding is moved from violations to suppressedViolations + expect(violations.filter((v) => v.ruleName === 'CDK-GameLift-004')).toEqual([]); + const suppressed = report.pluginReports + .filter((r) => r.pluginName === 'CloudFormation Validate') + .flatMap((r) => r.suppressedViolations ?? []); + expect(suppressed).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-004', + })); + }); + + test('includeDefaultRules: false opts out of the default rules', () => { + const app = testApp(); + core.Validations.of(app).addPlugins(new core.CloudFormationValidatePlugin({ + includeDefaultRules: false, + })); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'MyAlias', { + type: 'AWS::GameLift::Alias', + properties: { + Name: 'my-alias', + RoutingStrategy: { Type: 'SIMPLE', FleetId: 'fleet-1234', Message: 'goodbye' }, + }, + }); + + const violations = pluginViolations(loadValidationReport(app.synth())); + + expect(violations.filter((v) => v.ruleName.startsWith('CDK-GameLift'))).toEqual([]); + }); }); const FLEET_BASE = { From 53d2fc7d77a8f53f1fc0fa56ea262a6b7b480015 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:41:11 -0400 Subject: [PATCH 5/6] docs(core): align default Rego rule claims with observed GameLift behavior Empirically probing the GameLift API showed the two failure modes the default rules cover are different: the fleet rules (001-003) catch real deploy-time rejections (CreateFleet refuses inverted port ranges; UpdateFleetCapacity refuses out-of-range capacity values, surfacing as a CloudFormation NotStabilized rollback), while the alias routing rules (004-005) catch contradictory configuration that GameLift accepts and partially ignores - the deployment succeeds with one field silently unused. Reword the rule file, README, and integ test comment accordingly; no behavioral changes. --- .../test/core/test/integ.default-rego-rules.ts | 10 +++++----- packages/aws-cdk-lib/README.md | 15 ++++++++++----- .../core/lib/validation/rules/gamelift-fleet.rego | 8 ++++++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts index a770c07a20fef..e548e17ab2079 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.default-rego-rules.ts @@ -8,11 +8,11 @@ import { IntegTest } from '@aws-cdk/integ-tests-alpha'; * * The stack contains a compliant TERMINAL-routing GameLift alias (deployable * without a fleet, no cost) and a violating alias that both routes to a fleet - * and carries a terminal message (CDK-GameLift-005). The violating alias sits - * behind a never-true condition: the default Rego rules evaluate the full - * template regardless of conditions, so the finding is still reported at - * synth, but CloudFormation never creates the invalid resource — which the - * GameLift API would reject. + * and carries a terminal message (CDK-GameLift-005) — contradictory + * configuration that GameLift accepts but partially ignores. The violating + * alias sits behind a never-true condition: the default Rego rules evaluate + * the full template regardless of conditions, so the finding is still + * reported at synth, while CloudFormation never creates the resource. * * With the default warning posture the violation does not block deployment; * the snapshot captures the violating template so a regression in default diff --git a/packages/aws-cdk-lib/README.md b/packages/aws-cdk-lib/README.md index b27bea5f855a3..a875a33476718 100644 --- a/packages/aws-cdk-lib/README.md +++ b/packages/aws-cdk-lib/README.md @@ -1769,11 +1769,11 @@ addition to* the CDK default rules described below; pass In addition to the built-in rule set of the validation engine, the CDK ships its own default rules, written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/). These check cross-field invariants that the CloudFormation resource schemas -cannot express — mistakes that would otherwise pass template validation and -only fail at the service API during deployment. Because they run on the -synthesized template, they also cover resources defined through L1 constructs, -escape hatches, and `CfnInclude`, which construct-level (L2) validation cannot -see. +cannot express: mistakes that pass template validation but fail at the service +API during deployment, and contradictory configuration that the service +accepts but partially ignores. Because they run on the synthesized template, +they also cover resources defined through L1 constructs, escape hatches, and +`CfnInclude`, which construct-level (L2) validation cannot see. The current rules cover Amazon GameLift resources: @@ -1785,6 +1785,11 @@ The current rules cover Amazon GameLift resources: | `CDK-GameLift-004` | An alias with `SIMPLE` routing does not carry a terminal `Message` | | `CDK-GameLift-005` | An alias with `TERMINAL` routing does not reference a fleet | +The fleet rules (001–003) catch deployment failures: the GameLift API rejects +these values, rolling back the stack mid-deployment. The alias rules (004–005) +catch contradictory routing configuration that deploys successfully but leaves +one of the two fields silently unused. + Like all findings of this plugin, violations are reported as warnings unless the `@aws-cdk/core:validateAgainstDefaultRules` context key is set to `true`, in which case they become errors and fail synthesis. Individual rules can be diff --git a/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego index 5609dbe8d088a..51aed30ff66d7 100644 --- a/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego +++ b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego @@ -8,6 +8,14 @@ import rego.v1 # limits (lengths, item counts, per-field ranges, patterns) are already # covered by the engine's built-in rules (F3031-F3034). # +# The fleet rules (001-003) catch verified deploy-time failures: the GameLift +# API rejects inverted port ranges at CreateFleet and out-of-range capacity +# values at UpdateFleetCapacity, so these mistakes otherwise surface as a +# CloudFormation rollback mid-deployment. The alias rules (004-005) catch +# contradictory configuration that the service accepts but partially ignores: +# a routing strategy carrying both a fleet and a terminal message deploys +# successfully, with one of the two fields silently unused. +# # Because these run on the synthesized template, they also cover L1 # constructs, escape hatches, and CfnInclude, and token-valued properties # are already resolved. From ba98771e41e726ba60c73f7d4f0da4b2a9c1eb06 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:14:46 +0000 Subject: [PATCH 6/6] feat(core): add cross-resource GameLift launch-path rules (CDK-GameLift-006/007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Join a fleet to the AWS::GameLift::Build it references through the template's Ref graph and check that every server-process launch path lives under the install root dictated by the build's operating system (C:\game for Windows, /local/game for Linux). A mismatch is schema-valid and deploys, but the fleet then activates into ERROR state because no server process can start. This is the first cross-resource default rule: the invariant spans two resources, so no single construct can validate it — BuildFleet only sees an IBuild interface, which erases the build's operating system. The rules stay silent for imported builds (literal BuildId) and builds that omit OperatingSystem, since the OS is unknowable in those cases. --- packages/aws-cdk-lib/README.md | 15 +- .../lib/validation/rules/gamelift-fleet.rego | 68 ++++++++ .../validation/default-rego-rules.test.ts | 150 ++++++++++++++++++ 3 files changed, 231 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/README.md b/packages/aws-cdk-lib/README.md index a875a33476718..e30688a3a2b9c 100644 --- a/packages/aws-cdk-lib/README.md +++ b/packages/aws-cdk-lib/README.md @@ -1773,7 +1773,9 @@ cannot express: mistakes that pass template validation but fail at the service API during deployment, and contradictory configuration that the service accepts but partially ignores. Because they run on the synthesized template, they also cover resources defined through L1 constructs, escape hatches, and -`CfnInclude`, which construct-level (L2) validation cannot see. +`CfnInclude`, which construct-level (L2) validation cannot see — and they can +check invariants *across* resources by following the template's `Ref` graph, +which no single construct can validate in isolation. The current rules cover Amazon GameLift resources: @@ -1784,11 +1786,20 @@ The current rules cover Amazon GameLift resources: | `CDK-GameLift-003` | A fleet location's `DesiredEC2Instances` lies within `[MinSize, MaxSize]` | | `CDK-GameLift-004` | An alias with `SIMPLE` routing does not carry a terminal `Message` | | `CDK-GameLift-005` | An alias with `TERMINAL` routing does not reference a fleet | +| `CDK-GameLift-006` | A fleet referencing a Windows build launches server processes from `C:\game` | +| `CDK-GameLift-007` | A fleet referencing a Linux build launches server processes from `/local/game` | The fleet rules (001–003) catch deployment failures: the GameLift API rejects these values, rolling back the stack mid-deployment. The alias rules (004–005) catch contradictory routing configuration that deploys successfully but leaves -one of the two fields silently unused. +one of the two fields silently unused. The launch-path rules (006–007) are +cross-resource: they join a fleet to the `AWS::GameLift::Build` it references +and check that every server-process launch path lives under the install root +dictated by the build's operating system. A mismatch deploys, but the fleet +then activates into `ERROR` state because no server process can start. These +rules only fire when the build is defined in the same template; a fleet +referencing an imported build (a literal build ID) is not checked, since the +build's operating system is not knowable from the template. Like all findings of this plugin, violations are reported as warnings unless the `@aws-cdk/core:validateAgainstDefaultRules` context key is set to `true`, diff --git a/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego index 51aed30ff66d7..61b223f5606b8 100644 --- a/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego +++ b/packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego @@ -16,6 +16,16 @@ import rego.v1 # a routing strategy carrying both a fleet and a terminal message deploys # successfully, with one of the two fields silently unused. # +# The cross-resource rules (006-007) join a fleet to the build it references +# through the template's Ref graph — an invariant no construct can check in +# isolation: a Build's operating system determines the filesystem layout on +# fleet instances, so every server-process launch path must live under the +# OS-specific install root. A mismatch is schema-valid and deploys, then the +# fleet activates and lands in ERROR state when no server process can start. +# The join only exists when the build is defined in the same template; for an +# imported build (literal BuildId string) the OS is unknowable and the rules +# stay silent. +# # Because these run on the synthesized template, they also cover L1 # constructs, escape hatches, and CfnInclude, and token-valued properties # are already resolved. @@ -106,3 +116,61 @@ violation contains v if { "Alias with TERMINAL routing must not reference a fleet; either route to a fleet or set a terminal message, not both", ) } + +# Game builds are installed on fleet instances at an OS-specific root: +# C:\game on Windows, /local/game on Linux. +# https://docs.aws.amazon.com/gamelift/latest/developerguide/fleets-multiprocess.html +windows_launch_root := "C:\\game" + +linux_launch_root := "/local/game" + +# The Build a fleet references, when it is defined in the same template. +# resolve() follows the Ref to the build's logical ID; for an imported build +# the BuildId is a literal fleet-external ID and the resource lookup fails, +# so the cross-resource rules stay silent. +build_for_fleet(fleet_name) := build if { + build_name := resolve(fleet_name, "Properties.BuildId") + is_string(build_name) + build := input.resources[build_name] + build.resourceType == "AWS::GameLift::Build" +} + +# A fleet on a Windows build must launch server processes from C:\game +violation contains v if { + some fleet_name in resources_of_type("AWS::GameLift::Fleet") + build := build_for_fleet(fleet_name) + os := build.properties.OperatingSystem + is_string(os) + startswith(os, "WINDOWS_") + processes := resolve(fleet_name, "Properties.RuntimeConfiguration.ServerProcesses") + is_array(processes) + some i, process in processes + launch_path := process.LaunchPath + is_string(launch_path) + not startswith(launch_path, windows_launch_root) + v := make_diag_at( + "CDK-GameLift-006", "ERROR", fleet_name, + sprintf("Properties.RuntimeConfiguration.ServerProcesses.%d.LaunchPath", [i]), + sprintf("Launch path %v does not match the referenced build's Windows operating system (%v); Windows launch paths must start with C:\\game", [launch_path, os]), + ) +} + +# A fleet on a Linux build must launch server processes from /local/game +violation contains v if { + some fleet_name in resources_of_type("AWS::GameLift::Fleet") + build := build_for_fleet(fleet_name) + os := build.properties.OperatingSystem + is_string(os) + startswith(os, "AMAZON_LINUX") + processes := resolve(fleet_name, "Properties.RuntimeConfiguration.ServerProcesses") + is_array(processes) + some i, process in processes + launch_path := process.LaunchPath + is_string(launch_path) + not startswith(launch_path, linux_launch_root) + v := make_diag_at( + "CDK-GameLift-007", "ERROR", fleet_name, + sprintf("Properties.RuntimeConfiguration.ServerProcesses.%d.LaunchPath", [i]), + sprintf("Launch path %v does not match the referenced build's Linux operating system (%v); Linux launch paths must start with /local/game", [launch_path, os]), + ) +} diff --git a/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts index e5c9351cd9162..4f7431f43ba98 100644 --- a/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts +++ b/packages/aws-cdk-lib/core/test/validation/default-rego-rules.test.ts @@ -122,6 +122,156 @@ describe('default GameLift rules', () => { })); }); + test('CDK-GameLift-006 fires when a fleet on a Windows build launches from a non-Windows path', () => { + const app = testApp(); + const stack = new core.Stack(app, 'TestStack'); + const build = new core.CfnResource(stack, 'MyBuild', { + type: 'AWS::GameLift::Build', + properties: { + Name: 'my-build', + OperatingSystem: 'WINDOWS_2016', + }, + }); + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + ...FLEET_BASE, + BuildId: build.ref, + RuntimeConfiguration: { + ServerProcesses: [ + { LaunchPath: '/local/game/server', ConcurrentExecutions: 1 }, + ], + }, + }, + }); + + expect(pluginViolations(loadValidationReport(app.synth()))).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-006', + description: expect.stringContaining('does not match the referenced build\'s Windows operating system'), + })); + }); + + test('CDK-GameLift-007 fires when a fleet on a Linux build launches from a non-Linux path', () => { + const app = testApp(); + const stack = new core.Stack(app, 'TestStack'); + const build = new core.CfnResource(stack, 'MyBuild', { + type: 'AWS::GameLift::Build', + properties: { + Name: 'my-build', + OperatingSystem: 'AMAZON_LINUX_2023', + }, + }); + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + ...FLEET_BASE, + BuildId: build.ref, + RuntimeConfiguration: { + ServerProcesses: [ + { LaunchPath: 'C:\\game\\server.exe', ConcurrentExecutions: 1 }, + ], + }, + }, + }); + + expect(pluginViolations(loadValidationReport(app.synth()))).toContainEqual(expect.objectContaining({ + ruleName: 'CDK-GameLift-007', + description: expect.stringContaining('does not match the referenced build\'s Linux operating system'), + })); + }); + + test('cross-resource rules stay silent for an imported build', () => { + const app = testApp(); + const stack = new core.Stack(app, 'TestStack'); + // BuildId is a literal external ID — the build's OS is unknowable, so no + // launch-path finding regardless of path shape. + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + ...FLEET_BASE, + BuildId: 'build-11111111-2222-3333-4444-555555555555', + RuntimeConfiguration: { + ServerProcesses: [ + { LaunchPath: 'some/relative/path', ConcurrentExecutions: 1 }, + ], + }, + }, + }); + + const violations = pluginViolations(loadValidationReport(app.synth())); + expect(violations.filter((v) => v.ruleName === 'CDK-GameLift-006' || v.ruleName === 'CDK-GameLift-007')).toEqual([]); + }); + + test('cross-resource rules stay silent when the build omits OperatingSystem', () => { + const app = testApp(); + const stack = new core.Stack(app, 'TestStack'); + // GameLift applies a server-side default OS; rather than guess it, the + // rules only fire when the OS is stated in the template. + const build = new core.CfnResource(stack, 'MyBuild', { + type: 'AWS::GameLift::Build', + properties: { + Name: 'my-build', + }, + }); + new core.CfnResource(stack, 'MyFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + ...FLEET_BASE, + BuildId: build.ref, + RuntimeConfiguration: { + ServerProcesses: [ + { LaunchPath: 'some/relative/path', ConcurrentExecutions: 1 }, + ], + }, + }, + }); + + const violations = pluginViolations(loadValidationReport(app.synth())); + expect(violations.filter((v) => v.ruleName === 'CDK-GameLift-006' || v.ruleName === 'CDK-GameLift-007')).toEqual([]); + }); + + test('no cross-resource findings when launch paths match the build OS', () => { + const app = testApp(); + const stack = new core.Stack(app, 'TestStack'); + const windowsBuild = new core.CfnResource(stack, 'WindowsBuild', { + type: 'AWS::GameLift::Build', + properties: { Name: 'windows-build', OperatingSystem: 'WINDOWS_2016' }, + }); + const linuxBuild = new core.CfnResource(stack, 'LinuxBuild', { + type: 'AWS::GameLift::Build', + properties: { Name: 'linux-build', OperatingSystem: 'AMAZON_LINUX_2023' }, + }); + new core.CfnResource(stack, 'WindowsFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + ...FLEET_BASE, + Name: 'windows-fleet', + BuildId: windowsBuild.ref, + RuntimeConfiguration: { + ServerProcesses: [ + { LaunchPath: 'C:\\game\\server.exe', ConcurrentExecutions: 1 }, + ], + }, + }, + }); + new core.CfnResource(stack, 'LinuxFleet', { + type: 'AWS::GameLift::Fleet', + properties: { + ...FLEET_BASE, + Name: 'linux-fleet', + BuildId: linuxBuild.ref, + RuntimeConfiguration: { + ServerProcesses: [ + { LaunchPath: '/local/game/server', ConcurrentExecutions: 1 }, + ], + }, + }, + }); + + const violations = pluginViolations(loadValidationReport(app.synth())); + expect(violations.filter((v) => v.ruleName.startsWith('CDK-GameLift'))).toEqual([]); + }); + test('no GameLift findings for compliant resources', () => { const app = testApp(); const stack = new core.Stack(app, 'TestStack');