prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Incorrect E1029 errors when literal YAML block style is used
  *cfn-lint version: 0.51.0
  
  Embedded parameters are being flagged for not being wrapped in a `!Sub`, but they are in fact wrapped in a `!Sub`.
  
  Please provide as much information as possible:
  
  Sample template (extraneous lines have been removed):
  ```yaml
  Resources:
    SomeStackset:
      Type: AWS::CloudFormation::StackSet
      Properties:
        TemplateBody: |
          Resources:
            SomeRole:
              Type: AWS::IAM::Role
              Properties:
                Policies:
                  -
                    PolicyName: SomeName
                    PolicyDocument:
                      Version: 2012-10-17
                      Statement:
                        -
                          Effect: Allow
                          Action:
                            - iam:GetSomething
                          Resource:
                            - !Sub arn:aws:iam::${AWS::AccountId}:role/SomeRole*
  ```
  
  The error is an [E1029](https://github.com/aws-cloudformation/cfn-lint/blob/main/src/cfnlint/rules/functions/SubNeeded.py), and the line number referenced is the line number where the YAML literal appears (here, the line that contains `TemplateBody: |`)
  
  This appears to be a new issue with [release 0.51.0](https://github.com/aws-cloudformation/cfn-lint/releases/tag/v0.51.0). The code in question above hasn't changed in my repo for a long time, and I don't get the error on previous releases.
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `src/cfnlint/rules/functions/SubNeeded.py`
  Content:
  ```
  1 """
  2 Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  3 SPDX-License-Identifier: MIT-0
  4 """
  5 from functools import reduce  # pylint: disable=redefined-builtin
  6 import re
  7 import copy
  8 import six
  9 from cfnlint.rules import CloudFormationLintRule
  10 from cfnlint.rules import RuleMatch
  11 
  12 
  13 class SubNeeded(CloudFormationLintRule):
  14     """Check if a substitution string exists without a substitution function"""
  15     id = 'E1029'
  16     shortdesc = 'Sub is required if a variable is used in a string'
  17     description = 'If a substitution variable exists in a string but isn\'t wrapped with the Fn::Sub function the deployment will fail.'
  18     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'
  19     tags = ['functions', 'sub']
  20 
  21     def __init__(self):
  22         """Init"""
  23         super(SubNeeded, self).__init__()
  24         self.config_definition = {
  25             'custom_excludes': {
  26                 'default': '',
  27                 'type': 'string'
  28             }
  29         }
  30         self.configure()
  31         self.subParameterRegex = re.compile(r'(\$\{[A-Za-z0-9_:\.]+\})')
  32 
  33     def _match_values(self, cfnelem, path):
  34         """Recursively search for values matching the searchRegex"""
  35         values = []
  36         if isinstance(cfnelem, dict):
  37             for key in cfnelem:
  38                 pathprop = path[:]
  39                 pathprop.append(key)
  40                 values.extend(self._match_values(cfnelem[key], pathprop))
  41         elif isinstance(cfnelem, list):
  42             for index, item in enumerate(cfnelem):
  43                 pathprop = path[:]
  44                 pathprop.append(index)
  45                 values.extend(self._match_values(item, pathprop))
  46         else:
  47             # Leaf node
  48             if isinstance(cfnelem, six.string_types):  # and re.match(searchRegex, cfnelem):
  49                 for variable in re.findall(self.subParameterRegex, cfnelem):
  50                     values.append(path + [variable])
  51 
  52         return values
  53 
  54     def match_values(self, cfn):
  55         """
  56             Search for values in all parts of the templates that match the searchRegex
  57         """
  58         results = []
  59         results.extend(self._match_values(cfn.template, []))
  60         # Globals are removed during a transform.  They need to be checked manually
  61         results.extend(self._match_values(cfn.template.get('Globals', {}), []))
  62         return results
  63 
  64     def _api_exceptions(self, value):
  65         """ Key value exceptions """
  66         parameter_search = re.compile(r'^\$\{stageVariables\..*\}$')
  67         return re.match(parameter_search, value)
  68 
  69     def _variable_custom_excluded(self, value):
  70         """ User-defined exceptions for variables, anywhere in the file """
  71         custom_excludes = self.config['custom_excludes']
  72         if custom_excludes:
  73             custom_search = re.compile(custom_excludes)
  74             return re.match(custom_search, value)
  75         return False
  76 
  77     def match(self, cfn):
  78         matches = []
  79 
  80         refs = cfn.get_valid_refs()
  81         getatts = cfn.get_valid_getatts()
  82 
  83         # Get a list of paths to every leaf node string containing at least one ${parameter}
  84         parameter_string_paths = self.match_values(cfn)
  85         # We want to search all of the paths to check if each one contains an 'Fn::Sub'
  86         for parameter_string_path in parameter_string_paths:
  87             # Get variable
  88             var = parameter_string_path[-1]
  89 
  90             # Step Function State Machine has a Definition Substitution that allows usage of special variables outside of a !Sub
  91             # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definitionsubstitutions.html
  92 
  93             if 'DefinitionString' in parameter_string_path:
  94                 modified_parameter_string_path = copy.copy(parameter_string_path)
  95                 index = parameter_string_path.index('DefinitionString')
  96                 modified_parameter_string_path[index] = 'DefinitionSubstitutions'
  97                 modified_parameter_string_path = modified_parameter_string_path[:index+1]
  98                 modified_parameter_string_path.append(var[2:-1])
  99                 if reduce(lambda c, k: c.get(k, {}), modified_parameter_string_path, cfn.template):
  100                     continue
  101 
  102             # Exclude variables that match custom exclude filters, if configured
  103             # (for third-party tools that pre-process templates before uploading them to AWS)
  104             if self._variable_custom_excluded(var):
  105                 continue
  106 
  107             # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)
  108             if var.startswith('${!'):
  109                 continue
  110 
  111             var_stripped = var[2:-1].strip()
  112 
  113             # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly
  114             if not 'Fn::Sub' in parameter_string_path:
  115                 if (var_stripped in refs or var_stripped in getatts) or 'DefinitionString' in parameter_string_path:
  116                     # Remove the last item (the variable) to prevent multiple errors on 1 line errors
  117                     path = parameter_string_path[:-1]
  118                     message = 'Found an embedded parameter "{}" outside of an "Fn::Sub" at {}'.format(
  119                         var, '/'.join(map(str, path)))
  120                     matches.append(RuleMatch(path, message))
  121 
  122         return matches
  123
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/src/cfnlint/rules/functions/SubNeeded.py b/src/cfnlint/rules/functions/SubNeeded.py
  --- a/src/cfnlint/rules/functions/SubNeeded.py
  +++ b/src/cfnlint/rules/functions/SubNeeded.py
  @@ -18,6 +18,8 @@
       source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'
       tags = ['functions', 'sub']
   
  +    exceptions = ['TemplateBody']
  +
       def __init__(self):
           """Init"""
           super(SubNeeded, self).__init__()
  @@ -111,7 +113,7 @@
               var_stripped = var[2:-1].strip()
   
               # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly
  -            if not 'Fn::Sub' in parameter_string_path:
  +            if not 'Fn::Sub' in parameter_string_path and parameter_string_path[-2] not in self.exceptions:
                   if (var_stripped in refs or var_stripped in getatts) or 'DefinitionString' in parameter_string_path:
                       # Remove the last item (the variable) to prevent multiple errors on 1 line errors
                       path = parameter_string_path[:-1]
  
verification_info: {"golden_diff": "diff --git a/src/cfnlint/rules/functions/SubNeeded.py b/src/cfnlint/rules/functions/SubNeeded.py\n--- a/src/cfnlint/rules/functions/SubNeeded.py\n+++ b/src/cfnlint/rules/functions/SubNeeded.py\n@@ -18,6 +18,8 @@\n     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'\n     tags = ['functions', 'sub']\n \n+    exceptions = ['TemplateBody']\n+\n     def __init__(self):\n         \"\"\"Init\"\"\"\n         super(SubNeeded, self).__init__()\n@@ -111,7 +113,7 @@\n             var_stripped = var[2:-1].strip()\n \n             # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly\n-            if not 'Fn::Sub' in parameter_string_path:\n+            if not 'Fn::Sub' in parameter_string_path and parameter_string_path[-2] not in self.exceptions:\n                 if (var_stripped in refs or var_stripped in getatts) or 'DefinitionString' in parameter_string_path:\n                     # Remove the last item (the variable) to prevent multiple errors on 1 line errors\n                     path = parameter_string_path[:-1]\n", "issue": "Incorrect E1029 errors when literal YAML block style is used\n*cfn-lint version: 0.51.0\r\n\r\nEmbedded parameters are being flagged for not being wrapped in a `!Sub`, but they are in fact wrapped in a `!Sub`.\r\n\r\nPlease provide as much information as possible:\r\n\r\nSample template (extraneous lines have been removed):\r\n```yaml\r\nResources:\r\n  SomeStackset:\r\n    Type: AWS::CloudFormation::StackSet\r\n    Properties:\r\n      TemplateBody: |\r\n        Resources:\r\n          SomeRole:\r\n            Type: AWS::IAM::Role\r\n            Properties:\r\n              Policies:\r\n                -\r\n                  PolicyName: SomeName\r\n                  PolicyDocument:\r\n                    Version: 2012-10-17\r\n                    Statement:\r\n                      -\r\n                        Effect: Allow\r\n                        Action:\r\n                          - iam:GetSomething\r\n                        Resource:\r\n                          - !Sub arn:aws:iam::${AWS::AccountId}:role/SomeRole*\r\n```\r\n\r\nThe error is an [E1029](https://github.com/aws-cloudformation/cfn-lint/blob/main/src/cfnlint/rules/functions/SubNeeded.py), and the line number referenced is the line number where the YAML literal appears (here, the line that contains `TemplateBody: |`)\r\n\r\nThis appears to be a new issue with [release 0.51.0](https://github.com/aws-cloudformation/cfn-lint/releases/tag/v0.51.0). The code in question above hasn't changed in my repo for a long time, and I don't get the error on previous releases.\r\n\n", "before_files": [{"content": "\"\"\"\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: MIT-0\n\"\"\"\nfrom functools import reduce  # pylint: disable=redefined-builtin\nimport re\nimport copy\nimport six\nfrom cfnlint.rules import CloudFormationLintRule\nfrom cfnlint.rules import RuleMatch\n\n\nclass SubNeeded(CloudFormationLintRule):\n    \"\"\"Check if a substitution string exists without a substitution function\"\"\"\n    id = 'E1029'\n    shortdesc = 'Sub is required if a variable is used in a string'\n    description = 'If a substitution variable exists in a string but isn\\'t wrapped with the Fn::Sub function the deployment will fail.'\n    source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'\n    tags = ['functions', 'sub']\n\n    def __init__(self):\n        \"\"\"Init\"\"\"\n        super(SubNeeded, self).__init__()\n        self.config_definition = {\n            'custom_excludes': {\n                'default': '',\n                'type': 'string'\n            }\n        }\n        self.configure()\n        self.subParameterRegex = re.compile(r'(\\$\\{[A-Za-z0-9_:\\.]+\\})')\n\n    def _match_values(self, cfnelem, path):\n        \"\"\"Recursively search for values matching the searchRegex\"\"\"\n        values = []\n        if isinstance(cfnelem, dict):\n            for key in cfnelem:\n                pathprop = path[:]\n                pathprop.append(key)\n                values.extend(self._match_values(cfnelem[key], pathprop))\n        elif isinstance(cfnelem, list):\n            for index, item in enumerate(cfnelem):\n                pathprop = path[:]\n                pathprop.append(index)\n                values.extend(self._match_values(item, pathprop))\n        else:\n            # Leaf node\n            if isinstance(cfnelem, six.string_types):  # and re.match(searchRegex, cfnelem):\n                for variable in re.findall(self.subParameterRegex, cfnelem):\n                    values.append(path + [variable])\n\n        return values\n\n    def match_values(self, cfn):\n        \"\"\"\n            Search for values in all parts of the templates that match the searchRegex\n        \"\"\"\n        results = []\n        results.extend(self._match_values(cfn.template, []))\n        # Globals are removed during a transform.  They need to be checked manually\n        results.extend(self._match_values(cfn.template.get('Globals', {}), []))\n        return results\n\n    def _api_exceptions(self, value):\n        \"\"\" Key value exceptions \"\"\"\n        parameter_search = re.compile(r'^\\$\\{stageVariables\\..*\\}$')\n        return re.match(parameter_search, value)\n\n    def _variable_custom_excluded(self, value):\n        \"\"\" User-defined exceptions for variables, anywhere in the file \"\"\"\n        custom_excludes = self.config['custom_excludes']\n        if custom_excludes:\n            custom_search = re.compile(custom_excludes)\n            return re.match(custom_search, value)\n        return False\n\n    def match(self, cfn):\n        matches = []\n\n        refs = cfn.get_valid_refs()\n        getatts = cfn.get_valid_getatts()\n\n        # Get a list of paths to every leaf node string containing at least one ${parameter}\n        parameter_string_paths = self.match_values(cfn)\n        # We want to search all of the paths to check if each one contains an 'Fn::Sub'\n        for parameter_string_path in parameter_string_paths:\n            # Get variable\n            var = parameter_string_path[-1]\n\n            # Step Function State Machine has a Definition Substitution that allows usage of special variables outside of a !Sub\n            # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definitionsubstitutions.html\n\n            if 'DefinitionString' in parameter_string_path:\n                modified_parameter_string_path = copy.copy(parameter_string_path)\n                index = parameter_string_path.index('DefinitionString')\n                modified_parameter_string_path[index] = 'DefinitionSubstitutions'\n                modified_parameter_string_path = modified_parameter_string_path[:index+1]\n                modified_parameter_string_path.append(var[2:-1])\n                if reduce(lambda c, k: c.get(k, {}), modified_parameter_string_path, cfn.template):\n                    continue\n\n            # Exclude variables that match custom exclude filters, if configured\n            # (for third-party tools that pre-process templates before uploading them to AWS)\n            if self._variable_custom_excluded(var):\n                continue\n\n            # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)\n            if var.startswith('${!'):\n                continue\n\n            var_stripped = var[2:-1].strip()\n\n            # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly\n            if not 'Fn::Sub' in parameter_string_path:\n                if (var_stripped in refs or var_stripped in getatts) or 'DefinitionString' in parameter_string_path:\n                    # Remove the last item (the variable) to prevent multiple errors on 1 line errors\n                    path = parameter_string_path[:-1]\n                    message = 'Found an embedded parameter \"{}\" outside of an \"Fn::Sub\" at {}'.format(\n                        var, '/'.join(map(str, path)))\n                    matches.append(RuleMatch(path, message))\n\n        return matches\n", "path": "src/cfnlint/rules/functions/SubNeeded.py"}], "after_files": [{"content": "\"\"\"\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: MIT-0\n\"\"\"\nfrom functools import reduce  # pylint: disable=redefined-builtin\nimport re\nimport copy\nimport six\nfrom cfnlint.rules import CloudFormationLintRule\nfrom cfnlint.rules import RuleMatch\n\n\nclass SubNeeded(CloudFormationLintRule):\n    \"\"\"Check if a substitution string exists without a substitution function\"\"\"\n    id = 'E1029'\n    shortdesc = 'Sub is required if a variable is used in a string'\n    description = 'If a substitution variable exists in a string but isn\\'t wrapped with the Fn::Sub function the deployment will fail.'\n    source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'\n    tags = ['functions', 'sub']\n\n    exceptions = ['TemplateBody']\n\n    def __init__(self):\n        \"\"\"Init\"\"\"\n        super(SubNeeded, self).__init__()\n        self.config_definition = {\n            'custom_excludes': {\n                'default': '',\n                'type': 'string'\n            }\n        }\n        self.configure()\n        self.subParameterRegex = re.compile(r'(\\$\\{[A-Za-z0-9_:\\.]+\\})')\n\n    def _match_values(self, cfnelem, path):\n        \"\"\"Recursively search for values matching the searchRegex\"\"\"\n        values = []\n        if isinstance(cfnelem, dict):\n            for key in cfnelem:\n                pathprop = path[:]\n                pathprop.append(key)\n                values.extend(self._match_values(cfnelem[key], pathprop))\n        elif isinstance(cfnelem, list):\n            for index, item in enumerate(cfnelem):\n                pathprop = path[:]\n                pathprop.append(index)\n                values.extend(self._match_values(item, pathprop))\n        else:\n            # Leaf node\n            if isinstance(cfnelem, six.string_types):  # and re.match(searchRegex, cfnelem):\n                for variable in re.findall(self.subParameterRegex, cfnelem):\n                    values.append(path + [variable])\n\n        return values\n\n    def match_values(self, cfn):\n        \"\"\"\n            Search for values in all parts of the templates that match the searchRegex\n        \"\"\"\n        results = []\n        results.extend(self._match_values(cfn.template, []))\n        # Globals are removed during a transform.  They need to be checked manually\n        results.extend(self._match_values(cfn.template.get('Globals', {}), []))\n        return results\n\n    def _api_exceptions(self, value):\n        \"\"\" Key value exceptions \"\"\"\n        parameter_search = re.compile(r'^\\$\\{stageVariables\\..*\\}$')\n        return re.match(parameter_search, value)\n\n    def _variable_custom_excluded(self, value):\n        \"\"\" User-defined exceptions for variables, anywhere in the file \"\"\"\n        custom_excludes = self.config['custom_excludes']\n        if custom_excludes:\n            custom_search = re.compile(custom_excludes)\n            return re.match(custom_search, value)\n        return False\n\n    def match(self, cfn):\n        matches = []\n\n        refs = cfn.get_valid_refs()\n        getatts = cfn.get_valid_getatts()\n\n        # Get a list of paths to every leaf node string containing at least one ${parameter}\n        parameter_string_paths = self.match_values(cfn)\n        # We want to search all of the paths to check if each one contains an 'Fn::Sub'\n        for parameter_string_path in parameter_string_paths:\n            # Get variable\n            var = parameter_string_path[-1]\n\n            # Step Function State Machine has a Definition Substitution that allows usage of special variables outside of a !Sub\n            # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definitionsubstitutions.html\n\n            if 'DefinitionString' in parameter_string_path:\n                modified_parameter_string_path = copy.copy(parameter_string_path)\n                index = parameter_string_path.index('DefinitionString')\n                modified_parameter_string_path[index] = 'DefinitionSubstitutions'\n                modified_parameter_string_path = modified_parameter_string_path[:index+1]\n                modified_parameter_string_path.append(var[2:-1])\n                if reduce(lambda c, k: c.get(k, {}), modified_parameter_string_path, cfn.template):\n                    continue\n\n            # Exclude variables that match custom exclude filters, if configured\n            # (for third-party tools that pre-process templates before uploading them to AWS)\n            if self._variable_custom_excluded(var):\n                continue\n\n            # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)\n            if var.startswith('${!'):\n                continue\n\n            var_stripped = var[2:-1].strip()\n\n            # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly\n            if not 'Fn::Sub' in parameter_string_path and parameter_string_path[-2] not in self.exceptions:\n                if (var_stripped in refs or var_stripped in getatts) or 'DefinitionString' in parameter_string_path:\n                    # Remove the last item (the variable) to prevent multiple errors on 1 line errors\n                    path = parameter_string_path[:-1]\n                    message = 'Found an embedded parameter \"{}\" outside of an \"Fn::Sub\" at {}'.format(\n                        var, '/'.join(map(str, path)))\n                    matches.append(RuleMatch(path, message))\n\n        return matches\n", "path": "src/cfnlint/rules/functions/SubNeeded.py"}]}