Repository: aws-cloudformation/cfn-lint

Issue:
E0001 Error transforming template: Fn::ForEach could not be resolved
### CloudFormation Lint Version

0.87.4

### What operating system are you using?

Mac

### Describe the bug

When using `Fn::ForEach` with a dynamic value for the second argument (the value is determined via a !FindInMap call), it appears cfn-lint is not happy if second-level key in the !FindInMap call is of CF parameter type `AWS::SSM::Parameter::Value<String>`.  For e.g. below snippet 

```
Parameters:
  Environment:
    Type: AWS::SSM::Parameter::Value<String>
    Description: Which Account type
    Default: /global/account/accounttype    # this value could be either of DEV/STG/PROD depending on AWS account where executed
    AllowedValues:
      - /global/account/accounttype

...
Mappings:
  Environments:
    DBInstances:  # no. of DB instances in Aurora cluster
      DEV: ['1']
      STG: ['1', '2']
      PROD: ['1', '2', '3']  # HA

...

  Fn::ForEach::DBInstances:
    - DBNum
    - !FindInMap [Environments, "DBInstances", !Ref Environment]
    - RDSDBInstance${DBNum}:
        Type: AWS::RDS::DBInstance
        Properties:
          Engine: !Ref Engine
          DBInstanceClass: db.serverless
          DBClusterIdentifier: !Ref RDSDBCluster
```

I am able to successfully create the desired stack with the above snippet (without cfn-lint validation), however cfn-lint validation fails against above code with the error `E0001 Error transforming template: Fn::ForEach could not be resolved`

If I change CF parameter `Environment` to type `String`, then cfn-lint validation succeeds.


### Expected behavior

cfn-lint is able to validate given code snippet without any issues

### Reproduction template

```
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::LanguageExtensions'
Description: 'Aurora Serverless Cluster Stack'

Parameters:
  Environment:
    Type: AWS::SSM::Parameter::Value<String>
    Description: Which Account type
    Default: /global/account/accounttype
    AllowedValues:
      - /global/account/accounttype
    ConstraintDescription: Must be /global/account/accounttype
  DatabaseName:
    Type: String
    Description: Name of the Database
  DatabaseUsername:
    Type: String
    Description: The database username
  Engine:
    Description: Aurora Database engine
    Type: String
  EngineVersion:
    Description: Aurora Database engine version
    Type: String
  SnapshotId:
    Description: Pass in db snapshot when restoring or cloning a new db
    Type: String
    Default: ''   # no snapshot
  DeletionProtection:
    Type: String
    Description: Deletion Protection for the database cluster
  DatabaseDeletionPolicy:
    Type: String
    Description: Deletion Policy in case of stack deletion/updates
  MinCapacity:
    Type: Number
    Description: Minimum compute capacity for the database cluster
  MaxCapacity:
    Type: Number
    Description: Maximum compute capacity for the database cluster

Mappings:
  Config:
    DBInstances:  # no. of DB instances in Aurora cluster
      dev: ['1']
      stg: ['1', '2']
      prd: ['1', '2', '3']  # HA
  Iterator:
    Num:
      '1': 0
      '2': 1
      '3': 2

Conditions:
  SnapshotRestore: !Not [!Equals [!Ref SnapshotId, '']]

Resources:
  AuroraMasterSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub '${AWS::StackName}-db-credentials'
      Description: !Sub "This secret has a dynamically generated secret password for ${AWS::StackName} database"
      GenerateSecretString:
        SecretStringTemplate: !Join ['', ['{"username": "', !Ref DatabaseUsername, '"}']]
        GenerateStringKey: "password"
        PasswordLength: 15
        ExcludePunctuation: true
        ExcludeCharacters: '"@/\'

  RDSDBCluster:
    Type: AWS::RDS::DBCluster
    DeletionPolicy: !Ref DatabaseDeletionPolicy
    UpdateReplacePolicy: !Ref DatabaseDeletionPolicy
    Properties:
      Engine: !Ref Engine
      EngineVersion: !Ref EngineVersion
      EngineMode: provisioned   # serverless v2 uses 'provisioned' for 'EngineMode' attribute
      ServerlessV2ScalingConfiguration:
        MinCapacity: !Ref MinCapacity
        MaxCapacity: !Ref MaxCapacity
      DeletionProtection: !Ref DeletionProtection
      EnableHttpEndpoint: true   # serverless v2 postgresql supports Data API
      MasterUsername: !If
        - SnapshotRestore
        - !Ref "AWS::NoValue"
        - !Sub '{{resolve:secretsmanager:${AuroraMasterSecret}:SecretString:username}}'
      MasterUserPassword: !If
        - SnapshotRestore
        - !Ref "AWS::NoValue"
        - !Sub '{{resolve:secretsmanager:${AuroraMasterSecret}:SecretString:password}}'
      DBSubnetGroupName: test
      DatabaseName: !If [SnapshotRestore, !Ref "AWS::NoValue", !Ref DatabaseName]
      SnapshotIdentifier: !If [SnapshotRestore, !Ref SnapshotId, !Ref "AWS::NoValue"]
      BackupRetentionPeriod: 7
      StorageEncrypted: true
      VpcSecurityGroupIds: [sg-1234]

  Fn::ForEach::DBInstances:
    - DBNum
    - !FindInMap [Config, "DBInstances", !Ref Environment]
    - RDSDBInstance${DBNum}:
        Type: AWS::RDS::DBInstance
        Properties:
          Engine: !Ref Engine
          DBInstanceClass: db.serverless
          DBClusterIdentifier: !Ref RDSDBCluster
          AvailabilityZone: !Select [!FindInMap [Iterator, Num, !Ref DBNum], {Fn::GetAZs: ""}]
```


Discussion:

Resolving patch:
diff --git a/src/cfnlint/template/transforms/_language_extensions.py b/src/cfnlint/template/transforms/_language_extensions.py
--- a/src/cfnlint/template/transforms/_language_extensions.py
+++ b/src/cfnlint/template/transforms/_language_extensions.py
@@ -51,6 +51,7 @@ def language_extension(cfn: Any) -> TransformResult:
     try:
         return transform.transform(cfn)
     except (_ValueError, _TypeError, _ResolveError) as e:
+        LOGGER.debug(e, exc_info=True)
         # pylint: disable=import-outside-toplevel
         from cfnlint.match import Match  # pylint: disable=cyclic-import
         from cfnlint.rules import TransformError  # pylint: disable=cyclic-import
@@ -81,6 +82,7 @@ def language_extension(cfn: Any) -> TransformResult:
             )
         ], None
     except Exception as e:  # pylint: disable=broad-exception-caught
+        LOGGER.debug(e, exc_info=True)
         # pylint: disable=import-outside-toplevel
         from cfnlint.match import Match  # pylint: disable=cyclic-import
         from cfnlint.rules import TransformError  # pylint: disable=cyclic-import
@@ -369,6 +371,15 @@ def value(
             except _ResolveError as e:
                 if len(self._map) == 4 and default_on_resolver_failure:
                     return self._map[3].value(cfn, params, only_params)
+                # no default value and map 1 exists
+                try:
+                    for _, v in mapping.get(
+                        t_map[1].value(cfn, params, only_params), {}
+                    ).items():
+                        if isinstance(v, list):
+                            return v
+                except _ResolveError:
+                    pass
                 raise _ResolveError("Can't resolve Fn::FindInMap", self._obj) from e
 
         if len(self._map) == 4 and default_on_resolver_failure:
@@ -447,6 +458,9 @@ def value(
         if not p:
             raise _ResolveError("Can't resolve Fn::Ref", self._obj)
         t = p.get("Type", "String")
+
+        if t.startswith("AWS::SSM::Parameter"):
+            raise _ResolveError("Can't resolve Fn::Ref", self._obj)
         default = p.get("Default")
         if default:
             if "List" in t:


Test patch:
diff --git a/test/unit/module/template/transforms/test_language_extensions.py b/test/unit/module/template/transforms/test_language_extensions.py
--- a/test/unit/module/template/transforms/test_language_extensions.py
+++ b/test/unit/module/template/transforms/test_language_extensions.py
@@ -105,6 +105,11 @@ def setUp(self) -> None:
                     "AccountIds": {
                         "Type": "CommaDelimitedList",
                     },
+                    "SSMParameter": {
+                        "Type": "AWS::SSM::Parameter::Value<String>",
+                        "Default": "/global/account/accounttype",
+                        "AllowedValues": ["/global/account/accounttype"],
+                    },
                 },
             }
         )
@@ -154,6 +159,10 @@ def test_ref(self):
             ],
         )
 
+        fe = _ForEachValue.create({"Ref": "SSMParameter"})
+        with self.assertRaises(_ResolveError):
+            fe.value(self.cfn)
+
 
 class TestFindInMap(TestCase):
     def setUp(self) -> None:
@@ -172,6 +181,13 @@ def setUp(self) -> None:
                 },
                 "Mappings": {
                     "Bucket": {"Production": {"Names": ["foo", "bar"]}},
+                    "Config": {
+                        "DBInstances": {
+                            "Development": "1",
+                            "Stage": ["1", "2"],
+                            "Production": ["1", "2", "3"],
+                        }
+                    },
                 },
             }
         )
@@ -230,6 +246,19 @@ def test_find_in_map_values_with_default(self):
         with self.assertRaises(_ResolveError):
             map.value(self.cfn, None, False, False)
 
+    def test_find_in_map_values_without_default_resolve_error(self):
+        map = _ForEachValueFnFindInMap(
+            "a", ["Bucket", "Production", {"Ref": "SSMParameter"}]
+        )
+
+        self.assertEqual(map.value(self.cfn, None, False, True), ["foo", "bar"])
+
+        map = _ForEachValueFnFindInMap(
+            "a", ["Config", "DBInstances", {"Ref": "SSMParameter"}]
+        )
+
+        self.assertEqual(map.value(self.cfn, None, False, True), ["1", "2"])
+
     def test_find_in_map_values_without_default(self):
         map = _ForEachValueFnFindInMap("a", ["Bucket", {"Ref": "Foo"}, "Key"])