Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ resolver = "2"
name = "cloudformation-validate"

[workspace.package]
version = "1.3.0"
version = "1.4.0"
edition = "2024"
license = "Apache-2.0"
description = "AWS CloudFormation Validate"
Expand Down
2 changes: 1 addition & 1 deletion src/bindings-jvm/tests/kotlin/src/test/kotlin/SmokeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ class SmokeTest {

@Test
fun engineVersionMatchesWorkspaceVersion() {
val expected = "1.3.0"
val expected = "1.4.0"
assertEquals(expected, readWorkspaceVersion(), "expected version must match workspace Cargo.toml")
assertEquals(expected, CEL.validateDetailed(templateFile("good/generic.yaml"), defaultConfig()).engineVersion, "cel: engineVersion")
assertEquals(expected, REGO.validateDetailed(templateFile("good/generic.yaml"), defaultConfig()).engineVersion, "rego: engineVersion")
Expand Down
2 changes: 1 addition & 1 deletion src/bindings-wasm/tests/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@

describe("version", () => {
it("returns the crate version from workspace Cargo.toml", () => {
expect(version()).toBe(readWorkspaceVersion());

Check failure on line 102 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > version > returns the crate version from workspace Cargo.toml

AssertionError: expected '1.3.0' to be '1.4.0' // Object.is equality Expected: "1.4.0" Received: "1.3.0" ❯ tests/smoke.test.ts:102:23
});
});

Expand Down Expand Up @@ -391,7 +391,7 @@
for (const rel of EXPECTED_TEMPLATES) {
it(rel, () => {
const actual = engine.validateDetailed(loadTemplate(rel), { severityLevel: "DEBUG" });
expect(stripGoldenExcludedFields(actual, rel)).toEqual(stripGoldenExcludedFields(loadGolden(rel)));

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/codepipeline_bad_artifact_counts.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -34,24 +34,24 @@ "startColumn": 3, "startLine": 4, }, { "category": "Best Practice", - "endColumn": 14, - "endLine": 7, + "endColumn": 11, + "endLine": 4, "message": "Property 'RoleArn' has a hardcoded ARN - use Ref, GetAtt, or a parameter instead", "phase": "LINT", "propertyPath": "Properties.RoleArn", "resourceId": "Pipeline", "resourceType": "AWS::CodePipeline::Pipeline", "ruleDescription": "Hardcoded ARN property", "ruleId": "W9002", "section": "Resources", "severity": "WARN", "source": "ENGINE", - "startColumn": 7, - "startLine": 7, + "startColumn": 3, + "startLine": 4, }, { "category": "Best Practice", "endColumn": 11, "endLine": 4, ❯ tests/smoke.test.ts:394:58

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/cloudfront_bad_origin.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -1,44 +1,44 @@ { "diagnostics": [ { "category": "Resource", - "endColumn": 25, - "endLine": 14, + "endColumn": 15, + "endLine": 4, "message": "TargetOriginId 'NonExistentOrigin' does not match any Origin Id in the distribution", "phase": "LINT", "propertyPath": "Properties.DistributionConfig.DefaultCacheBehavior.TargetOriginId", "resourceId": "Distribution", "resourceType": "AWS::CloudFront::Distribution", "ruleDescription": "Validate that CloudFront TargetOriginId is a specified Origin", "ruleId": "E3057", "section": "Resources", "severity": "ERROR", "source": "CFN_LINT", - "startColumn": 11, - "startLine": 14, + "startColumn": 3, + "startLine": 4, "suggestedFix": "Set TargetOriginId to match one of the Origin Ids defined in Origins", }, { "category": "Deprecation", "context": { "lifecycle": "deprecated", }, - "endColumn": 25, - "endLine": 7, + "endColumn": 15, + "endLine": 4, "message": "Property 'DistributionConfig' is deprecated", "phase": "SCHEMA", "propertyPath": "Properties.DistributionConfig", "resourceId": "Distribution", "resourceType": "AWS::CloudFront::Distribution", "ruleDescription": "Resource type sunset or shutdown", "ruleId": "W9009", "section": "Resources", "severity": "WARN", "source": "ENGINE", - "startColumn": 7, - "startLine": 7, + "startColumn": 3, + "startLine": 4, }, { "category": "Best Practice", "endColumn": 15, "endLine": 4, ❯ tests/smoke.test.ts:394:58

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/cloudfront_bad_alias.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -20,24 +20,24 @@ { "category": "Deprecation", "context": { "lifecycle": "deprecated", }, - "endColumn": 25, - "endLine": 7, + "endColumn": 7, + "endLine": 4, "message": "Property 'DistributionConfig' is deprecated", "phase": "SCHEMA", "propertyPath": "Properties.DistributionConfig", "resourceId": "Dist", "resourceType": "AWS::CloudFront::Distribution", "ruleDescription": "Resource type sunset or shutdown", "ruleId": "W9009", "section": "Resources", "severity": "WARN", "source": "ENGINE", - "startColumn": 7, - "startLine": 7, + "startColumn": 3, + "startLine": 4, }, { "category": "Best Practice", "endColumn": 7, "endLine": 4, ❯ tests/smoke.test.ts:394:58

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/aurora_with_allocated_storage.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -1,24 +1,24 @@ { "diagnostics": [ { "category": "Best Practice", "documentationUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git", - "endColumn": 23, - "endLine": 10, + "endColumn": 11, + "endLine": 4, "message": "100 is not of type 'string' - automatically coerced (number to string)", "phase": "SCHEMA", "propertyPath": "Properties.AllocatedStorage", "resourceId": "AuroraDB", "resourceType": "AWS::RDS::DBInstance", "ruleDescription": "Property type coercion warning", "ruleId": "W9003", "section": "Resources", "severity": "WARN", "source": "ENGINE", - "startColumn": 7, - "startLine": 10, + "startColumn": 3, + "startLine": 4, }, { "category": "Security", "endColumn": 11, "endLine": 4, @@ -71,24 +71,24 @@ "category": "Best Practice", "context": { "lifecycle": "create-only", }, "documentationUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git", - "endColumn": 26, - "endLine": 8, + "endColumn": 11, + "endLine": 4, "message": "Property 'DBClusterIdentifier' is create-only; updating it will cause resource replacement", "phase": "SCHEMA", "propertyPath": "Properties.DBClusterIdentifier", "resourceId": "AuroraDB", "resourceType": "AWS::RDS::DBInstance", "ruleDescription": "Create-only property updated triggers resource replacement", "ruleId": "I9001", "section": "Resources", "severity": "INFO", "source": "ENGINE", - "startColumn": 7, - "startLine": 8, + "startColumn": 3, + "startLine": 4, }, { "category": "Best Practice", "endColumn": 11, "endLine": 4, ❯ tests/smoke.test.ts:394:58

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/W3010_full_coverage.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -34,92 +34,92 @@ "startColumn": 3, "startLine": 6, }, { "category": "Best Practice", - "endColumn": 23, - "endLine": 18, + "endColumn": 7, + "endLine": 15, "message": "Avoid hardcoding availability zones 'us-east-1a'", "phase": "LINT", "propertyPath": "Properties.AvailabilityZone", "resourceId": "Host", "resourceType": "AWS::EC2::Host", "ruleDescription": "Availability zone properties should not be hardcoded", "ruleId": "W3010", "section": "Resources", "severity": "WARN", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 18, + "startColumn": 3, + "startLine": 15, }, { "category": "Best Practice", - "endColumn": 23, - "endLine": 23, + "endColumn": 11, + "endLine": 20, "message": "Avoid hardcoding availability zones 'us-east-1a'", "phase": "LINT", "propertyPath": "Properties.AvailabilityZone", "resourceId": "Instance", "resourceType": "AWS::EC2::Instance", "ruleDescription": "Availability zone properties should not be hardcoded", "ruleId": "W3010", "section": "Resources", "severity": "WARN", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 23, + "startColumn": 3, + "startLine": 20, }, { "category": "Best Practice", - "endColumn": 27, - "endLine": 31, + "endColumn": 5, + "endLine": 26, "message": "Avoid hardcoding availability zones 'us-east-1a'", "phase": "LINT", "propertyPath": "Properties.LaunchTemplateData.Placement.AvailabilityZone", "resourceId": "Lt", "resourceType": "AWS::EC2::LaunchTemplate", "ruleDescription": "Availability zone properties should not be hardcoded", "ruleId": "W3010", "section": "Resources", "severity": "WARN", "source": "CFN_LINT", - "startColumn": 11, - "startLine": 31, + "startColumn": 3, + "startLine": 26, }, { "category": "Best Practice", - "endColumn": 23, - "endLine": 37, + "endColumn": 9, + "endLine": 32, "message": "Avoid hardcoding availability zones 'us-east-1a'", "phase": "LINT", "propertyPath": "Properties.AvailabilityZone", "resourceId": "Subnet", "resourceType": "AWS::EC2::Subnet", "ruleDescription": "Availability zone properties should not be hardcoded", "ruleId": "W3010", "section": "Resources", "severity": "WARN", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 37, + "startColumn": 3, + "startLine": 32, }, { "category": "Best Practice", - "endColumn": 23, - "endLine": 41, + "endColumn": 9, + "endLine": 38, "message": "Avoid hardcoding availability zones 'us-east-1a'", "phase": "LINT", "propertyPath": "Properties.AvailabilityZone", "resourceId": "Volume", "resourceType": "AWS::EC2::Volume", "ruleDescription": "Availability zone properties should not be hardcoded", "ruleId": "W3010", "section": "Resources", "severity": "WARN", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 41, + "startColumn": 3, + "startLine": 38, }, { "category": "Best Practice", "endColumn": 6, "endLine": 43, @@ -136,41 +136,41 @@ "startColumn": 3, "startLine": 43, }, { "category": "Best Practice", - "endColumn": 14, - "endLine": 58, + "endColumn": 5, + "endLine": 52, "message": "Avoid hardcoding availability zones 'us-east-1a'", "phase": "LINT", "propertyPath": "Properties.Targets.0.AvailabilityZone", "resourceId": "Tg",

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/W1028_allowedvalues_excludes_literal.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -4,24 +4,24 @@ "category": "Best Practice", "context": { "lifecycle": "create-only", "resolutionSource": "Fn::If on condition 'IsProd'", }, - "endColumn": 17, - "endLine": 28, + "endColumn": 9, + "endLine": 25, "message": "Property 'BucketName' is create-only; updating it will cause resource replacement", "phase": "SCHEMA", "propertyPath": "Properties.BucketName", "resourceId": "Bucket", "resourceType": "AWS::S3::Bucket", "ruleDescription": "Create-only property updated triggers resource replacement", "ruleId": "I9001", "section": "Resources", "severity": "INFO", "source": "ENGINE", - "startColumn": 7, - "startLine": 28, + "startColumn": 3, + "startLine": 25, }, { "category": "Best Practice", "endColumn": 9, "endLine": 25, ❯ tests/smoke.test.ts:394:58

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/F2002_ssm_parameter_type_invalid.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -18,24 +18,24 @@ "category": "Best Practice", "context": { "lifecycle": "create-only", "resolutionSource": "parameter 'parameter BadType value unknown' (type AWS::SSM::Parameter::Type)", }, - "endColumn": 17, - "endLine": 12, + "endColumn": 9, + "endLine": 9, "message": "Property 'BucketName' is create-only; updating it will cause resource replacement", "phase": "SCHEMA", "propertyPath": "Properties.BucketName", "resourceId": "Bucket", "resourceType": "AWS::S3::Bucket", "ruleDescription": "Create-only property updated triggers resource replacement", "ruleId": "I9001", "section": "Resources", "severity": "INFO", "source": "ENGINE", - "startColumn": 7, - "startLine": 12, + "startColumn": 3, + "startLine": 9, }, { "category": "Best Practice", "endColumn": 9, "endLine": 9, ❯ tests/smoke.test.ts:394:58

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/E3019_four_way_group.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -1,154 +1,154 @@ { "diagnostics": [ { "category": "Resource", - "endColumn": 17, - "endLine": 10, + "endColumn": 4, + "endLine": 7, "message": "Primary identifiers {'BucketName': 'shared-name'} should have unique values across the resources {'A', 'B', 'C', 'D'}", "phase": "LINT", "propertyPath": "Properties.BucketName", "resourceId": "A", "resourceType": "AWS::S3::Bucket", "ruleDescription": "Validate that all resources have unique primary identifiers", "ruleId": "E3019", "section": "Resources", "severity": "ERROR", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 10, + "startColumn": 3, + "startLine": 7, }, { "category": "Resource", - "endColumn": 17, - "endLine": 14, + "endColumn": 4, + "endLine": 11, "message": "Primary identifiers {'BucketName': 'shared-name'} should have unique values across the resources {'A', 'B', 'C', 'D'}", "phase": "LINT", "propertyPath": "Properties.BucketName", "resourceId": "B", "resourceType": "AWS::S3::Bucket", "ruleDescription": "Validate that all resources have unique primary identifiers", "ruleId": "E3019", "section": "Resources", "severity": "ERROR", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 14, + "startColumn": 3, + "startLine": 11, }, { "category": "Resource", - "endColumn": 17, - "endLine": 18, + "endColumn": 4, + "endLine": 15, "message": "Primary identifiers {'BucketName': 'shared-name'} should have unique values across the resources {'A', 'B', 'C', 'D'}", "phase": "LINT", "propertyPath": "Properties.BucketName", "resourceId": "C", "resourceType": "AWS::S3::Bucket", "ruleDescription": "Validate that all resources have unique primary identifiers", "ruleId": "E3019", "section": "Resources", "severity": "ERROR", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 18, + "startColumn": 3, + "startLine": 15, }, { "category": "Resource", - "endColumn": 17, - "endLine": 22, + "endColumn": 4, + "endLine": 19, "message": "Primary identifiers {'BucketName': 'shared-name'} should have unique values across the resources {'A', 'B', 'C', 'D'}", "phase": "LINT", "propertyPath": "Properties.BucketName", "resourceId": "D", "resourceType": "AWS::S3::Bucket", "ruleDescription": "Validate that all resources have unique primary identifiers", "ruleId": "E3019", "section": "Resources", "severity": "ERROR", "source": "CFN_LINT", - "startColumn": 7, - "startLine": 22, + "startColumn": 3, + "startLine": 19, }, { "category": "Best Practice", "context": { "lifecycle": "create-only", }, - "endColumn": 17, - "endLine": 10, + "endColumn": 4, + "endLine": 7, "message": "Property 'BucketName' is create-only; updating it will cause resource replacement", "phase": "SCHEMA", "propertyPath": "Properties.BucketName", "resourceId": "A", "resourceType": "AWS::S3::Bucket", "ruleDescription": "Create-only property updated triggers resource replacement", "ruleId": "I9001", "section": "Resources", "severity": "INFO", "source": "ENGINE", - "startColumn": 7, - "startLine": 10, + "startColumn": 3, + "startLine": 7, }, { "category": "Best Practice", "context": { "lifecycle": "create-only", }, - "endColumn": 17, - "endLine": 14, + "endColumn": 4, + "endLine": 11, "message": "Property 'B

Check failure on line 394 in src/bindings-wasm/tests/smoke.test.ts

View workflow job for this annotation

GitHub Actions / validate / test-wasm

tests/smoke.test.ts > golden file validation > rego detailed matches golden > bad/E1150_network_interfaces_groupset_multi.yaml

AssertionError: expected { …(4) } to deeply equal { …(4) } - Expected + Received @@ -42,66 +42,66 @@ "startColumn": 3, "startLine": 7, }, { "category": "Security", - "endColumn": 14, - "endLine": 10, + "endColumn": 11, + "endLine": 7, "message": "Hardcoded AMI ID - use a parameter or mapping for portability", "phase": "LINT", "propertyPath": "Properties.ImageId", "resourceId": "Instance", "resourceType": "AWS::EC2::Instance", "ruleDescription": "Hardcoded AMI ID", "ruleId": "W9010", "section": "Resources", "severity": "WARN", "source": "ENGINE", - "startColumn": 7, - "startLine": 10, + "startColumn": 3, + "startLine": 7, }, { "category": "Best Practice", "context": { "lifecycle": "create-only", }, "documentationUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-ec2.git", - "endColumn": 14, - "endLine": 10, + "endColumn": 11, + "endLine": 7, "message": "Property 'ImageId' is create-only; updating it will cause resource replacement", "phase": "SCHEMA", "propertyPath": "Properties.ImageId", "resourceId": "Instance", "resourceType": "AWS::EC2::Instance", "ruleDescription": "Create-only property updated triggers resource replacement", "ruleId": "I9001", "section": "Resources", "severity": "INFO", "source": "ENGINE", - "startColumn": 7, - "startLine": 10, + "startColumn": 3, + "startLine": 7, }, { "category": "Best Practice", "context": { "lifecycle": "create-only", }, "documentationUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-ec2.git", - "endColumn": 24, - "endLine": 12, + "endColumn": 11, + "endLine": 7, "message": "Property 'NetworkInterfaces' is create-only; updating it will cause resource replacement", "phase": "SCHEMA", "propertyPath": "Properties.NetworkInterfaces", "resourceId": "Instance", "resourceType": "AWS::EC2::Instance", "ruleDescription": "Create-only property updated triggers resource replacement", "ruleId": "I9001", "section": "Resources", "severity": "INFO", "source": "ENGINE", - "startColumn": 7, - "startLine": 12, + "startColumn": 3, + "startLine": 7, }, { "category": "Best Practice", "endColumn": 11, "endLine": 7, ❯ tests/smoke.test.ts:394:58
});
}
});
Expand Down Expand Up @@ -419,7 +419,7 @@

// The full built-in rule set is fixed and evaluated by both engines with no category filters.
const EXPECTED_RULES_EVALUATED = 278;
const EXPECTED_ENGINE_VERSION = "1.3.0";
const EXPECTED_ENGINE_VERSION = "1.4.0";

it("rulesEvaluated is the full built-in rule count under both engines", () => {
for (const [name, engine] of [["cel", CEL], ["rego", REGO]] as const) {
Expand Down
4 changes: 2 additions & 2 deletions src/cel-engine/src/rules/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,8 @@ fn has_language_extensions(model: &SemanticModel) -> bool {
/// (`AWS::CloudFormation::Stack`) are already skipped entirely before this check,
/// so the only type that reaches here needing the exemption is the provisioned
/// product. Every other dotted attribute (e.g. `Tags.0` on a bucket) is a real
/// attribute-validity error, matching the reference tool — an object/array
/// attribute is NOT itself indexable via GetAtt.
/// attribute-validity error, because CloudFormation does not expose an
/// object/array attribute as itself indexable via GetAtt.
fn getatt_attr_is_map_member(attr: &str, rtype: &str) -> bool {
rtype == "AWS::ServiceCatalog::CloudFormationProvisionedProduct" && attr.starts_with("Outputs.")
}
Expand Down
76 changes: 36 additions & 40 deletions src/cel-engine/src/rules/resources_extra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ fn resolve_concrete(m: &SemanticModel, rid: &str, path: &str) -> Option<serde_js
/// `OwnershipControls.Rules` array is present with at least one entry. An absent
/// `OwnershipControls`, an absent `Rules`, or an empty `Rules: []` is not
/// effective (S3 Object Ownership defaults to BucketOwnerEnforced, which disables
/// ACLs, so an `AccessControl` value would be ignored). Mirrors the reference
/// linter's `minItems: 1` requirement on `Rules`.
/// ACLs, so an `AccessControl` value would be ignored). The schema requires at
/// least one entry in `Rules` (`minItems: 1`).
fn has_effective_ownership_controls(m: &SemanticModel, rid: &str) -> bool {
resolve_concrete(m, rid, "Properties.OwnershipControls.Rules")
.as_ref()
Expand Down Expand Up @@ -913,8 +913,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
.unwrap_or(false);
// The string branch only tests a genuinely literal SourceArn. A value
// supplied via a Ref/GetAtt/Sub (even one that resolves to a concrete
// account-less string) is not folded into the pattern check — the
// reference tool only pattern-matches a literal string here.
// account-less string) is not folded into the pattern check — only a
// literal string is pattern-matched here.
let source_arn_string_without_account = !m.is_from_intrinsic(name, "Properties.SourceArn")
&& resolve_concrete(m, name, "Properties.SourceArn")
.as_ref()
Expand Down Expand Up @@ -1272,10 +1272,9 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
let key = format!("{owner}/{category}/{provider}");
if let Some(counts) = ctx.cached_data.codepipeline_artifact_counts.get(&key) {
// An artifact list may be authored directly or wrapped
// in an Fn::If; enumerate every branch's count (like the
// reference tool, which walks each path) so a violation
// in ANY branch is reported, and dedupe so equal counts
// are not double-reported.
// in an Fn::If; enumerate every branch's count (walk each
// path) so a violation in ANY branch is reported, and
// dedupe so equal counts are not double-reported.
for actual_in in artifact_count_scenarios(action.get("InputArtifacts")) {
if actual_in < counts.min_input {
out.push(make_resource_diagnostic(
Expand Down Expand Up @@ -2326,8 +2325,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
&& let Some(deploy_api) = m.follow_ref(deployment_name, "Properties.RestApiId")
&& stage_api != deploy_api
{
// The reference linter reports this through a $data const constraint
// whose error renders the Stage's own RestApiId value as
// A Deployment's RestApiId must match the Stage's RestApiId; the
// finding renders the Stage's own RestApiId value as
// "<value> was expected".
out.push(make_resource_diagnostic(
"E3698",
Expand All @@ -2343,9 +2342,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
for name in m.resources_of_type("AWS::AutoScaling::AutoScalingGroup") {
// MinSize/MaxSize are typically authored as strings ('10'), which
// CloudFormation coerces to numbers; compare on the coerced integers so
// string forms are checked too. The reference linter anchors the finding at MaxSize
// and renders it as the constraint that is violated (the max is below
// the minimum), so mirror that message and location.
// string forms are checked too. Anchor the finding at MaxSize and render
// it as the constraint that is violated (the max is below the minimum).
let min_raw = resolve_concrete(m, name, "Properties.MinSize");
let max_raw = resolve_concrete(m, name, "Properties.MaxSize");
if let (Some(min_raw), Some(max_raw)) = (min_raw.as_ref(), max_raw.as_ref())
Expand Down Expand Up @@ -2439,13 +2437,12 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
for name in m.resources_of_type("AWS::Lambda::Function") {
if resolve_concrete(m, name, "Properties.PackageType").as_ref().and_then(|v| v.as_str()) == Some("Image") {
// A container-image function must not set Handler, Runtime, or Layers.
// The reference linter reports this once with a fixed message, anchored at the
// first offending property present (schema order), no matter how many
// are set — so collapse to a single diagnostic. Presence is keyed on
// the property being SET (the reference tool's `dependentExcluded` is
// key-presence), not on it resolving to a concrete value — so an
// excluded property whose value is an unresolved Ref/intrinsic still
// anchors the finding.
// Report this once with a fixed message, anchored at the first
// offending property present (schema order), no matter how many are
// set — so collapse to a single diagnostic. Presence is keyed on the
// property being SET (key-presence), not on it resolving to a concrete
// value — so an excluded property whose value is an unresolved
// Ref/intrinsic still anchors the finding.
let props = m.resources.get(name.as_str()).map(|r| &r.properties);
if let Some(first_excluded) = ["Handler", "Runtime", "Layers"]
.iter()
Expand Down Expand Up @@ -2502,9 +2499,9 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
let iops_path = format!("{}.Ebs.Iops", bdm_path);
// The numeric bounds only apply to a literal Iops. An Iops supplied via a
// parameter Ref (whose default happens to be out of range) or another
// intrinsic is not flagged — the reference tool does not fold the
// parameter default into the bound check. The required-when-absent check
// still holds regardless, since absence is unambiguous.
// intrinsic is not flagged — the parameter default is not folded into the
// bound check. The required-when-absent check still holds regardless,
// since absence is unambiguous.
let iops_is_literal = !m.is_from_parameter(name, &iops_path) && !m.is_from_intrinsic(name, &iops_path);
match ebs.get("Iops").and_then(cfn_coerce_to_integer) {
None => {
Expand Down Expand Up @@ -2786,8 +2783,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
// match applies, so the class must be in ALL matching branches'
// enums (the intersection); a dynamic or unmatched Engine leaves no
// matching branch and is not validated. The Engine value is matched
// case-insensitively, mirroring the reference linter (which lowercases
// Engine for DBInstance before evaluating this schema).
// case-insensitively for DBInstance (the Engine value is lowercased
// before evaluating this schema).
for name in m.resources_of_type("AWS::RDS::DBInstance") {
let Some(serde_json::Value::String(val)) = resolve_concrete(m, name, "Properties.DBInstanceClass")
else {
Expand All @@ -2813,8 +2810,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
}

// E3694: RDS DBCluster DBClusterInstanceClass. Like E3025 this is a
// conditional schema keyed on Engine, but the reference linter does NOT
// lowercase Engine for DBCluster, so match the const case-sensitively.
// conditional schema keyed on Engine, but Engine is NOT lowercased for
// DBCluster, so match the const case-sensitively.
if let Some(region_data) = ctx
.cached_data
.enum_data
Expand Down Expand Up @@ -2996,10 +2993,9 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
}

// A mis-cased property name is already reported as an unknown property
// (F3002, "Additional properties are not allowed"), exactly as the reference
// linter reports it. A separate casing diagnostic under E3011 is both a
// false positive (the reference linter's E3011 is a name-length limit, not a
// casing check) and an engine-only finding, so it is not emitted.
// (F3002, "Additional properties are not allowed"). A separate casing
// diagnostic under E3011 would be a false positive, since E3011 enforces a
// name-length limit rather than a casing check, so it is not emitted.

// Route53 RecordSet validation
for name in m.resources_of_type("AWS::Route53::RecordSet") {
Expand Down Expand Up @@ -3766,13 +3762,13 @@ fn check_bdm_virtualname_ignored(
/// properties (via `resolve_prop`). `target_prop` is the class property the enum
/// constrains (`DBInstanceClass` for RDS DBInstance, `DBClusterInstanceClass` for
/// DBCluster) and is excluded from the required-const match. Returns one enum per
/// matching branch (the reference tool evaluates the whole conditional schema, so
/// EVERY matching branch's enum applies), or an empty vec when no branch matches —
/// so a resource with a dynamic or unmatched Engine is not validated.
/// matching branch (the whole conditional schema is evaluated, so EVERY matching
/// branch's enum applies), or an empty vec when no branch matches — so a resource
/// with a dynamic or unmatched Engine is not validated.
///
/// The reference linter lowercases the `Engine` value before matching it against
/// the (all-lowercase) Engine consts for RDS DBInstance but NOT for DBCluster;
/// `normalize_engine_case` selects that behavior.
/// The `Engine` value is lowercased before matching it against the (all-lowercase)
/// Engine consts for RDS DBInstance but NOT for DBCluster; `normalize_engine_case`
/// selects that behavior.
fn conditional_instance_class_enums<'a, F>(
region_data: &'a serde_json::Value,
target_prop: &str,
Expand Down Expand Up @@ -3825,9 +3821,9 @@ where
/// Given the matching-branch enums for a conditional instance-class schema and a
/// class value, returns the enum to render in the diagnostic when the value is
/// invalid, or `None` when the value is valid. A value is valid only when it is
/// in EVERY matching branch's enum (the intersection). When invalid, the
/// reference linter reports the largest branch enum the value is missing from, so
/// return that branch's sorted enum.
/// in EVERY matching branch's enum (the intersection). When invalid, report the
/// largest branch enum the value is missing from, so return that branch's sorted
/// enum.
fn invalid_class_branch_enum<'a>(branch_enums: &[HashSet<&'a str>], value: &str) -> Option<Vec<&'a str>> {
let failing_largest =
branch_enums.iter().filter(|allowed| !allowed.contains(value)).max_by_key(|allowed| allowed.len())?;
Expand Down
4 changes: 2 additions & 2 deletions src/cel-engine/tests/conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,8 +677,8 @@ Resources:
fn e9004_dotted_attribute_on_object_attribute_is_still_invalid() {
// A dotted GetAtt whose leading segment is an object/array-typed property
// (here S3 Bucket `Tags`, an array) is NOT a valid map-member reference:
// GetAtt cannot index into such an attribute. The engine must still flag
// it, matching the reference tool. Only nested-stack / provisioned-product
// GetAtt cannot index into such an attribute, so CloudFormation rejects it
// and the engine must still flag it. Only nested-stack / provisioned-product
// `Outputs.<key>` is an open-ended map member.
let ids = validate(
r#"
Expand Down
2 changes: 1 addition & 1 deletion src/cfn-validate/src/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ fn main() {
.max(1);

// Hardcoded: benchmarks always use DETAILED format and DEBUG severity to capture
// all diagnostics. This ensures parity across native/wasm/jvm harnesses.
// all diagnostics, so the native/wasm/jvm harnesses all measure the same work.
let detail_level = DetailLevel::Detailed;
let severity_level = Severity::Debug;
let format_str = "detailed";
Expand Down
Loading
Loading