Repository: aws-cloudformation/cfn-lint

Issue:
Support dynamic parameters in password fields
*cfn-lint version: 0.5.1*

*Description of issue.*

`W2501` shows the following warning when using the new dynamic parameters

```
W2501 Password shouldn't be hardcoded for Resources/DBInstance/Properties/MasterUserPassword
```

The template is:
```
MasterUserPassword: '{{resolve:ssm-secure:DBMasterPassword:1}}'
```


Discussion:
Agreed. I’ll try to get that fixed today.

Resolving patch:
diff --git a/src/cfnlint/helpers.py b/src/cfnlint/helpers.py
--- a/src/cfnlint/helpers.py
+++ b/src/cfnlint/helpers.py
@@ -40,6 +40,9 @@
 REGEX_CIDR = re.compile(r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$')
 REGEX_IPV4 = re.compile(r'^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')
 REGEX_IPV6 = re.compile(r'^(((?=.*(::))(?!.*\3.+\3))\3?|[\dA-F]{1,4}:)([\dA-F]{1,4}(\3|:\b)|\2){5}(([\dA-F]{1,4}(\3|:\b|$)|\2){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})\Z', re.I | re.S)
+REGEX_DYN_REF_SSM = re.compile(r'^.*{{resolve:ssm:[a-zA-Z0-9_.-/]+:\d+}}.*$')
+REGEX_DYN_REF_SSM_SECURE = re.compile(r'^.*{{resolve:ssm-secure:[a-zA-Z0-9_.-/]+:\d+}}.*$')
+
 
 AVAILABILITY_ZONES = [
     'us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d', 'us-east-1e', 'us-east-1f',
diff --git a/src/cfnlint/rules/resources/properties/Password.py b/src/cfnlint/rules/resources/properties/Password.py
--- a/src/cfnlint/rules/resources/properties/Password.py
+++ b/src/cfnlint/rules/resources/properties/Password.py
@@ -14,9 +14,11 @@
   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 re
 import six
 from cfnlint import CloudFormationLintRule
 from cfnlint import RuleMatch
+from cfnlint.helpers import REGEX_DYN_REF_SSM, REGEX_DYN_REF_SSM_SECURE
 
 
 class Password(CloudFormationLintRule):
@@ -41,10 +43,16 @@ def match(self, cfn):
             trees = [x for x in trees if x[0] == 'Resources']
             for tree in trees:
                 obj = tree[-1]
-                if isinstance(obj, (six.text_type, six.string_types)):
-                    message = 'Password shouldn\'t be hardcoded for %s' % (
-                        '/'.join(map(str, tree[:-1])))
-                    matches.append(RuleMatch(tree[:-1], message))
+                if isinstance(obj, (six.string_types)):
+                    if not re.match(REGEX_DYN_REF_SSM_SECURE, obj):
+                        if re.match(REGEX_DYN_REF_SSM, obj):
+                            message = 'Password should use a secure dynamic reference for %s' % (
+                                '/'.join(map(str, tree[:-1])))
+                            matches.append(RuleMatch(tree[:-1], message))
+                        else:
+                            message = 'Password shouldn\'t be hardcoded for %s' % (
+                                '/'.join(map(str, tree[:-1])))
+                            matches.append(RuleMatch(tree[:-1], message))
                 elif isinstance(obj, dict):
                     if len(obj) == 1:
                         for key, value in obj.items():


Test patch:
diff --git a/test/fixtures/templates/bad/properties_password.yaml b/test/fixtures/templates/bad/properties_password.yaml
--- a/test/fixtures/templates/bad/properties_password.yaml
+++ b/test/fixtures/templates/bad/properties_password.yaml
@@ -31,3 +31,12 @@ Resources:
       MasterUsername: MyName
       MasterUserPassword: !Ref MyNewPassword
     DeletionPolicy: Snapshot
+  myThirdDb:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      AllocatedStorage: '5'
+      DBInstanceClass: db.m1.small
+      Engine: MySQL
+      MasterUsername: MyName
+      MasterUserPassword: '{{resolve:ssm:IAMUserPassword:10}}'
+    DeletionPolicy: Snapshot
diff --git a/test/fixtures/templates/good/resources/properties/password.yaml b/test/fixtures/templates/good/resources/properties/password.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/properties/password.yaml
@@ -0,0 +1,47 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Description: >
+  Good Template for testing Password Parameters
+Parameters:
+  MyPassword:
+    Type: String
+    Default: String
+    Description: String
+    NoEcho: True
+  Environment:
+    Type: String
+    Default: "Dev"
+Resources:
+  MyDB:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      AllocatedStorage: '5'
+      DBInstanceClass: db.m1.small
+      Engine: MySQL
+      MasterUsername: MyName
+      MasterUserPassword: !Ref MyPassword
+    DeletionPolicy: Snapshot
+  myNewDb:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      AllocatedStorage: '5'
+      DBInstanceClass: db.m1.small
+      Engine: MySQL
+      MasterUsername: MyName
+      MasterUserPassword: '{{resolve:ssm-secure:MySecurePassword:1}}'
+    DeletionPolicy: Snapshot
+  myThirdDb:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      AllocatedStorage: '5'
+      DBInstanceClass: db.m1.small
+      Engine: MySQL
+      MasterUsername: MyName
+      MasterUserPassword: 'Prefix-{{resolve:ssm-secure:MySecurePassword:1}}-Postfix'
+    DeletionPolicy: Snapshot
+  MyIAMUser:
+    Type: AWS::IAM::User
+    Properties:
+      UserName: !Sub ${Environment}-{{resolve:ssm:MyUsername:1}}
+      LoginProfile:
+        Password: !Sub 'list-{{resolve:ssm-secure:MySecurePassword:1}}'
diff --git a/test/rules/resources/properties/test_password.py b/test/rules/resources/properties/test_password.py
--- a/test/rules/resources/properties/test_password.py
+++ b/test/rules/resources/properties/test_password.py
@@ -24,6 +24,9 @@ def setUp(self):
         """Setup"""
         super(TestPropertyPassword, self).setUp()
         self.collection.register(Password())
+        self.success_templates = [
+            'fixtures/templates/good/resources/properties/password.yaml'
+        ]
 
     def test_file_positive(self):
         """Test Positive"""
@@ -31,4 +34,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('fixtures/templates/bad/properties_password.yaml', 2)
+        self.helper_file_negative('fixtures/templates/bad/properties_password.yaml', 3)