Repository: aws-cloudformation/cfn-lint

Issue:
E3002 false positives with intrinsics returning complex types
### CloudFormation Lint Version

0.72.5

### What operating system are you using?

Linux (Fedora)

### Describe the bug

When using intrinsic functions to retrieve complex (ie structured) data from custom resources for other resources E3002 incorrectly marks the intrinsic as invalid properties of the resource.

For example, given a custom resource which returns an AWS::Lambda::Function _Code_ object:
```
"Code": { 
   "S3Bucket": "<name>",
   "S3Key": "<object-key>",
   "S3ObjectVersion": "<object-version>"
} 
```

Then referencing this as the Code: property of another AWS::Lambda::Function, eg
``` Code: !GetAtt CustomResource.Output.Code ```

cfn-lint raises an E3002 of the form:
```E3002 Invalid Property Resources/Helper/Properties/Code/Fn::GetAtt```
   
This can be suppressed in the resource's Metadata, but only by suppressing all E3002 errors for that resource, where other instances of the error could be legitimate.    While I imagine that the vast majority of values returned from custom resources are scalars, they are no required to be, and therefore can provide structure as well as values.  Certainly at least in the specific case described here the various Code.<Name> attributes are all correctly referenced when actually running the template.


### Expected behavior

E3002 Should not be raised when an intrinsic is used to return information for a valid partial attribute path.
Since scalars are the most common return values it may be a reasonable compromise to raise a context specific W or I code
At the very least even if metadata annotation is required to suppress this case it should be a unique code, which can be supressed without also supressing common errors such as attribute name typos

### Reproduction template

```yaml
Parameters:
    RoleArn:
        Type: String
    FunctionArn:
        Type: String

Resources:

    GetCodeForLatestS3Object:
        Type: Custom::LambdaSource
        Properties:
            ServiceToken: !Ref FunctionArn
            S3Url: "s3://some-bucket/lambdazips/helper.zip"

    Helper:
        Type: 'AWS::Lambda::Function'
        Properties:
            Handler: 'helper.lambda_handler'
            Role: !Ref RoleArn
            Code: !GetAtt GetCodeForLatestS3Object.Code
```


Discussion:
Yea I think we are handling custom resources for simple types but not for complex types.  Let me double check it and correct it.

Resolving patch:
diff --git a/src/cfnlint/rules/resources/properties/Properties.py b/src/cfnlint/rules/resources/properties/Properties.py
--- a/src/cfnlint/rules/resources/properties/Properties.py
+++ b/src/cfnlint/rules/resources/properties/Properties.py
@@ -380,6 +380,21 @@ def propertycheck(self, text, proptype, parenttype, resourcename, path, root):
                     and text.get(prop) == "AWS::NoValue"
                 ):
                     pass
+                elif len(text) == 1 and prop in "Fn::GetAtt":
+                    getatt = text.get(prop)
+                    getatt_type = (
+                        self.cfn.template.get("Resources", {})
+                        .get(getatt[0], {})
+                        .get("Type", "")
+                    )
+                    if (
+                        getatt_type == "AWS::CloudFormation::CustomResource"
+                        or getatt_type.startswith("Custom::")
+                    ):
+                        pass
+                    else:
+                        message = f'GetAtt must refer to a custom resource {"/".join(map(str, proppath))}'
+                        matches.append(RuleMatch(proppath, message))
                 elif not supports_additional_properties:
                     close_match = False
                     for key in resourcespec.keys():


Test patch:
diff --git a/test/fixtures/templates/bad/resource_properties.yaml b/test/fixtures/templates/bad/resource_properties.yaml
--- a/test/fixtures/templates/bad/resource_properties.yaml
+++ b/test/fixtures/templates/bad/resource_properties.yaml
@@ -48,3 +48,13 @@ Resources:
       VpcConfig:
         SubnetIds: !ImportValue foobar1
         SecurityGroupIds: !ImportValue foobar2
+  Custom:
+    Type: AWS::CloudFormation::CustomResource
+    Properties:
+      ServiceToken: arn.example
+  Helper:
+    Type: 'AWS::Lambda::Function'
+    Properties:
+      Handler: 'helper.lambda_handler'
+      Role: arn:aws:iam::123456789012:role/role-name-with-path
+      Code: !GetAtt myInstance.AvailabilityZone  # returns a string not an object
diff --git a/test/fixtures/templates/good/resource_properties.yaml b/test/fixtures/templates/good/resource_properties.yaml
--- a/test/fixtures/templates/good/resource_properties.yaml
+++ b/test/fixtures/templates/good/resource_properties.yaml
@@ -641,7 +641,12 @@ Resources:
     Type: AWS::CloudFormation::CustomResource
     Properties:
       ServiceToken: arn.example
-
+  Helper:
+    Type: 'AWS::Lambda::Function'
+    Properties:
+      Handler: 'helper.lambda_handler'
+      Role: arn:aws:iam::123456789012:role/role-name-with-path
+      Code: !GetAtt Custom.Code
   LB1:
     Type: AWS::ElasticLoadBalancingV2::LoadBalancer
     Properties:
diff --git a/test/unit/rules/resources/properties/test_properties.py b/test/unit/rules/resources/properties/test_properties.py
--- a/test/unit/rules/resources/properties/test_properties.py
+++ b/test/unit/rules/resources/properties/test_properties.py
@@ -42,7 +42,7 @@ def test_file_negative_2(self):
     def test_file_negative_3(self):
         """Failure test"""
         self.helper_file_negative(
-            "test/fixtures/templates/bad/resource_properties.yaml", 7
+            "test/fixtures/templates/bad/resource_properties.yaml", 8
         )
 
     def test_E3012_in_bad_template(self):