Repository: aws-cloudformation/cfn-lint

Issue:
cfn-lint is not validating the FindInMap function for Mappings under RegionMap.
### CloudFormation Lint Version

1.16.1

### What operating system are you using?

Mac

### Describe the bug

cfn-lint is not validating the FindInMap function for values under the Mappings section in RegionMap. In the example below, an invalid reference like 'HVM150' should throw an error since it is not defined in either the east or west regions.
Sample Yaml file:

Mappings:
  RegionMap:
    us-east-1:
      HVM64: "ami-0ff8a91507f77f867"
      HVMG2: "ami-0a584ac55a7631c0c"
    us-west-1:
      HVM64: "ami-0bdb828fd58c52235"
      HVMG2: "ami-066ee5fd4a9ef77f1"
Resources:
  myEC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !FindInMap
        - RegionMap
        - !Ref 'AWS::Region'
        - HVM150
      InstanceType: t2.micro

### Expected behavior

Validation should work similarly to non-RegionMap mappings.

Example of working validation yaml

Mappings:
  InstanceMap:
    Small:
      "InstanceType12": "t2.micro"
      "KeyName": "my-key-pair-small"
      Resources:
  MyEC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: "ami-0ff8a91507f77f867"
      InstanceType:
        Fn::FindInMap:
          - InstanceMap
          - Small
          - "InstanceType12"

### Reproduction template

AWSTemplateFormatVersion: '2010-09-09'
Description: AWS CloudFormation Template with FindInMap for non-region section

Mappings:
  RegionMap:
    us-east-1:
      HVM64: "ami-0ff8a91507f77f867"
      HVMG2: "ami-0a584ac55a7631c0c"
    us-west-1:
      HVM64: "ami-0bdb828fd58c52235"
      HVMG2: "ami-066ee5fd4a9ef77f1"
Resources:
  myEC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !FindInMap
        - RegionMap
        - !Ref 'AWS::Region'
        - HVM150
      InstanceType: t2.micro


Discussion:

Patch:
diff --git a/src/cfnlint/jsonschema/_resolvers_cfn.py b/src/cfnlint/jsonschema/_resolvers_cfn.py
index 8b2be3e2ed..2c0bb487e9 100644
--- a/src/cfnlint/jsonschema/_resolvers_cfn.py
+++ b/src/cfnlint/jsonschema/_resolvers_cfn.py
@@ -15,6 +15,7 @@
     AVAILABILITY_ZONES,
     PSEUDOPARAMS,
     REGEX_SUB_PARAMETERS,
+    REGIONS,
     is_function,
 )
 from cfnlint.jsonschema import ValidationError, Validator
@@ -98,9 +99,75 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
         if validator.context.mappings.maps[map_name].is_transform:
             continue
 
+        k, v = is_function(instance[2])
+        if k == "Ref" and v in PSEUDOPARAMS:
+            continue
+
         k, v = is_function(instance[1])
         if k == "Ref" and v in PSEUDOPARAMS:
+            if isinstance(instance[2], str):
+                found_top_level_key = False
+                found_second_key = False
+                for top_level_key, top_values in validator.context.mappings.maps[
+                    map_name
+                ].keys.items():
+                    if v == "AWS::AccountId":
+                        if not re.match("^[0-9]{12}$", top_level_key):
+                            continue
+                    elif v == "AWS::Region":
+                        if top_level_key not in REGIONS:
+                            continue
+                    found_top_level_key = True
+                    for second_level_key, second_v, _ in validator.resolve_value(
+                        instance[2]
+                    ):
+                        if second_level_key in top_values.keys:
+                            for value in validator.context.mappings.maps[
+                                map_name
+                            ].find_in_map(
+                                top_level_key,
+                                second_level_key,
+                            ):
+                                found_second_key = True
+                                yield (
+                                    value,
+                                    validator.evolve(
+                                        context=validator.context.evolve(
+                                            path=validator.context.path.evolve(
+                                                value_path=deque(
+                                                    [
+                                                        "Mappings",
+                                                        map_name,
+                                                        top_level_key,
+                                                        second_level_key,
+                                                    ]
+                                                )
+                                            )
+                                        )
+                                    ),
+                                    None,
+                                )
+
+                if not found_top_level_key:
+                    yield None, validator, ValidationError(
+                        (
+                            f"{instance[1]!r} is not a "
+                            f"first level key for mapping {map_name!r}"
+                        ),
+                        path=deque([1]),
+                    )
+                elif not found_second_key:
+                    yield None, validator, ValidationError(
+                        (
+                            f"{instance[2]!r} is not a "
+                            "second level key when "
+                            f"{instance[1]!r} is resolved "
+                            f"for mapping {map_name!r}"
+                        ),
+                        path=deque([2]),
+                    )
             continue
+
         for top_level_key, top_v, _ in validator.resolve_value(instance[1]):
             if validator.is_type(top_level_key, "integer"):
                 top_level_key = str(top_level_key)
@@ -134,9 +201,6 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
             ):
                 continue
 
-            k, v = is_function(instance[2])
-            if k == "Ref" and v in PSEUDOPARAMS:
-                continue
             for second_level_key, second_v, err in validator.resolve_value(instance[2]):
                 if validator.is_type(second_level_key, "integer"):
                     second_level_key = str(second_level_key)


Test patch:
diff --git a/test/unit/module/jsonschema/test_resolvers_cfn.py b/test/unit/module/jsonschema/test_resolvers_cfn.py
index f61793784c..b0d316a32f 100644
--- a/test/unit/module/jsonschema/test_resolvers_cfn.py
+++ b/test/unit/module/jsonschema/test_resolvers_cfn.py
@@ -8,7 +8,7 @@
 import pytest
 
 from cfnlint.context._mappings import Mappings
-from cfnlint.context.context import Context
+from cfnlint.context.context import Context, Parameter
 from cfnlint.jsonschema import ValidationError
 from cfnlint.jsonschema.validators import CfnTemplateValidator
 
@@ -247,7 +247,7 @@ def test_invalid_functions(name, instance, response):
                         (
                             "'bar' is not one of ['foo', "
                             "'transformFirstKey', 'transformSecondKey', "
-                            "'integers']"
+                            "'integers', 'accounts', 'environments']"
                         ),
                         path=deque(["Fn::FindInMap", 0]),
                     ),
@@ -296,9 +296,75 @@ def test_invalid_functions(name, instance, response):
             [],
         ),
         (
-            "Valid FindInMap with an top level key that is a Ref to pseudo param",
+            "Valid FindInMap with an top level key that is a Ref to an account",
+            {"Fn::FindInMap": ["accounts", {"Ref": "AWS::AccountId"}, "dev"]},
+            [
+                (
+                    "bar",
+                    deque(["Mappings", "accounts", "123456789012", "dev"]),
+                    None,
+                )
+            ],
+        ),
+        (
+            (
+                "Valid FindInMap with an top level key "
+                "that is a Ref to non account non region"
+            ),
+            {"Fn::FindInMap": ["accounts", {"Ref": "AWS::StackName"}, "dev"]},
+            [
+                (
+                    "bar",
+                    deque(["Mappings", "accounts", "123456789012", "dev"]),
+                    None,
+                )
+            ],
+        ),
+        (
+            "Valid FindInMap with an top level key that is a Ref to a region",
+            {"Fn::FindInMap": ["accounts", {"Ref": "AWS::Region"}, "bar"]},
+            [
+                (
+                    "foo",
+                    deque(["Mappings", "accounts", "us-east-1", "bar"]),
+                    None,
+                )
+            ],
+        ),
+        (
+            "Invalid FindInMap with an top level key that is a Ref to an account",
+            {"Fn::FindInMap": ["accounts", {"Ref": "AWS::AccountId"}, "bar"]},
+            [
+                (
+                    None,
+                    deque([]),
+                    ValidationError(
+                        (
+                            "'bar' is not a second level key "
+                            "when {'Ref': 'AWS::AccountId'} is "
+                            "resolved for mapping 'accounts'"
+                        ),
+                        path=deque(["Fn::FindInMap", 2]),
+                    ),
+                )
+            ],
+        ),
+        (
+            "Invalid FindInMap with an top level key that is a Ref to pseudo param",
             {"Fn::FindInMap": ["foo", {"Ref": "AWS::AccountId"}, "second"]},
-            [],
+            [
+                (
+                    None,
+                    deque([]),
+                    ValidationError(
+                        (
+                            "{'Ref': 'AWS::AccountId'} is not a "
+                            "first level key for mapping 'foo'"
+                        ),
+                        path=deque(["Fn::FindInMap", 1]),
+                    ),
+                )
+            ],
         ),
         (
             "Valid FindInMap with a second level key that is a Ref to pseudo param",
@@ -327,6 +393,78 @@ def test_invalid_functions(name, instance, response):
             {"Fn::FindInMap": ["integers", 1, 2]},
             [("Value", deque(["Mappings", "integers", "1", "2"]), None)],
         ),
+        (
+            (
+                "Valid FindInMap with a Ref to a parameter "
+                "with allowed values for top level key"
+            ),
+            {"Fn::FindInMap": ["environments", {"Ref": "Environment"}, "foo"]},
+            [
+                ("one", deque(["Mappings", "environments", "dev", "foo"]), None),
+                ("two", deque(["Mappings", "environments", "test", "foo"]), None),
+            ],
+        ),
+        (
+            "Valid FindInMap with a Ref to a parameter for top level key",
+            {"Fn::FindInMap": ["environments", {"Ref": "RandomString"}, "foo"]},
+            [],
+        ),
+        (
+            (
+                "Valid FindInMap with a Ref to accounts for top level "
+                "key Ref to a ramom string for second level key"
+            ),
+            {
+                "Fn::FindInMap": [
+                    "accounts",
+                    {"Ref": "AWS::AccountId"},
+                    {"Ref": "RandomString"},
+                ]
+            },
+            [],
+        ),
+        (
+            (
+                "Valid FindInMap with a Ref to accounts for top level "
+                "key Ref to an allowed value parameter for second level key"
+            ),
+            {
+                "Fn::FindInMap": [
+                    "accounts",
+                    {"Ref": "AWS::AccountId"},
+                    {"Ref": "Environment"},
+                ]
+            },
+            [],
+        ),
+        (
+            (
+                "Valid FindInMap with a Ref to a parameter "
+                "with allowed values for second level key"
+            ),
+            {"Fn::FindInMap": ["environments", "lion", {"Ref": "Environment"}]},
+            [
+                ("one", deque(["Mappings", "environments", "lion", "dev"]), None),
+                ("two", deque(["Mappings", "environments", "lion", "test"]), None),
+                ("three", deque(["Mappings", "environments", "lion", "prod"]), None),
+            ],
+        ),
+        (
+            "Valid FindInMap with a Ref to a parameter for top level key",
+            {"Fn::FindInMap": ["environments", {"Ref": "RandomString"}, "foo"]},
+            [],
+        ),
+        (
+            "Valid FindInMap with a Ref to a parameter and a Ref to pseudo parameter",
+            {
+                "Fn::FindInMap": [
+                    "accounts",
+                    {"Ref": "AWS::AccountId"},
+                    {"Ref": "RandomString"},
+                ]
+            },
+            [],
+        ),
         (
             "Valid FindInMap with a bad second key and default",
             {"Fn::FindInMap": ["foo", "first", "third", {"DefaultValue": "default"}]},
@@ -356,14 +494,41 @@ def test_invalid_functions(name, instance, response):
 )
 def test_valid_functions(name, instance, response):
     context = Context(
+        parameters={
+            "RandomString": Parameter(
+                {
+                    "Type": "String",
+                }
+            ),
+            "Environment": Parameter(
+                {
+                    "Type": "String",
+                    "AllowedValues": ["dev", "test", "prod"],
+                }
+            ),
+        },
         mappings=Mappings.create_from_dict(
             {
                 "foo": {"first": {"second": "bar"}},
                 "transformFirstKey": {"Fn::Transform": {"second": "bar"}},
                 "transformSecondKey": {"first": {"Fn::Transform": "bar"}},
                 "integers": {"1": {"2": "Value"}},
+                "accounts": {
+                    "123456789012": {"dev": "bar"},
+                    "us-east-1": {"bar": "foo"},
+                },
+                "environments": {
+                    "dev": {"foo": "one"},
+                    "test": {"foo": "two"},
+                    "prod": {"bar": "three"},
+                    "lion": {
+                        "dev": "one",
+                        "test": "two",
+                        "prod": "three",
+                    },
+                },
             }
-        )
+        ),
     )
     _resolve(name, instance, response, context=context)