14
14
# Copyright (c) OWASP Foundation. All Rights Reserved.
15
15
16
16
17
- from json import loads as json_loads
18
- from typing import Any , Dict , Iterable , List , Optional , Type , Union
17
+ from typing import TYPE_CHECKING , Any , Dict , Iterable , List , Optional , Tuple , Type , Union
19
18
from warnings import warn
20
19
from xml .etree .ElementTree import Element # nosec B405
21
20
22
21
import serializable
23
- from serializable import ObjectMetadataLibrary , ViewType
24
22
from serializable .helpers import BaseHelper
25
23
from sortedcontainers import SortedSet
26
24
27
25
from .._internal .compare import ComparableTuple as _ComparableTuple
28
- from ..exception .model import MutuallyExclusivePropertiesException
29
- from ..model import ExternalReference , HashType , _HashTypeRepositorySerializationHelper
30
- from ..model .component import Component
31
- from ..model .service import Service
26
+ from ..schema import SchemaVersion
32
27
from ..schema .schema import SchemaVersion1Dot4 , SchemaVersion1Dot5 , SchemaVersion1Dot6
28
+ from . import ExternalReference , HashType , _HashTypeRepositorySerializationHelper
29
+ from .component import Component
30
+ from .service import Service
31
+
32
+ if TYPE_CHECKING : # pragma: no cover
33
+ from serializable import ObjectMetadataLibrary , ViewType
33
34
34
35
35
36
@serializable .serializable_class
@@ -164,6 +165,25 @@ def __hash__(self) -> int:
164
165
def __repr__ (self ) -> str :
165
166
return f'<Tool name={ self .name } , version={ self .version } , vendor={ self .vendor } >'
166
167
168
+ @classmethod
169
+ def from_component (cls : Type ['Tool' ], component : 'Component' ) -> 'Tool' :
170
+ return cls (
171
+ vendor = component .group ,
172
+ name = component .name ,
173
+ version = component .version ,
174
+ hashes = component .hashes ,
175
+ external_references = component .external_references ,
176
+ )
177
+
178
+ @classmethod
179
+ def from_service (cls : Type ['Tool' ], service : 'Service' ) -> 'Tool' :
180
+ return cls (
181
+ vendor = service .group ,
182
+ name = service .name ,
183
+ version = service .version ,
184
+ external_references = service .external_references ,
185
+ )
186
+
167
187
168
188
class ToolsRepository :
169
189
"""
@@ -180,7 +200,6 @@ def __init__(
180
200
if tools :
181
201
warn ('Using Tool is deprecated as of CycloneDX v1.5. Components and Services should be used now. '
182
202
'See https://cyclonedx.org/docs/1.5/' , DeprecationWarning )
183
-
184
203
self ._components = SortedSet (components or [])
185
204
self ._services = SortedSet (services or [])
186
205
self ._tools = SortedSet (tools or [])
@@ -223,7 +242,9 @@ def __len__(self) -> int:
223
242
+ len (self ._services )
224
243
225
244
def __bool__ (self ) -> bool :
226
- return any ((self ._tools , self ._components , self ._services ))
245
+ return len (self ._tools ) > 0 \
246
+ or len (self ._components ) > 0 \
247
+ or len (self ._services ) > 0
227
248
228
249
def __eq__ (self , other : object ) -> bool :
229
250
if not isinstance (other , ToolsRepository ):
@@ -238,77 +259,38 @@ def __hash__(self) -> int:
238
259
239
260
240
261
class ToolsRepositoryHelper (BaseHelper ):
241
- """
242
- Helps with serializing and deserializing ToolsRepository objects.
243
- """
244
262
245
- @classmethod
246
- def convert_new_to_old (cls , components : Iterable [Component ], services : Iterable [Service ]) -> 'SortedSet[Tool]' :
247
- """
248
- "Down converts" Component and Service objects to Tools so they can be rendered by
249
- the library for schemas less than version 1.5.
250
-
251
- Returns:
252
- A sorted set of Tools
253
- """
254
- tools_to_render : 'SortedSet[Tool]' = SortedSet ()
255
-
256
- for c in components :
257
- tools_to_render .add (Tool (
258
- name = c .name ,
259
- vendor = c .group ,
260
- version = c .version ,
261
- hashes = c .hashes ,
262
- external_references = c .external_references ,
263
- ))
264
-
265
- for s in services :
266
- if s .provider :
267
- vendor = s .provider .name
268
- else :
269
- vendor = None
270
- tools_to_render .add (Tool (
271
- name = s .name ,
272
- vendor = vendor ,
273
- version = s .version ,
274
- external_references = s .external_references ,
275
- ))
263
+ @staticmethod
264
+ def __all_as_tools (o : ToolsRepository ) -> Tuple [Tool , ...]:
265
+ return (
266
+ * o .tools ,
267
+ * map (Tool .from_component , o .components ),
268
+ * map (Tool .from_service , o .services ),
269
+ )
276
270
277
- return tools_to_render
271
+ @staticmethod
272
+ def __supports_components_and_services (view : Any ) -> bool :
273
+ try :
274
+ return view is not None and view ().schema_version_enum >= SchemaVersion .V1_5
275
+ except Exception :
276
+ return False
278
277
279
278
@classmethod
280
279
def json_normalize (cls , o : ToolsRepository , * ,
281
- view : Optional [Type [ViewType ]],
280
+ view : Optional [Type [' ViewType' ]],
282
281
** __ : Any ) -> Any :
283
- if not any ([ o . tools , o . components , o . services ]) :
282
+ if not o :
284
283
return None
285
-
286
- result = {}
287
-
288
- if (view ().schema_version_enum >= SchemaVersion1Dot5 ().schema_version_enum # type: ignore[union-attr, misc]
289
- and not o .tools ):
290
- if o .components :
291
- result ['components' ] = [json_loads (Component .as_json (c , view_ = view )) # type: ignore[attr-defined]
292
- for c in o .components ]
293
-
294
- if o .services :
295
- result ['services' ] = [json_loads (Service .as_json (s , view_ = view )) # type: ignore[attr-defined]
296
- for s in o .services ]
297
-
298
- if result :
299
- return result
300
-
301
- tools_to_render : 'SortedSet[Tool]' = SortedSet (o .tools )
302
- # We "down-convert" Components and Services to Tools so we can render to older schemas
303
- # or when there are existing Tool objects
304
- tools_to_render .update (cls .convert_new_to_old (o .components , o .services ))
305
-
306
- return [json_loads (Tool .as_json (t , view_ = view )) for t in tools_to_render ] # type: ignore[attr-defined]
284
+ return cls .__all_as_tools (o ) \
285
+ if len (o .tools ) > 0 or not cls .__supports_components_and_services (view ) \
286
+ else {
287
+ 'components' : tuple (o .components ) if len (o .components ) > 0 else None ,
288
+ 'services' : tuple (o .services ) if len (o .services ) > 0 else None ,
289
+ }
307
290
308
291
@classmethod
309
292
def json_denormalize (cls , o : Union [List [Dict [str , Any ]], Dict [str , Any ]],
310
293
** __ : Any ) -> ToolsRepository :
311
-
312
294
components = []
313
295
services = []
314
296
tools = []
@@ -331,58 +313,39 @@ def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
331
313
@classmethod
332
314
def xml_normalize (cls , o : ToolsRepository , * ,
333
315
element_name : str ,
334
- view : Optional [Type [ViewType ]],
316
+ view : Optional [Type [' ViewType' ]],
335
317
xmlns : Optional [str ],
336
318
** __ : Any ) -> Optional [Element ]:
337
- if not any ([ o . tools , o . components , o . services ]): # pylint: disable=protected-access
319
+ if not o :
338
320
return None
339
-
340
321
elem = Element (element_name )
341
-
342
- if (view ().schema_version_enum >= SchemaVersion1Dot5 ().schema_version_enum # type: ignore[union-attr, misc]
343
- and not o .tools ):
322
+ if len (o .tools ) > 0 or not cls .__supports_components_and_services (view ):
323
+ elem .extend (
324
+ ti .as_xml ( # type:ignore[attr-defined]
325
+ view_ = view , as_string = False , element_name = 'tool' , xmlns = xmlns )
326
+ for ti in cls .__all_as_tools (o )
327
+ )
328
+ else :
344
329
if o .components :
345
- c_elem = Element ('{' + xmlns + '}' + 'components' ) # type: ignore[operator]
346
-
347
- c_elem .extend (
348
- c .as_xml ( # type: ignore[attr-defined]
330
+ elem_c = Element (f'{{{ xmlns } }}components' if xmlns else 'components' )
331
+ elem_c .extend (
332
+ ci .as_xml ( # type:ignore[attr-defined]
349
333
view_ = view , as_string = False , element_name = 'component' , xmlns = xmlns )
350
- for c in o .components
351
- )
352
-
353
- elem .append (c_elem )
354
-
334
+ for ci in o .components )
335
+ elem .append (elem_c )
355
336
if o .services :
356
- s_elem = Element ('{' + xmlns + '}' + 'services' ) # type: ignore[operator]
357
-
358
- s_elem .extend (
359
- s .as_xml ( # type: ignore[attr-defined]
337
+ elem_s = Element (f'{{{ xmlns } }}services' if xmlns else 'services' )
338
+ elem_s .extend (
339
+ si .as_xml ( # type:ignore[attr-defined]
360
340
view_ = view , as_string = False , element_name = 'service' , xmlns = xmlns )
361
- for s in o .services
362
- )
363
-
364
- elem .append (s_elem )
365
-
366
- if len (elem ) > 0 :
367
- return elem
368
-
369
- tools_to_render : 'SortedSet[Tool]' = SortedSet (o .tools )
370
- # We "down-convert" Components and Services to Tools so we can render to older schemas
371
- # or when there are existing Tool objects
372
- tools_to_render .update (cls .convert_new_to_old (o .components , o .services ))
373
-
374
- elem .extend (
375
- t .as_xml ( # type: ignore[attr-defined]
376
- view_ = view , as_string = False , element_name = 'tool' , xmlns = xmlns )
377
- for t in tools_to_render
378
- )
379
-
341
+ for si in o .services )
342
+ elem .append (elem_s )
380
343
return elem
381
344
382
345
@classmethod
383
346
def xml_denormalize (cls , o : Element , * ,
384
347
default_ns : Optional [str ],
385
- prop_info : ObjectMetadataLibrary .SerializableProperty ,
348
+ prop_info : ' ObjectMetadataLibrary.SerializableProperty' ,
386
349
ctx : Type [Any ],
387
350
** kwargs : Any ) -> ToolsRepository :
388
351
tools : List [Tool ] = []
0 commit comments