Repository: aws-cloudformation/cfn-lint

Issue:
E3002 error on Cloudformation Custom Resource
cfn-lint version: 0.5.0

I got a lint error with the following resource:
```
Resources:
  CoreLookup:
    Properties:
      ServiceToken: !GetAtt
        - LookupStackOutputs
        - Arn
      StackName: !Join
        - '-'
        - - Core
          - !Ref EnvName
    Type: 'AWS::CloudFormation::CustomResource'
```

Accordingly to the documentation is it possible to add more properties to the custom resources:
[https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html]

According to the documentation:

```   
"AWS::CloudFormation::CustomResource": {
      "AdditionalProperties": true,
      "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html",
      "Properties": {
        "ServiceToken": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html#cfn-customresource-servicetoken",
          "PrimitiveType": "String",
          "Required": true,
          "UpdateType": "Immutable"
        }
      }
```

So I think that the linter is ignoring the "AdditionalProperties" flag.



Discussion:
Seems to be true.  I don't see any reference to "AdditionalProperties" in the code.  This hasn't come up for me because I use the "Custom::*" type, which removes my custom resources from being linted.  If I switch one to "AWS:::CloudFormation::CustomResource" I get the same types of errors.

We'll need some special logic for AWS::CloudFormation::CustomResource, and maybe use the same for Custom:: as well.
From my point of view, the linter should take in count the "AdditionalProperties: true" and allow 
Yep we are in agreement.  We will be working on a fix for this. 

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
@@ -144,12 +144,15 @@ def propertycheck(self, text, proptype, parenttype, resourcename, path, root):
                 resourcetype = str.format('{0}.{1}', parenttype, proptype)
 
         resourcespec = specs[resourcetype].get('Properties', {})
+        supports_additional_properties = specs[resourcetype].get('AdditionalProperties', False)
+
         if text == 'AWS::NoValue':
             return matches
         if not isinstance(text, dict):
             message = 'Expecting an object at %s' % ('/'.join(map(str, path)))
             matches.append(RuleMatch(path, message))
             return matches
+
         for prop in text:
             proppath = path[:]
             proppath.append(prop)
@@ -160,7 +163,7 @@ def propertycheck(self, text, proptype, parenttype, resourcename, path, root):
                         matches.extend(self.propertycheck(
                             cond_value['Value'], proptype, parenttype, resourcename,
                             proppath + cond_value['Path'], root))
-                elif prop != 'Metadata':
+                elif prop != 'Metadata' and not supports_additional_properties:
                     message = 'Invalid Property %s' % ('/'.join(map(str, proppath)))
                     matches.append(RuleMatch(proppath, message))
             else:
@@ -241,6 +244,8 @@ def match(self, cfn):
         for resourcename, resourcevalue in cfn.get_resources().items():
             if 'Properties' in resourcevalue and 'Type' in resourcevalue:
                 resourcetype = resourcevalue.get('Type', None)
+                if resourcetype.startswith('Custom::'):
+                    resourcetype = 'AWS::CloudFormation::CustomResource'
                 if resourcetype in self.resourcetypes:
                     path = ['Resources', resourcename, 'Properties']
                     matches.extend(self.propertycheck(
diff --git a/src/cfnlint/rules/resources/properties/Required.py b/src/cfnlint/rules/resources/properties/Required.py
--- a/src/cfnlint/rules/resources/properties/Required.py
+++ b/src/cfnlint/rules/resources/properties/Required.py
@@ -128,6 +128,8 @@ def match(self, cfn):
         for resourcename, resourcevalue in cfn.get_resources().items():
             if 'Properties' in resourcevalue and 'Type' in resourcevalue:
                 resourcetype = resourcevalue['Type']
+                if resourcetype.startswith('Custom::'):
+                    resourcetype = 'AWS::CloudFormation::CustomResource'
                 if resourcetype in self.resourcetypes:
                     tree = ['Resources', resourcename, 'Properties']
                     matches.extend(self.propertycheck(


Test patch:
diff --git a/test/fixtures/templates/bad/properties_required.yaml b/test/fixtures/templates/bad/properties_required.yaml
--- a/test/fixtures/templates/bad/properties_required.yaml
+++ b/test/fixtures/templates/bad/properties_required.yaml
@@ -723,3 +723,13 @@ Resources:
         Type: S3
         Location:
           Ref: ArtifactStoreS3Location
+  CustomResource1:
+    Type: 'AWS::CloudFormation::CustomResource'
+    Properties:
+      # ServiceToken: arn
+      StackName: StackName
+  CustomResource2:
+    Type: 'Custom::CustomResource'
+    Properties:
+      # ServiceToken: arn
+      StackName: StackName
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
@@ -593,6 +593,16 @@ Resources:
           KeyType: HASH
         - AttributeName: !Ref SortKeyName
           KeyType: RANGE
+  CustomResource1:
+    Type: 'AWS::CloudFormation::CustomResource'
+    Properties:
+      ServiceToken: arn
+      StackName: StackName
+  CustomResource2:
+    Type: 'Custom::CustomResource'
+    Properties:
+      ServiceToken: arn
+      StackName: StackName
   Distribution:
     Type: "AWS::CloudFront::Distribution"
     Properties:
diff --git a/test/rules/resources/properties/test_required.py b/test/rules/resources/properties/test_required.py
--- a/test/rules/resources/properties/test_required.py
+++ b/test/rules/resources/properties/test_required.py
@@ -31,7 +31,7 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('fixtures/templates/bad/properties_required.yaml', 10)
+        self.helper_file_negative('fixtures/templates/bad/properties_required.yaml', 12)
 
     def test_file_negative_generic(self):
         """Generic Test failure"""