Skip to content

Commit a5fdd6a

Browse files
committed
Add AffectedPackageV2 for advisory v2
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent b560955 commit a5fdd6a

1 file changed

Lines changed: 94 additions & 7 deletions

File tree

vulnerabilities/importer.py

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@
1111
import datetime
1212
import functools
1313
import logging
14-
import os
15-
import shutil
1614
import traceback
1715
import xml.etree.ElementTree as ET
18-
from pathlib import Path
1916
from typing import Iterable
2017
from typing import List
2118
from 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)
343422
class 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

Comments
 (0)