Repository: aws-cloudformation/cfn-lint

Issue:
construct_yaml_map leading to TypeError: unhashable type: 'dict_node'
*cfn-lint version: 0.58.1*

*Unhandled `TypeError: unhashable type: 'dict_node'` thrown from construct_yaml_map.*

Please provide as much information as possible:
Utilising some slightly edited form of the example eks-nodegroup cfn, [see here for the failing cfn](https://github.com/SaltKhan/_cfn-breaking-cfn-lint/blob/main/eks-nodegroup.yaml) or [here, for the stacktrace](https://github.com/SaltKhan/_cfn-breaking-cfn-lint/blob/main/README.md). The problem occurrs on line 259 `VPCZoneIdentifier: !Join [ ',', [ !ImportValue Fn::Sub: "${NetworkStackName}-SubnetsPrivate01", !ImportValue Fn::Sub: "${NetworkStackName}-SubnetsPrivate02" ] ]`. (Was trying to provide a list by string concat as that's what's listed as the example input when subnets is a parameter as in the example cfn, but I've changed that for the example I provide here.) I changed the line to an expanded `!Join` ~
```
      VPCZoneIdentifier: !Join
                           - ','
                           - - Fn::ImportValue: !Sub "${NetworkStackName}-SubnetsPrivate01"
                             - Fn::ImportValue: !Sub "${NetworkStackName}-SubnetsPrivate02"
```
And it now correctly yields a normal error, rather than hitting the exception.
I found this similar previous issue ~ https://github.com/aws-cloudformation/cfn-lint/issues/348


Discussion:
Looking into it.

Resolving patch:
diff --git a/src/cfnlint/decode/cfn_yaml.py b/src/cfnlint/decode/cfn_yaml.py
--- a/src/cfnlint/decode/cfn_yaml.py
+++ b/src/cfnlint/decode/cfn_yaml.py
@@ -103,7 +103,21 @@ def construct_yaml_map(self, node):
                             ),
                         ]
                     )
-            mapping[key] = value
+            try:
+                mapping[key] = value
+            except:
+                raise CfnParseError(
+                        self.filename,
+                        [
+                            build_match(
+                                filename=self.filename,
+                                message=f'Unhashable type "{key}" (line {key.start_mark.line + 1})',
+                                line_number=key.start_mark.line,
+                                column_number=key.start_mark.column,
+                                key=key
+                            ),
+                        ]
+                    )
 
         obj, = SafeConstructor.construct_yaml_map(self, node)
 


Test patch:
diff --git a/test/fixtures/templates/bad/core/parse_invalid_map.yaml b/test/fixtures/templates/bad/core/parse_invalid_map.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/core/parse_invalid_map.yaml
@@ -0,0 +1,23 @@
+Resources:
+  NodeGroup:
+    Type: "AWS::AutoScaling::AutoScalingGroup"
+    Properties:
+      DesiredCapacity: !Ref NodeAutoScalingGroupDesiredCapacity
+      LaunchTemplate:
+        LaunchTemplateId: !Ref NodeLaunchTemplate
+        Version: !GetAtt NodeLaunchTemplate.LatestVersionNumber
+      MaxSize: !Ref NodeAutoScalingGroupMaxSize
+      MinSize: !Ref NodeAutoScalingGroupMinSize
+      Tags:
+        - Key: Name
+          PropagateAtLaunch: true
+          Value: !Sub Cluster-Group-Node
+        - Key: !Sub kubernetes.io/cluster/Cluster
+          PropagateAtLaunch: true
+          Value: owned
+      VPCZoneIdentifier: !Join [ ',', [ !ImportValue Fn::Sub: "${NetworkStackName}-SubnetsPrivate01", !ImportValue Fn::Sub: "${NetworkStackName}-SubnetsPrivate02" ] ]
+    UpdatePolicy:
+      AutoScalingRollingUpdate:
+        MaxBatchSize: 1
+        MinInstancesInService: !Ref NodeAutoScalingGroupDesiredCapacity
+        PauseTime: PT5M
\ No newline at end of file
diff --git a/test/unit/module/cfn_yaml/test_yaml.py b/test/unit/module/cfn_yaml/test_yaml.py
--- a/test/unit/module/cfn_yaml/test_yaml.py
+++ b/test/unit/module/cfn_yaml/test_yaml.py
@@ -61,3 +61,10 @@ def test_success_parse_stdin(self):
                 matches.extend(self.rules.run(filename, cfn))
                 assert len(matches) == failures, 'Expected {} failures, got {} on {}'.format(
                     failures, len(matches), values.get('filename'))
+
+
+    def test_map_failure(self):
+        """Test a failure is passed on unhashable map"""
+        filename = 'test/fixtures/templates/bad/core/parse_invalid_map.yaml'
+
+        self.assertRaises(cfnlint.decode.cfn_yaml.CfnParseError, cfnlint.decode.cfn_yaml.load, filename)