Skip to content

feat: Add metadata context validation rules (I4010, I4011, I4012) - #4655

Merged
kddejong merged 15 commits into
aws-cloudformation:mainfrom
chrisqm-dev:context-metadata-validation-v2
Sep 17, 2026
Merged

kddejong merged 15 commits into
aws-cloudformation:mainfrom
chrisqm-dev:context-metadata-validation-v2

Conversation

@chrisqm-dev

@chrisqm-dev chrisqm-dev commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Three new experimental rules that check for structured design context (Metadata["com.aws.cloudformation.Context"]) on CloudFormation templates and resources. All three are off by default; their severities differ by intent (see Severity):

Rule Severity What it checks
I4010 informational Template or architecture-relevant resource has no Context block
W4011 warning Context block present but has no why rationale (and no reduced-confidence trust block)
W4012 warning Supplied Context field fails schema validation (wrong type, unrecognized field, misplaced level)

Severity

The severity split follows what triggers each rule:

  • I4010 is informational because it fires when context is absent. Suggesting that an author add documentation they never wrote is advisory, not a defect.
  • W4011 and W4012 are warnings because they fire only when the author has supplied a Context block (both continue when no block is present). The author opted in, and the block is then incomplete (W4011, no why) or invalid against the schema (W4012). A supplied-but-broken block is a real defect in something the author chose to write, so it warrants a warning rather than an info note.
  • No rule uses E. Per project convention, errors are reserved for problems that fail a CloudFormation deployment. Context metadata is documentation and never blocks a deploy.

Numbering is intentionally left as I4010 / W4011 / W4012 (the rule number and severity prefix are independent in cfn-lint). If maintainers prefer a contiguous prefix, these can be renumbered.

Off by default

All three rules are experimental=True, so a default cfn-lint run emits nothing and exits 0 — the experimental gate blocks them regardless of severity.

Enablement then differs by severity:

  • I4010 (informational) requires both --include-experimental and --include-checks I.
  • W4011 / W4012 (warnings) require only --include-experimental — warnings are already in cfn-lint's default check set, so no --include-checks is needed once experimental rules are included.

Verified end-to-end:

  • No flags: exit 0, silent (all three)
  • --include-experimental alone: W4011/W4012 fire on a supplied-but-invalid Context block; I4010 stays silent
  • --include-experimental --include-checks I: all three active

Schema

The context.json schema matches the published CloudFormation Metadata Context schema:

  • ResourceContext: why, must, mutable, mutability, trust, deps (additionalProperties: false)
  • TemplateContext: arch, must, ref, owner (additionalProperties: false)
  • TrustObject: src (required), conf (required), cite, note

Resource selection

I4010 uses a significance gate -- not every resource is required to carry context:

  • Incidental/framework resources are excluded: CDK metadata, provider-framework handlers (LogRetention, framework-onEvent/isComplete/onTimeout), and the AwsCustomResource provider singleton. CDK's own context aspect (aws/aws-cdk#38381) deliberately skips these resources, so they arrive in the synthesized template without context by design. The pattern set approximates by naming convention a distinction CDK draws structurally via defaultChild. It is not exhaustive -- BucketDeployment handlers and others are not matched. lambdaPurpose is the axis to extend along. Users can extend per-run via additional_incidental_patterns.
  • Low-value subordinate types are excluded: AWS::Logs::LogGroup, AWS::Logs::LogStream, AWS::IAM::Policy, AWS::S3::BucketPolicy, AWS::SNS::TopicPolicy, AWS::SQS::QueuePolicy, AWS::Lambda::Permission. Resource policies and permission grants are exempt because the policy body is the rationale and the template already contains it. AWS::IAM::Role and AWS::IAM::ManagedPolicy are deliberately kept -- both have real hand-authored forms where why adds value. Users can extend via additional_low_value_types.
  • MODULE pseudo-resources (*::MODULE) are excluded: the resources they expand to live in the registered module body, so architectural significance cannot be determined from the Type.
  • Context supplied on any exempt resource is still validated by W4011/W4012.

CDK templates

CDK-synthesized templates are out of scope: all three rules return no findings when a CDK marker is present (AWS::CDK::Metadata, the CDKMetadata logical ID, or any resource carrying aws:cdk:path). The author wrote L2/L3 constructs, not raw CloudFormation, so raw-resource context expectations do not apply.

Aggregate design

I4010 emits at most two findings per template:

  1. A template diagnostic when >= 2 significant resources exist and no top-level Context block is present.
  2. A resource aggregate listing every significant resource missing context in a single finding. The message is the remediation instruction and includes every resource so consumers can act on the complete set without a second run. The enumeration is intentionally not truncated.

Per-resource suppression via Metadata.cfn-lint.config.ignore_checks: [I4010] removes individual resources from the aggregate rather than suppressing the entire finding. (W4011/W4012 are per-resource / per-field, matching cfn-lint's per-location convention.)

Remediation guidance

When why is unknown, I4010 and W4011 guide users to record reduced confidence rather than guessing:

set trust to {src: infer, conf: low, note: "rationale not documented"}

This uses the schema's trust.note field ("Reason for reduced confidence, typically when conf=low") as the honest-unknown path.

Upstream status

This convention is being adopted alongside:

Both the CDK feature and its RFC are still open; the Context convention is not yet finalized. This is the primary reason these rules ship experimental and off by default. The wire format was verified against #38381's integration snapshot: why, must, mutable, mutability, trust (src/conf), arch, ref (at/has/scope), owner, and all four mutability tokens agree.

Configuration

# .cfnlintrc
configure_rule:
  I4010:
    additional_incidental_patterns:
      - "CustomCDKBucketDeployment"
    additional_low_value_types:
      - "AWS::Events::EventBus"

additional_incidental_patterns is available on all three rules; additional_low_value_types is I4010-only (W4011/W4012 validate supplied context everywhere, so low-value types are not exempt from validation).

Testing

  • Unit tests live in test/unit/rules/metadata/, split one-file-per-rule (test_context_missing.py, test_context_missing_why.py, test_context_schema.py, test_context_targeting.py, test_context_enablement.py) -- 76 passing.
  • Integration/snapshot fixtures are unaffected by the warning severity: quickstart templates carry no Context blocks, so W4011/W4012 never fire on them and I4010 output is unchanged -- 27 integration tests passing.
  • Rules are one-per-file with shared infrastructure in _BaseContext.py (CDK/incidental detection, resource iteration, shared config).
  • Pre-commit (ruff, ruff-format, isort, mypy, bandit) clean.

@chrisqm-dev
chrisqm-dev force-pushed the context-metadata-validation-v2 branch 3 times, most recently from d84b19c to 07d524b Compare August 24, 2026 21:15
@codecov

codecov Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.78%. Comparing base (07a8e77) to head (d0182f2).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4655      +/-   ##
==========================================
+ Coverage   94.67%   94.78%   +0.10%     
==========================================
  Files         435      439       +4     
  Lines       15584    15805     +221     
  Branches     3021     3055      +34     
==========================================
+ Hits        14754    14980     +226     
+ Misses        453      450       -3     
+ Partials      377      375       -2     
Flag Coverage Δ
unittests 94.78% <100.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chrisqm-dev
chrisqm-dev force-pushed the context-metadata-validation-v2 branch 2 times, most recently from bf651b1 to 0494029 Compare August 24, 2026 22:07
@r-heimann

Copy link
Copy Markdown
Contributor

Always great to see AWS IaC devs contribute to cfn-lint!
I would like to ask why cfn-lint should throw warnings for not using https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-metadata.html. When you write "pure" / non-CDK CloudFormation it usually is not needed since a user can just use yaml comments to add more context. In my opinion it would add additional lines of code that is not mandatory for using CloudFormation and that is why I don't believe it would be a good idea to "enforce" this parameter on every resource. This also should be considered a breaking-change, since it affects all CloudFormation templates that can be deployed but are considered "broken" by cfn-lint's ruleset.

Would this be information that can contribute to an LLM context? This RFC aws/aws-cdk-rfcs#981 seems to be more of a CDK change.

@atennak1

atennak1 commented Sep 2, 2026

Copy link
Copy Markdown

... a user can just use yaml comments to add more context. In my opinion it would add additional lines of code that is not mandatory for using CloudFormation and that is why I don't believe it would be a good idea to "enforce" this parameter on every resource. This also should be considered a breaking-change, since it affects all CloudFormation templates that can be deployed but are considered "broken" by cfn-lint's ruleset.

Agreed @r-heimann . I think we're leaning towards defaulting to disabling this for YAML completely, or disabling when we see any YAML comments. Do you see any issue with leaving it enabled for JSON?

... Would this be information that can contribute to an LLM context? ...

Yes that's the intent. We'll update the PR description soon with more context (pun intended).

@r-heimann

r-heimann commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Do you see any issue with leaving it enabled for JSON?

I never really used the JSON version of CloudFormation, but i think it doesn't support something like JSONC so it may be a good idea to include it.

Yes that's the intent. We'll update the PR description soon with more context (pun intended).

Got it, thank you

@kddejong

Copy link
Copy Markdown
Contributor

On severity + default enablement

The bigger risk here is the blast radius at Warning + on-by-default. --non-zero-exit-code defaults to informational, so a Warning match sets a non-zero exit (4). W4010 fires on essentially every template that hasn't adopted the convention — the ~55 rewritten result fixtures in this PR are the proof — so every green pipeline that gates on exit code goes red overnight. That's a breaking change even though nothing about deployability changed.

Severity letter alone isn't quite the lever people expect: dropping to informational matters because I rules are excluded from the default include_checks (["W","E"]), not because of the exit level — so I401x is genuinely off until someone opts in with --include-checks I.

Suggestion: ship this family as experimental + I401x, which encodes two independently-true things:

  • experimental — the convention is still stabilizing (aws-cdk#38381, RFC ElasticLoadBalancingV2 TCP_UDP/UDP support #981); key/fields/enums may change.
  • informational — absence of optional documentation is advisory, not a correctness defect.

Both gates are silent by default, so no pipeline breaks. Teams adopting Context opt in explicitly. Graduation path: drop experimental once it stabilizes, keep it I401x opt-in — it never needs to become a default-on Warning. This also lets the fixture changes mostly revert and makes the per-rule severity config option unnecessary (opt-in gating is already covered by --mandatory-checks / --non-zero-exit-code).

@chrisqm-dev
chrisqm-dev force-pushed the context-metadata-validation-v2 branch from 0494029 to 4f9a56a Compare September 14, 2026 14:59
@chrisqm-dev chrisqm-dev changed the title feat: Add metadata context validation rules (W4010, W4011, W4012) feat: Add metadata context validation rules (I4010, I4011, I4012) Sep 14, 2026
@chrisqm-dev
chrisqm-dev force-pushed the context-metadata-validation-v2 branch 4 times, most recently from b1e1441 to 90d2700 Compare September 14, 2026 18:22
@chrisqm-dev

chrisqm-dev commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

On severity + default enablement

The bigger risk here is the blast radius at Warning + on-by-default. --non-zero-exit-code defaults to informational, so a Warning match sets a non-zero exit (4). W4010 fires on essentially every template that hasn't adopted the convention — the ~55 rewritten result fixtures in this PR are the proof — so every green pipeline that gates on exit code goes red overnight. That's a breaking change even though nothing about deployability changed.

Severity letter alone isn't quite the lever people expect: dropping to informational matters because I rules are excluded from the default include_checks (["W","E"]), not because of the exit level — so I401x is genuinely off until someone opts in with --include-checks I.

Suggestion: ship this family as experimental + I401x, which encodes two independently-true things:

  • experimental — the convention is still stabilizing (aws-cdk#38381, RFC ElasticLoadBalancingV2 TCP_UDP/UDP support #981); key/fields/enums may change.
  • informational — absence of optional documentation is advisory, not a correctness defect.

Both gates are silent by default, so no pipeline breaks. Teams adopting Context opt in explicitly. Graduation path: drop experimental once it stabilizes, keep it I401x opt-in — it never needs to become a default-on Warning. This also lets the fixture changes mostly revert and makes the per-rule severity config option unnecessary (opt-in gating is already covered by --mandatory-checks / --non-zero-exit-code).

Thank you for the feedback. Updated this PR so that the missing context rule is informational instead of warning. We will keep the other two rules as warnings, since it flags invalid syntax and will only flag if the context is already present in the template. Also, added the rules only in the experimental section for now. What would the graduation path and "stabilization" look like? These metadata contexts are already released to the public

@@ -1,4 +1,31 @@
[
{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These test fixtures use the experimental mode and need to account for the new rules

@chrisqm-dev
chrisqm-dev force-pushed the context-metadata-validation-v2 branch 6 times, most recently from 2d684cb to 32acbfb Compare September 16, 2026 15:16
@kddejong

Copy link
Copy Markdown
Contributor

Follow-up on the CDK skip: _BaseContext._is_cdk_template reimplements CDK detection instead of using Template.is_cdk_template(). I know why the private copy is broader — core only keys off the AWS::CDK::Metadata resource, so it misses templates synthesized with analyticsReporting disabled, while this version also checks aws:cdk:path.

Rather than carry two definitions that can drift, should we push the aws:cdk:path signal into core is_cdk_template() and have these rules call it? I checked the other consumers: AvailabilityZone and SubNotJoin both use cfn.is_cdk_template() as a whole-template noise suppressor, and both currently under-suppress on analyticsReporting: false templates for exactly this reason. Broadening core fixes all three and gives us a single source of truth.

Separate, non-blocking thought: since the Context rules validate per-resource, is there appetite for a per-resource is_cdk_resource (keyed on aws:cdk:path) so a mixed template can validate hand-authored resources while skipping synthesized ones, rather than the all-or-nothing template bail? Just flagging the shape — not needed for this PR.

@chrisqm-dev
chrisqm-dev force-pushed the context-metadata-validation-v2 branch from d7f6175 to d0182f2 Compare September 17, 2026 15:27
@kddejong
kddejong merged commit 7d05589 into aws-cloudformation:main Sep 17, 2026
22 checks passed
@chrisqm-dev

Copy link
Copy Markdown
Contributor Author

Follow-up on the CDK skip: _BaseContext._is_cdk_template reimplements CDK detection instead of using Template.is_cdk_template(). I know why the private copy is broader — core only keys off the AWS::CDK::Metadata resource, so it misses templates synthesized with analyticsReporting disabled, while this version also checks aws:cdk:path.

Rather than carry two definitions that can drift, should we push the aws:cdk:path signal into core is_cdk_template() and have these rules call it? I checked the other consumers: AvailabilityZone and SubNotJoin both use cfn.is_cdk_template() as a whole-template noise suppressor, and both currently under-suppress on analyticsReporting: false templates for exactly this reason. Broadening core fixes all three and gives us a single source of truth.

Separate, non-blocking thought: since the Context rules validate per-resource, is there appetite for a per-resource is_cdk_resource (keyed on aws:cdk:path) so a mixed template can validate hand-authored resources while skipping synthesized ones, rather than the all-or-nothing template bail? Just flagging the shape — not needed for this PR.

For the per-resource note, we decided that for now we'll go with the all-or-nothing approach until at least the rule is out of experimental. There isn't much appetite for mixed template validation at the moment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants