Repository: aws-cloudformation/cfn-lint

Issue:
seeing E3031 for valid SSM
### CloudFormation Lint Version

1.11.0

### What operating system are you using?

Mac/Ubuntu

### Describe the bug

The linter is raising a `E3031` error saying 

```shell 
'<redacted>' does not match '^(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?$'
```

Instead of passing in the full ARN for the resource we are referencing an SSM parameter: `!Ref ...`. That SSM parameter key `<redacted>` points to the value of the actual ARN.

We don't expect that string to pass that regex, because that string isn't an ARN but it's still a valid CF template. 

### Expected behavior

The expectation would be for it to be able to see that it is a SSM parameter key and check what that points to instead.

### Reproduction template

I am not allowed to share this.


Discussion:
I am told it is similar in nature to: https://github.com/aws-cloudformation/cfn-lint/issues/1570
@badrobit what resource and property are you getting this error in?
> @badrobit what resource and property are you getting this error in?

The location where the defined string is located is: 
```yaml
  AuthorizerFunctionArn:
    Type: AWS::SSM::Parameter::Value<String>
    Default: <string>
```

The line in our `template.yaml` that the error is referencing is: 
```yaml
Resources:
```
`Resources`?  Missing something here.  If I can at least get the resource type and the property I should be able to replicate the issue.  I've tried a few options and cannot replicate this.
sorry clearly not the devops person working on getting your a minimum reproducible error. Appreciate your prompt responses! 
I used this template
```yaml
Parameters:
  AuthorizerFunctionArn:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /prod/AuthorizerFunctionArn
Resources:
  Parameter:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref AuthorizerFunctionArn
      Principal: apigateway.amazonaws.com
      SourceArn: !Ref AuthorizerFunctionArn
```

SourceArn definition
```json
"SourceArn": {
   "maxLength": 1024,
   "minLength": 12,
   "pattern": "^arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\\-])+:([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1})?:(\\d{12})?:(.*)$",
   "type": "string"
  }
```

With this template I don't get the error.  This regex matches the regex you have above so I have to be close.  So in general this should be covered but maybe they are using it in a few nested functions or maybe I'm missing something else.
We are able to reproduce using the following template: 

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: mvp

Parameters:
  AuthorizerFunctionArn:
    Type: AWS::SSM::Parameter::Value<String>
    Default: authorizer.arn
    Description: Gatekeeper Authorizer Function Arn

Resources:
  RestApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: my-api
      StageName: v1
      EndpointConfiguration:
        Type: REGIONAL
      BinaryMediaTypes:
        - "*~1*"
      Auth:
        Authorizers:
          GatekeeperAuth:
            FunctionArn: !Ref AuthorizerFunctionArn
        DefaultAuthorizer: GatekeeperAuth
```
If we use the same template with `Default: authorizer` it works and we thought it might be the `arn` but `Default authorizer.func` also fails so maybe it is the "." ? 
The value you provide in your example `Default: /prod/AuthorizerFunctionArn` also doesn't match the regex from the error we see but the linter isn't warning on that: 
<img width="917" alt="image" src="https://github.com/user-attachments/assets/0d1b9bb9-9946-452a-b6ba-3bb5097472a9">

In case it is a bit vague this is the error we are getting when running `v1.11.1` against the provided failing example

```shell
E3031 'authorizer.arn' does not match '^(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?$'
template-error.yaml:11:1
```
The part I was missing was the SAM transform.  Which when cfn-lint run the transform the resulting template has the following.

```
"RestApiGatekeeperAuthAuthorizerPermission": {
      "Properties": {
       "Action": "lambda:InvokeFunction",
       "FunctionName": "authorizer.arn",
```

And the FunctionName is failing.  I need to look into what we are passing here to SAM transform and or a workaround for this scenario.

So to the SAM translator library we are passing the template.   So in this case it looks like the SAM translator is basically subbing it in.  My guess is when this operates on the service side that this value must be changed by the time the transform runs. 
@badrobit I created a related issue on the SAM translator side.  I may get some push back from it but going to see if they can give me options to not have this Default value substitution happen. This would be an issue for all of our validator keywords: enum, pattern, minLength, maxLength, minimum, maximum, etc. 

If I can't get a fix in SAM we will have to come up with a workaround.  Off the top of my head I would probably look at template Default values of SSM parameters then in the validation of those keywords if any of the values match those SSM Default values we skip validation.

Thanks for the super prompt responses and digging into this for us! 
I may have to implement a workaround as I have yet to hear anything on my issue.



Patch:
diff --git a/src/cfnlint/context/context.py b/src/cfnlint/context/context.py
index 0773d798b7..e75efeb1b5 100644
--- a/src/cfnlint/context/context.py
+++ b/src/cfnlint/context/context.py
@@ -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
@@ -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)
@@ -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
@@ -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<"):
@@ -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):
diff --git a/src/cfnlint/rules/resources/properties/Pattern.py b/src/cfnlint/rules/resources/properties/Pattern.py
index 1e63337f21..52b758727a 100644
--- a/src/cfnlint/rules/resources/properties/Pattern.py
+++ b/src/cfnlint/rules/resources/properties/Pattern.py
@@ -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
 
@@ -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


Test patch:
diff --git a/test/unit/rules/resources/properties/test_pattern_ssm.py b/test/unit/rules/resources/properties/test_pattern_ssm.py
new file mode 100644
index 0000000000..6836cc18a4
--- /dev/null
+++ b/test/unit/rules/resources/properties/test_pattern_ssm.py
@@ -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}"