Repository: aws-cloudformation/cfn-lint

Issue:
Errors after upgrading to v1.3
### CloudFormation Lint Version

1.3.3

### What operating system are you using?

macOS

### Describe the bug

after upgrade to v1.3, cfn-lint fails unexpectedly, It generates multiple errors

E1019 {'Ref': 'additionalSubnetBits'} is not of type 'integer' when 'Ref' is resolved when 'Ref' is resolved
E3024 {'Ref': 'additionalSubnetBits'} is not of type 'integer' when 'Ref' is resolved when 'Ref' is resolved
E1020 {'Ref': 'additionalSubnetBits'} is not of type 'integer' when 'Ref' is resolved

There are identical Select functions, one in !If function passes , but the line in !Sub function fails

The code is deployed in CloudFormation, so we know it's valid


### Expected behavior

Code is valid, cfn-lint shouldn't fail

### Reproduction template

```yaml
  AdditionalSubnet1:
    Type: 'AWS::EC2::Subnet'
    Condition: isAdditionalVpc
    DependsOn: AdditionalVpcCIDR
    Properties:
      AvailabilityZone: !If [usesetaz, !Select [0, !Split [",", !Ref physicalazlist]], !Select [0, !GetAZs '']]
      CidrBlock:
        !If
          - isAdditionalCidr1Empty
          - !Select [0, !Cidr [!Ref additionalVpcCidr, 4, !Ref additionalSubnetBits] ]
          - !Ref AdditionalSubnet1Cidr
      MapPublicIpOnLaunch: false
      Tags:
      - Key: Name
        Value: !Sub
        - ${appCode}-${envType}-additional1-${az}-${cidr}
        -
          az: !If [usesetaz, !Select [0, !Split [",", !Ref physicalazlist]], !Select [0, !GetAZs '']]
          cidr: !Select [0, !Cidr [!Ref additionalVpcCidr, 4, !Ref additionalSubnetBits] ]
      VpcId: !Ref vpc
```


Discussion:
The issue here comes down to the Fn::Sub.

This is invalid.  Once we are evaluating the elements to a function we need to turn off strict type checking.  The output type from the function has to be a string.

```yaml
Resources:
  MyPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          Action: "*"
          Resource: "*"
          Effect: Allow
  Parameter:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Value:
        Fn::Sub: 
        - "${Value}"
        - Value: !GetAtt MyPolicy.AttachmentCount
```



Patch:
diff --git a/src/cfnlint/helpers.py b/src/cfnlint/helpers.py
index 21903d6c24..a7bc44e641 100644
--- a/src/cfnlint/helpers.py
+++ b/src/cfnlint/helpers.py
@@ -564,7 +564,9 @@ def _translate_types(types: Sequence[str]) -> list[str]:
 
 
 def is_types_compatible(
-    source_types: str | Sequence[str], destination_types: str | Sequence[str]
+    source_types: str | Sequence[str],
+    destination_types: str | Sequence[str],
+    strict_types: bool = False,
 ) -> bool:
     """
     Validate if desination types are compatible with source types.
@@ -576,7 +578,8 @@ def is_types_compatible(
     Returns:
         bool: If any type of source is compatible with any type in the destination
     """
-    source_types = _translate_types(ensure_list(source_types))
+    if not strict_types:
+        source_types = _translate_types(ensure_list(source_types))
     destination_types = ensure_list(destination_types)
 
     if any(schema_type in source_types for schema_type in destination_types):
diff --git a/src/cfnlint/jsonschema/_keywords_cfn.py b/src/cfnlint/jsonschema/_keywords_cfn.py
index 919297f63e..95851220ec 100644
--- a/src/cfnlint/jsonschema/_keywords_cfn.py
+++ b/src/cfnlint/jsonschema/_keywords_cfn.py
@@ -59,7 +59,8 @@ def validate(
             for (index, item), subschema in zip(enumerate(instance), s):
                 yield from validator.evolve(
                     context=validator.context.evolve(
-                        functions=subschema.get("functions", [])
+                        functions=subschema.get("functions", []),
+                        strict_types=False,
                     ),
                 ).descend(
                     instance=item,
@@ -69,7 +70,10 @@ def validate(
         else:
             for index, item in enumerate(instance):
                 yield from validator.evolve(
-                    context=validator.context.evolve(functions=s.get("functions", [])),
+                    context=validator.context.evolve(
+                        functions=s.get("functions", []),
+                        strict_types=False,
+                    ),
                 ).descend(
                     instance=item,
                     schema=s.get("schema", {}),
diff --git a/src/cfnlint/rules/functions/GetAtt.py b/src/cfnlint/rules/functions/GetAtt.py
index 699b3199fb..03e2020464 100644
--- a/src/cfnlint/rules/functions/GetAtt.py
+++ b/src/cfnlint/rules/functions/GetAtt.py
@@ -132,7 +132,9 @@ def _resolve_getatt(
 
                         types = ensure_list(s.get("type"))
 
-                        if is_types_compatible(types, schema_types):
+                        if is_types_compatible(
+                            types, schema_types, validator.context.strict_types
+                        ):
                             continue
 
                         reprs = ", ".join(repr(type) for type in types)
diff --git a/src/cfnlint/rules/functions/Sub.py b/src/cfnlint/rules/functions/Sub.py
index e522ccc7f4..9e4c5b708a 100644
--- a/src/cfnlint/rules/functions/Sub.py
+++ b/src/cfnlint/rules/functions/Sub.py
@@ -90,6 +90,7 @@ def _validate_string(
                 path=validator.context.path.descend(
                     path=key,
                 ),
+                strict_types=True,
             ),
             function_filter=validator.function_filter.evolve(
                 add_cfn_lint_keyword=False,


Test patch:
diff --git a/test/unit/module/jsonschema/test_keywords_cfn.py b/test/unit/module/jsonschema/test_keywords_cfn.py
index a6c71f6ef1..312f196f9d 100644
--- a/test/unit/module/jsonschema/test_keywords_cfn.py
+++ b/test/unit/module/jsonschema/test_keywords_cfn.py
@@ -8,7 +8,7 @@
 from typing import List
 
 from cfnlint.context import Context
-from cfnlint.jsonschema._keywords_cfn import cfn_type
+from cfnlint.jsonschema._keywords_cfn import FnItems, cfn_type
 from cfnlint.jsonschema.exceptions import UnknownType
 from cfnlint.jsonschema.validators import CfnTemplateValidator
 
@@ -136,3 +136,18 @@ def test_cfn_type_failure(self):
         self.assertIn(
             "Unknown type 'bar' for validator with schema", str(err.exception)
         )
+
+
+class TestFnItems(Base):
+
+    def test_change_type(self):
+        schema = [
+            {
+                "schema": {"type": "string"},
+            },
+        ]
+        validator = CfnTemplateValidator({})
+
+        items = list(FnItems().validate(validator, schema, [1], {}))
+
+        self.assertListEqual(items, [])
diff --git a/test/unit/rules/functions/test_getatt.py b/test/unit/rules/functions/test_getatt.py
index 40e3bd3a04..9097e9a01d 100644
--- a/test/unit/rules/functions/test_getatt.py
+++ b/test/unit/rules/functions/test_getatt.py
@@ -158,10 +158,32 @@ def validate(self, validator, s, instance, schema):
             "Valid GetAtt with integer to string",
             {"Fn::GetAtt": "MyCodePipeline.Version"},
             {"type": ["integer"]},
-            {},
+            {
+                "strict_types": False,
+            },
             {},
             [],
         ),
+        (
+            "Invalid GetAtt with integer to string",
+            {"Fn::GetAtt": "MyCodePipeline.Version"},
+            {"type": ["integer"]},
+            {
+                "strict_types": True,
+            },
+            {},
+            [
+                ValidationError(
+                    (
+                        "{'Fn::GetAtt': 'MyCodePipeline.Version'} "
+                        "is not of type 'integer'"
+                    ),
+                    path=deque(["Fn::GetAtt"]),
+                    schema_path=deque(["type"]),
+                    validator="fn_getatt",
+                )
+            ],
+        ),
         (
             "Valid GetAtt with one good response type",
             {"Fn::GetAtt": "MyBucket.Arn"},
@@ -244,4 +266,9 @@ def test_validate(
     rule.child_rules = child_rules
     validator = CfnTemplateValidator({}, context=context, cfn=cfn)
     errs = list(rule.fn_getatt(validator, schema, instance, {}))
+
+    for err in errs:
+        print(err.validator)
+        print(err.path)
+        print(err.schema_path)
     assert errs == expected, f"Test {name!r} got {errs!r}"
diff --git a/test/unit/rules/functions/test_sub.py b/test/unit/rules/functions/test_sub.py
index 2a56a18daf..28b1059f7d 100644
--- a/test/unit/rules/functions/test_sub.py
+++ b/test/unit/rules/functions/test_sub.py
@@ -29,6 +29,7 @@ def cfn():
             "Resources": {
                 "MyResource": {"Type": "AWS::S3::Bucket"},
                 "MySimpleAd": {"Type": "AWS::DirectoryService::SimpleAD"},
+                "MyPolicy": {"Type": "AWS::IAM::ManagedPolicy"},
             },
         },
         regions=["us-east-1"],
@@ -95,7 +96,7 @@ def context(cfn):
                 ValidationError(
                     (
                         "'bar' is not one of ["
-                        "'MyResource', 'MySimpleAd', 'AWS::AccountId', "
+                        "'MyResource', 'MySimpleAd', 'MyPolicy', 'AWS::AccountId', "
                         "'AWS::NoValue', 'AWS::NotificationARNs', "
                         "'AWS::Partition', 'AWS::Region', "
                         "'AWS::StackId', 'AWS::StackName', 'AWS::URLSuffix']"
@@ -114,7 +115,7 @@ def context(cfn):
                 ValidationError(
                     (
                         "'foo' is not one of ["
-                        "'MyResource', 'MySimpleAd', 'AWS::AccountId', "
+                        "'MyResource', 'MySimpleAd', 'MyPolicy', 'AWS::AccountId', "
                         "'AWS::NoValue', 'AWS::NotificationARNs', "
                         "'AWS::Partition', 'AWS::Region', "
                         "'AWS::StackId', 'AWS::StackName', 'AWS::URLSuffix']"
@@ -228,7 +229,7 @@ def context(cfn):
             {"type": "string"},
             [
                 ValidationError(
-                    "'Foo' is not one of ['MyResource', 'MySimpleAd']",
+                    "'Foo' is not one of ['MyResource', 'MySimpleAd', 'MyPolicy']",
                     path=deque(["Fn::Sub"]),
                     schema_path=deque([]),
                     validator="fn_sub",
@@ -241,7 +242,21 @@ def context(cfn):
             {"type": "string"},
             [
                 ValidationError(
-                    ("'MySimpleAd.DnsIpAddresses' is not " "of type 'string'"),
+                    ("'MySimpleAd.DnsIpAddresses' is not of type 'string'"),
+                    instance="MySimpleAd.DnsIpAddresses",
+                    path=deque(["Fn::Sub"]),
+                    schema_path=deque([]),
+                    validator="fn_sub",
+                ),
+            ],
+        ),
+        (
+            "Invalid Fn::Sub with a GetAtt to an integer",
+            {"Fn::Sub": "${MyPolicy.AttachmentCount}"},
+            {"type": "string"},
+            [
+                ValidationError(
+                    ("'MyPolicy.AttachmentCount' is not of type 'string'"),
                     instance="MySimpleAd.DnsIpAddresses",
                     path=deque(["Fn::Sub"]),
                     schema_path=deque([]),