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
- from typing import Any , TYPE_CHECKING , NoReturn , Callable
14
+ from typing import Any , TYPE_CHECKING , NoReturn , Callable , cast
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 : dict [Path , bool ] = {}
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,52 @@ 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
+
165
+ m = re .match (r'\$\{file\((.+\.ya?ml)\)\}$' , v .value )
166
+ if m is None :
167
+ continue
168
+
169
+ path = (Path (self .filename ).parent / m [1 ]).resolve ()
170
+ if path in self .files_loaded :
171
+ raise CfnParseError (
172
+ filename = node .filename if hasattr (node , 'filename' ) else self .filename ,
173
+ message = f'Circular include of { m [1 ]} ' ,
174
+ line_number = node .start_mark .line ,
175
+ column_number = node .start_mark .column
176
+ )
177
+ else :
178
+ self .files_loaded [path ] = True
179
+ content = read_file_with_any_encoding (file_path = path )
180
+ node .value [i ] = MarkedLoader (content , m [1 ], None ).get_single_node ()
181
+ self .mark_with_filename (node .value [i ], m [1 ])
182
+
146
183
obj , = SafeConstructor .construct_yaml_seq (self , node ) # type:ignore[no-untyped-call]
147
184
assert isinstance (obj , list ) # nosec
148
185
return ListNode (obj , node .start_mark , node .end_mark ) # nosec
149
186
150
187
def construct_yaml_null_error (self , node : Node ) -> NoReturn :
151
188
"""Throw a null error"""
152
189
raise CfnParseError (
153
- filename = self .filename ,
190
+ filename = node . filename if hasattr ( node , 'filename' ) else self .filename ,
154
191
message = f"Null value at line { node .start_mark .line + 1 } column { node .start_mark .column + 1 } " ,
155
192
line_number = node .start_mark .line ,
156
193
column_number = node .start_mark .column ,
@@ -179,7 +216,7 @@ def __init__(self, stream: str, filename: str, content_type: ContentType | None
179
216
def construct_mapping (self , node : MappingNode , deep : bool = False ) -> dict [Hashable , Any ]:
180
217
mapping = super (MarkedLoader , self ).construct_mapping (node , deep = deep )
181
218
# Add 1 so line numbering starts at 1
182
- # mapping['__line__ '] = node.start_mark.line + 1
219
+ mapping ['__file__ ' ] = node .filename if hasattr ( node , 'filename' ) else self . filename
183
220
mapping ['__startline__' ] = node .start_mark .line + 1
184
221
mapping ['__endline__' ] = node .end_mark .line + 1
185
222
return mapping
@@ -199,7 +236,7 @@ def multi_constructor(loader: MarkedLoader, tag_suffix: str, node: ScalarNode) -
199
236
constructor = construct_getatt
200
237
elif tag_suffix == "Ref" and (isinstance (node .value , list ) or isinstance (node .value , dict )):
201
238
raise CfnParseError (
202
- filename = "" ,
239
+ filename = node . filename if hasattr ( node , 'filename' ) else loader . filename ,
203
240
message = 'Invalid !Ref: {}' .format (node .value ),
204
241
line_number = 0 ,
205
242
column_number = 0 )
@@ -232,18 +269,30 @@ def loads(yaml_string: str, fname: str, content_type: ContentType | None = None)
232
269
"""
233
270
Load the given YAML string
234
271
"""
272
+ if len (yaml_string ) == 0 :
273
+ return {}
274
+
235
275
loader = MarkedLoader (yaml_string , fname , content_type )
236
276
loader .add_multi_constructor ('!' , multi_constructor ) # type:ignore[no-untyped-call]
237
277
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
278
+ try :
279
+ return cast (DictNode | dict [str , Any ], loader .get_single_data ())
280
+ except MarkedYAMLError as e :
281
+ logging .error (f'YAML error parsing { fname } : { e } ' )
282
+ if e .problem and e .problem_mark :
283
+ raise CfnParseError (
284
+ filename = fname ,
285
+ message = e .problem ,
286
+ line_number = e .problem_mark .line ,
287
+ column_number = e .problem_mark .column ) from e
288
+ else :
289
+ raise CfnParseError (filename = fname , message = str (e ), line_number = 0 , column_number = 0 ) from e
290
+ except YAMLError as e :
291
+ logging .error (f'YAML error parsing { fname } : { e } ' )
292
+ raise CfnParseError (filename = fname , message = str (e ), line_number = 0 , column_number = 0 ) from e
244
293
245
294
246
- def load (filename : str | Path , content_type : ContentType ) -> tuple [dict [str , Any ], list [tuple [int , str ]]]:
295
+ def load (filename : str | Path , content_type : ContentType | None ) -> tuple [dict [str , Any ], list [tuple [int , str ]]]:
247
296
"""
248
297
Load the given YAML file
249
298
"""
0 commit comments