Skip to content

fix(E3051): stop firing on Content behind an intrinsic function - #4956

Merged
kddejong merged 1 commit into
aws-cloudformation:mainfrom
kddejong:fix/issue/e3051-content-keyword
Oct 29, 2026
Merged

kddejong merged 1 commit into
aws-cloudformation:mainfrom
kddejong:fix/issue/e3051-content-keyword

Conversation

@kddejong

Copy link
Copy Markdown
Contributor

Problem

E3051 reports false positives on current main for any AWS::SSM::Document whose Content comes from an intrinsic function.

PR #4844 (commit fecf750f9) moved E3051's keyword up one level:

-            ["Resources/AWS::SSM::Document/Properties/Content"],
+            ["Resources/AWS::SSM::Document/Properties"],

That let validate() read the sibling DocumentType off the instance, but .../Properties/Content sits behind cfn-lint's intrinsic-function machinery and .../Properties does not. The rule went from receiving a resolved Content value to receiving a raw dict, and then validated that dict against the Automation document schema.

This template deploys (CREATE_COMPLETE, us-east-1, 2026-10-28, with a well-formed stringified Content):

Parameters:
  DocBody:
    Type: String
    Default: '{"schemaVersion":"2.2","description":"d","mainSteps":[]}'
Resources:
  Doc:
    Type: AWS::SSM::Document
    Properties:
      DocumentType: Command
      Content: !Ref DocBody

fecf750f9~1 is silent on it. Current main reports three findings:

E3051 'schemaVersion' is a required property
E3051 {'Ref': 'DocBody'} is not of type 'object' when 'Ref' is resolved
E3051 {'Ref': 'DocBody'} is not of type 'object'

Ref, Fn::GetAtt and Fn::Sub are all affected. Fn::If on Content is not — the condition filter still handles it.

Fix

Put the keyword back at Content and read DocumentType through Template.get_cfn_path, the helper built for exactly this. E1041 (rules/functions/RefFormat.py) reads a sibling VpcId the same way.

The logical ID comes from validator.context.path.path[1], the established positional read used by E3035, E3036, E3055, W2530, W3660 and others. Index 1 rather than -1 because Fn::If on a value appends ['Fn::If', <branch>] to the path, so the tail is not stable while the Resources/<id> prefix is.

get_cfn_path yields each Fn::If branch with its condition bound in context, so a conditional DocumentType is now handled. Neither #4844 nor its predecessor covered that: #4844 saw the unresolved {"Fn::If": [...]} dict, found it absent from the AppConfig tuple, and validated — a false positive when both branches are AppConfig types.

Two behaviour notes:

  • An absent or unresolvable DocumentType still validates. DocumentType is not required by the resource schema (only Content is) and the service defaults it to Command, which does require schemaVersion, so failing closed is correct. get_cfn_path yields nothing for an absent key, which falls through to validation on its own.
  • A mixed conditional (!If [C, ApplicationConfiguration, Command]) skips. Any branch being an AppConfig type means some scenario of the template is valid without a schemaVersion, and a missed error costs one failed deployment while a false positive breaks CI for a template that was always correct.

The 4-line comment from #4844 is replaced by one line, and no new isinstance/is None guards were added — get_cfn_path handles the shapes.

One deviation from a straight revert

err.path.appendleft("Content") and path=["Content"] are removed. They were correct at the Properties keyword, but at the Content keyword the context path already ends in Content, so appending it again produced a doubled path. Measured before removing:

E3051 ['Resources', 'Doc', 'Properties', 'Content', 'Content'] 'schemaVersion' is a required property

and after:

E3051 ['Resources', 'Doc', 'Properties', 'Content'] 'schemaVersion' is a required property

Line and column numbers were correct either way (the extra segment does not resolve), so only json/sarif Path output was affected. This matches fecf750f9~1.

Evidence

Measured with cfn-lint -f parseable against upstream/main (027035536) and this branch, after cfn-lint --update-specs.

Case fecf750f9~1 main this branch
Content: !Ref DocBody silent 3 x E3051 silent
Content: !GetAtt Stack1.Outputs.Body silent E1010 + E3051 silent
Content: !Sub '{"schemaVersion":"${Ver}"...}' silent E1019 + E3051 silent
Content: !If [C, <valid>, <valid>] silent silent silent
DocumentType: !If [C, AppConfig, AppConfigSchema] E3051 E3051 silent
DocumentType: !If [C, Command, Automation], bad content E3051 E3051 E3051
DocumentType: ApplicationConfiguration E3051 silent silent
DocumentType: ApplicationConfigurationSchema E3051 silent silent
DocumentType: Command, no schemaVersion E3051 E3051 E3051
DocumentType absent, no schemaVersion E3051 E3051 E3051
DocumentType absent, valid content silent silent silent
Stringified Content, well-formed E3051 E3051 E3051
Stringified Content, malformed E3051 E3051 E3051

The E1010 and E1019 findings in the main column come from E3051's own inner validator: validate() deliberately preserves fn_* errors, so running the document schema over an unresolved function dict surfaces them. They disappear with the keyword position.

Stringified Content is unchanged, including the pre-existing defect in which a well-formed JSON string still draws Document is not valid ('Template could not be parsed: a string or stream input is required'). That is #4894, fixed separately by #4902 — see below.

Good-fixture sweep

cfn-lint -t "test/fixtures/templates/good/**/*.yaml" -f parseable

168 lines on main, 168 on this branch, byte-identical apart from log timestamps. Exit code 6 on both. (test/integration/test_good_templates.py is a curated ~20-template list and does not sweep that directory, so this was run by hand.)

Test suite

3263 passed, 6 failed

The same 6 failures on main and on this branch, from optional dependencies absent locally (junit-xml, sarif, pydot/pygraphviz). Failure sets diffed identical.

Tests

test/unit/rules/resources/ssm/test_document.py now passes Content values to validate() again, with the context path set to Resources/<id>/Properties/Content the way W2530's tests do.

  • test_validate — the existing value-level cases, with the Content path prefix dropped from the expectations
  • test_validate_document_type_scoping — 10 resources covering both AppConfig types (object and string Content), Automation, Command, absent DocumentType, {"Ref": ...} DocumentType, and the three Fn::If shapes
  • test_keyword_registration — template-level via cfnlint.api.lint, which is the only level that can prove the regression: Ref, Fn::GetAtt and Fn::Sub silent, Fn::If on Content validating each branch, Command without schemaVersion firing, ApplicationConfiguration skipping

24 tests in the file, up from 15.

Effect on #4902

#4902 becomes meaningful again, and needs a one-line test edit to go green. Measured, not inferred.

#4902 is based on main without #4844, so its own CI is green, but it does not survive a rebase onto current main:

The edit #4902 needs is the context path. Because the rule now derives the logical ID from validator.context.path.path[1], a validate() call against the default validator fixture (empty Path) raises IndexError: deque index out of range. Adding the path parametrization the rest of the file uses is enough; the production path always has the prefix.

I have not touched #4902 or its branch.

Net change

src/cfnlint/rules/resources/ssm/Document.py: 13 insertions, 14 deletions — net -1 line.

aws-cloudformation#4844 moved E3051's keyword from
Resources/AWS::SSM::Document/Properties/Content up to
Resources/AWS::SSM::Document/Properties so validate() could read the
sibling DocumentType off the instance. Properties sits in front of
cfn-lint's intrinsic-function machinery, so the rule began receiving a
raw dict for Content instead of a resolved value and reported
'schemaVersion' is a required property on every document whose Content
comes from Ref, Fn::GetAtt or Fn::Sub.

Put the keyword back at Content and read DocumentType through
Template.get_cfn_path, the same way E1041 reads a sibling VpcId. The
logical ID comes from validator.context.path.path[1], as in E3035,
E3036, E3055 and W2530. get_cfn_path yields each Fn::If branch with its
condition bound, so a conditional DocumentType is now handled too;
aws-cloudformation#4844 and its predecessor both validated it as an unresolved dict.

The AppConfig exemption from aws-cloudformation#4844 is unchanged: ApplicationConfiguration
and ApplicationConfigurationSchema still skip the Automation schema, an
absent or unresolvable DocumentType still validates, and a Command
document without a schemaVersion still errors.

Errors are no longer prefixed with Content: at this keyword the path
already ends in Content, so appending it produced a doubled
Resources/<id>/Properties/Content/Content in json and sarif output.
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.71%. Comparing base (0270355) to head (363b289).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4956      +/-   ##
==========================================
+ Coverage   94.69%   94.71%   +0.02%     
==========================================
  Files         442      442              
  Lines       16092    16087       -5     
  Branches     3121     3120       -1     
==========================================
- Hits        15238    15237       -1     
+ Misses        470      468       -2     
+ Partials      384      382       -2     
Flag Coverage Δ
unittests 94.71% <100.00%> (+0.02%) ⬆️

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.

@kddejong
kddejong marked this pull request as ready for review October 29, 2026 18:25
@kddejong
kddejong merged commit 008ead6 into aws-cloudformation:main Oct 29, 2026
24 checks passed
@kddejong
kddejong deleted the fix/issue/e3051-content-keyword branch October 29, 2026 18:26
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.

1 participant