Imported from aws-cloudformation/cloudformation-validate (
.kiro/skills/cloudformation-validate-development/SKILL.md). Install upstream withnpx skills add aws-cloudformation/cloudformation-validate --skill cloudformation-validate-development. Copyright stays with the author (Apache-2.0).
cloudformation-validate development
cloudformation-validate is a fast, offline validator for AWS CloudFormation templates: parse JSON/YAML →
structured diagnostics (schema, semantic, security, best-practice). Rules and schemas compile into the binary - no
network, no credentials. Ships as a Rust CLI, Rust library, Node WASM package, Python package, Go module, and JVM
(Kotlin/Java) library over one shared core.
Code quality - read the references first
Before writing or modifying code, read the applicable reference file and apply every rule in it:
- references/source-code-rules.md - applies to ALL source code
- references/test-code-rules.md - applies to ALL test code
Also read sibling files in the directory you are editing to learn existing patterns, naming, error handling, and style - then match them. Always follow the code style of the project.
Commands and validation selection
All cargo commands run from src/ (the Cargo workspace lives in src/, not the repo root). The toolchain is
pinned by src/rust-toolchain.toml. Select checks by the actual files and behavior changed. Run a command only when it
can exercise the change; if no files changed, run no tests or validation commands.
cd src
# Build
cargo build # whole workspace (debug)
cargo build -p cfn-validate # CLI -> target/debug/cfn-validate (add --release for optimized)
# Core Rust tests - only when they cover the changed behavior
cargo test -p cloudformation-validate-cel-engine <name> # single crate / filtered test - preferred while iterating
cargo test --workspace 2>&1 | tee ../tmp/test-output.txt # broad core changes only; at most once at completion
# CI runs coverage, not plain test: cargo llvm-cov --locked --release --workspace --no-fail-fast
# Required after every Rust source change
cargo fmt --all
cargo clippy --locked --all-targets --workspace -- -D warnings
# Run the CLI
cargo run -p cfn-validate -- <template|dir> --engine rego|cel --format standard|detailed --level fatal|error|warn|info|debug
cargo run -p cfn-validate -- --list-rules
# Regenerate after a change that alters diagnostics; verifies engine parity and rewrites the snapshot chunk files.
cargo run --release -p resources --example generate_validation_reports
Apply these rules when choosing validation:
- Core Rust (
*.rs) changes: always run format and workspace clippy. Run the narrowest Cargo tests that cover the changed behavior. Use the full workspace suite only for broad or cross-crate core changes for which it provides meaningful coverage. - Binding-layer Rust changes: always run Rust format and clippy, then the affected binding's
build.shandtests/run.sh. Do not usecargo testas a substitute; it does not exercise the packaged Node.js, JVM, Python, or Go API. Generated binding artifacts are owned and committed by thebuild-artifactsworkflow. If generation is needed for local testing, generate, test, and then revert every generated binding artifact. - Non-Rust-only changes such as documentation, GitHub workflows, scripts, or binding-language code: do not run Cargo format, clippy, or tests unless the file is a Cargo/build input and the command actually exercises it. Use the relevant syntax checker, build, test runner, or dry-run instead.
- Rule, schema-data, or template changes: run the focused validator, parity, corpus, and snapshot checks that exercise the changed diagnostics. Their non-Rust extension does not remove domain-specific validation or justify unrelated Cargo tests.
- Mixed changes: use the union of checks relevant to each changed surface.
Debugging tools (use these, not println!)
# Dump the full SemanticModel - ALWAYS start here. If the model is wrong, fix template-model.
cargo run -p cloudformation-validate-template-model --example inspect -- <template>
# Accuracy vs cfn-lint. Requires a local cfn-lint checkout - first check whether cfn-lint is available on the
# machine (`cfn-lint --version`), then ask the user for the checkout path; never assume or hardcode a location.
CFN_LINT_ROOT=<path> python3 scripts/compare_cfnlint.py --engine rego|cel
If deeper investigation is needed, write a small standalone script or Rust example that isolates the behavior.
Scratch files, debug output, and tool artifacts go in ./tmp/ at the project root - never scatter them in the tree.
Prefer LSP operations (goto definition, find references, symbol search) over text grep for symbol-level navigation.
Architecture (the non-obvious rules)
- The two engines must stay at parity.
EngineType::Rego(default) andEngineType::Celmust produce identical diagnostics (ID, severity, location, message) for any template - divergence is a bug. Every rule exists in both engines or in neither; add/fix it in both in the same change. Rego rules are hand-written policies inrego-engine/handwritten/rego/; CEL rules are native Rust incel-engine/src/rules/(the CEL interpreter is only for user-supplied custom rules). rules/src/registry.rs(RULE_REGISTRY) is the single source of truth for every rule's ID, severity, category, and description. A rule that evaluates but isn't registered is a bug. IDs match[FEWID]\d{4}(F=Fatal, E=Error, W=Warn, I=Info, D=Debug; enum variantWarn, serializedWARN).- Never hand-edit
data-source/generated/, and never run the regeneration pipeline yourself. It is committed generated code; regeneration is a maintainer-run operation - if a change requires regenerating these artifacts, stop and ask the user to run it.data-source/handwritten/holds reusable JSON reference tables; add one only when a rule needs a data table. - Generated binding artifacts are workflow-owned. The
build-artifactsworkflow commits them. Local generation is temporary and permitted only when needed to test a hand-maintained change; revert all generated output afterward. - Custom rules load from CLI/library as CEL (
.json), Rego (.rego), or Guard DSL (.guard, translated to an engine-agnostic IR byguard-translator). Seesrc/CUSTOM_RULES.md.
Correctness rules - non-negotiable
- Derive expected validation behavior from first principles using compiled CloudFormation schemas and template syntax, official documentation and resource specifications, intrinsic/resource semantics, and focused valid and invalid examples. External implementations are comparison evidence, not unquestionable truth.
- Fatal (F) rules must reflect what CloudFormation itself rejects per the compiled resource schemas - no semantic
interpretation or cross-resource analysis. cfn-lint has no Fatal severity: both Fatal and Error here map to a
cfn-lint Error. A promoted rule keeps the number with E→F (for example,
F3006↔ cfn-lintE3006). - A rule whose number matches cfn-lint SHOULD implement the same check and behave similarly on firing and location. This includes a schema-grounded E→F promotion with the same numeric portion. Shared numbering is a compatibility contract, not proof that cfn-lint's implementation is correct.
- Comparison against cfn-lint is required for cfn-lint-equivalent rules. A mismatch is evidence to investigate, not
proof that this project is wrong. Never copy cfn-lint behavior solely to make a comparison pass. If stronger
CloudFormation evidence shows cfn-lint is incorrect, intentionally diverge, add focused regression coverage for
accepted and rejected cases, and record the evidence and rationale in the change description. Messages may and
should be more accurate;
scripts/compare_cfnlint.pymatches rule ID + resource + path, not message text. - A finding absent from cfn-lint is a candidate false positive when the rule has a cfn-lint equivalent. Accept it only with authoritative evidence and regression coverage, and never relabel it as engine-extra to excuse a mismatch.
- Zero false positives against valid CloudFormation behavior and
resources/templates/good/. - No silent failures / no half measures. Return an
Erron unexpected states rather than a plausible default. Error diagnostics are reserved for problems in the template; a parse error is the only failure surfaced as a diagnostic instead of anErr. - No panics, no hard crashes. Errors propagate through language boundaries as catchable errors, never process
aborts. Fallible FFI entry points use
validation_engine::catch_panicsas a last-resort backstop.unwrap()/expect()/panic!are never error handling on reachable paths.
Rule origin taxonomy
For each rule in rules/src/registry.rs, its TRUE origin is, in priority order:
- Schema - grounded in compiled CloudFormation resource schemas or CloudFormation-defined template syntax and shape, even if cfn-lint performs the same check. Schema rules are Fatal or Error; a promoted rule keeps the cfn-lint number with E→F.
- Exact cfn-lint ID → CfnLint - only when not schema-grounded.
- Engine ID that aliases a cfn-lint rule → CfnLint (split/generic, for example
E9003/E9004←E1010,E9006←E3690), again only when not schema-grounded. - Otherwise → Engine (or
Engine(collision)if the number exists under another prefix).
A cfn-lint number is reserved for the equivalent check and must never be reused for a different check. Engine-assigned rules use the 9xxx range. A rule is engine-extra only when it has no cfn-lint equivalent. Unmatched behavior for a rule with an equivalent follows the evidence-based mismatch procedure above. W9003 is a known intentional divergence: cfn-lint coerces silently while this engine warns.
Fix priority order
Fix at the highest-leverage layer: template-model first (benefits everything downstream), then the data-source
pipeline (both engines inherit it), then - only if neither applies - schema-validator/rego-engine/cel-engine,
applied to both engines in the same change.
Validating a diagnostic behavior fix
Apply every relevant step below; do not apply this procedure wholesale to documentation or workflow-only changes.
- Establish expected behavior independently from CloudFormation schemas, official documentation/specifications, intrinsic/resource semantics, and focused valid and invalid examples.
- Add or reproduce a template under
src/resources/templates/(bad/for the repro andgood/for a counter-example). - Check the SemanticModel with
inspect; fixtemplate-modelif the model is wrong. - Compare against cfn-lint for E/W/I or cfn-guard for Guard. Investigate a cfn-lint mismatch using the independent evidence rather than assuming cfn-lint is correct. Check Fatal rules against the compiled schemas.
- Run
cfn-validatewith both engines on the repro, then run the corpus. Preserve parity and zero false positives; regenerate the snapshot files if diagnostics legitimately changed. - For core Rust changes, run only Cargo tests that exercise the change; use the workspace suite once only when its broad coverage is relevant. For every Rust change, run format and clippy.
- For binding changes, build and test the affected packaged consumer artifact instead of using Cargo tests as a substitute. Revert every locally generated binding artifact after testing.
Project-specific conventions (MUST FOLLOW)
- Self-documenting names; no
data/info/temp/result/manager/processor. Comments explain why, never what. Full rules are in references/source-code-rules.md. - No hardcoded rule IDs in code comments - IDs can change and the comment goes stale. Describe the behavior, not the ID.
- Do not reference cfn-lint in code comments - not by name and not by euphemism ("reference linter", "the
linter", "the baseline tool", etc.). This is a standalone tool; cfn-lint may only be named in Python scripts
(
scripts/), insrc/data-source, and in the rule registry (rules/src/registry.rs). - Never reference CEL in
rego-engineor Rego incel-engine. The engines are standalone. - Imports from other workspace crates always at the top of the file - no inline
crate::paths. External crates follow standard Rust conventions. Does not apply tobindings-jvmorbindings-wasm. unsafe_codeis forbidden workspace-wide.