diff --git a/src/Cargo.lock b/src/Cargo.lock index b861990..2510e4c 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -196,7 +196,7 @@ checksum = "4372b9543397a4b86050cc5e7ee36953edf4bac9518e8a774c2da694977fb6e4" [[package]] name = "bindings-jvm" -version = "1.3.0" +version = "1.4.0" dependencies = [ "cel-engine", "diagnostics", @@ -211,7 +211,7 @@ dependencies = [ [[package]] name = "bindings-wasm" -version = "1.3.0" +version = "1.4.0" dependencies = [ "cel-engine", "console_error_panic_hook", @@ -350,7 +350,7 @@ dependencies = [ [[package]] name = "cel-engine" -version = "1.3.0" +version = "1.4.0" dependencies = [ "anyhow", "cel-interpreter", @@ -404,7 +404,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfn-validate" -version = "1.3.0" +version = "1.4.0" dependencies = [ "cel-engine", "chrono", @@ -566,7 +566,7 @@ dependencies = [ [[package]] name = "data-source" -version = "1.3.0" +version = "1.4.0" dependencies = [ "anyhow", "chrono", @@ -615,7 +615,7 @@ dependencies = [ [[package]] name = "diagnostics" -version = "1.3.0" +version = "1.4.0" dependencies = [ "log", "rules", @@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "guard-translator" -version = "1.3.0" +version = "1.4.0" dependencies = [ "guard-lang", "indexmap 2.14.0", @@ -1387,7 +1387,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rego-engine" -version = "1.3.0" +version = "1.4.0" dependencies = [ "anyhow", "data-source", @@ -1427,7 +1427,7 @@ dependencies = [ [[package]] name = "resources" -version = "1.3.0" +version = "1.4.0" dependencies = [ "serde_json", ] @@ -1448,7 +1448,7 @@ dependencies = [ [[package]] name = "rules" -version = "1.3.0" +version = "1.4.0" dependencies = [ "fancy-regex 0.18.0", "log", @@ -1543,7 +1543,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schema-validator" -version = "1.3.0" +version = "1.4.0" dependencies = [ "data-source", "diagnostics", @@ -1786,7 +1786,7 @@ dependencies = [ [[package]] name = "template-model" -version = "1.3.0" +version = "1.4.0" dependencies = [ "base64", "diagnostics", @@ -2124,7 +2124,7 @@ dependencies = [ [[package]] name = "validation-engine" -version = "1.3.0" +version = "1.4.0" dependencies = [ "diagnostics", "guard-translator", diff --git a/src/Cargo.toml b/src/Cargo.toml index 9deb0b8..c3851a6 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -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" diff --git a/src/bindings-jvm/tests/kotlin/src/test/kotlin/SmokeTest.kt b/src/bindings-jvm/tests/kotlin/src/test/kotlin/SmokeTest.kt index f218fae..3f17b70 100644 --- a/src/bindings-jvm/tests/kotlin/src/test/kotlin/SmokeTest.kt +++ b/src/bindings-jvm/tests/kotlin/src/test/kotlin/SmokeTest.kt @@ -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") diff --git a/src/bindings-wasm/tests/smoke.test.ts b/src/bindings-wasm/tests/smoke.test.ts index 2dc41cc..688cb91 100644 --- a/src/bindings-wasm/tests/smoke.test.ts +++ b/src/bindings-wasm/tests/smoke.test.ts @@ -419,7 +419,7 @@ describe("report fields excluded from golden", () => { // 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) { diff --git a/src/cel-engine/src/rules/intrinsics.rs b/src/cel-engine/src/rules/intrinsics.rs index e7357f6..ed1f85b 100644 --- a/src/cel-engine/src/rules/intrinsics.rs +++ b/src/cel-engine/src/rules/intrinsics.rs @@ -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.") } diff --git a/src/cel-engine/src/rules/resources_extra.rs b/src/cel-engine/src/rules/resources_extra.rs index 1dc6d12..ec31130 100644 --- a/src/cel-engine/src/rules/resources_extra.rs +++ b/src/cel-engine/src/rules/resources_extra.rs @@ -228,8 +228,8 @@ fn resolve_concrete(m: &SemanticModel, rid: &str, path: &str) -> Option bool { resolve_concrete(m, rid, "Properties.OwnershipControls.Rules") .as_ref() @@ -913,8 +913,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { .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() @@ -1272,10 +1272,9 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { 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( @@ -2326,8 +2325,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { && 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 // " was expected". out.push(make_resource_diagnostic( "E3698", @@ -2343,9 +2342,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { 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()) @@ -2439,13 +2437,12 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { 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() @@ -2502,9 +2499,9 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { 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 => { @@ -2786,8 +2783,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { // 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 { @@ -2813,8 +2810,8 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { } // 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 @@ -2996,10 +2993,9 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { } // 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") { @@ -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, @@ -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> { let failing_largest = branch_enums.iter().filter(|allowed| !allowed.contains(value)).max_by_key(|allowed| allowed.len())?; diff --git a/src/cel-engine/tests/conformance.rs b/src/cel-engine/tests/conformance.rs index 24505b7..f952312 100644 --- a/src/cel-engine/tests/conformance.rs +++ b/src/cel-engine/tests/conformance.rs @@ -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.` is an open-ended map member. let ids = validate( r#" diff --git a/src/cfn-validate/src/benchmark.rs b/src/cfn-validate/src/benchmark.rs index a0fbc29..1f6cc0a 100644 --- a/src/cfn-validate/src/benchmark.rs +++ b/src/cfn-validate/src/benchmark.rs @@ -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"; diff --git a/src/cfn-validate/tests/github_issues.rs b/src/cfn-validate/tests/github_issues.rs index c973d01..09d0da9 100644 --- a/src/cfn-validate/tests/github_issues.rs +++ b/src/cfn-validate/tests/github_issues.rs @@ -41,7 +41,7 @@ fn debug_config() -> ValidateConfig { /// Run both engines with the default config and return their diagnostics tagged /// by engine name. Every assertion helper checks both, so a test that passes is -/// asserting engine parity for that fact. +/// asserting both engines agree on that fact. fn validate_both(fixture: &str) -> Vec<(&'static str, Vec)> { vec![ ("rego", validate_with(&*REGO, fixture, debug_config())), @@ -162,7 +162,7 @@ fn issue_37_w3697_fires_on_autoscaling_launchconfiguration() { /// Issue #37: a per-service exclude filter silences W3697 for every AutoScaling /// resource — the resolution the issue asked for. The filter is applied after -/// evaluation in both engines, so the two stay at parity. The `service` string is +/// evaluation in both engines, so the two agree. The `service` string is /// the fully qualified `service-provider::service-name` prefix (`AWS::AutoScaling`) /// matched verbatim against the resource type; the rule id scopes it to W3697, /// leaving the rest of the service untouched. @@ -270,10 +270,9 @@ fn issue_41_no_w9013_on_join_ref_accountid() { /// Issue #42: an omitted `HealthCheckPort` on an ECS dynamic-port (HostPort 0) /// TargetGroup defaults to `traffic-port` — the correct setting — so the finding /// is advisory (I3049 INFO), not an Error. The ECS dynamic-port health-check -/// check is severity-split from the reference linter's single Error: an omitted -/// port is informational (I3049), a concrete non-`traffic-port` value is a -/// warning (W3049, exercised by the `bad/` corpus). The template deploys and -/// works in the omitted case, so no Error is warranted. +/// check is severity-split: an omitted port is informational (I3049), a concrete +/// non-`traffic-port` value is a warning (W3049, exercised by the `bad/` corpus). +/// The template deploys and works in the omitted case, so no Error is warranted. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/42 #[test] fn issue_42_omitted_healthcheckport_is_info_not_error() { @@ -289,8 +288,8 @@ fn issue_42_omitted_healthcheckport_is_info_not_error() { /// (a `Ref` to a no-default parameter) is unknowable at validation time, so the /// dynamic-port health-check rule must stay silent in both engines — neither the /// advisory I3049 nor the warning W3049 fires. Guards the false-positive fix: an -/// opaque value must not be treated like a fixed non-`traffic-port` port. This -/// matches the reference linter, which is also silent here. +/// opaque value must not be treated like a fixed non-`traffic-port` port, because +/// CloudFormation cannot know the value at deploy time. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/42 #[test] fn issue_42_no_finding_on_deploy_time_healthcheckport() { @@ -304,8 +303,8 @@ fn issue_42_no_finding_on_deploy_time_healthcheckport() { /// across ALL its branches, in both engines identically. One branch pins a fixed /// `8080` (wrong for dynamic port mapping) and the other is `traffic-port`, so the /// warning W3049 fires (on the fixed branch) and the omitted-default advisory -/// I3049 does not (the property is present). Guards engine parity on conditionals -/// — a divergence here (one engine reading only a single branch) is a bug. +/// I3049 does not (the property is present). Guards that both engines agree on +/// conditionals — one engine reading only a single branch would be a bug. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/42 #[test] fn issue_42_conditional_healthcheckport_warns_on_wrong_branch() { @@ -464,8 +463,8 @@ fn issue_53_f3004_fires_on_real_dependson_cycle() { } /// Issue #54 (fixed): an S3 bucket with non-Private AccessControl and missing -/// OwnershipControls reports only E3045 (Error), matching the reference linter. -/// The extension's `then.required: [OwnershipControls]` used to also fire a +/// OwnershipControls reports only E3045 (Error). The extension's +/// `then.required: [OwnershipControls]` used to also fire a /// duplicate FATAL F3003 for the same concern; that false positive is now /// suppressed because E3045 already covers this trigger. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/54 @@ -505,7 +504,7 @@ fn issue_54_no_required_ownershipcontrols_on_bare_bucket() { /// the OwnershipControl requirement is satisfied. This guards the bug's scope: /// the false positive must be tied to *missing* OwnershipControls, and the data /// constraint must not regress into firing on a valid bucket. Only the W3045 -/// AccessControl-deprecation warning remains, matching the reference linter. +/// AccessControl-deprecation warning remains. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/54 #[test] fn issue_54_no_f3003_when_ownershipcontrols_present() { @@ -516,11 +515,11 @@ fn issue_54_no_f3003_when_ownershipcontrols_present() { assert_fires_with_severity(&diags, "W3045", Severity::Warn); } -/// Issue #54 (parity gap): when `AccessControl` is a symbolic `{Ref}` to a +/// Issue #54 (engine divergence): when `AccessControl` is a symbolic `{Ref}` to a /// parameter with no default, the property IS present so the deprecation warning -/// W3045 should fire (the reference linter keys on presence, not value). CEL does -/// fire it; rego does not (it keys on the resolved value, which is unresolvable -/// here) — a rego false-negative and a rego/cel divergence. Pinned with inline +/// W3045 should fire (the deprecation is about presence of the property, not its +/// value). CEL does fire it; rego does not (it keys on the resolved value, which +/// is unresolvable here) — a rego false-negative and a rego/cel divergence. Pinned with inline /// bytes because the fixture diverges between engines and so cannot live in the /// rego==cel golden corpus. Tighten to both-fire once rego keys on presence. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/54 @@ -580,8 +579,8 @@ fn issue_57_no_e3057_on_valid_origin_group_id() { /// Issue #57 (positive boundary): widening the valid-target set to include /// OriginGroup ids must not silence E3057 on a genuinely dangling -/// `TargetOriginId` that matches neither an Origin nor an OriginGroup. Matches -/// the reference linter, which validates only `DefaultCacheBehavior`. +/// `TargetOriginId` that matches neither an Origin nor an OriginGroup. Only +/// `DefaultCacheBehavior` is validated here. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/57 #[test] fn issue_57_e3057_still_fires_on_dangling_target_origin_id() { @@ -641,7 +640,7 @@ fn issue_62_f3032_fatal_on_empty_unconstrained_array() { /// Issue #63: E2001 fires on an intrinsic (`Fn::GetStackOutput`) used in a /// parameter Default, because CloudFormation never evaluates intrinsics in -/// `Parameters.*.Default`. Working as intended (matches the reference linter). +/// `Parameters.*.Default`. Working as intended. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/63 #[test] fn issue_63_e2001_on_intrinsic_in_parameter_default() { @@ -948,7 +947,7 @@ fn fatal_rule_suppressed_by_exclude_range() { } } -/// Engine parity for the hardcoded-ARN warning when the ARN sits behind an +/// Both engines flag the hardcoded-ARN warning when the ARN sits behind an /// `Fn::If`. The Rego rule resolves the property (collapsing the conditional to /// its true branch), so it must fire; the native rule previously matched only a /// plain concrete string and silently skipped the conditional, diverging from @@ -974,7 +973,7 @@ Resources: assert_count(&diags, "W9002", 1); } -/// Engine parity for multiple hardcoded ARNs on one resource: every `*Arn` +/// Both engines flag multiple hardcoded ARNs on one resource: every `*Arn` /// property with a literal ARN is a separate finding. The native rule previously /// stopped after the first match on a resource, reporting one warning where Rego /// reported one per property. @@ -995,8 +994,8 @@ Resources: assert_count(&diags, "W9002", 2); } -/// An output whose value is not a string is a guaranteed template error (F6101, -/// the promoted form of the reference linter's output value-type check). A +/// An output whose value is not a string is a guaranteed template error (F6101): +/// CloudFormation requires output values to resolve to a string. A /// literal list or object, and a list-returning function (`Fn::GetAZs`, /// `Fn::Split`, `Fn::Cidr`), each fire; an `Fn::If` is transparent, so a list in /// a branch fires on that branch. Both engines must agree. @@ -1030,8 +1029,8 @@ Outputs: /// String-valued outputs, and shapes that only look non-string, must NOT fire /// F6101 in either engine: scalars coerce to strings; `Ref`, `Fn::Sub`, /// `Fn::Join`, `Fn::Select`, and `Fn::FindInMap` produce (or are treated as) -/// strings — including a `Fn::FindInMap` that resolves to a list, which the -/// reference linter does not flag here. Empty containers are also accepted. +/// strings — including a `Fn::FindInMap` that resolves to a list, which is not +/// flagged here. Empty containers are also accepted. #[test] fn string_output_values_not_flagged_in_either_engine() { let template = br#" diff --git a/src/cfn-validate/tests/golden_tests.rs b/src/cfn-validate/tests/golden_tests.rs index a9994c7..6d9eda1 100644 --- a/src/cfn-validate/tests/golden_tests.rs +++ b/src/cfn-validate/tests/golden_tests.rs @@ -155,7 +155,7 @@ fn rules_evaluated_is_full_rule_count() { } } -const EXPECTED_ENGINE_VERSION: &str = "1.3.0"; +const EXPECTED_ENGINE_VERSION: &str = "1.4.0"; #[test] fn engine_version_matches_workspace_version() { diff --git a/src/cfn-validate/tests/security_tests.rs b/src/cfn-validate/tests/security_tests.rs index 4d6d1be..77a95fb 100644 --- a/src/cfn-validate/tests/security_tests.rs +++ b/src/cfn-validate/tests/security_tests.rs @@ -246,7 +246,7 @@ fn custom_cel_rule_that_fails_to_evaluate_is_a_hard_error_not_a_diagnostic() { // compiles, but fails at execution. That failure must surface as a hard // validation error (an exception) — never silently dropped, never reported // as a diagnostic — matching the Rego engine's custom-rule semantics - // (engine parity, no silent failures). + // (no silent failures). let escape_rule = r#"{"rules": [ { "rule_id": "SBXCEL001", diff --git a/src/rego-engine/handwritten/rego/resources/codepipeline/artifact_counts.rego b/src/rego-engine/handwritten/rego/resources/codepipeline/artifact_counts.rego index 0ac9f93..f3c1214 100644 --- a/src/rego-engine/handwritten/rego/resources/codepipeline/artifact_counts.rego +++ b/src/rego-engine/handwritten/rego/resources/codepipeline/artifact_counts.rego @@ -5,8 +5,8 @@ import rego.v1 # E3702: CodePipeline action artifact-count validation. The count check must # enumerate every Fn::If branch of an InputArtifacts/OutputArtifacts list (an # artifact list authored behind a condition can violate the min/max in one -# branch but not another). That branch enumeration is shared with the CEL engine -# via the pipeline_artifact_count_issues builtin so both engines stay at parity. +# branch but not another). That branch enumeration is provided by the +# pipeline_artifact_count_issues builtin. violation contains make_diag("E3702", "ERROR", name, issue.message) if { some name in resources_of_type("AWS::CodePipeline::Pipeline") result := pipeline_artifact_count_issues(name) diff --git a/src/rego-engine/handwritten/rego/resources/ec2/ebs_iops.rego b/src/rego-engine/handwritten/rego/resources/ec2/ebs_iops.rego index a2f63b8..43c1989 100644 --- a/src/rego-engine/handwritten/rego/resources/ec2/ebs_iops.rego +++ b/src/rego-engine/handwritten/rego/resources/ec2/ebs_iops.rego @@ -41,8 +41,8 @@ violation contains make_diag_at("E3671", "ERROR", name, } # Iops below the minimum for its VolumeType. The bound only applies to a literal -# Iops — a value supplied via a parameter Ref/intrinsic is not folded into the -# check, matching the reference tool. +# Iops — a value supplied via a parameter Ref/intrinsic has no known value at +# validation time, so it is not folded into the check. violation contains make_diag_at("E3671", "ERROR", name, sprintf("%s.%d.Ebs.Iops", [base_path, i]), sprintf("%d is less than the minimum of %d", [iops, bounds.min])) if { diff --git a/src/rego-engine/handwritten/rego/resources/elb/ssl_certificate.rego b/src/rego-engine/handwritten/rego/resources/elb/ssl_certificate.rego index 552759b..83d0fa8 100644 --- a/src/rego-engine/handwritten/rego/resources/elb/ssl_certificate.rego +++ b/src/rego-engine/handwritten/rego/resources/elb/ssl_certificate.rego @@ -12,7 +12,7 @@ violation contains make_diag_at("E3679", "ERROR", name, is_object(listener) proto := object.get(listener, "Protocol", "") proto in {"HTTPS", "SSL"} - # The reference tool requires only that the key be PRESENT (a JSON Schema + # The schema requires only that the key be PRESENT (a JSON Schema # `required`); an explicit empty-string certificate id satisfies it. Test for # an absent key, not an empty value — `not object.get(..., null)` never holds # (null is truthy in Rego), so check the key set directly. diff --git a/src/rego-engine/handwritten/rego/resources/lambda/permission_source_account.rego b/src/rego-engine/handwritten/rego/resources/lambda/permission_source_account.rego index c0c19ce..3baab3d 100644 --- a/src/rego-engine/handwritten/rego/resources/lambda/permission_source_account.rego +++ b/src/rego-engine/handwritten/rego/resources/lambda/permission_source_account.rego @@ -22,7 +22,7 @@ violation contains make_diag_full("W3663", "WARN", name, # Branch 2: SourceArn is a literal ARN string without a 12-digit account segment. # Guard on `not is_from_intrinsic` so a SourceArn supplied via Ref/GetAtt (whose # resolved value is not a concrete literal) does not trip the pattern check — -# matching the reference tool, which only tests a literal string here. +# the account-id pattern check applies only to a literal ARN string here. violation contains make_diag_full("W3663", "WARN", name, "Properties", "Lambda Permission with a SourceArn that has no account id should also specify SourceAccount", diff --git a/src/rego-engine/handwritten/rego/resources/rds/dbinstanceclass_enum_enum.rego b/src/rego-engine/handwritten/rego/resources/rds/dbinstanceclass_enum_enum.rego index 177bee1..4c2178a 100644 --- a/src/rego-engine/handwritten/rego/resources/rds/dbinstanceclass_enum_enum.rego +++ b/src/rego-engine/handwritten/rego/resources/rds/dbinstanceclass_enum_enum.rego @@ -10,7 +10,7 @@ import rego.v1 # intersection). `invalid_instance_class_enum` returns the enum to render when the # class is invalid, or undefined when it is valid or no branch matches (so a # dynamic or unmatched Engine is not validated). The Engine is matched -# case-insensitively for DBInstance, mirroring the reference tool. +# case-insensitively for DBInstance. violation contains make_diag_full("E3025", "ERROR", name, "Properties.DBInstanceClass", sprintf("'%s' is not one of %s in '%s'", [val, render_list(reported), region]), @@ -26,8 +26,8 @@ violation contains make_diag_full("E3025", "ERROR", name, } # E3694: Engine-conditional RDS DBClusterInstanceClass validation. Same conditional -# schema shape as E3025, but the reference tool does NOT lowercase Engine for -# DBCluster, so match the const case-sensitively (false). +# schema shape as E3025, but Engine is matched case-sensitively for DBCluster +# (false). violation contains make_diag_full("E3694", "ERROR", name, "Properties.DBClusterInstanceClass", sprintf("'%s' is not one of %s in '%s'", [val, render_list(reported), region]), diff --git a/src/rego-engine/src/builtins.rs b/src/rego-engine/src/builtins.rs index 0a8dd88..f0e7508 100644 --- a/src/rego-engine/src/builtins.rs +++ b/src/rego-engine/src/builtins.rs @@ -996,7 +996,7 @@ fn register_property_can_be_absent(rego: &mut regorus::Engine, holder: SharedMod /// E3025/E3694 diagnostic when `value` is invalid, or `undefined` when the value /// is valid or no branch matches. A value is valid only when it is in EVERY /// matching branch's enum (the intersection); when invalid, the largest failing -/// branch's enum is returned, matching the reference linter's reported enum. +/// branch's enum is returned. fn register_conditional_instance_class_enum(rego: &mut regorus::Engine) { let _ = rego.add_extension( "invalid_instance_class_enum".into(), @@ -1020,7 +1020,8 @@ fn register_conditional_instance_class_enum(rego: &mut regorus::Engine) { /// Collects every `then..enum` from a conditional region document /// whose `allOf` branch `if.required` consts all match `props`. Returns one enum /// per matching branch. The `Engine` const is matched case-insensitively when -/// `normalize_engine_case` is set (RDS DBInstance), mirroring the reference tool. +/// `normalize_engine_case` is set, because RDS DBInstance treats the engine name +/// case-insensitively. fn conditional_instance_class_enums( schema: &serde_json::Value, target_prop: &str, diff --git a/src/schema-validator/src/store.rs b/src/schema-validator/src/store.rs index 4b74b92..bf39bfd 100644 --- a/src/schema-validator/src/store.rs +++ b/src/schema-validator/src/store.rs @@ -151,8 +151,8 @@ pub struct LifecycleStore { runtime_lifecycle: HashMap, } -/// Per-runtime lifecycle dates used to reconstruct the reference tool's dated -/// deprecation message. +/// Per-runtime lifecycle dates used to reconstruct the dated runtime-deprecation +/// message. #[derive(Clone)] pub struct RuntimeLifecycle { pub deprecated: String, diff --git a/src/schema-validator/src/validate.rs b/src/schema-validator/src/validate.rs index 8bde7d6..06ce94b 100644 --- a/src/schema-validator/src/validate.rs +++ b/src/schema-validator/src/validate.rs @@ -9,12 +9,15 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, LazyLock}; use template_model::SemanticModel; use template_model::coercion::{CoerceResult, cfn_coerce_to_number, cfn_coerce_to_string, cfn_coerce_value}; -use template_model::consts::{FN_CONDITION, FN_IF, FN_PREFIX, FN_REF, KEY_PROPERTIES}; +use template_model::consts::{ + FN_CONDITION, FN_IF, FN_PREFIX, FN_REF, KEY_PROPERTIES, KEY_TYPE, PARAM_TYPE_COMMA_DELIMITED_LIST, + PARAM_TYPE_NUMBER, PARAM_TYPE_STRING, SAM_FUNCTION_TYPE, SAM_SERVERLESS_TYPE_PREFIX, +}; use template_model::model::ResolvedResource; use template_model::resolver::{RefKind, ResolvedValue}; /// Properties that accept a string value when used with `aws cloudformation package`. -/// The baseline skips type checks for these paths when the value is a string. +/// Type checks are skipped for these paths when the value is a string. const PACKAGING_PROPERTY_PATHS: &[(&str, &str)] = &[ ("AWS::Lambda::Function", "Properties.Code"), ("AWS::Lambda::LayerVersion", "Properties.Content"), @@ -30,7 +33,7 @@ const PACKAGING_PROPERTY_PATHS: &[(&str, &str)] = &[ ("AWS::CodeCommit::Repository", "Properties.Code.S3"), ]; -/// Property paths where the baseline skips type validation entirely because +/// Property paths where type validation is skipped entirely because /// the property accepts free-form user-defined content. const TYPE_CHECK_EXEMPT_PATHS: &[(&str, &str)] = &[ ("AWS::Lambda::Function", "Properties.Environment.Variables"), @@ -61,9 +64,10 @@ pub fn validate_all_resources( for rtype in &relevant { // Custom resources, modules, and SAM resources (rewritten by the SAM // transform before deployment) are not region-scoped provider types, so - // the region check skips them — matching the reference linter, which - // validates the post-transform template. - if rtype.ends_with("::MODULE") || rtype.starts_with("Custom::") || rtype.starts_with("AWS::Serverless::") { + // the region check skips them — CloudFormation validates the + // post-transform template. + if rtype.ends_with("::MODULE") || rtype.starts_with("Custom::") || rtype.starts_with(SAM_SERVERLESS_TYPE_PREFIX) + { continue; } @@ -79,8 +83,8 @@ pub fn validate_all_resources( // A resource guarded by a Condition that cannot hold in the target // region is never created there, so its type's absence in that // region is not an error. The satisfiability check pins - // AWS::Region to the target region (mirroring the reference - // linter's per-region scenario evaluation), so a condition like + // AWS::Region to the target region and asks whether the condition + // can hold there, so a condition like // `!Equals [AWS::Region, us-east-1]` is unsatisfiable at any other // region and the finding is skipped — even at the DEFAULT region, // where AWS::Region is otherwise a free SAT variable. @@ -90,14 +94,14 @@ pub fn validate_all_resources( { continue; } - // Match the reference linter's region-availability message and - // Type-node location (its Error is promoted to the Fatal F3006 here). + // Report the region-availability message at the Type node + // (a Fatal F3006 here). out.push(build_diagnostic( "F3006", &format!("Resource type '{}' does not exist in '{}'", rtype, region), model, rid, - "Type", + KEY_TYPE, None, )); } @@ -124,7 +128,7 @@ pub fn validate_all_resources( // the raw resource schema (required properties, etc. are supplied or // relaxed during expansion). Validating the pre-transform shape would // flag requirements the transform fills in. - if rtype.starts_with("AWS::Serverless::") { + if rtype.starts_with(SAM_SERVERLESS_TYPE_PREFIX) { continue; } validate_resource(&mut out, store, model, rid, res, schema, region); @@ -1761,9 +1765,9 @@ fn validate_reference_type( fn cfn_param_type_to_schema_type(param_type: &str) -> &str { match param_type { - "Number" => "number", - "String" => "string", - "CommaDelimitedList" => "array", + PARAM_TYPE_NUMBER => "number", + PARAM_TYPE_STRING => "string", + PARAM_TYPE_COMMA_DELIMITED_LIST => "array", t if t.starts_with("List<") => "array", t if t.starts_with("AWS::SSM::Parameter::") => "string", _ => "string", @@ -1877,7 +1881,7 @@ fn validate_lifecycle(out: &mut Vec, store: &CompiledSchemaStore, mo out.push(build_diagnostic(rule_id, &msg, model, rid, "", None)); } - if (res.resource_type == "AWS::Lambda::Function" || res.resource_type == "AWS::Serverless::Function") + if (res.resource_type == "AWS::Lambda::Function" || res.resource_type == SAM_FUNCTION_TYPE) // Only a literal Runtime string is validated against the deprecation // list; a Runtime produced by an intrinsic (e.g. Fn::FindInMap) // resolves at deploy time and is handled by the intrinsic rules @@ -1915,7 +1919,7 @@ fn validate_lifecycle(out: &mut Vec, store: &CompiledSchemaStore, mo } } -/// Builds the dated runtime-deprecation message the reference tool emits for all +/// Builds the dated runtime-deprecation message CloudFormation reports for all /// three bands: "Runtime 'X' was deprecated on 'D'. Creation was disabled on 'C' /// and update on 'U'. Please consider updating to 'S'". A string value renders /// single-quoted; a missing successor renders as bare `None` (Python `repr`). @@ -2010,7 +2014,7 @@ fn validate_cfn_gather( for (prop_name, prop_def) in props { // A gather property spec is either a bare JSON-pointer string // ("/RestApiId") or an object ({"path": "/FifoQueue", "default": - // false}). Both forms appear in the upstream data; reading only + // false}). Both forms appear in the extension data; reading only // the object form silently drops the value for the string form // and makes the whole cross-resource check a no-op. let (path, default_val) = match prop_def { @@ -2045,9 +2049,9 @@ fn validate_cfn_gather( /// listener-certificate rule (E3676), which fires on the same trigger. /// - RDS DBInstance `BackupRetentionPeriod` is a retention-period advisory the /// dedicated retention rule (I3013) already emits; the extension carrying a -/// `then.required` for it must not additionally raise a Fatal F3003 (the -/// reference tool treats a missing retention period as informational, never a -/// required-property error). +/// `then.required` for it must not additionally raise a Fatal F3003 (a +/// missing retention period is informational, never a required-property +/// error). fn extension_required_covered_by_dedicated_rule(resource_type: &str, prop_name: &str) -> bool { matches!( (resource_type, prop_name), @@ -2080,14 +2084,13 @@ fn validate_extension_if_then_else( // already reports under its own specific ID (e.g. the S3 // AccessControl→OwnershipControls extension is the dedicated S3 // access-control rule). Emitting the generic F3003 on top of that - // dedicated diagnostic is a double-report the reference tool never - // produces, so skip it. + // dedicated diagnostic would be a double-report, so skip it. if extension_required_covered_by_dedicated_rule(&res.resource_type, prop_name) { continue; } // Dedup: compiled base schema's if_then_else may already have - // emitted a required-property diagnostic for the same required property (extensions - // upstream sometimes mirror the base schema's conditional + // emitted a required-property diagnostic for the same required property (the + // extension schemas sometimes mirror the base schema's conditional // requirements). Skip to avoid double-reporting. let already_reported = out.iter().any(|d| { d.rule_id == "F3003" @@ -2142,8 +2145,8 @@ fn validate_extension_if_then_else( if !is_satisfiable(model, conds) { continue; } - // NOTE: the reference tool applies these extension enums with - // per-enum case sensitivity — case-insensitive for engine names + // NOTE: these extension enums use per-enum case + // sensitivity — case-insensitive for engine names // (Engine: "MySQL" is accepted against "mysql") but case-sensitive // for others (ReplicaMode: "Mounted" is rejected against // "mounted"). Distinguishing them requires the per-rule mapping @@ -2325,7 +2328,7 @@ fn evaluate_gather_schema( return; }; // NOTE: this generic gather path only surfaces top-level const mismatches as - // E3030. The reference linter reports these cross-resource constraints under specific + // E3030. These cross-resource constraints belong under specific dedicated // rule IDs (E3699, E3707, E3709, …) and dedicated native rules already cover // the reachable cases (E3502, E3707, E3698, …). Broadening this path to also // unwrap `cfnContext` or evaluate bare `properties` schemas made it emit diff --git a/src/template-model/src/conditions.rs b/src/template-model/src/conditions.rs index d0228b3..a17b207 100644 --- a/src/template-model/src/conditions.rs +++ b/src/template-model/src/conditions.rs @@ -177,10 +177,10 @@ impl ConditionModel { } /// Like [`Self::is_satisfiable`], but with the target region pinned as the - /// only candidate value for the `AWS::Region` pseudo-parameter — mirroring the - /// reference linter's per-region scenario evaluation (it asks whether a - /// condition can hold *in that region*, not whether the region could be - /// anything). Used by the region-availability check so a resource guarded by a + /// only candidate value for the `AWS::Region` pseudo-parameter — a per-region + /// satisfiability query that asks whether a condition can hold *in that + /// region*, not whether the region could be anything. Used by the + /// region-availability check so a resource guarded by a /// condition that cannot hold in the target region (e.g. /// `!Equals [AWS::Region, other-region]`) is correctly treated as never /// created there — even when no explicit `--region` override pins the diff --git a/src/template-model/src/model.rs b/src/template-model/src/model.rs index 201a061..6ef1755 100644 --- a/src/template-model/src/model.rs +++ b/src/template-model/src/model.rs @@ -2115,11 +2115,11 @@ Resources: #[test] fn output_find_in_map_list_value_not_flagged() { - // Fn::FindInMap can resolve to a list, but the reference linter does not - // flag it here (the mapping's shape is validated elsewhere): only literal - // lists and list-returning functions are string-type violations. This is - // exactly the case a resolved-value check would get wrong, since the - // resolved value is indistinguishable from a literal list. + // Fn::FindInMap can resolve to a list, but it is not flagged here (the + // mapping's shape is validated elsewhere): only literal lists and + // list-returning functions are string-type violations. This is exactly + // the case a resolved-value check would get wrong, since the resolved + // value is indistinguishable from a literal list. let template = "Mappings:\n M:\n k:\n l:\n - a\n - b\nResources:\n R:\n Type: T\nOutputs:\n O:\n Value: !FindInMap [M, k, l]\n"; assert_eq!(output_string_type_diagnostics(template), 0); } diff --git a/src/template-model/src/parser/builder.rs b/src/template-model/src/parser/builder.rs index 7f6f116..bdf2ed5 100644 --- a/src/template-model/src/parser/builder.rs +++ b/src/template-model/src/parser/builder.rs @@ -68,7 +68,7 @@ impl Builder { } /// A malformed `Fn::GetStackOutput` argument (error). No function-name prefix — - /// the message mirrors the JSON-Schema wording the reference tooling emits. + /// the message uses standard JSON-Schema wording. /// Anchored at `at_path` (the function node, or a specific offending key) so it /// lands exactly where the offending value is written. fn get_stack_output_error(&mut self, message: String, at_path: &str) { diff --git a/src/validation-engine/src/engine.rs b/src/validation-engine/src/engine.rs index 8714c44..91d157e 100644 --- a/src/validation-engine/src/engine.rs +++ b/src/validation-engine/src/engine.rs @@ -796,7 +796,7 @@ fn gate_cdk_suppressed_rules(diagnostics: &mut Vec, model: &Semantic /// Attaches a source span to any diagnostic still missing one. Location is part of /// both the standard and detailed reports, so this runs regardless of detail level -/// and independently of the engine, keeping the two engines at parity. +/// and independently of the engine. /// /// Most diagnostics are located at construction (via `resource_span` / /// `resolve_section_span`), but some emission paths cannot reach a span there: @@ -1471,7 +1471,8 @@ Resources: assert_eq!(diags.len(), 2); } - /// Regression for the CEL WASM-vs-native parity bug. + /// Regression for a dedup bug where the same rule/message/line was duplicated + /// across native and WASM builds, separated by a sibling diagnostic. /// /// Two diagnostics for rule X message M at the same line/col, separated by a sibling /// for the same rule but a different message M', must still be deduped. Before the fix