Repository: aws-cloudformation/cfn-lint

Issue:
Statement IDs (SID) must be alpha-numeric
*cfn-lint version: cfn-lint 0.15.0*

*When creating a AWS::IAM::ManagedPolicy and using the `Sid` statement, cfn-lint is not detecting an invalidly-formed Sid statement*

Please provide as much information as possible:
* Template linting issues:
  * Please provide a CloudFormation sample that generated the issue.
```yaml
  ManagedPolicyCodeSuiteKmsKey:
    Type: AWS::IAM::ManagedPolicy
    Condition: CreateCodePipelineRole
    Properties:
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: Allow use of KMS key in Operations Production Account
            Effect: Allow
            Resource: !Sub 'arn:aws:kms::123123123123:key/*'
            Action:
              - "kms:Encrypt"
              - "kms:Decrypt"
              - "kms:ReEncrypt*"
              - "kms:GenerateDataKey*"
              - "kms:DescribeKey"
```
  * If present, please add links to the (official) documentation for clarification.
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html#policies-grammar-notes-strings
> **sid_string**
> > Provides a way to include information about an individual statement. For IAM policies, basic alphanumeric characters (A-Z,a-z,0-9) are the only allowed characters in the Sid value. Other AWS services that support resource policies may have other requirements for the Sid value. For example, some services require this value to be unique within an AWS account, and some services allow additional characters such as spaces in the Sid value.

  * Validate if the issue still exists with the latest version of `cfn-lint` and/or the latest Spec files
    I do not know how to install latest version, I just ran `pip install`

* Feature request:
  * My CloudFormation template fails to Create or Update because the Sid uses invalid characters, and cfn-lint does not detect that.


**NOTE: I have executed `cfn-lint -u` and it downloaded updates, but the problem remains.

----
Below is the output from CloudFormation:
```
The following resource(s) failed to update: [ManagedPolicyCodeSuiteKmsKey].
--
  | 11:20:11 UTC+1000 | UPDATE_FAILED | AWS::IAM::ManagedPolicy | ManagedPolicyCodeSuiteKmsKey | Statement IDs (SID) must be alpha-numeric. Check that your input satisfies the regular expression [0-9A-Za-z]* (Service: AmazonIdentityManagement; Status Code: 400; Error Code: MalformedPolicyDocument; Request ID: a632726a-42d2-11e9-8725-21992a944eb3)
```



Discussion:
Hi @felipe1982 ,

This is indeed not checked yet. Although I'm working on a generic approach to catch these formatting rules (https://github.com/aws-cloudformation/cfn-python-lint/pull/625). We can add in this property in that rule once it's merged.

Thanx for the clear issue, this makes it pretty easy to solve 👍 
Thank you for an awesome tool to use when developing and also for CI. Your issue template helped me create the issue so thanks.
Ah, a few notes:

- The `AllowedPattern` rule cannot be used since the IAM policy is not specified in the Spec file (it's a `json`
- It's not as simple as it looks, the documentation says:

```For example, some services require this value to be unique within an AWS account, and some services allow additional characters such as spaces in the Sid value.```

🤔 
Would setting the rule level to experimental / informational help with the ambiguity between services' differing requirements? 
It could help if we could only do this generically.   Looking at just the extra allowed characters in the SID comment.   I'm curious if that is just a service based policy instead of an identity based policy issue.  If we know the type of services/resources that allow additional characters we may be able to come up with a rule that switches the REGEX based on the type of resource.  The question may be is does every service that allow additional characters follow the same standards of what characters they allow.  

The best way I've found to do this in the past is to deploy each service and see if they take a space or not.  Or will build the construct of a rule that could be used for additional services/resources but just starts with ManagedPolicy as we know that one won't work.
I just wanted to note that the following IAM Resources are affected by this, since all of them can create an IAM Policy:

- AWS::IAM::ManagedPolicy

```yaml
ManagedPolicy:
  Type: AWS::IAM::ManagedPolicy
  Properties:
    PolicyDocument:
      Version: 2012-10-17
      Statement:
        - Sid: "Space Space" # <------------
          Effect: Allow
          Action: "*"
          Resource: "*"
```

- AWS::IAM::Policy

```yaml
Policy:
  Type: AWS::IAM::Policy
  Properties:
    Roles:
      - !Ref IAMRole
    PolicyName: Policy
    PolicyDocument:
      Version: 2012-10-17
      Statement:
        - Sid: "Space Space" # <------------
          Effect: Allow
          Action: "*"
          Resource: "*"
```

- AWS::IAM::RolePolicy

```yaml
RolePolicy:
  Type: AWS::IAM::RolePolicy
  Properties:
    RoleName: !Ref IAMRole
    PolicyName: RolePolicy
    PolicyDocument:
      Version: 2012-10-17
      Statement:
        - Sid: "Space Space" # <------------
          Effect: Allow
          Action: "*"
          Resource: "*"
```

- AWS::IAM::Role

```yaml
  Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: InlinePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: "Space Space" # <------------
                Effect: Allow
                Action: "*"
                Resource: "*"
```

Not entirely sure if the IAM Team ever plans to remove this limitation, but if they plan to i'm not sure if this check is actually needed. For example, the KMS Key Policy doesn't have this kind of limitation:

```yaml
  KMSKey:
    Type: AWS::KMS::Key
    Properties:
      KeyPolicy:
        Version: 2012-10-17
        Id: !Sub "${AWS::AccountId}-${AWS::StackName}"
        Statement:
          - Sid: "Allow root" # <------- This works
            Effect: Allow
            Action: kms:*
            Resource: "*"
            Principal:
              AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
```
I think we are in a much better spot to handle this in v1.  Going to look at this shortly.



Patch:
diff --git a/src/cfnlint/data/schemas/other/iam/policy.json b/src/cfnlint/data/schemas/other/iam/policy.json
index 88cd56f670..fe3482694a 100644
--- a/src/cfnlint/data/schemas/other/iam/policy.json
+++ b/src/cfnlint/data/schemas/other/iam/policy.json
@@ -9,6 +9,7 @@
    "items": {
     "type": "string"
    },
+   "minItems": 1,
    "type": [
     "string",
     "array"
diff --git a/src/cfnlint/data/schemas/other/iam/policy_identity.json b/src/cfnlint/data/schemas/other/iam/policy_identity.json
index e3a2bc3746..e33c5a3c9f 100644
--- a/src/cfnlint/data/schemas/other/iam/policy_identity.json
+++ b/src/cfnlint/data/schemas/other/iam/policy_identity.json
@@ -43,7 +43,8 @@
      "$ref": "policy#/definitions/Resource"
     },
     "Sid": {
-     "$ref": "policy#/definitions/Statement/properties/Sid"
+     "$ref": "policy#/definitions/Statement/properties/Sid",
+     "pattern": "^[A-Za-z0-9]+$"
     }
    }
   }
diff --git a/src/cfnlint/rules/resources/iam/IdentityPolicy.py b/src/cfnlint/rules/resources/iam/IdentityPolicy.py
index 24a4edf0e8..241a70790f 100644
--- a/src/cfnlint/rules/resources/iam/IdentityPolicy.py
+++ b/src/cfnlint/rules/resources/iam/IdentityPolicy.py
@@ -27,6 +27,9 @@ def __init__(self):
                 "Resources/AWS::IAM::Role/Properties/Policies/*/PolicyDocument",
                 "Resources/AWS::IAM::User/Properties/Policies/*/PolicyDocument",
                 "Resources/AWS::SSO::PermissionSet/Properties/InlinePolicy",
+                "Resources/AWS::IAM::UserPolicy/Properties/PolicyDocument",
+                "Resources/AWS::IAM::RolePolicy/Properties/PolicyDocument",
+                "Resources/AWS::IAM::GroupPolicy/Properties/PolicyDocument",
             ],
             "identity",
             "policy_identity.json",


Test patch:
diff --git a/test/unit/rules/resources/iam/test_identity_policy.py b/test/unit/rules/resources/iam/test_identity_policy.py
index 592beea8cd..f660d8b724 100644
--- a/test/unit/rules/resources/iam/test_identity_policy.py
+++ b/test/unit/rules/resources/iam/test_identity_policy.py
@@ -3,11 +3,12 @@
 SPDX-License-Identifier: MIT-0
 """
 
+from collections import deque
 from unittest import TestCase
 
 from cfnlint.context import Context
 from cfnlint.helpers import FUNCTIONS
-from cfnlint.jsonschema import CfnTemplateValidator
+from cfnlint.jsonschema import CfnTemplateValidator, ValidationError
 from cfnlint.rules.resources.iam.IdentityPolicy import IdentityPolicy
 
 
@@ -238,3 +239,45 @@ def test_duplicate_sid(self):
         self.assertEqual(len(errs), 1, errs)
         self.assertEqual(errs[0].message, "array items are not unique for keys ['Sid']")
         self.assertListEqual(list(errs[0].path), ["Statement"])
+
+    def test_pattern_sid(self):
+        validator = CfnTemplateValidator()
+
+        policy = {
+            "Version": "2012-10-17",
+            "Statement": [
+                {
+                    "Sid": "A ",
+                    "Effect": "Allow",
+                    "Action": "*",
+                    "Resource": "*",
+                },
+            ],
+        }
+
+        errs = list(
+            self.rule.validate(
+                validator=validator, policy=policy, schema={}, policy_type=None
+            )
+        )
+        self.assertListEqual(
+            errs,
+            [
+                ValidationError(
+                    message="'A ' does not match '^[A-Za-z0-9]+$'",
+                    validator="pattern",
+                    path=deque(["Statement", 0, "Sid"]),
+                    schema_path=deque(
+                        [
+                            "properties",
+                            "Statement",
+                            "items",
+                            "properties",
+                            "Sid",
+                            "pattern",
+                        ]
+                    ),
+                    rule=IdentityPolicy(),
+                )
+            ],
+        )