diff --git a/src/cfnlint/context/context.py b/src/cfnlint/context/context.py index e3bd457c5b..021191dc5e 100644 --- a/src/cfnlint/context/context.py +++ b/src/cfnlint/context/context.py @@ -78,9 +78,6 @@ class Context: init=True, default_factory=lambda: set(PSEUDOPARAMS) ) - # can we use dynamic references - dynamic_references: bool = field(init=True, default=False) - # Combiniation of storing any resolved ref # and adds in any Refs available from things like Fn::Sub ref_values: Dict[str, Any] = field(init=True, default_factory=dict) diff --git a/src/cfnlint/jsonschema/_filter.py b/src/cfnlint/jsonschema/_filter.py index 78ab7c9f11..0828a1683e 100644 --- a/src/cfnlint/jsonschema/_filter.py +++ b/src/cfnlint/jsonschema/_filter.py @@ -93,11 +93,9 @@ def _filter_schemas(self, schema, validator: Any) -> Tuple[Any, Any]: def filter(self, validator: Any, instance: Any, schema: Any): # Lets validate dynamic references when appropriate if validator.is_type(instance, "string"): - if validator.context: - if validator.context.dynamic_references: - if REGEX_DYN_REF.findall(instance): - yield (instance, {"dynamicReference": schema}) - return + if REGEX_DYN_REF.findall(instance): + yield (instance, {"dynamicReference": schema}) + return # dependencies, required, minProperties, maxProperties # need to have filtered properties to validate diff --git a/src/cfnlint/rules/functions/DynamicReference.py b/src/cfnlint/rules/functions/DynamicReference.py index 9e975c186b..093f94bca1 100644 --- a/src/cfnlint/rules/functions/DynamicReference.py +++ b/src/cfnlint/rules/functions/DynamicReference.py @@ -8,7 +8,7 @@ from cfnlint.helpers import REGEX_DYN_REF from cfnlint.jsonschema import ValidationError, Validator -from cfnlint.rules import CloudFormationLintRule +from cfnlint.rules.functions._BaseFn import BaseFn _all = { "items": [ @@ -62,14 +62,9 @@ } -class DynamicReference(CloudFormationLintRule): - """ - Check if Dynamic Reference Secure Strings are - only used in the correct locations - """ - +class DynamicReference(BaseFn): id = "E1050" - shortdesc = "Check dynamic references secure strings are in supported locations" + shortdesc = "Validate the structure of a dynamic reference" description = ( "Dynamic References Secure Strings are only supported for a small set of" " resource properties. Validate that they are being used in the correct" @@ -79,6 +74,13 @@ class DynamicReference(CloudFormationLintRule): source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html" tags = ["functions", "dynamic reference"] + def __init__(self) -> None: + super().__init__() + self.child_rules = { + "E1051": None, + "E1027": None, + } + def _clean_errors(self, err: ValidationError) -> ValidationError: err.rule = self err.path = deque([]) @@ -91,6 +93,10 @@ def dynamicReference( if not validator.is_type(instance, "string"): return + # SSM parameters can be used in Resources and Outputs and Parameters + # SSM secrets are only used in a small number of locations + # Secrets manager can be used only in resource properties + for v in REGEX_DYN_REF.findall(instance): parts = v.split(":") @@ -101,14 +107,24 @@ def dynamicReference( found = True if found: - return + continue - if parts[1] in ["ssm", "ssm-secure"]: + if parts[1] == "ssm": evolved = validator.evolve(schema=_ssm) - elif parts[2] == "arn": - evolved = validator.evolve(schema=_secrets_manager_arn) + elif parts[1] == "ssm-secure": + evolved = validator.evolve(schema=_ssm) + rule = self.child_rules["E1027"] + if rule: + yield from rule.validate(validator, {}, v, schema) else: - evolved = validator.evolve(schema=_secrets_manager) + if parts[2] == "arn": + evolved = validator.evolve(schema=_secrets_manager_arn) + else: # this is secrets manager + evolved = validator.evolve(schema=_secrets_manager) + rule = self.child_rules["E1051"] + if rule: + yield from rule.validate(validator, {}, v, schema) for err in evolved.iter_errors(parts): yield self._clean_errors(err) + found = True diff --git a/src/cfnlint/rules/functions/DynamicReferenceSecretsManagerPath.py b/src/cfnlint/rules/functions/DynamicReferenceSecretsManagerPath.py new file mode 100644 index 0000000000..efe758e5d4 --- /dev/null +++ b/src/cfnlint/rules/functions/DynamicReferenceSecretsManagerPath.py @@ -0,0 +1,38 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from typing import Any + +from cfnlint.jsonschema import ValidationError, Validator +from cfnlint.rules import CloudFormationLintRule + + +class DynamicReferenceSecretsManagerPath(CloudFormationLintRule): + id = "E1051" + shortdesc = ( + "Validate dynamic references to secrets manager are only in resource properties" + ) + description = ( + "Dynamic references from secrets manager can only be used " + "in resource properties" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-secretsmanager" + tags = ["functions", "dynamic reference"] + + def validate(self, validator: Validator, s: Any, instance: Any, schema: Any): + if len(validator.context.path) >= 3: + if ( + validator.context.path[0] == "Resources" + and validator.context.path[2] == "Properties" + ): + return + + yield ValidationError( + ( + f"Dynamic reference {instance!r} to secrets manager can only be " + "used in resource properties" + ), + rule=self, + ) diff --git a/src/cfnlint/rules/functions/DynamicReferenceSecureString.py b/src/cfnlint/rules/functions/DynamicReferenceSecureString.py index 7324fd80d7..cefa0db1cd 100644 --- a/src/cfnlint/rules/functions/DynamicReferenceSecureString.py +++ b/src/cfnlint/rules/functions/DynamicReferenceSecureString.py @@ -3,18 +3,13 @@ SPDX-License-Identifier: MIT-0 """ -import regex as re +from typing import Any -import cfnlint.helpers -from cfnlint.rules import CloudFormationLintRule, RuleMatch +from cfnlint.jsonschema import ValidationError, Validator +from cfnlint.rules.functions._BaseFn import BaseFn -class DynamicReferenceSecureString(CloudFormationLintRule): - """ - Check if Dynamic Reference Secure Strings are - only used in the correct locations - """ - +class DynamicReferenceSecureString(BaseFn): id = "E1027" shortdesc = "Check dynamic references secure strings are in supported locations" description = ( @@ -29,102 +24,29 @@ class DynamicReferenceSecureString(CloudFormationLintRule): def __init__(self): """Init""" super().__init__() - self.property_specs = [] - self.resource_specs = [] - self.exceptions = { - "AWS::DirectoryService::MicrosoftAD": "Password", - "AWS::DirectoryService::SimpleAD": "Password", - "AWS::ElastiCache::ReplicationGroup": "AuthToken", - "AWS::IAM::User": { - "LoginProfile": "Password", - }, - "AWS::KinesisFirehose::DeliveryStream": { - "RedshiftDestinationConfiguration": "Password", - }, - "AWS::OpsWorks::App": { - "AppSource": "Password", - }, - "AWS::OpsWorks::Stack": { - "RdsDbInstances": "DbPassword", - "CustomCookbooksSource": "Password", - }, - "AWS::RDS::DBCluster": "MasterUserPassword", - "AWS::RDS::DBInstance": "MasterUserPassword", - "AWS::Redshift::Cluster": "MasterUserPassword", - } - - def _match_values(self, cfnelem, path): - """Recursively search for values matching the searchRegex""" - values = [] - if isinstance(cfnelem, dict): - for key in cfnelem: - pathprop = path[:] - pathprop.append(key) - values.extend(self._match_values(cfnelem[key], pathprop)) - elif isinstance(cfnelem, list): - for index, item in enumerate(cfnelem): - pathprop = path[:] - pathprop.append(index) - values.extend(self._match_values(item, pathprop)) - else: - # Leaf node - if isinstance(cfnelem, str): # and re.match(searchRegex, cfnelem): - for variable in re.findall( - cfnlint.helpers.REGEX_DYN_REF_SSM_SECURE, cfnelem - ): - values.append(path + [variable]) - - return values - - def match_values(self, cfn): - """ - Search for values in all parts of the templates that match the searchRegex - """ - results = [] - results.extend(self._match_values(cfn.template, [])) - # Globals are removed during a transform. They need to be checked manually - results.extend(self._match_values(cfn.template.get("Globals", {}), [])) - return results - - def _test_list(self, parent_list, child_list): - result = False - for idx in range(len(parent_list) - len(child_list) + 1): - if parent_list[idx : idx + len(child_list)] == child_list: - result = True - break - - return result - - def match(self, cfn): - matches = [] - paths = self.match_values(cfn) - - for path in paths: - message = ( - "Dynamic reference secure strings are not supported at this location" - ) - if path[0] == "Resources": - resource_type = ( - cfn.template.get("Resources", {}).get(path[1]).get("Type") - ) - exception = self.exceptions.get(resource_type) - if not exception: - matches.append(RuleMatch(path[:-1], message=message)) - continue - - if isinstance(exception, dict): - matched = False - for k, v in exception.items(): - if self._test_list(path, ["Properties", k]): - if v in path: - matched = True - if not matched: - matches.append(RuleMatch(path[:-1], message=message)) - continue - if not self._test_list(path, ["Properties", exception]): - matches.append(RuleMatch(path[:-1], message=message)) - - else: - matches.append(RuleMatch(path[:-1], message=message)) - - return matches + self.exceptions = [ + "Resources/AWS::DirectoryService::MicrosoftAD/Properties/Password", + "Resources/AWS::DirectoryService::SimpleAD/Properties/Password", + "Resources/AWS::ElastiCache::ReplicationGroup/Properties/AuthToken", + "Resources/AWS::IAM::User/Properties/LoginProfile/Password", + "Resources/AWS::KinesisFirehose::DeliveryStream/Properties/RedshiftDestinationConfiguration/Password", + "Resources/AWS::OpsWorks::App/Properties/AppSource/Password", + "Resources/AWS::OpsWorks::Stack/Properties/RdsDbInstances/DbPassword", + "Resources/AWS::OpsWorks::Stack/Properties/CustomCookbooksSource/Password", + "Resources/AWS::RDS::DBCluster/Properties/MasterUserPassword", + "Resources/AWS::RDS::DBInstance/Properties/MasterUserPassword", + "Resources/AWS::Redshift::Cluster/Properties/MasterUserPassword", + ] + + def validate(self, validator: Validator, s: Any, instance: Any, schema: Any): + keyword = self.get_keyword(validator) + if keyword in self.exceptions: + return + + yield ValidationError( + ( + f"Dynamic reference {instance!r} to SSM secure strings " + "can only be used in resource properties" + ), + rule=self, + ) diff --git a/src/cfnlint/rules/functions/DynamicReferenceSsmPath.py b/src/cfnlint/rules/functions/DynamicReferenceSsmPath.py new file mode 100644 index 0000000000..3514d56098 --- /dev/null +++ b/src/cfnlint/rules/functions/DynamicReferenceSsmPath.py @@ -0,0 +1,40 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from typing import Any + +from cfnlint.jsonschema import ValidationError, Validator +from cfnlint.rules import CloudFormationLintRule + + +class DynamicReferenceSsmPath(CloudFormationLintRule): + id = "E1052" + shortdesc = "Validate dynamic references to SSM are in a valid location" + description = ( + "Dynamic references to SSM parameters are only supported " + "in certain locations" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-ssm" + tags = ["functions", "dynamic reference"] + + def validate(self, validator: Validator, s: Any, instance: Any, schema: Any): + if len(validator.context.path) > 0: + if validator.context.path[0] == "Parameters": + if len(validator.context.path) >= 3: + if validator.context.path[2] in ["Default", "AllowedValues"]: + return + elif validator.context.path[0] == "Resources": + if len(validator.context.path) >= 3: + if validator.context.path[2] in ["Properties", "Metadata"]: + return + elif validator.context.path[0] == "Outputs": + if len(validator.context.path) >= 3: + if validator.context.path[2] in ["Value"]: + return + + yield ValidationError( + (f"Dynamic reference {instance!r} to SSM parameters are not allowed here"), + rule=self, + ) diff --git a/src/cfnlint/rules/resources/properties/Properties.py b/src/cfnlint/rules/resources/properties/Properties.py index f7abbdecab..f248fa8769 100644 --- a/src/cfnlint/rules/resources/properties/Properties.py +++ b/src/cfnlint/rules/resources/properties/Properties.py @@ -68,7 +68,6 @@ def cfnresourceproperties(self, validator: Validator, _, instance: Any, schema): validator = validator.evolve( context=validator.context.evolve( functions=FUNCTIONS, - dynamic_references=True, ) ) diff --git a/test/fixtures/templates/bad/functions/dynamic_reference.yaml b/test/fixtures/templates/bad/functions/dynamic_reference.yaml deleted file mode 100644 index f59005aae4..0000000000 --- a/test/fixtures/templates/bad/functions/dynamic_reference.yaml +++ /dev/null @@ -1,40 +0,0 @@ ---- -AWSTemplateFormatVersion: "2010-09-09" -Description: > - Bad Template for testing Dynamic References -Resources: - MyDB: - Type: AWS::RDS::DBInstance - Properties: - AllocatedStorage: '5' - DBInstanceClass: db.m1.small - Engine: MySQL - MasterUsername: MyName - MasterUserPassword: '{{resolve:ssm-secure:MySecurePassword:1}}' - DeletionPolicy: Snapshot - MyIAMUser: - Type: AWS::IAM::User - Properties: - # Username field not supported for secure strings - UserName: '{{resolve:ssm-secure:MyUsername:1}}' - LoginProfile: - Password: !Sub 'list-{{resolve:ssm-secure:MySecurePassword:1}}' - OpsWorkStack: - Type: AWS::OpsWorks::Stack - Properties: - Name: Test - ServiceRoleArn: service:arn - DefaultInstanceProfileArn: profile:arn - RdsDbInstances: - - - DbPassword: '{{resolve:ssm-secure:MySecurePassword:1}}' - # DB User Not Supported for secure strings - DbUser: !Sub '{{resolve:ssm-secure:MyUsername:1}}' - RdsDbInstanceArn: String - Bucket: - Type: AWS::S3::Bucket - Properties: - BucketName: !Sub '{{resolve:ssm-secure:MyUsername:1}}' -Outputs: - Test: - Value: !Sub '{{resolve:ssm-secure:MyUsername:1}}' diff --git a/test/fixtures/templates/good/functions/dynamic_reference.yaml b/test/fixtures/templates/good/functions/dynamic_reference.yaml deleted file mode 100644 index 19405052bb..0000000000 --- a/test/fixtures/templates/good/functions/dynamic_reference.yaml +++ /dev/null @@ -1,39 +0,0 @@ ---- -AWSTemplateFormatVersion: "2010-09-09" -Description: > - Good Template for testing Dynamic References -Parameters: - myPrefix: - Type: String -Resources: - MyDB: - Type: AWS::RDS::DBInstance - Properties: - AllocatedStorage: '5' - DBInstanceClass: db.m1.small - Engine: MySQL - MasterUsername: '{{resolve:ssm:MyUsername:1}}' - MasterUserPassword: '{{resolve:ssm-secure:My-Secure-Password:1}}' - DeletionPolicy: Snapshot - MyIAMUser: - Type: AWS::IAM::User - Properties: - UserName: myUser - LoginProfile: - Password: !Sub 'list-{{resolve:ssm-secure:MySecurePassword:1}}' - OpsWorkStack: - Type: AWS::OpsWorks::Stack - Properties: - Name: Test - ServiceRoleArn: service:arn - DefaultInstanceProfileArn: profile:arn - RdsDbInstances: - - - DbPassword: '{{resolve:ssm-secure:MySecurePassword:1}}' - DbUser: '{{resolve:ssm:DbUsername:1}}' - RdsDbInstanceArn: String - CustomCookbooksSource: - Password: - Fn::Sub: - - '${prefix}-{{resolve:ssm-secure:MySecurePassword:1}}' - - prefix: !Ref myPrefix diff --git a/test/unit/rules/functions/test_dynamic_reference.py b/test/unit/rules/functions/test_dynamic_reference.py index 3a01408265..92b39a40a7 100644 --- a/test/unit/rules/functions/test_dynamic_reference.py +++ b/test/unit/rules/functions/test_dynamic_reference.py @@ -11,6 +11,12 @@ from cfnlint.template import Template +class _TestRule: + def validate(self, validator, s, instance, schema): + return + yield + + @pytest.fixture(scope="module") def rule(): rule = DynamicReference() @@ -32,30 +38,76 @@ def context(cfn): @pytest.mark.parametrize( - "name,instance,schema,expected", + "name,instance,schema,child_rules,expected", [ ( "Valid SSM Parameter", "{{resolve:ssm:Parameter}}", {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, + [], + ), + ( + "Valid SSM Parameter with no rules", + "{{resolve:ssm:Parameter}}", + {"type": "test"}, + { + "E1051": None, + "E1027": None, + }, + [], + ), + ( + "Valid SSM secure Parameter", + "{{resolve:ssm-secure:Parameter}}", + {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, + [], + ), + ( + "Valid SSM secure string with no rules", + "{{resolve:ssm-secure:Parameter}}", + {"type": "test"}, + { + "E1051": None, + "E1027": None, + }, [], ), ( "Valid SSM Parameter with integer", "{{resolve:ssm:Parameter:1}}", {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, [], ), ( "Valid when item isn't a string", {}, {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, [], ), ( "Basic error when wrong length", "{{resolve:ssm}}", {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, [ ValidationError( "['resolve', 'ssm'] is too short (3)", @@ -68,6 +120,10 @@ def context(cfn): "Invalid SSM Parameter with string version", "{{resolve:ssm:Parameter:a}}", {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, [ ValidationError( "'a' does not match '\\\\d+'", @@ -80,23 +136,46 @@ def context(cfn): "Valid secret manager", "{{resolve:secretsmanager:Secret}}", {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, + [], + ), + ( + "Valid secret manager with no child rules", + "{{resolve:secretsmanager:Secret}}", + {"type": "test"}, + { + "E1051": None, + "E1027": None, + }, [], ), ( "Valid secret manager with secretstring", "{{resolve:secretsmanager:Secret:SecretString}}", {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, [], ), ( "Valid secret manager from another account", "{{resolve:secretsmanager:arn:aws:secretsmanager:us-west-2:123456789012:secret:MySecret-a1b2c3:SecretString:password}}", {"type": "test"}, + { + "E1051": _TestRule(), + "E1027": _TestRule(), + }, [], ), ], ) -def test_validate(name, instance, schema, expected, rule, context, cfn): +def test_validate(name, instance, schema, child_rules, expected, rule, context, cfn): + rule.child_rules = child_rules validator = CfnTemplateValidator(context=context, cfn=cfn) errs = list(rule.dynamicReference(validator, schema, instance, {})) assert errs == expected, f"Test {name!r} got {errs!r}" diff --git a/test/unit/rules/functions/test_dynamic_reference_secrets_manager_path.py b/test/unit/rules/functions/test_dynamic_reference_secrets_manager_path.py new file mode 100644 index 0000000000..44470865c1 --- /dev/null +++ b/test/unit/rules/functions/test_dynamic_reference_secrets_manager_path.py @@ -0,0 +1,66 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +import pytest + +from cfnlint.context import create_context_for_template +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError +from cfnlint.rules.functions.DynamicReferenceSecretsManagerPath import ( + DynamicReferenceSecretsManagerPath, +) +from cfnlint.template import Template + + +@pytest.fixture(scope="module") +def rule(): + rule = DynamicReferenceSecretsManagerPath() + yield rule + + +@pytest.fixture(scope="module") +def cfn(): + return Template( + "", + {}, + regions=["us-east-1"], + ) + + +@pytest.fixture(scope="module") +def context(cfn): + return create_context_for_template(cfn) + + +@pytest.mark.parametrize( + "name,instance,path,expected", + [ + ( + "Valid secrets manager", + "{{resolve:secretsmanager:Parameter}}", + ["Resources", "MyResource", "Properties", "LoginProfile", "Password"], + [], + ), + ( + "Invalid SSM secure location", + "{{resolve:secretsmanager:Parameter}}", + ["Outputs", "MyOutput", "Value"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:secretsmanager:Parameter}}' " + "to secrets manager can only be used in resource properties" + ), + rule=DynamicReferenceSecretsManagerPath(), + ) + ], + ), + ], +) +def test_validate(name, instance, path, expected, rule, context, cfn): + for p in path: + context = context.evolve(path=p) + validator = CfnTemplateValidator(context=context, cfn=cfn) + errs = list(rule.validate(validator, {"type": "string"}, instance, {})) + assert errs == expected, f"Test {name!r} got {errs!r}" diff --git a/test/unit/rules/functions/test_dynamic_reference_secure_string.py b/test/unit/rules/functions/test_dynamic_reference_secure_string.py index a49071496e..dd33a98869 100644 --- a/test/unit/rules/functions/test_dynamic_reference_secure_string.py +++ b/test/unit/rules/functions/test_dynamic_reference_secure_string.py @@ -3,30 +3,71 @@ SPDX-License-Identifier: MIT-0 """ -from test.unit.rules import BaseRuleTestCase +import pytest +from cfnlint.context import create_context_for_template +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError from cfnlint.rules.functions.DynamicReferenceSecureString import ( - DynamicReferenceSecureString, # pylint: disable=E0401 + DynamicReferenceSecureString, ) +from cfnlint.template import Template -class TestDynamicReferenceSecureString(BaseRuleTestCase): - """Test Rules Dynamic References exists""" +@pytest.fixture(scope="module") +def rule(): + rule = DynamicReferenceSecureString() + yield rule - def setUp(self): - """Setup""" - super(TestDynamicReferenceSecureString, self).setUp() - self.collection.register(DynamicReferenceSecureString()) - self.success_templates = [ - "test/fixtures/templates/good/functions/dynamic_reference.yaml" - ] - def test_file_positive(self): - """Test Positive""" - self.helper_file_positive() +@pytest.fixture(scope="module") +def cfn(): + return Template( + "", + { + "Resources": { + "MyResource": { + "Type": "AWS::IAM::User", + "Properties": {"LoginProfile": {"Password": "Foo"}}, + } + } + }, + regions=["us-east-1"], + ) - def test_file_negative(self): - """Test failure""" - self.helper_file_negative( - "test/fixtures/templates/bad/functions/dynamic_reference.yaml", 4 - ) + +@pytest.fixture(scope="module") +def context(cfn): + return create_context_for_template(cfn) + + +@pytest.mark.parametrize( + "name,instance,path,expected", + [ + ( + "Valid SSM Secure Parameter", + "{{resolve:ssm-secure:Parameter}}", + ["Resources", "MyResource", "Properties", "LoginProfile", "Password"], + [], + ), + ( + "Invalid SSM secure location", + "{{resolve:ssm-secure:Parameter}}", + ["Outputs", "MyOutput", "Value"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm-secure:Parameter}}' " + "to SSM secure strings can only be used in resource properties" + ), + rule=DynamicReferenceSecureString(), + ) + ], + ), + ], +) +def test_validate(name, instance, path, expected, rule, context, cfn): + for p in path: + context = context.evolve(path=p) + validator = CfnTemplateValidator(context=context, cfn=cfn) + errs = list(rule.validate(validator, {"type": "string"}, instance, {})) + assert errs == expected, f"Test {name!r} got {errs!r}" diff --git a/test/unit/rules/functions/test_dynamic_reference_ssm_path.py b/test/unit/rules/functions/test_dynamic_reference_ssm_path.py new file mode 100644 index 0000000000..3483cc840f --- /dev/null +++ b/test/unit/rules/functions/test_dynamic_reference_ssm_path.py @@ -0,0 +1,174 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +import pytest + +from cfnlint.context import create_context_for_template +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError +from cfnlint.rules.functions.DynamicReferenceSsmPath import DynamicReferenceSsmPath +from cfnlint.template import Template + + +@pytest.fixture(scope="module") +def rule(): + rule = DynamicReferenceSsmPath() + yield rule + + +@pytest.fixture(scope="module") +def cfn(): + return Template( + "", + {}, + regions=["us-east-1"], + ) + + +@pytest.fixture(scope="module") +def context(cfn): + return create_context_for_template(cfn) + + +@pytest.mark.parametrize( + "name,instance,path,expected", + [ + ( + "Valid SSM Parameter", + "{{resolve:ssm:Parameter}}", + ["Resources", "MyResource", "Properties", "LoginProfile", "Password"], + [], + ), + ( + "Valid SSM Parameter in outputs", + "{{resolve:ssm:Parameter}}", + ["Outputs", "MyOutput", "Value"], + [], + ), + ( + "Valid SSM Parameter in parameters", + "{{resolve:ssm:Parameter}}", + ["Parameters", "MyParameter", "Default"], + [], + ), + ( + "Invalid SSM secure location", + "{{resolve:ssm:Parameter}}", + ["Outputs", "MyOutput", "Export", "Name"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ( + "Invalid SSM secure location in Outputs", + "{{resolve:ssm:Parameter}}", + ["Outputs"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ( + "Invalid SSM parameter type in parameters", + "{{resolve:ssm:Parameter}}", + ["Parameters", "MyParameter", "Type"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ( + "Invalid SSM parameter type in parameters 2", + "{{resolve:ssm:Parameter}}", + ["Parameters", "MyParameter"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ( + "Invalid SSM parameter in Resources", + "{{resolve:ssm:Parameter}}", + ["Resources", "MyResource"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ( + "Invalid SSM parameter in Resource Type", + "{{resolve:ssm:Parameter}}", + ["Resources", "MyResource", "Type"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ( + "Invalid SSM parameter in Metadata", + "{{resolve:ssm:Parameter}}", + ["Metadata"], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ( + "Invalid SSM parameter in no location", + "{{resolve:ssm:Parameter}}", + [], + [ + ValidationError( + ( + "Dynamic reference '{{resolve:ssm:Parameter}}' " + "to SSM parameters are not allowed here" + ), + rule=DynamicReferenceSsmPath(), + ) + ], + ), + ], +) +def test_validate(name, instance, path, expected, rule, context, cfn): + for p in path: + context = context.evolve(path=p) + validator = CfnTemplateValidator(context=context, cfn=cfn) + errs = list(rule.validate(validator, {"type": "string"}, instance, {})) + assert errs == expected, f"Test {name!r} got {errs!r}"