diff --git a/src/cfnlint/data/schemas/other/ssm/__init__.py b/src/cfnlint/data/schemas/other/ssm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/cfnlint/data/schemas/other/ssm/document.json b/src/cfnlint/data/schemas/other/ssm/document.json new file mode 100644 index 0000000000..f02293cf68 --- /dev/null +++ b/src/cfnlint/data/schemas/other/ssm/document.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "additionalProperties": true, + "properties": { + "assumeRole": { + "type": "string" + }, + "description": { + "type": "string" + }, + "schemaVersion": { + "description": "The schema version to use.", + "enum": [ + "0.3", + "1.2", + "2.0", + "2.2" + ], + "type": "string" + } + }, + "required": [ + "schemaVersion" + ], + "title": "JSON schema for AWS Automation Documents", + "type": "object" +} diff --git a/src/cfnlint/rules/resources/ssm/Document.py b/src/cfnlint/rules/resources/ssm/Document.py new file mode 100644 index 0000000000..27c9b36c77 --- /dev/null +++ b/src/cfnlint/rules/resources/ssm/Document.py @@ -0,0 +1,84 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from __future__ import annotations + +from typing import Any + +import cfnlint.data.schemas.other.ssm +from cfnlint.decode import decode_str +from cfnlint.jsonschema import ValidationError, ValidationResult, Validator +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails +from cfnlint.schema.resolver import RefResolver + + +class Document(CfnLintJsonSchema): + id = "E3051" + shortdesc = "Validate the structure of a SSM document" + description = ( + "SSM documents are nested JSON/YAML in CloudFormation " + "this rule adds validation to those documents" + ) + source_url = "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html" + tags = ["properties", "ssm", "document"] + + def __init__(self): + super().__init__( + ["Resources/AWS::SSM::Document/Properties/Content"], + schema_details=SchemaDetails( + module=cfnlint.data.schemas.other.ssm, + filename="document.json", + ), + ) + + store = { + "document": self.schema, + } + + self.resolver = RefResolver.from_schema(self.schema, store=store) + + # pylint: disable=unused-argument + def validate( + self, + validator: Validator, + _: Any, + instance: Any, + schema: dict[str, Any], + ) -> ValidationResult: + # First time child rules are configured against the rule + # so we can run this now + + if validator.is_type(instance, "string"): + ssm_validator = validator.evolve( + context=validator.context.evolve( + functions=[], + strict_types=True, + ), + resolver=self.resolver, + schema=self.schema, + ) + + instance, errs = decode_str(instance) + if errs: + yield ValidationError( + "Document is not of type 'object'", + validator="type", + rule=self, + ) + return + else: + ssm_validator = validator.evolve( + cfn=validator.cfn, + context=validator.context.evolve( + strict_types=True, + ), + resolver=self.resolver, + schema=self.schema, + ) + + for err in ssm_validator.iter_errors(instance): + if not err.validator.startswith("fn_") and err.validator not in ["cfnLint"]: + err.rule = self + yield err diff --git a/src/cfnlint/rules/resources/ssm/__init__.py b/src/cfnlint/rules/resources/ssm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/unit/rules/resources/ssm/__init__.py b/test/unit/rules/resources/ssm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/unit/rules/resources/ssm/test_document.py b/test/unit/rules/resources/ssm/test_document.py new file mode 100644 index 0000000000..be0eb92ef4 --- /dev/null +++ b/test/unit/rules/resources/ssm/test_document.py @@ -0,0 +1,128 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.jsonschema.validators import ValidationError +from cfnlint.rules.resources.ssm.Document import Document + + +@pytest.fixture(scope="module") +def rule(): + rule = Document() + yield rule + + +@pytest.mark.parametrize( + "name,document,expected", + [ + ( + "Valid string yaml", + """ + schemaVersion: "2.2" + mainSteps: + - action: aws:runShellScript + """, + [], + ), + ( + "Valid string json", + """ + { + "schemaVersion": "2.2", + "mainSteps": [ + { + "action": "aws:runShellScript" + } + ] + } + """, + [], + ), + ( + "Valid object", + { + "schemaVersion": "2.2", + "mainSteps": [ + {"action": "aws:runShellScript"}, + ], + }, + [], + ), + ( + "InValid string yaml", + """ + schemaVersion: 2.2 + mainSteps: + - action: aws:runShellScript + """, + [ + ValidationError( + "2.2 is not of type 'string'", + rule=Document(), + validator="type", + schema_path=deque(["properties", "schemaVersion", "type"]), + path=deque(["schemaVersion"]), + ) + ], + ), + ( + "Not a valid json or yaml object", + "arn:aws-us-gov:iam::123456789012:role/test", + [ + ValidationError( + "Document is not of type 'object'", + rule=Document(), + validator="type", + schema_path=deque([]), + path=deque([]), + ) + ], + ), + ( + "Invalid schema version in object", + { + "schemaVersion": 2.2, + "mainSteps": [ + {"action": "aws:runShellScript"}, + ], + }, + [ + ValidationError( + "2.2 is not of type 'string'", + rule=Document(), + validator="type", + schema_path=deque(["properties", "schemaVersion", "type"]), + path=deque(["schemaVersion"]), + ) + ], + ), + ( + "Invalid type", + [], + [ + ValidationError( + "[] is not of type 'object'", + rule=Document(), + validator="type", + schema_path=deque(["type"]), + path=deque([]), + ) + ], + ), + ], +) +def test_validate( + name, + document, + expected, + rule, + validator, +): + errs = list(rule.validate(validator, {}, document, {})) + + assert errs == expected