7
7
import json
8
8
import logging
9
9
import platform
10
+ import re
10
11
from collections .abc import Hashable
11
12
from enum import Enum
12
13
from pathlib import Path
13
14
from typing import Any , TYPE_CHECKING , NoReturn , Callable
14
15
15
- from yaml import MappingNode
16
- from yaml import ScalarNode
17
- from yaml import SequenceNode
16
+ from yaml import MappingNode , ScalarNode , SequenceNode
18
17
from yaml .composer import Composer
19
- from yaml .constructor import ConstructorError
20
- from yaml .constructor import SafeConstructor
18
+ from yaml .constructor import ConstructorError , SafeConstructor
21
19
from yaml .reader import Reader
22
20
from yaml .resolver import Resolver
23
21
from yaml .scanner import Scanner
22
+ from yaml .error import MarkedYAMLError , YAMLError
24
23
from charset_normalizer import from_path
25
24
26
25
from checkov .common .parsers .json .decoder import SimpleDecoder
@@ -98,6 +97,7 @@ def __init__(self, filename: str, content_type: ContentType | None = None) -> No
98
97
NodeConstructor .construct_yaml_null_error ,
99
98
)
100
99
self .filename = filename
100
+ self .files_loaded = {}
101
101
102
102
# To support lazy loading, the original constructors first yield
103
103
# an empty object, then fill them in when iterated. Due to
@@ -142,15 +142,49 @@ def construct_yaml_str(self, node: ScalarNode) -> StrNode:
142
142
assert isinstance (obj , str ) # nosec
143
143
return StrNode (obj , node .start_mark , node .end_mark )
144
144
145
+ def mark_with_filename (self , root : Node | None , filename : str ) -> None :
146
+ if not root :
147
+ return
148
+
149
+ setattr (root , 'filename' , filename ) # noqa: B010
150
+ if isinstance (root , SequenceNode ):
151
+ for v in root .value :
152
+ self .mark_with_filename (v , filename )
153
+ if isinstance (root , MappingNode ):
154
+ for k , v in root .value :
155
+ self .mark_with_filename (k , filename )
156
+ self .mark_with_filename (v , filename )
157
+
145
158
def construct_yaml_seq (self , node : SequenceNode ) -> ListNode :
159
+ # Handle serverless file() expansions on SequenceNode
160
+ if isinstance (node .value , list ) and len (node .value ) > 0 :
161
+ for i , v in enumerate (node .value ):
162
+ if not isinstance (v , ScalarNode ) or not isinstance (node .value [i ].value , str ):
163
+ continue
164
+ m = re .match (r'\$\{file\((.+\.ya?ml)\)\}$' , v .value )
165
+ path = (Path (self .filename ).parent / m [1 ]).resolve () if m else None
166
+
167
+ if m and path in self .files_loaded :
168
+ raise CfnParseError (
169
+ filename = node .filename if hasattr (node , 'filename' ) else self .filename ,
170
+ message = f'Circular include of { m [1 ]} ' ,
171
+ line_number = node .start_mark .line ,
172
+ column_number = node .start_mark .column
173
+ )
174
+ if m :
175
+ self .files_loaded [path ] = True
176
+ content = read_file_with_any_encoding (file_path = path )
177
+ node .value [i ] = MarkedLoader (content , m [1 ], None ).get_single_node ()
178
+ self .mark_with_filename (node .value [i ], m [1 ])
179
+
146
180
obj , = SafeConstructor .construct_yaml_seq (self , node ) # type:ignore[no-untyped-call]
147
181
assert isinstance (obj , list ) # nosec
148
182
return ListNode (obj , node .start_mark , node .end_mark ) # nosec
149
183
150
184
def construct_yaml_null_error (self , node : Node ) -> NoReturn :
151
185
"""Throw a null error"""
152
186
raise CfnParseError (
153
- filename = self .filename ,
187
+ filename = node . filename if hasattr ( node , 'filename' ) else self .filename ,
154
188
message = f"Null value at line { node .start_mark .line + 1 } column { node .start_mark .column + 1 } " ,
155
189
line_number = node .start_mark .line ,
156
190
column_number = node .start_mark .column ,
@@ -179,7 +213,7 @@ def __init__(self, stream: str, filename: str, content_type: ContentType | None
179
213
def construct_mapping (self , node : MappingNode , deep : bool = False ) -> dict [Hashable , Any ]:
180
214
mapping = super (MarkedLoader , self ).construct_mapping (node , deep = deep )
181
215
# Add 1 so line numbering starts at 1
182
- # mapping['__line__ '] = node.start_mark.line + 1
216
+ mapping ['__file__ ' ] = node .filename if hasattr ( node , 'filename' ) else self . filename
183
217
mapping ['__startline__' ] = node .start_mark .line + 1
184
218
mapping ['__endline__' ] = node .end_mark .line + 1
185
219
return mapping
@@ -199,7 +233,7 @@ def multi_constructor(loader: MarkedLoader, tag_suffix: str, node: ScalarNode) -
199
233
constructor = construct_getatt
200
234
elif tag_suffix == "Ref" and (isinstance (node .value , list ) or isinstance (node .value , dict )):
201
235
raise CfnParseError (
202
- filename = "" ,
236
+ filename = node . filename if hasattr ( node , 'filename' ) else loader . filename ,
203
237
message = 'Invalid !Ref: {}' .format (node .value ),
204
238
line_number = 0 ,
205
239
column_number = 0 )
@@ -232,18 +266,29 @@ def loads(yaml_string: str, fname: str, content_type: ContentType | None = None)
232
266
"""
233
267
Load the given YAML string
234
268
"""
269
+ if len (yaml_string ) == 0 :
270
+ return {}
271
+
235
272
loader = MarkedLoader (yaml_string , fname , content_type )
236
273
loader .add_multi_constructor ('!' , multi_constructor ) # type:ignore[no-untyped-call]
237
274
238
- template : "DictNode | dict[str, Any]" = loader .get_single_data ()
239
- # Convert an empty file to an empty dict
240
- if template is None :
241
- template = {}
242
-
243
- return template
244
-
275
+ try :
276
+ return loader .get_single_data ()
277
+ except MarkedYAMLError as e :
278
+ logging .error (f'YAML error parsing { fname } : { e } ' )
279
+ if e .problem and e .problem_mark :
280
+ raise CfnParseError (
281
+ filename = fname ,
282
+ message = e .problem ,
283
+ line_number = e .problem_mark .line ,
284
+ column_number = e .problem_mark .column ) from e
285
+ else :
286
+ raise CfnParseError (filename = fname , message = str (e ), line_number = 0 , column_number = 0 ) from e
287
+ except YAMLError as e :
288
+ logging .error (f'YAML error parsing { fname } : { e } ' )
289
+ raise CfnParseError (filename = fname , message = str (e ), line_number = 0 , column_number = 0 ) from e
245
290
246
- def load (filename : str | Path , content_type : ContentType ) -> tuple [dict [str , Any ], list [tuple [int , str ]]]:
291
+ def load (filename : str | Path , content_type : ContentType | None ) -> tuple [dict [str , Any ], list [tuple [int , str ]]]:
247
292
"""
248
293
Load the given YAML file
249
294
"""
0 commit comments