Repository: aws-cloudformation/cfn-lint

Issue:
AWS::ServiceCatalog::CloudFormationProvisionedProduct.Outputs
In July 2020, support for retrieving the outputs of provisioned Service Catalog products [was added to CloudFormation](https://aws.amazon.com/blogs/mt/how-to-launch-secure-and-governed-aws-resources-with-aws-cloudformation-and-aws-service-catalog/).

Given the following example template:

```yaml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  ProvisionedProduct:
    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
    Properties:
      ProductName: example
      ProvisioningArtifactName: v1

Outputs:
  OutputValue:
    Value: !GetAtt ProvisionedProduct.Outputs.Example

  OutputSub:
    Value: !Sub '${ProvisionedProduct.Outputs.Example}-example'
```

The following linting errors are output when using cfn-lint 0.35.0:

```
E1010 Invalid GetAtt ProvisionedProduct.Outputs.Example for resource OutputValue
example.yml:12:5

E1019 Parameter ProvisionedProduct.Outputs.Example for Fn::Sub not found at Outputs/OutputSub/Value/Fn::Sub
example.yml:15:5
```

Access to these outputs using `!GetAtt` is documented in the announcement of this functionality (e.g. `!GetAtt ApacheALB.Outputs.ALBTarget`). Additionally the example template given above works correctly when deployed.

`Output` is documented in the [AWS::ServiceCatalog::CloudFormationProvisionedProduct return values](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicecatalog-cloudformationprovisionedproduct.html#aws-resource-servicecatalog-cloudformationprovisionedproduct-return-values) and appears to already be defined within cfn-lint CloudSpecs as a `Map` type.

I believe that this a false positive and am not aware of any alternative syntax to access these outputs (e.g. `!FindInMap` doesn't appear to be usable in this instance).


Discussion:
Submitted this to the wrong issue.  I'm looking into this now.

Resolving patch:
diff --git a/src/cfnlint/rules/functions/GetAtt.py b/src/cfnlint/rules/functions/GetAtt.py
--- a/src/cfnlint/rules/functions/GetAtt.py
+++ b/src/cfnlint/rules/functions/GetAtt.py
@@ -80,6 +80,15 @@ def match(self, cfn):
             if resname:
                 if resname in valid_getatts:
                     if restype is not None:
+                        # Check for maps
+                        restypeparts = restype.split('.')
+                        if restypeparts[0] in valid_getatts[resname] and len(restypeparts) >= 2:
+                            if valid_getatts[resname][restypeparts[0]].get('Type') != 'Map':
+                                message = 'Invalid GetAtt {0}.{1} for resource {2}'
+                                matches.append(RuleMatch(
+                                    getatt[:-1], message.format(resname, restype, getatt[1])))
+                            else:
+                                continue
                         if restype not in valid_getatts[resname] and '*' not in valid_getatts[resname]:
                             message = 'Invalid GetAtt {0}.{1} for resource {2}'
                             matches.append(RuleMatch(
diff --git a/src/cfnlint/rules/functions/Sub.py b/src/cfnlint/rules/functions/Sub.py
--- a/src/cfnlint/rules/functions/Sub.py
+++ b/src/cfnlint/rules/functions/Sub.py
@@ -115,19 +115,19 @@ def _test_parameter(self, parameter, cfn, parameters, tree):
                         tree, message.format(parameter, '/'.join(map(str, tree)))))
             for resource, attributes in get_atts.items():
                 for attribute_name, attribute_values in attributes.items():
-                    if resource == parameter.split('.')[0] and attribute_name == '*':
-                        if attribute_values.get('Type') == 'List':
-                            message = 'Fn::Sub cannot use list {0} at {1}'
-                            matches.append(RuleMatch(
-                                tree, message.format(parameter, '/'.join(map(str, tree)))))
-                        found = True
-                    elif (resource == parameter.split('.')[0] and
-                          attribute_name == '.'.join(parameter.split('.')[1:])):
-                        if attribute_values.get('Type') == 'List':
-                            message = 'Fn::Sub cannot use list {0} at {1}'
-                            matches.append(RuleMatch(
-                                tree, message.format(parameter, '/'.join(map(str, tree)))))
-                        found = True
+                    if resource == parameter.split('.')[0]:
+                        if attribute_name == '*':
+                            found = True
+                        elif attribute_name == '.'.join(parameter.split('.')[1:]):
+                            if attribute_values.get('Type') == 'List':
+                                message = 'Fn::Sub cannot use list {0} at {1}'
+                                matches.append(RuleMatch(
+                                    tree, message.format(parameter, '/'.join(map(str, tree)))))
+                            found = True
+                        else:
+                            if attribute_name == parameter.split('.')[1] and attribute_values.get('Type') == 'Map':
+                                found = True
+
             if not found:
                 message = 'Parameter {0} for Fn::Sub not found at {1}'
                 matches.append(RuleMatch(


Test patch:
diff --git a/test/fixtures/templates/bad/functions/getatt.yaml b/test/fixtures/templates/bad/functions/getatt.yaml
--- a/test/fixtures/templates/bad/functions/getatt.yaml
+++ b/test/fixtures/templates/bad/functions/getatt.yaml
@@ -45,7 +45,16 @@ Resources:
         # Shouldn't raise an error when using a Ref for the attribute
         RdsMasterUsername: !If [ UseSSM, "Fn::GetAtt": [ RDSCredentials, !Select [0, !Ref SSMMasterDBUsernameKeyFullName] ], !Ref PGMasterUser ]
         RdsMasterPassword: !If [ UseSSM, "Fn::GetAtt": [ RDSCredentials, [!Ref SSMMasterDBPasswordKeyFullName] ], !Ref PGMasterPassword ]
+  ProvisionedProduct:
+    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
+    Properties:
+      ProductName: example
+      ProvisioningArtifactName: v1
 Outputs:
   ElasticSearchHostname:
     Value:
       Fn::GetAtt: ElasticSearchDomain.DomainEndpoint
+  OutputBadOutput:
+    Value: !GetAtt ProvisionedProduct.Output.Example
+  OutputBadName:
+    Value: !GetAtt myElasticSearch.DomainEndpoint.Example
diff --git a/test/fixtures/templates/good/functions/getatt.yaml b/test/fixtures/templates/good/functions/getatt.yaml
--- a/test/fixtures/templates/good/functions/getatt.yaml
+++ b/test/fixtures/templates/good/functions/getatt.yaml
@@ -57,6 +57,11 @@ Resources:
       Tags:
         - Key: foo
           Value: bar
+  ProvisionedProduct:
+    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
+    Properties:
+      ProductName: example
+      ProvisioningArtifactName: v1
 Outputs:
   ElasticSearchHostname:
     Value:
@@ -65,3 +70,5 @@ Outputs:
     Description: Endpoint address for Redshift cluster.
     Value:
       Fn::GetAtt: myCluster.Endpoint.Address
+  OutputValue:
+    Value: !GetAtt ProvisionedProduct.Outputs.Example
diff --git a/test/fixtures/templates/good/functions/sub.yaml b/test/fixtures/templates/good/functions/sub.yaml
--- a/test/fixtures/templates/good/functions/sub.yaml
+++ b/test/fixtures/templates/good/functions/sub.yaml
@@ -59,3 +59,11 @@ Resources:
           'Fn::Sub':
             'Fn::Transform':  # Doesn't fail on Transform
               Name: DynamicUserData
+  ProvisionedProduct:
+    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
+    Properties:
+      ProductName: example
+      ProvisioningArtifactName: v1
+Outputs:
+  OutputSub:
+    Value: !Sub '${ProvisionedProduct.Outputs.Example}-example'
diff --git a/test/unit/rules/functions/test_get_att.py b/test/unit/rules/functions/test_get_att.py
--- a/test/unit/rules/functions/test_get_att.py
+++ b/test/unit/rules/functions/test_get_att.py
@@ -27,4 +27,4 @@ def test_file_negative(self):
 
     def test_file_negative_getatt(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/functions/getatt.yaml', 3)
+        self.helper_file_negative('test/fixtures/templates/bad/functions/getatt.yaml', 5)