Repository: getmoto/moto

Issue:
CloudFormation parameters marked with NoEcho are being echoed
## How to reproduce the issue

```python
import json
from pprint import pprint

import boto3
import moto


def create_stack_and_print_params():
    cf = boto3.resource("cloudformation")

    # Create a stack.
    cf.create_stack(
        StackName="MyCoolStack",
        TemplateBody=json.dumps(
            {
                "Resources": {
                    "MyCoolResource": {
                        "Type": "AWS::SSM::Parameter",
                        "Properties": {
                            "Name": " MyCoolParameter",
                            "Value": "Hey, this is a cool parameter!",
                        },
                    }
                },
                "Parameters": {
                    "JustAString": {"Type": "String"},
                    "NoEchoString": {"Type": "String", "NoEcho": True},
                },
            }
        ),
        Parameters=[
            {"ParameterKey": "JustAString", "ParameterValue": "This is just a string"},
            {
                "ParameterKey": "NoEchoString",
                "ParameterValue": "This should not be echoed",
            },
        ],
    )

    # Get parameters from stack.
    stack = cf.Stack("MyCoolStack")
    pprint(stack.parameters)

    # Delete stack
    stack.delete()


if __name__ == "__main__":
    print("Without moto:")
    create_stack_and_print_params()

    with moto.mock_cloudformation():
        print("With moto:")
        create_stack_and_print_params()
```

Note that the resource here isn't important. It's only present because CloudFormation needs to have at least one resource.

## What I expect to happen

The value of the parameter with NoEcho should be replaced with "****".

```
[{'ParameterKey': 'NoEchoString', 'ParameterValue': '****'},
 {'ParameterKey': 'JustAString', 'ParameterValue': 'This is just a string'}]
```

## What actually happens

The value isn't replaced.

```
[{'ParameterKey': 'JustAString', 'ParameterValue': 'This is just a string'},
 {'ParameterKey': 'NoEchoString',
  'ParameterValue': 'This should not be echoed'}]
```

## Versions and installation method

Using moto 1.3.7, boto3 1.9.79 and botocore 1.12.79 installed with pipenv.


Discussion:

Resolving patch:
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -425,11 +425,18 @@ def load_parameters(self):
             self.resolved_parameters[parameter_name] = parameter.get('Default')
 
         # Set any input parameters that were passed
+        self.no_echo_parameter_keys = []
         for key, value in self.input_parameters.items():
             if key in self.resolved_parameters:
-                value_type = parameter_slots[key].get('Type', 'String')
+                parameter_slot = parameter_slots[key]
+
+                value_type = parameter_slot.get('Type', 'String')
                 if value_type == 'CommaDelimitedList' or value_type.startswith("List"):
                     value = value.split(',')
+
+                if parameter_slot.get('NoEcho'):
+                    self.no_echo_parameter_keys.append(key)
+
                 self.resolved_parameters[key] = value
 
         # Check if there are any non-default params that were not passed input
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -654,7 +654,11 @@ def update_stack_instances(self):
         {% for param_name, param_value in stack.stack_parameters.items() %}
           <member>
             <ParameterKey>{{ param_name }}</ParameterKey>
-            <ParameterValue>{{ param_value }}</ParameterValue>
+            {% if param_name in stack.resource_map.no_echo_parameter_keys %}
+                <ParameterValue>****</ParameterValue>
+            {% else %}
+                <ParameterValue>{{ param_value }}</ParameterValue>
+            {% endif %}
           </member>
         {% endfor %}
         </Parameters>


Test patch:
diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py
--- a/tests/test_cloudformation/test_stack_parsing.py
+++ b/tests/test_cloudformation/test_stack_parsing.py
@@ -83,6 +83,18 @@
     }
 }
 
+parameters = {
+    "Parameters": {
+        "Param": {
+            "Type": "String",
+        },
+        "NoEchoParam": {
+            "Type": "String",
+            "NoEcho": True
+        }
+    }
+}
+
 split_select_template = {
     "AWSTemplateFormatVersion": "2010-09-09",
     "Resources": {
@@ -157,6 +169,9 @@
 get_availability_zones_template = dict(
     list(dummy_template.items()) + list(get_availability_zones_output.items()))
 
+parameters_template = dict(
+    list(dummy_template.items()) + list(parameters.items()))
+
 dummy_template_json = json.dumps(dummy_template)
 name_type_template_json = json.dumps(name_type_template)
 output_type_template_json = json.dumps(outputs_template)
@@ -165,6 +180,7 @@
     get_attribute_outputs_template)
 get_availability_zones_template_json = json.dumps(
     get_availability_zones_template)
+parameters_template_json = json.dumps(parameters_template)
 split_select_template_json = json.dumps(split_select_template)
 sub_template_json = json.dumps(sub_template)
 export_value_template_json = json.dumps(export_value_template)
@@ -290,6 +306,18 @@ def test_parse_stack_with_bad_get_attribute_outputs():
         "test_id", "test_stack", bad_output_template_json, {}, "us-west-1").should.throw(ValidationError)
 
 
+def test_parse_stack_with_parameters():
+    stack = FakeStack(
+        stack_id="test_id",
+        name="test_stack",
+        template=parameters_template_json,
+        parameters={"Param": "visible value", "NoEchoParam": "hidden value"},
+        region_name='us-west-1')
+
+    stack.resource_map.no_echo_parameter_keys.should.have("NoEchoParam")
+    stack.resource_map.no_echo_parameter_keys.should_not.have("Param")
+
+
 def test_parse_equals_condition():
     parse_condition(
         condition={"Fn::Equals": [{"Ref": "EnvType"}, "prod"]},