Repository: Skyscanner/cfripper

Issue:
Error while parsing template with pycfmodel
Hi guys. I get the following error when I ran the lambda

```
[ERROR] TypeError: ModelMetaclass object argument after ** must be a mapping, not str
Traceback (most recent call last):
  File "/var/task/cfripper/main.py", line 119, in handler
    cfmodel = pycfmodel.parse(template).resolve()
  File "/var/task/pycfmodel/__init__.py", line 19, in parse
    return CFModel(**template)
```
I've checked and it seems that the `template` is parsed indeed, but instead of a dict, it is a string. Do you have any clues why this might be happening?

Atm I am triggering the lambda with a test input like this
```
{
  "project": "test",
  "stack": {
    "name": "stack-name"
  },
  "region": "eu-west-1",
  "account": {
    "id": 1122334455
  }
}
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7877480..8b362c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 # Changelog
 All notable changes to this project will be documented in this file.
 
+## [0.11.2] - 2019-11-26
+### Fixes
+- Fix `get_template` when AWS doesn't return a dict.
+
 ## [0.11.1] - 2019-11-25
 ### Changed
 - `HardcodedRDSPasswordRule` now reports two different messages when there is a missing echo or a readable password.
diff --git a/cfripper/boto3_client.py b/cfripper/boto3_client.py
index 73e7121..29a4a7a 100644
--- a/cfripper/boto3_client.py
+++ b/cfripper/boto3_client.py
@@ -15,6 +15,7 @@ specific language governing permissions and limitations under the License.
 import logging
 from datetime import datetime
 from time import sleep
+from typing import Dict, Optional
 
 import boto3
 from botocore.exceptions import ClientError
@@ -47,7 +48,7 @@ class Boto3Client:
         self.region = region
         self.stack_id = stack_id
 
-    def get_template(self):
+    def get_template(self) -> Optional[Dict]:
         client = self.session.client("cloudformation", region_name=self.region)
         stack_content = None
         i = 0
@@ -70,7 +71,10 @@ class Boto3Client:
                         f" {self.stack_id} on {self.account_id} - {self.region}"
                     )
             i += 1
-        return stack_content
+        # Fix when AWS doesn't return a dict
+        # https://github.com/boto/botocore/issues/1058
+        # https://github.com/boto/boto3/issues/1468
+        return convert_json_or_yaml_to_dict(stack_content) if isinstance(stack_content, str) else stack_content
 
     def download_template_to_dictionary(self, s3_url):
         """
diff --git a/requirements.txt b/requirements.txt
index bcf3522..44b2162 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,16 +4,16 @@
 #
 #    make freeze
 #
-boto3==1.10.6
-botocore==1.13.6          # via boto3, s3transfer
-cfn-flip==1.2.1
+boto3==1.10.27
+botocore==1.13.27         # via boto3, s3transfer
+cfn-flip==1.2.2
 click==7.0                # via cfn-flip
 docutils==0.15.2          # via botocore
 jmespath==0.9.4           # via boto3, botocore
-pycfmodel==0.5.0
-pydantic==1.0             # via pycfmodel
+pycfmodel==0.6.0
+pydantic==1.1.1           # via pycfmodel
 python-dateutil==2.8.0    # via botocore
 pyyaml==5.1.2
 s3transfer==0.2.1         # via boto3
-six==1.12.0               # via cfn-flip, python-dateutil
-urllib3==1.25.6           # via botocore
+six==1.13.0               # via cfn-flip, python-dateutil
+urllib3==1.25.7           # via botocore
diff --git a/setup.py b/setup.py
index 89a3f9c..ffd58d8 100644
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,7 @@ dev_requires = [
 
 setup(
     name="cfripper",
-    version="0.11.1",
+    version="0.11.2",
     author="Skyscanner Product Security",
     author_email="security@skyscanner.net",
     long_description=long_description,


Test patch:
diff --git a/tests/test_boto3_client.py b/tests/test_boto3_client.py
index 755a6ca..2105921 100644
--- a/tests/test_boto3_client.py
+++ b/tests/test_boto3_client.py
@@ -43,9 +43,11 @@ def boto3_client():
 @pytest.mark.parametrize(
     "aws_responses, expected_template",
     [
-        (["nice template"], "nice template"),
+        ([{"A": "a"}], {"A": "a"}),
         ([DUMMY_CLIENT_ERROR] * 10, None),
-        ([DUMMY_CLIENT_ERROR, "nice template"], "nice template"),
+        ([DUMMY_CLIENT_ERROR, {"A": "a"}], {"A": "a"}),
+        (['{"A": "a"}'], {"A": "a"}),
+        (["A: a"], {"A": "a"}),
     ],
 )
 def test_get_template(aws_responses, expected_template, boto3_client):