prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  False positive: Sub is required if a variable is used in a string in parameter descriptions
  *cfn-lint version: 0.26.0*
  
  *Description of issue.*
  Parameter descriptions fail E1029 if they contain text which looks like variable substitution:
  
  e.g.
  
  ```yaml
    MyContentBucket:
      Description: "Bucket name for content (usually ${VPCName}-my-content), use 'none' to disable creation"
      Type: String
  ```
  
  Gives an error:
  
      [E1029: Sub is required if a variable is used in a string] (Found an embedded parameter outside of an "Fn::Sub" at Parameters/MyContentBucket/Description)
  
  
  --- 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 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  3 SPDX-License-Identifier: MIT-0
  4 """
  5 import re
  6 from cfnlint.rules import CloudFormationLintRule
  7 from cfnlint.rules import RuleMatch
  8 
  9 
  10 class SubNeeded(CloudFormationLintRule):
  11     """Check if a substitution string exists without a substitution function"""
  12     id = 'E1029'
  13     shortdesc = 'Sub is required if a variable is used in a string'
  14     description = 'If a substitution variable exists in a string but isn\'t wrapped with the Fn::Sub function the deployment will fail.'
  15     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'
  16     tags = ['functions', 'sub']
  17 
  18     # Free-form text properties to exclude from this rule
  19     # content is part of AWS::CloudFormation::Init
  20     excludes = ['UserData', 'ZipFile', 'Condition', 'AWS::CloudFormation::Init',
  21                 'CloudWatchAlarmDefinition', 'TopicRulePayload']
  22     api_excludes = ['Uri', 'Body']
  23 
  24     # IAM Policy has special variables that don't require !Sub, Check for these
  25     # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html
  26     # https://docs.aws.amazon.com/iot/latest/developerguide/basic-policy-variables.html
  27     # https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html
  28     # https://docs.aws.amazon.com/transfer/latest/userguide/users.html#users-policies-scope-down
  29     # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html
  30     resource_excludes = ['${aws:CurrentTime}', '${aws:EpochTime}',
  31                          '${aws:TokenIssueTime}', '${aws:principaltype}',
  32                          '${aws:SecureTransport}', '${aws:SourceIp}',
  33                          '${aws:UserAgent}', '${aws:userid}',
  34                          '${aws:username}', '${ec2:SourceInstanceARN}',
  35                          '${iot:Connection.Thing.ThingName}',
  36                          '${iot:Connection.Thing.ThingTypeName}',
  37                          '${iot:Connection.Thing.IsAttached}',
  38                          '${iot:ClientId}', '${transfer:HomeBucket}',
  39                          '${transfer:HomeDirectory}', '${transfer:HomeFolder}',
  40                          '${transfer:UserName}', '${redshift:DbUser}',
  41                          '${cognito-identity.amazonaws.com:aud}',
  42                          '${cognito-identity.amazonaws.com:sub}',
  43                          '${cognito-identity.amazonaws.com:amr}']
  44 
  45     # https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-identity-based.html
  46     condition_excludes = [
  47         '${redshift:DbUser}',
  48     ]
  49 
  50     def _match_values(self, searchRegex, cfnelem, path):
  51         """Recursively search for values matching the searchRegex"""
  52         values = []
  53         if isinstance(cfnelem, dict):
  54             for key in cfnelem:
  55                 pathprop = path[:]
  56                 pathprop.append(key)
  57                 values.extend(self._match_values(searchRegex, cfnelem[key], pathprop))
  58         elif isinstance(cfnelem, list):
  59             for index, item in enumerate(cfnelem):
  60                 pathprop = path[:]
  61                 pathprop.append(index)
  62                 values.extend(self._match_values(searchRegex, item, pathprop))
  63         else:
  64             # Leaf node
  65             if isinstance(cfnelem, str) and re.match(searchRegex, cfnelem):
  66                 # Get all variables as seperate paths
  67                 regex = re.compile(r'(\$\{.*?\.?.*?})')
  68                 for variable in re.findall(regex, cfnelem):
  69                     values.append(path + [variable])
  70 
  71         return values
  72 
  73     def match_values(self, searchRegex, cfn):
  74         """
  75             Search for values in all parts of the templates that match the searchRegex
  76         """
  77         results = []
  78         results.extend(self._match_values(searchRegex, cfn.template, []))
  79         # Globals are removed during a transform.  They need to be checked manually
  80         results.extend(self._match_values(searchRegex, cfn.template.get('Globals', {}), []))
  81         return results
  82 
  83     def _api_exceptions(self, value):
  84         """ Key value exceptions """
  85         parameter_search = re.compile(r'^\$\{stageVariables\..*\}$')
  86         return re.match(parameter_search, value)
  87 
  88     def match(self, cfn):
  89         """Basic Rule Matching"""
  90 
  91         matches = []
  92 
  93         # Generic regex to match a string containing at least one ${parameter}
  94         parameter_search = re.compile(r'^.*(\$\{.*\}.*(\$\{.*\}.*)*)$')
  95 
  96         # Get a list of paths to every leaf node string containing at least one ${parameter}
  97         parameter_string_paths = self.match_values(parameter_search, cfn)
  98 
  99         # We want to search all of the paths to check if each one contains an 'Fn::Sub'
  100         for parameter_string_path in parameter_string_paths:
  101             # Exxclude the special IAM variables
  102             variable = parameter_string_path[-1]
  103 
  104             if 'Resource' in parameter_string_path:
  105                 if variable in self.resource_excludes:
  106                     continue
  107             if 'Condition' in parameter_string_path:
  108                 if variable in self.condition_excludes:
  109                     continue
  110 
  111             # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)
  112             if variable.startswith('${!'):
  113                 continue
  114 
  115             found_sub = False
  116             # Does the path contain an 'Fn::Sub'?
  117             for step in parameter_string_path:
  118                 if step in self.api_excludes:
  119                     if self._api_exceptions(parameter_string_path[-1]):
  120                         found_sub = True
  121                 elif step == 'Fn::Sub' or step in self.excludes:
  122                     found_sub = True
  123 
  124             # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly
  125             if not found_sub:
  126                 # Remove the last item (the variable) to prevent multiple errors on 1 line errors
  127                 path = parameter_string_path[:-1]
  128                 message = 'Found an embedded parameter outside of an "Fn::Sub" at {}'.format(
  129                     '/'.join(map(str, path)))
  130                 matches.append(RuleMatch(path, message))
  131 
  132         return matches
  133
  ```
  --- 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
  @@ -98,6 +98,8 @@
   
           # We want to search all of the paths to check if each one contains an 'Fn::Sub'
           for parameter_string_path in parameter_string_paths:
  +            if parameter_string_path[0] in ['Parameters']:
  +                continue
               # Exxclude the special IAM variables
               variable = 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@@ -98,6 +98,8 @@\n \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+            if parameter_string_path[0] in ['Parameters']:\n+                continue\n             # Exxclude the special IAM variables\n             variable = parameter_string_path[-1]\n", "issue": "False positive: Sub is required if a variable is used in a string in parameter descriptions\n*cfn-lint version: 0.26.0*\r\n\r\n*Description of issue.*\r\nParameter descriptions fail E1029 if they contain text which looks like variable substitution:\r\n\r\ne.g.\r\n\r\n```yaml\r\n  MyContentBucket:\r\n    Description: \"Bucket name for content (usually ${VPCName}-my-content), use 'none' to disable creation\"\r\n    Type: String\r\n```\r\n\r\nGives an error:\r\n\r\n    [E1029: Sub is required if a variable is used in a string] (Found an embedded parameter outside of an \"Fn::Sub\" at Parameters/MyContentBucket/Description)\r\n\n", "before_files": [{"content": "\"\"\"\nCopyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: MIT-0\n\"\"\"\nimport re\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    # Free-form text properties to exclude from this rule\n    # content is part of AWS::CloudFormation::Init\n    excludes = ['UserData', 'ZipFile', 'Condition', 'AWS::CloudFormation::Init',\n                'CloudWatchAlarmDefinition', 'TopicRulePayload']\n    api_excludes = ['Uri', 'Body']\n\n    # IAM Policy has special variables that don't require !Sub, Check for these\n    # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html\n    # https://docs.aws.amazon.com/iot/latest/developerguide/basic-policy-variables.html\n    # https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html\n    # https://docs.aws.amazon.com/transfer/latest/userguide/users.html#users-policies-scope-down\n    # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html\n    resource_excludes = ['${aws:CurrentTime}', '${aws:EpochTime}',\n                         '${aws:TokenIssueTime}', '${aws:principaltype}',\n                         '${aws:SecureTransport}', '${aws:SourceIp}',\n                         '${aws:UserAgent}', '${aws:userid}',\n                         '${aws:username}', '${ec2:SourceInstanceARN}',\n                         '${iot:Connection.Thing.ThingName}',\n                         '${iot:Connection.Thing.ThingTypeName}',\n                         '${iot:Connection.Thing.IsAttached}',\n                         '${iot:ClientId}', '${transfer:HomeBucket}',\n                         '${transfer:HomeDirectory}', '${transfer:HomeFolder}',\n                         '${transfer:UserName}', '${redshift:DbUser}',\n                         '${cognito-identity.amazonaws.com:aud}',\n                         '${cognito-identity.amazonaws.com:sub}',\n                         '${cognito-identity.amazonaws.com:amr}']\n\n    # https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-identity-based.html\n    condition_excludes = [\n        '${redshift:DbUser}',\n    ]\n\n    def _match_values(self, searchRegex, 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(searchRegex, 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(searchRegex, item, pathprop))\n        else:\n            # Leaf node\n            if isinstance(cfnelem, str) and re.match(searchRegex, cfnelem):\n                # Get all variables as seperate paths\n                regex = re.compile(r'(\\$\\{.*?\\.?.*?})')\n                for variable in re.findall(regex, cfnelem):\n                    values.append(path + [variable])\n\n        return values\n\n    def match_values(self, searchRegex, 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(searchRegex, cfn.template, []))\n        # Globals are removed during a transform.  They need to be checked manually\n        results.extend(self._match_values(searchRegex, 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 match(self, cfn):\n        \"\"\"Basic Rule Matching\"\"\"\n\n        matches = []\n\n        # Generic regex to match a string containing at least one ${parameter}\n        parameter_search = re.compile(r'^.*(\\$\\{.*\\}.*(\\$\\{.*\\}.*)*)$')\n\n        # Get a list of paths to every leaf node string containing at least one ${parameter}\n        parameter_string_paths = self.match_values(parameter_search, cfn)\n\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            # Exxclude the special IAM variables\n            variable = parameter_string_path[-1]\n\n            if 'Resource' in parameter_string_path:\n                if variable in self.resource_excludes:\n                    continue\n            if 'Condition' in parameter_string_path:\n                if variable in self.condition_excludes:\n                    continue\n\n            # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)\n            if variable.startswith('${!'):\n                continue\n\n            found_sub = False\n            # Does the path contain an 'Fn::Sub'?\n            for step in parameter_string_path:\n                if step in self.api_excludes:\n                    if self._api_exceptions(parameter_string_path[-1]):\n                        found_sub = True\n                elif step == 'Fn::Sub' or step in self.excludes:\n                    found_sub = True\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 found_sub:\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                    '/'.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 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: MIT-0\n\"\"\"\nimport re\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    # Free-form text properties to exclude from this rule\n    # content is part of AWS::CloudFormation::Init\n    excludes = ['UserData', 'ZipFile', 'Condition', 'AWS::CloudFormation::Init',\n                'CloudWatchAlarmDefinition', 'TopicRulePayload']\n    api_excludes = ['Uri', 'Body']\n\n    # IAM Policy has special variables that don't require !Sub, Check for these\n    # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html\n    # https://docs.aws.amazon.com/iot/latest/developerguide/basic-policy-variables.html\n    # https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html\n    # https://docs.aws.amazon.com/transfer/latest/userguide/users.html#users-policies-scope-down\n    # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html\n    resource_excludes = ['${aws:CurrentTime}', '${aws:EpochTime}',\n                         '${aws:TokenIssueTime}', '${aws:principaltype}',\n                         '${aws:SecureTransport}', '${aws:SourceIp}',\n                         '${aws:UserAgent}', '${aws:userid}',\n                         '${aws:username}', '${ec2:SourceInstanceARN}',\n                         '${iot:Connection.Thing.ThingName}',\n                         '${iot:Connection.Thing.ThingTypeName}',\n                         '${iot:Connection.Thing.IsAttached}',\n                         '${iot:ClientId}', '${transfer:HomeBucket}',\n                         '${transfer:HomeDirectory}', '${transfer:HomeFolder}',\n                         '${transfer:UserName}', '${redshift:DbUser}',\n                         '${cognito-identity.amazonaws.com:aud}',\n                         '${cognito-identity.amazonaws.com:sub}',\n                         '${cognito-identity.amazonaws.com:amr}']\n\n    # https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-identity-based.html\n    condition_excludes = [\n        '${redshift:DbUser}',\n    ]\n\n    def _match_values(self, searchRegex, 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(searchRegex, 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(searchRegex, item, pathprop))\n        else:\n            # Leaf node\n            if isinstance(cfnelem, str) and re.match(searchRegex, cfnelem):\n                # Get all variables as seperate paths\n                regex = re.compile(r'(\\$\\{.*?\\.?.*?})')\n                for variable in re.findall(regex, cfnelem):\n                    values.append(path + [variable])\n\n        return values\n\n    def match_values(self, searchRegex, 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(searchRegex, cfn.template, []))\n        # Globals are removed during a transform.  They need to be checked manually\n        results.extend(self._match_values(searchRegex, 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 match(self, cfn):\n        \"\"\"Basic Rule Matching\"\"\"\n\n        matches = []\n\n        # Generic regex to match a string containing at least one ${parameter}\n        parameter_search = re.compile(r'^.*(\\$\\{.*\\}.*(\\$\\{.*\\}.*)*)$')\n\n        # Get a list of paths to every leaf node string containing at least one ${parameter}\n        parameter_string_paths = self.match_values(parameter_search, cfn)\n\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            if parameter_string_path[0] in ['Parameters']:\n                continue\n            # Exxclude the special IAM variables\n            variable = parameter_string_path[-1]\n\n            if 'Resource' in parameter_string_path:\n                if variable in self.resource_excludes:\n                    continue\n            if 'Condition' in parameter_string_path:\n                if variable in self.condition_excludes:\n                    continue\n\n            # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)\n            if variable.startswith('${!'):\n                continue\n\n            found_sub = False\n            # Does the path contain an 'Fn::Sub'?\n            for step in parameter_string_path:\n                if step in self.api_excludes:\n                    if self._api_exceptions(parameter_string_path[-1]):\n                        found_sub = True\n                elif step == 'Fn::Sub' or step in self.excludes:\n                    found_sub = True\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 found_sub:\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                    '/'.join(map(str, path)))\n                matches.append(RuleMatch(path, message))\n\n        return matches\n", "path": "src/cfnlint/rules/functions/SubNeeded.py"}]}