1111import datetime
1212import functools
1313import logging
14- import os
15- import shutil
1614import traceback
1715import xml .etree .ElementTree as ET
18- from pathlib import Path
1916from typing import Iterable
2017from typing import List
2118from typing import Mapping
@@ -339,6 +336,88 @@ def from_dict(cls, affected_pkg: dict):
339336 )
340337
341338
339+ @functools .total_ordering
340+ @dataclasses .dataclass (eq = True )
341+ class AffectedPackageV2 :
342+ """
343+ Relate a Package URL with a range of affected versions and fixed versions.
344+ The Package URL must *not* have a version.
345+ AffectedPackage must contain either ``affected_version_range`` or ``fixed_version_range``.
346+ """
347+
348+ package : PackageURL
349+ affected_version_range : Optional [VersionRange ] = None
350+ fixed_version_range : Optional [VersionRange ] = None
351+
352+ def __post_init__ (self ):
353+ if self .package .version :
354+ raise ValueError (f"Affected Package URL { self .package !r} cannot have a version." )
355+
356+ if not (self .affected_version_range or self .fixed_version_range ):
357+ raise ValueError (
358+ f"Affected Package { self .package !r} should have either fixed version range or an "
359+ "affected version range."
360+ )
361+
362+ def __lt__ (self , other ):
363+ if not isinstance (other , AffectedPackage ):
364+ return NotImplemented
365+ return self ._cmp_key () < other ._cmp_key ()
366+
367+ # TODO: Add cache
368+ def _cmp_key (self ):
369+ return (
370+ str (self .package ),
371+ str (self .affected_version_range or "" ),
372+ str (self .fixed_version_range or "" ),
373+ )
374+
375+ def to_dict (self ):
376+ """Return a serializable dict that can be converted back using self.from_dict"""
377+
378+ affected_version_range = (
379+ str (self .affected_version_range ) if self .affected_version_range else None
380+ )
381+ fixed_version_range = str (self .fixed_version_range ) if self .fixed_version_range else None
382+ return {
383+ "package" : purl_to_dict (self .package ),
384+ "affected_version_range" : affected_version_range ,
385+ "fixed_version_range" : fixed_version_range ,
386+ }
387+
388+ @classmethod
389+ def from_dict (cls , affected_pkg : dict ):
390+ """Return an AffectedPackage object from dict generated by self.to_dict"""
391+
392+ package = PackageURL (** affected_pkg ["package" ])
393+ affected_version_range = None
394+ fixed_version_range = None
395+ affected_range = affected_pkg ["affected_version_range" ]
396+ fixed_range = affected_pkg ["fixed_version_range" ]
397+
398+ try :
399+ affected_version_range = VersionRange .from_string (affected_range )
400+ fixed_version_range = VersionRange .from_string (fixed_range )
401+ except :
402+ tb = traceback .format_exc ()
403+ logger .error (
404+ f"Cannot create AffectedPackage with invalid or unknown range: { affected_pkg !r} with error: { tb !r} "
405+ )
406+ return
407+
408+ if not fixed_version_range and not affected_version_range :
409+ logger .error (
410+ f"Cannot create AffectedPackage without fixed or affected range: { affected_pkg !r} "
411+ )
412+ return
413+
414+ return cls (
415+ package = package ,
416+ affected_version_range = affected_version_range ,
417+ fixed_version_range = fixed_version_range ,
418+ )
419+
420+
342421@dataclasses .dataclass (order = True )
343422class AdvisoryData :
344423 """
@@ -355,7 +434,9 @@ class AdvisoryData:
355434 advisory_id : str = ""
356435 aliases : List [str ] = dataclasses .field (default_factory = list )
357436 summary : Optional [str ] = ""
358- affected_packages : List [AffectedPackage ] = dataclasses .field (default_factory = list )
437+ affected_packages : Union [List [AffectedPackage ], List [AffectedPackageV2 ]] = dataclasses .field (
438+ default_factory = list
439+ )
359440 references : List [Reference ] = dataclasses .field (default_factory = list )
360441 references_v2 : List [ReferenceV2 ] = dataclasses .field (default_factory = list )
361442 date_published : Optional [datetime .datetime ] = None
@@ -392,13 +473,19 @@ def to_dict(self):
392473 @classmethod
393474 def from_dict (cls , advisory_data ):
394475 date_published = advisory_data ["date_published" ]
476+ affected_packages = advisory_data ["affected_packages" ]
477+ affected_package_cls = AffectedPackage
478+ if affected_packages :
479+ affected_package_cls = (
480+ AffectedPackageV2
481+ if "fixed_version_range" in affected_packages [0 ]
482+ else AffectedPackage
483+ )
395484 transformed = {
396485 "aliases" : advisory_data ["aliases" ],
397486 "summary" : advisory_data ["summary" ],
398487 "affected_packages" : [
399- AffectedPackage .from_dict (pkg )
400- for pkg in advisory_data ["affected_packages" ]
401- if pkg is not None
488+ affected_package_cls .from_dict (pkg ) for pkg in affected_packages if pkg is not None
402489 ],
403490 "references" : [Reference .from_dict (ref ) for ref in advisory_data ["references" ]],
404491 "date_published" : datetime .datetime .fromisoformat (date_published )
0 commit comments