Skip to content

Commit

Permalink
Escape SSM pattern matching when using SAM and SSM (#3686)
Browse files Browse the repository at this point in the history
* Escape SSM pattern matching when using SAM and SSM
  • Loading branch information
kddejong authored Sep 12, 2024
1 parent cc1b2f7 commit 7b0ad97
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 3 deletions.
16 changes: 15 additions & 1 deletion src/cfnlint/context/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import InitVar, dataclass, field, fields
from functools import lru_cache
from typing import Any, Deque, Iterator, Sequence, Set, Tuple

from cfnlint.context._mappings import Mappings
Expand Down Expand Up @@ -41,6 +42,11 @@ def __post_init__(self, transforms) -> None:
continue
self._transforms.append(transform)

self.has_sam_transform = lru_cache()(self.has_sam_transform) # type: ignore
self.has_language_extensions_transform = lru_cache()( # type: ignore
self.has_language_extensions_transform
)

def has_language_extensions_transform(self):
lang_extensions_transform = "AWS::LanguageExtensions"
return bool(lang_extensions_transform in self._transforms)
Expand Down Expand Up @@ -282,12 +288,16 @@ class Parameter(_Ref):
default: Any = field(init=False)
allowed_values: Any = field(init=False)
description: str | None = field(init=False)
ssm_path: str | None = field(init=False, default=None)

parameter: InitVar[Any]

def __post_init__(self, parameter) -> None:
if not isinstance(parameter, dict):
raise ValueError("Parameter must be a object")

self.is_ssm_parameter = lru_cache()(self.is_ssm_parameter) # type: ignore

self.default = None
self.allowed_values = []
self.min_value = None
Expand All @@ -303,7 +313,8 @@ def __post_init__(self, parameter) -> None:

# SSM Parameter defaults and allowed values point to
# SSM paths not to the actual values
if self.type.startswith("AWS::SSM::Parameter::"):
if self.is_ssm_parameter():
self.ssm_path = parameter.get("Default", "")
return

if self.type == "CommaDelimitedList" or self.type.startswith("List<"):
Expand Down Expand Up @@ -349,6 +360,9 @@ def ref(self, context: Context) -> Iterator[Tuple[Any, deque]]:
if self.max_value is not None:
yield str(self.max_value), deque(["MaxValue"])

def is_ssm_parameter(self) -> bool:
return self.type.startswith("AWS::SSM::Parameter::")


@dataclass
class Resource(_Ref):
Expand Down
16 changes: 14 additions & 2 deletions src/cfnlint/rules/resources/properties/Pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
SPDX-License-Identifier: MIT-0
"""

from typing import Any

import regex as re

from cfnlint.jsonschema import ValidationResult, Validator
from cfnlint.jsonschema._keywords import pattern
from cfnlint.rules import CloudFormationLintRule

Expand Down Expand Up @@ -43,13 +46,22 @@ def _is_exception(self, instance: str) -> bool:
return False

# pylint: disable=unused-argument, arguments-renamed
def pattern(self, validator, patrn, instance, schema):
def pattern(
self, validator: Validator, patrn: str, instance: Any, schema: Any
) -> ValidationResult:
# https://github.com/aws-cloudformation/cfn-lint/issues/3640
if validator.context.transforms.has_sam_transform():
for _, param in validator.context.parameters.items():
if param.is_ssm_parameter():
if param.ssm_path == instance:
return

if (
len(validator.context.path.value_path) > 0
and validator.context.path.value_path[0] == "Parameters"
):
if self.child_rules.get("W2031"):
yield from self.child_rules["W2031"].pattern(
yield from self.child_rules["W2031"].pattern( # type: ignore
validator, patrn, instance, schema
)
return
Expand Down
70 changes: 70 additions & 0 deletions test/unit/rules/resources/properties/test_pattern_ssm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

import pytest

from cfnlint.jsonschema import ValidationError
from cfnlint.rules.parameters.ValuePattern import ValuePattern as ParameterPattern
from cfnlint.rules.resources.properties.Pattern import Pattern


@pytest.fixture(scope="module")
def rule():
rule = Pattern()
rule.child_rules["W2031"] = ParameterPattern()
yield rule


@pytest.fixture
def template():
return {
"Transform": ["AWS::Serverless-2016-10-31"],
"Parameters": {
"SSMParameter": {
"Type": "AWS::SSM::Parameter::Value<String>",
"Default": "foo",
},
"Parameter": {
"Type": "String",
"Default": "bar",
},
},
}


@pytest.mark.parametrize(
"name,instance,pattern,expected",
[
(
"Valid because SSM parameter default value",
"foo",
"bar",
[],
),
(
"Invalid because not the SSM parameter",
"bar",
"foo",
[
ValidationError(
message="'bar' does not match 'foo'",
)
],
),
(
"Invalid an unrelated to the parameters",
"foobar",
"foofoo",
[
ValidationError(
message="'foobar' does not match 'foofoo'",
)
],
),
],
)
def test_validate(name, instance, pattern, expected, rule, validator):
errs = list(rule.pattern(validator, pattern, instance, {}))
assert errs == expected, f"{name} got errors {errs!r}"

0 comments on commit 7b0ad97

Please sign in to comment.