We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unexpected E1029 error
*cfn-lint version: (0.29.1)*

*Description of issue.*

After this version was released, I started getting an error when linting a template.  This error specific to `BuildSpec` attributes for a `AWS::CodeBuild::Project` project.

    E1029 Found an embedded parameter outside of an "Fn::Sub" at 
    Resources/MyCodeBuild/Properties/Source/BuildSpec
    cloudformation.json:151:11

I mocked up a JSON template that showcases the problem and [attached](https://github.com/aws-cloudformation/cfn-python-lint/files/4383494/cloudformation.txt) it.


--- 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 import six
7 from cfnlint.rules import CloudFormationLintRule
8 from cfnlint.rules import RuleMatch
9 
10 
11 class SubNeeded(CloudFormationLintRule):
12     """Check if a substitution string exists without a substitution function"""
13     id = 'E1029'
14     shortdesc = 'Sub is required if a variable is used in a string'
15     description = 'If a substitution variable exists in a string but isn\'t wrapped with the Fn::Sub function the deployment will fail.'
16     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'
17     tags = ['functions', 'sub']
18 
19     # Free-form text properties to exclude from this rule
20     # content is part of AWS::CloudFormation::Init
21     excludes = ['UserData', 'ZipFile', 'Condition', 'AWS::CloudFormation::Init',
22                 'CloudWatchAlarmDefinition', 'TopicRulePayload']
23     api_excludes = ['Uri', 'Body']
24 
25     # IAM Policy has special variables that don't require !Sub, Check for these
26     # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html
27     # https://docs.aws.amazon.com/iot/latest/developerguide/basic-policy-variables.html
28     # https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html
29     # https://docs.aws.amazon.com/transfer/latest/userguide/users.html#users-policies-scope-down
30     # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html
31     resource_excludes = ['${aws:CurrentTime}', '${aws:EpochTime}',
32                          '${aws:TokenIssueTime}', '${aws:principaltype}',
33                          '${aws:SecureTransport}', '${aws:SourceIp}',
34                          '${aws:UserAgent}', '${aws:userid}',
35                          '${aws:username}', '${ec2:SourceInstanceARN}',
36                          '${iot:Connection.Thing.ThingName}',
37                          '${iot:Connection.Thing.ThingTypeName}',
38                          '${iot:Connection.Thing.IsAttached}',
39                          '${iot:ClientId}', '${transfer:HomeBucket}',
40                          '${transfer:HomeDirectory}', '${transfer:HomeFolder}',
41                          '${transfer:UserName}', '${redshift:DbUser}',
42                          '${cognito-identity.amazonaws.com:aud}',
43                          '${cognito-identity.amazonaws.com:sub}',
44                          '${cognito-identity.amazonaws.com:amr}']
45 
46     # https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-identity-based.html
47     condition_excludes = [
48         '${redshift:DbUser}',
49     ]
50 
51     def __init__(self):
52         """Init"""
53         super(SubNeeded, self).__init__()
54         self.config_definition = {
55             'custom_excludes': {
56                 'default': '',
57                 'type': 'string'
58             }
59         }
60         self.configure()
61         self.subParameterRegex = re.compile(r'(\$\{[A-Za-z0-9_:\.]+\})')
62 
63     def _match_values(self, cfnelem, path):
64         """Recursively search for values matching the searchRegex"""
65         values = []
66         if isinstance(cfnelem, dict):
67             for key in cfnelem:
68                 pathprop = path[:]
69                 pathprop.append(key)
70                 values.extend(self._match_values(cfnelem[key], pathprop))
71         elif isinstance(cfnelem, list):
72             for index, item in enumerate(cfnelem):
73                 pathprop = path[:]
74                 pathprop.append(index)
75                 values.extend(self._match_values(item, pathprop))
76         else:
77             # Leaf node
78             if isinstance(cfnelem, six.string_types):  # and re.match(searchRegex, cfnelem):
79                 for variable in re.findall(self.subParameterRegex, cfnelem):
80                     values.append(path + [variable])
81 
82         return values
83 
84     def match_values(self, cfn):
85         """
86             Search for values in all parts of the templates that match the searchRegex
87         """
88         results = []
89         results.extend(self._match_values(cfn.template, []))
90         # Globals are removed during a transform.  They need to be checked manually
91         results.extend(self._match_values(cfn.template.get('Globals', {}), []))
92         return results
93 
94     def _api_exceptions(self, value):
95         """ Key value exceptions """
96         parameter_search = re.compile(r'^\$\{stageVariables\..*\}$')
97         return re.match(parameter_search, value)
98 
99     def _variable_custom_excluded(self, value):
100         """ User-defined exceptions for variables, anywhere in the file """
101         custom_excludes = self.config['custom_excludes']
102         if custom_excludes:
103             custom_search = re.compile(custom_excludes)
104             return re.match(custom_search, value)
105         return False
106 
107     def match(self, cfn):
108         """Basic Rule Matching"""
109 
110         matches = []
111 
112         # Get a list of paths to every leaf node string containing at least one ${parameter}
113         parameter_string_paths = self.match_values(cfn)
114         # We want to search all of the paths to check if each one contains an 'Fn::Sub'
115         for parameter_string_path in parameter_string_paths:
116             if parameter_string_path[0] in ['Parameters']:
117                 continue
118             # Exclude the special IAM variables
119             variable = parameter_string_path[-1]
120 
121             if 'Resource' in parameter_string_path:
122                 if variable in self.resource_excludes:
123                     continue
124             if 'NotResource' in parameter_string_path:
125                 if variable in self.resource_excludes:
126                     continue
127             if 'Condition' in parameter_string_path:
128                 if variable in self.condition_excludes:
129                     continue
130 
131             # Exclude variables that match custom exclude filters, if configured
132             # (for third-party tools that pre-process templates before uploading them to AWS)
133             if self._variable_custom_excluded(variable):
134                 continue
135 
136             # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)
137             if variable.startswith('${!'):
138                 continue
139 
140             found_sub = False
141             # Does the path contain an 'Fn::Sub'?
142             for step in parameter_string_path:
143                 if step in self.api_excludes:
144                     if self._api_exceptions(parameter_string_path[-1]):
145                         found_sub = True
146                 elif step == 'Fn::Sub' or step in self.excludes:
147                     found_sub = True
148 
149             # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly
150             if not found_sub:
151                 # Remove the last item (the variable) to prevent multiple errors on 1 line errors
152                 path = parameter_string_path[:-1]
153                 message = 'Found an embedded parameter outside of an "Fn::Sub" at {}'.format(
154                     '/'.join(map(str, path)))
155                 matches.append(RuleMatch(path, message))
156 
157         return matches
158
```
--- 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()

```

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
@@ -19,7 +19,7 @@
     # Free-form text properties to exclude from this rule
     # content is part of AWS::CloudFormation::Init
     excludes = ['UserData', 'ZipFile', 'Condition', 'AWS::CloudFormation::Init',
-                'CloudWatchAlarmDefinition', 'TopicRulePayload']
+                'CloudWatchAlarmDefinition', 'TopicRulePayload', 'BuildSpec']
     api_excludes = ['Uri', 'Body']
 
     # IAM Policy has special variables that don't require !Sub, Check for these
@@ -150,8 +150,8 @@
             if not found_sub:
                 # Remove the last item (the variable) to prevent multiple errors on 1 line errors
                 path = parameter_string_path[:-1]
-                message = 'Found an embedded parameter outside of an "Fn::Sub" at {}'.format(
-                    '/'.join(map(str, path)))
+                message = 'Found an embedded parameter "{}" outside of an "Fn::Sub" at {}'.format(
+                    variable, '/'.join(map(str, path)))
                 matches.append(RuleMatch(path, message))
 
         return matches

{"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@@ -19,7 +19,7 @@\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+                'CloudWatchAlarmDefinition', 'TopicRulePayload', 'BuildSpec']\n     api_excludes = ['Uri', 'Body']\n \n     # IAM Policy has special variables that don't require !Sub, Check for these\n@@ -150,8 +150,8 @@\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+                message = 'Found an embedded parameter \"{}\" outside of an \"Fn::Sub\" at {}'.format(\n+                    variable, '/'.join(map(str, path)))\n                 matches.append(RuleMatch(path, message))\n \n         return matches\n", "issue": "Unexpected E1029 error\n*cfn-lint version: (0.29.1)*\r\n\r\n*Description of issue.*\r\n\r\nAfter this version was released, I started getting an error when linting a template.  This error specific to `BuildSpec` attributes for a `AWS::CodeBuild::Project` project.\r\n\r\n    E1029 Found an embedded parameter outside of an \"Fn::Sub\" at \r\n    Resources/MyCodeBuild/Properties/Source/BuildSpec\r\n    cloudformation.json:151:11\r\n\r\nI mocked up a JSON template that showcases the problem and [attached](https://github.com/aws-cloudformation/cfn-python-lint/files/4383494/cloudformation.txt) it.\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\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    # 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 __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        \"\"\"Basic Rule Matching\"\"\"\n\n        matches = []\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            if parameter_string_path[0] in ['Parameters']:\n                continue\n            # Exclude 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 'NotResource' 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 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(variable):\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\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    # 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', 'BuildSpec']\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 __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        \"\"\"Basic Rule Matching\"\"\"\n\n        matches = []\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            if parameter_string_path[0] in ['Parameters']:\n                continue\n            # Exclude 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 'NotResource' 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 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(variable):\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                    variable, '/'.join(map(str, path)))\n                matches.append(RuleMatch(path, message))\n\n        return matches\n", "path": "src/cfnlint/rules/functions/SubNeeded.py"}]}