Repository: aws-cloudformation/cfn-lint

Issue:
Validate ManagedPolicy Description
*cfn-lint version: (`cfn-lint --version`)* 0.7.1

*Description of issue.*

A recent deployment of a template with an IAM policy failed because a newline was accidentally included in the description, and IAM doesn't like that. Got this error:

> AWS::IAM::ManagedPolicy MyPolicy CREATE_FAILED: ... 'description' failed to satisfy constraint: Member must satisfy regular expression pattern: [\p{L}\p{M}\p{Z}\p{S}\p{N}\p{P}]* ...

This also isn't documented [on CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-managedpolicy.html#cfn-iam-managedpolicy-description) or in the [IAM API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html).



Discussion:
I'm trying to convert this regex into something we could use.  Looks like \p{Z} is for whitespace.  What I can't tell if that also includes new lines as it looks to be a subset of this one.
@cmmeyer looks like to make this easier on ourselves we should convert to the regex package.  https://pypi.org/project/regex/  it comes with easy unicode character matching and makes writing this rule a lot easier.  It looks like it can drop in replace the current 're' but it would be a new dependency.

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -59,7 +59,8 @@ def get_version(filename):
         'aws-sam-translator>=1.8.0',
         'jsonpatch',
         'jsonschema~=2.6',
-        'pathlib2>=2.3.0;python_version<"3.4"'
+        'pathlib2>=2.3.0;python_version<"3.4"',
+        'regex>=2018.11.07'
     ],
     python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
     entry_points={
diff --git a/src/cfnlint/rules/resources/iam/ManagedPolicyDescription.py b/src/cfnlint/rules/resources/iam/ManagedPolicyDescription.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/iam/ManagedPolicyDescription.py
@@ -0,0 +1,58 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+import regex
+from cfnlint import CloudFormationLintRule
+from cfnlint import RuleMatch
+
+
+class ManagedPolicyDescription(CloudFormationLintRule):
+    """Check if IAM Policy Description is syntax correct"""
+    id = 'E3507'
+    shortdesc = 'Check if IAM Managed Policy description follows supported regex'
+    description = 'IAM Managed Policy description much comply with the regex [\\p{L}\\p{M}\\p{Z}\\p{S}\\p{N}\\p{P}]*'
+    source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-managedpolicy.html'
+    tags = ['properties', 'iam']
+
+    def __init__(self):
+        """Init"""
+        super(ManagedPolicyDescription, self).__init__()
+        self.resource_property_types.append('AWS::IAM::ManagedPolicy')
+
+    def check_value(self, value, path):
+        """Check the value"""
+        regex_string = r'^[\p{L}\p{M}\p{Z}\p{S}\p{N}\p{P}]+$'
+        r = regex.compile(regex_string)
+        if not r.match(value):
+            message = 'ManagedPolicy Description needs to follow regex pattern "{0}"'
+            return [
+                RuleMatch(path[:], message.format(regex_string))
+            ]
+
+        return []
+
+    def match_resource_properties(self, properties, _, path, cfn):
+        """Check CloudFormation Properties"""
+        matches = []
+
+        matches.extend(
+            cfn.check_value(
+                obj=properties, key='Description',
+                path=path[:],
+                check_value=self.check_value
+            ))
+
+        return matches


Test patch:
diff --git a/test/fixtures/templates/bad/resources/iam/managed_policy_description.yaml b/test/fixtures/templates/bad/resources/iam/managed_policy_description.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/iam/managed_policy_description.yaml
@@ -0,0 +1,17 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Resources:
+  SomeManagedPolicy:
+    Type: "AWS::IAM::ManagedPolicy"
+    Properties:
+      Description: |
+        Example1
+        Managed Policy
+      PolicyDocument:
+        Version: "2012-10-17"
+        Statement:
+          - Effect: "Allow"
+            Action:
+              - "s3:ListBucket"
+              - "s3:GetObject"
+            Resource: '*'
diff --git a/test/fixtures/templates/good/resources/iam/managed_policy_description.yaml b/test/fixtures/templates/good/resources/iam/managed_policy_description.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/iam/managed_policy_description.yaml
@@ -0,0 +1,16 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Resources:
+  SomeManagedPolicy:
+    Type: "AWS::IAM::ManagedPolicy"
+    Properties:
+      Description: |
+        Example1? Managed Policy
+      PolicyDocument:
+        Version: "2012-10-17"
+        Statement:
+          - Effect: "Allow"
+            Action:
+              - "s3:ListBucket"
+              - "s3:GetObject"
+            Resource: '*'
diff --git a/test/rules/resources/iam/test_iam_managed_policy_description.py b/test/rules/resources/iam/test_iam_managed_policy_description.py
new file mode 100644
--- /dev/null
+++ b/test/rules/resources/iam/test_iam_managed_policy_description.py
@@ -0,0 +1,37 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+from cfnlint.rules.resources.iam.ManagedPolicyDescription import ManagedPolicyDescription  # pylint: disable=E0401
+from ... import BaseRuleTestCase
+
+
+class TestManagedPolicyDescription(BaseRuleTestCase):
+    """Test Managed Policy Description"""
+    def setUp(self):
+        """Setup"""
+        super(TestManagedPolicyDescription, self).setUp()
+        self.collection.register(ManagedPolicyDescription())
+        self.success_templates = [
+            'fixtures/templates/good/resources/iam/managed_policy_description.yaml'
+        ]
+
+    def test_file_positive(self):
+        """Test Positive"""
+        self.helper_file_positive()
+
+    def test_file_negative(self):
+        """Test failure"""
+        self.helper_file_negative('fixtures/templates/bad/resources/iam/managed_policy_description.yaml', 1)