DEV Community

Bala Paranj
Bala Paranj

Posted on

cfn_nag finds problems in templates. Stave finds problems in reality.

✓ Human-authored analysis; AI used for formatting and proofreading.

What cfn_nag does well

cfn_nag is a static analysis tool for CloudFormation templates. It looks for patterns that may indicate insecure infrastructure:

  • IAM rules with wildcards
  • Security groups open to 0.0.0.0/0
  • Missing encryption
  • Missing access logs
  • Password literals in templates

It runs before deployment, in CI pipelines, and catches problems at authoring time. It has a mature rule set, and supports per-resource suppression via metadata annotations.

For CloudFormation shops, cfn_nag is a valuable first line of defense.

The gap: templates are not reality

cfn_nag scans what you intend to deploy. It cannot answer:

  1. "Is my bucket public right now?" — The template might be correct, but someone changed the bucket policy via the console, the CLI, or another IaC tool.

  2. "How long has it been public?" — cfn_nag is stateless. It sees one template at one point in time. It doesn't track duration.

  3. "Is this GCP bucket public?" — cfn_nag only understands CloudFormation. It doesn't evaluate Terraform, GCP, Azure, DNS records, or any non-AWS resource.

  4. "Can I prove this bucket is compliant?" — cfn_nag produces warnings and failures. It doesn't produce a structured compliance report with regulatory citations, temporal evidence, or a formal reasoning chain.

What Stave does differently

Stave evaluates observed infrastructure state. The input is a JSON snapshot of what exists, captured by an extractor that reads cloud APIs. The evaluation happens offline, deterministically, against YAML controls compiled to CEL predicates.

Duration tracking

Finding: CTL.S3.PUBLIC.001 — my-bucket
  First unsafe: 2026-01-01T00:00:00Z
  Last seen:    2026-01-11T00:00:00Z
  Duration:     240 hours (threshold: 168 hours)
Enter fullscreen mode Exit fullscreen mode

cfn_nag says "this is wrong." Stave says "this has been wrong for 240 hours and your SLA is 168 hours." That distinction matters for compliance. A bucket that was public for 5 minutes during a deploy is different from one that's been public for 6 months.

Multi-cloud, multi-domain

Stave evaluates 246 controls across 29 domains without any code changes to the engine:

Domain Controls Vendor
AWS S3 149 aws
AWS IAM 340 aws
GCP Cloud Storage 7 gcp
DNS records 3 any (vendor-agnostic)

cfn_nag only understands CloudFormation. Stave evaluates any JSON-represented infrastructure. The engine doesn't know what cloud provider the data came from. It evaluates properties.* paths on assets with open-ended type and vendor strings.

Formal reasoning chain

stave apply --trace audit_trace.json
Enter fullscreen mode Exit fullscreen mode

The logic trace records every decision: exemption check, predicate evaluation, threshold check, verdict. For passing resources, this is "Proof of Pass". It is positive evidence that the control was evaluated and the asset was compliant. cfn_nag doesn't explain why something passed.

Compound risk detection

cfn_nag evaluates rules independently. Stave detects compound patterns:

  • COMPOUND.001: Public access + wildcard IAM actions = lateral movement
  • COMPOUND.002: Encryption enabled + public access = false security
  • COMPOUND.003: VPC endpoint + no endpoint policy = wormhole

Individual rules pass. The combination is dangerous. cfn_nag can't see this.

Comparison

Capability cfn_nag Stave
What it scans CloudFormation templates Observed infrastructure state (any JSON)
When it runs Pre-deployment (template scan) Pre-deployment (CI gate) AND post-deployment (continuous)
Duration tracking No Yes — SLA-based, temporal evidence
Multi-cloud No (CloudFormation only) Yes — AWS, GCP, DNS, any vendor
Offline/air-gapped Yes Yes
Rule format Ruby classes YAML + CEL predicates (no code)
Custom rules Write Ruby, inherit BaseRule Write YAML, make gencontrol scaffolds it
Suppression Per-resource metadata annotation Acknowledged exceptions with compensating controls
Output formats Text, JSON Text, JSON, SARIF
CI integration GitHub Action, CodePipeline Exit codes, baselines, gating, diff, fix-loop, SARIF
Pre-deployment blocking Yes (template scan) Yes (gate on new/any violations, block PRs)
Compliance mapping No HIPAA citations, CIS references
Compound risk No 3 compound detectors
Proof of pass No Logic trace with reasoning chain
SBOM No SPDX + CycloneDX generation
Deterministic Mostly Fully (--eval-time flag, policy hash)

The static analysis ceiling

cfn_nag is a linter. Linters evaluate rules independently against individual resources. This creates a hard ceiling:

No compound risk detection. cfn_nag can tell you a bucket is public (W warning) and that an IAM policy has wildcard actions (F failure) but it cannot tell you that the combination is the S3+IAM lateral movement pattern present in the majority of documented AWS breaches. Each rule runs in isolation. There is no cross-resource, cross-rule reasoning.

No temporal reasoning. A linter sees one template at one point in time. It cannot track that a misconfiguration has persisted for 6 months, or that it was briefly introduced and fixed within the SLA window. Duration is invisible.

No negative-space detection. cfn_nag can check what's in the template. It cannot check what's missing from infrastructure that exists outside CloudFormation such as console changes, SDK calls, cross-account policies, or resources created by other tools.

False positives from expression resolution. Static analyzers must parse the template language such as CloudFormation intrinsic functions, Terraform HCL expressions, for_each loops, try() fallbacks. When the parser can't fully resolve a complex expression, it guesses wrong. An example: Trivy reports "Bucket does not have versioning enabled" on a Terraform module where versioning IS enabled via for_each + try() in a locals block. The HCL expression is too complex for the parser to resolve, so it defaults to "not enabled." Stave doesn't have this problem. It evaluates the deployed bucket's actual versioning.enabled property, not the expression that computes it.

These aren't bugs in cfn_nag or Trivy. They're fundamental limits of static template analysis. A linter parses text. Stave evaluates state.

Where cfn_nag wins

  • CloudFormation-native: Understands Fn::If, Fn::FindInMap, parameters, conditions, pseudo-functions. Stave doesn't parse CloudFormation. It evaluates observed state.
  • Pre-deployment template analysis: Catches problems before any infrastructure exists. Stave needs an observation snapshot. It gates deployments against existing state, not proposed templates.
  • Template-level suppressions: Inline Metadata.cfn_nag.rules_to_suppress on the resource itself. Convenient for developers.
  • Parameter injection: Can substitute parameter values for static analysis. Stave evaluates actual values, not parameterized ones.
  • Zero infrastructure required: Runs on a template file alone. Stave requires an extractor to have produced observation snapshots first.

Where Stave goes beyond

1. Observed state, not intended state

Someone changes a bucket policy via the console. cfn_nag doesn't see it. It only scans templates. Stave evaluates the actual bucket configuration captured by the extractor.

2. Time is the threat

cfn_nag is binary: pass or fail. Stave adds the time dimension. A bucket that was public for 5 minutes is different from one public for 6 months. Duration tracking with SLA thresholds turns binary findings into risk-prioritized evidence.

3. Controls are data, not code

cfn_nag rules are Ruby classes that inherit from BaseRule. Adding a new check means writing Ruby code, understanding the object model, and testing against template fixtures.

Stave controls are YAML files with unsafe_predicate blocks. Adding a new check means writing 20 lines of YAML. The Policy Forge scaffolds it with validation and test fixtures in 30 seconds:

make gencontrol ID=CTL.S3.NEW.001 NAME="My Check" \
  FIELD=properties.storage.access.public_read \
  REMEDIATION="Enable Public Access Block."
Enter fullscreen mode Exit fullscreen mode

4. Formal proof system

Stave is not a scanner. It's a policy evaluation engine for JSON-represented infrastructure. The engine is cloud-agnostic. Asset types and vendors are open strings. Adding Route53 or Kubernetes support requires only YAML controls and a contract extension with zero engine changes.

5. Pre-deployment CI gating

Stave isn't just post-deployment. It blocks insecure changes in CI before they reach production:

# Save current state as baseline
stave ci baseline save --in current-eval.json --out baseline.json

# After a proposed change, evaluate the new state
stave apply --format json > proposed-eval.json

# Block the PR if it introduces new violations
stave ci gate --policy fail_on_new_violation \
  --in proposed-eval.json --baseline baseline.json
# Exit 3 = new violations introduced → PR blocked
Enter fullscreen mode Exit fullscreen mode

The ci diff command shows exactly what changed:

stave ci diff --current proposed-eval.json --baseline baseline.json
# Shows: new findings, resolved findings, unchanged findings
Enter fullscreen mode Exit fullscreen mode

This catches configuration changes that make existing infrastructure less secure even if the CloudFormation template looks fine. A template can be valid and still produce an insecure configuration when applied to existing state.

When to use which

Use cfn_nag when you want to lint CloudFormation templates at authoring time. It understands CloudFormation intrinsic functions and catches patterns before any infrastructure exists.

Use Stave when you want to evaluate actual infrastructure state, track duration, prove compliance, gate deployments against observed state, or evaluate multi-cloud infrastructure.

Use both for defense in depth: cfn_nag catches template-level patterns at authoring time, Stave catches everything in observed state including console changes, drift, configurations no template defined, and proposed changes that would degrade existing security posture.

The difference

cfn_nag asks: "Does this template follow security patterns?"

Stave asks: "Is this infrastructure safe, for how long has it been unsafe, and can I prove it?"

One is a linter. The other is a formal proof system. They solve different problems at different layers.


*Stave

Top comments (0)