Repository: aws-cloudformation/cfn-lint

Issue:
W8003 False positive
### CloudFormation Lint Version

0.86.0

### What operating system are you using?

mac

### Describe the bug

lint raises a W8003 for the below template, but I don't think it's true that it will always return True or False. It checks whether the value obtained from the mapping for the specified environment (!Ref Env) matches the string "No retention". If it does, then the condition evaluates to false, and if it doesn't, it evaluates to true.

### Expected behavior

W8003 isn't raised

### Reproduction template

```
Mappings:
  dwS3BucketEnvironmentConfigMap:
    # Create an entry for each environment

    prod:
      BucketName: 'digital-workspace-v2-prod'
      DeletionPolicy: Delete
    hotfix:
      BucketName: 'digital-workspace-v2-hotfix'
      DeletionPolicy: Delete
    staging:
      BucketName: 'digital-workspace-v2-staging'
      DeletionPolicy: Delete
    training:
      BucketName: 'digital-workspace-v2-training'
      DeletionPolicy: Delete
    dev:
      BucketName: 'digital-workspace-v2-dev'
      DeletionPolicy: Delete

Conditions:
  dwS3BucketApplyRetentionPolicy: !Not
    - !Equals
      - !FindInMap
        - dwS3BucketEnvironmentConfigMap
        - !Ref Env
        - RetentionMode
        - DefaultValue: No retention
      - No retention
  dwS3BucketRetentionPolicyDurationIsInDays: !Equals
    - !FindInMap
      - dwS3BucketEnvironmentConfigMap
      - !Ref Env
      - RetentionDurationType
      - DefaultValue: Days
    - Days
```


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
@@ -156,7 +156,7 @@ def _walk(self, item: Any, params: MutableMapping[str, Any], cfn: Any):
                 elif k == "Fn::FindInMap":
                     try:
                         mapping = _ForEachValueFnFindInMap(get_hash(v), v)
-                        map_value = mapping.value(cfn, params, True)
+                        map_value = mapping.value(cfn, params, True, False)
                         # if we get None this means its all strings but couldn't be resolved
                         # we will pass this forward
                         if map_value is None:
@@ -298,6 +298,7 @@ def value(
         cfn: Any,
         params: Optional[Mapping[str, Any]] = None,
         only_params: bool = False,
+        default_on_resolver_failure: bool = True,
     ) -> Any:
         if params is None:
             params = {}
@@ -361,11 +362,11 @@ def value(
                     t_map[2].value(cfn, params, only_params)
                 )
             except _ResolveError as e:
-                if len(self._map) == 4:
+                if len(self._map) == 4 and default_on_resolver_failure:
                     return self._map[3].value(cfn, params, only_params)
                 raise _ResolveError("Can't resolve Fn::FindInMap", self._obj) from e
 
-        if len(self._map) == 4:
+        if len(self._map) == 4 and default_on_resolver_failure:
             return self._map[3].value(cfn, params, only_params)
         raise _ResolveError("Can't resolve Fn::FindInMap", self._obj)
 


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
@@ -182,6 +182,32 @@ def test_bad_mappings(self):
         with self.assertRaises(_ValueError):
             _ForEachValueFnFindInMap("", ["foo"])
 
+    def test_find_in_map_values_with_default(self):
+        map = _ForEachValueFnFindInMap(
+            "a", ["Bucket", {"Ref": "Foo"}, "Key", {"DefaultValue": "bar"}]
+        )
+
+        self.assertEqual(map.value(self.cfn, None, False, True), "bar")
+        with self.assertRaises(_ResolveError):
+            map.value(self.cfn, None, False, False)
+
+    def test_find_in_map_values_without_default(self):
+        map = _ForEachValueFnFindInMap("a", ["Bucket", {"Ref": "Foo"}, "Key"])
+
+        with self.assertRaises(_ResolveError):
+            self.assertEqual(map.value(self.cfn, None, False, True), "bar")
+        with self.assertRaises(_ResolveError):
+            map.value(self.cfn, None, False, False)
+
+    def test_mapping_not_found(self):
+        map = _ForEachValueFnFindInMap(
+            "a", ["Foo", {"Ref": "Foo"}, "Key", {"DefaultValue": "bar"}]
+        )
+
+        self.assertEqual(map.value(self.cfn, None, False, True), "bar")
+        with self.assertRaises(_ResolveError):
+            map.value(self.cfn, None, False, False)
+
     def test_two_mappings(self):
         template_obj = deepcopy(self.template_obj)
         template_obj["Mappings"]["Foo"] = {"Bar": {"Key": ["a", "b"]}}