Skip to content

Commit 7b0ad97

Browse files
authored
Escape SSM pattern matching when using SAM and SSM (#3686)
* Escape SSM pattern matching when using SAM and SSM
1 parent cc1b2f7 commit 7b0ad97

File tree

3 files changed

+99
-3
lines changed

3 files changed

+99
-3
lines changed

src/cfnlint/context/context.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from abc import ABC, abstractmethod
99
from collections import deque
1010
from dataclasses import InitVar, dataclass, field, fields
11+
from functools import lru_cache
1112
from typing import Any, Deque, Iterator, Sequence, Set, Tuple
1213

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

45+
self.has_sam_transform = lru_cache()(self.has_sam_transform) # type: ignore
46+
self.has_language_extensions_transform = lru_cache()( # type: ignore
47+
self.has_language_extensions_transform
48+
)
49+
4450
def has_language_extensions_transform(self):
4551
lang_extensions_transform = "AWS::LanguageExtensions"
4652
return bool(lang_extensions_transform in self._transforms)
@@ -282,12 +288,16 @@ class Parameter(_Ref):
282288
default: Any = field(init=False)
283289
allowed_values: Any = field(init=False)
284290
description: str | None = field(init=False)
291+
ssm_path: str | None = field(init=False, default=None)
285292

286293
parameter: InitVar[Any]
287294

288295
def __post_init__(self, parameter) -> None:
289296
if not isinstance(parameter, dict):
290297
raise ValueError("Parameter must be a object")
298+
299+
self.is_ssm_parameter = lru_cache()(self.is_ssm_parameter) # type: ignore
300+
291301
self.default = None
292302
self.allowed_values = []
293303
self.min_value = None
@@ -303,7 +313,8 @@ def __post_init__(self, parameter) -> None:
303313

304314
# SSM Parameter defaults and allowed values point to
305315
# SSM paths not to the actual values
306-
if self.type.startswith("AWS::SSM::Parameter::"):
316+
if self.is_ssm_parameter():
317+
self.ssm_path = parameter.get("Default", "")
307318
return
308319

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

363+
def is_ssm_parameter(self) -> bool:
364+
return self.type.startswith("AWS::SSM::Parameter::")
365+
352366

353367
@dataclass
354368
class Resource(_Ref):

src/cfnlint/rules/resources/properties/Pattern.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
SPDX-License-Identifier: MIT-0
44
"""
55

6+
from typing import Any
7+
68
import regex as re
79

10+
from cfnlint.jsonschema import ValidationResult, Validator
811
from cfnlint.jsonschema._keywords import pattern
912
from cfnlint.rules import CloudFormationLintRule
1013

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

4548
# pylint: disable=unused-argument, arguments-renamed
46-
def pattern(self, validator, patrn, instance, schema):
49+
def pattern(
50+
self, validator: Validator, patrn: str, instance: Any, schema: Any
51+
) -> ValidationResult:
52+
# https://github.com/aws-cloudformation/cfn-lint/issues/3640
53+
if validator.context.transforms.has_sam_transform():
54+
for _, param in validator.context.parameters.items():
55+
if param.is_ssm_parameter():
56+
if param.ssm_path == instance:
57+
return
58+
4759
if (
4860
len(validator.context.path.value_path) > 0
4961
and validator.context.path.value_path[0] == "Parameters"
5062
):
5163
if self.child_rules.get("W2031"):
52-
yield from self.child_rules["W2031"].pattern(
64+
yield from self.child_rules["W2031"].pattern( # type: ignore
5365
validator, patrn, instance, schema
5466
)
5567
return
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
import pytest
7+
8+
from cfnlint.jsonschema import ValidationError
9+
from cfnlint.rules.parameters.ValuePattern import ValuePattern as ParameterPattern
10+
from cfnlint.rules.resources.properties.Pattern import Pattern
11+
12+
13+
@pytest.fixture(scope="module")
14+
def rule():
15+
rule = Pattern()
16+
rule.child_rules["W2031"] = ParameterPattern()
17+
yield rule
18+
19+
20+
@pytest.fixture
21+
def template():
22+
return {
23+
"Transform": ["AWS::Serverless-2016-10-31"],
24+
"Parameters": {
25+
"SSMParameter": {
26+
"Type": "AWS::SSM::Parameter::Value<String>",
27+
"Default": "foo",
28+
},
29+
"Parameter": {
30+
"Type": "String",
31+
"Default": "bar",
32+
},
33+
},
34+
}
35+
36+
37+
@pytest.mark.parametrize(
38+
"name,instance,pattern,expected",
39+
[
40+
(
41+
"Valid because SSM parameter default value",
42+
"foo",
43+
"bar",
44+
[],
45+
),
46+
(
47+
"Invalid because not the SSM parameter",
48+
"bar",
49+
"foo",
50+
[
51+
ValidationError(
52+
message="'bar' does not match 'foo'",
53+
)
54+
],
55+
),
56+
(
57+
"Invalid an unrelated to the parameters",
58+
"foobar",
59+
"foofoo",
60+
[
61+
ValidationError(
62+
message="'foobar' does not match 'foofoo'",
63+
)
64+
],
65+
),
66+
],
67+
)
68+
def test_validate(name, instance, pattern, expected, rule, validator):
69+
errs = list(rule.pattern(validator, pattern, instance, {}))
70+
assert errs == expected, f"{name} got errors {errs!r}"

0 commit comments

Comments
 (0)