Skip to content

Commit 6b706c0

Browse files
committed
Use new Advisory model for importers
Importing Advisory data is now 2 step process. 1) Import Advisory 2) Create relations (default improver) importers are now required to return AdvisoryData instead of Advisory. The data contained inside AdvisoryData is converted into a model named Advisory and then saved into the database. This will be useful later for improvers to work on existing Advisories. All the relationships for the Advisories are generated using a default improver at improvers/default.py. NOTE: There are errors in the UI due to models.py # TODO: Cannot resolve keyword 'resolved_vulnerabilities' into field # make vulnerabilities and resolved_vulnerabilities use the `fix` flag of PackageRelatedVulnerability Knows defects: - UI break (see above) - inconsistent naming - will be resolved in a different PR - might crash in multiple imports / improves - No improver than default improver is implemented yet - normalized function of ``AdvisoryData`` has no body - nginx importer still has remains of set_api etc - ??? Signed-off-by: Hritik Vijay <hritikxx8@gmail.com>
1 parent 925d0ef commit 6b706c0

9 files changed

Lines changed: 330 additions & 233 deletions

File tree

vulnerabilities/data_inference.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
import dataclasses
22
import logging
3-
from vulnerabilities.data_source import Advisory
3+
from typing import List
4+
5+
from vulnerabilities.data_source import AdvisoryData
46

57
logger = logging.getLogger(__name__)
68

9+
710
class OverConfidenceError(ValueError):
811
pass
912

13+
1014
class UnderConfidenceError(ValueError):
1115
pass
1216

17+
1318
MAX_CONFIDENCE = 100
1419

20+
1521
@dataclasses.dataclass(order=True)
1622
class Inference:
1723
"""
1824
This data class expresses the contract between data improvers and the improve runner.
25+
26+
Source and confidence correspond to the improver, only inferences with highest confidence
27+
for one vulnerability <-> package relationship is to be inserted into the database
1928
"""
20-
advisory: Advisory
29+
30+
advisory_data: AdvisoryData
2131
source: str
2232
confidence: int
2333

@@ -28,10 +38,12 @@ def __post_init__(self):
2838
if self.confidence < 0:
2939
raise UnderConfidenceError
3040

41+
3142
class Improver:
3243
"""
33-
All improvers should inherit this class and implement updated_inferences method to return
44+
All improvers should inherit this class and implement inferences method to return
3445
new inferences for a package or vulnerability
3546
"""
36-
def updated_inferences(self):
47+
48+
def inferences(self):
3749
raise NotImplementedError

vulnerabilities/data_source.py

Lines changed: 135 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

2323
import dataclasses
24+
import json
2425
import logging
2526
import os
2627
import shutil
@@ -47,7 +48,6 @@
4748
from vulnerabilities.severity_systems import ScoringSystem
4849
from vulnerabilities.helpers import is_cve
4950
from vulnerabilities.helpers import nearest_patched_package
50-
from vulnerabilities.helpers import AffectedPackage
5151

5252
logger = logging.getLogger(__name__)
5353

@@ -74,8 +74,36 @@ def normalized(self):
7474
return Reference(reference_id=self.reference_id, url=self.url, severities=severities)
7575

7676

77+
@dataclasses.dataclass(order=True, frozen=True)
78+
class AffectedPackage:
79+
# this package MUST NOT have a version
80+
package: PackageURL
81+
# the version specifier contains the version scheme as is: semver:>=1,3,4
82+
version_specifier: VersionSpecifier
83+
84+
def toJson(self):
85+
# TODO: VersionSpecifier.__str__ is not working
86+
# https://github.com/nexB/univers/issues/7
87+
# Adjust following code when it is fixed
88+
scheme = self.version_specifier.scheme
89+
ranges = ",".join(
90+
[f"{rng.operator}{rng.version.version_string}" for rng in self.version_specifier.ranges]
91+
)
92+
return json.dumps({"package": self.package, "version_specifier": f"{scheme}:{ranges}"})
93+
94+
@staticmethod
95+
def fromJson(affected_package_json):
96+
obj = json.loads(affected_package_json)
97+
affected_package = AffectedPackage(**obj)
98+
package = PackageURL(*affected_package.package)
99+
version_specifier = VersionSpecifier.from_version_spec_string(
100+
affected_package.version_specifier
101+
)
102+
return AffectedPackage(package=package, version_specifier=version_specifier)
103+
104+
77105
@dataclasses.dataclass(order=True)
78-
class Advisory:
106+
class AdvisoryData:
79107
"""
80108
This data class expresses the contract between data sources and the import runner.
81109
@@ -86,30 +114,44 @@ class Advisory:
86114

87115
summary: str
88116
vulnerability_id: Optional[str] = None
89-
affected_package_urls: Iterable[PackageURL] = dataclasses.field(default_factory=list)
90-
fixed_package_urls: Iterable[PackageURL] = dataclasses.field(default_factory=list)
117+
affected_packages: List[AffectedPackage] = dataclasses.field(default_factory=list)
118+
fixed_packages: List[AffectedPackage] = dataclasses.field(default_factory=list)
91119
references: List[Reference] = dataclasses.field(default_factory=list)
92-
93-
def __post_init__(self):
94-
if self.vulnerability_id and not is_cve(self.vulnerability_id):
95-
raise ValueError("CVE expected, found: {}".format(self.vulnerability_id))
120+
date_published: Optional[str] = None
96121

97122
def normalized(self):
98-
affected_package_urls = set(self.affected_package_urls)
99-
fixed_package_urls = set(self.fixed_package_urls)
100-
references = sorted(
101-
self.references, key=lambda reference: (reference.reference_id, reference.url)
102-
)
103-
for index, _ in enumerate(self.references):
104-
references[index] = references[index].normalized()
105-
106-
return Advisory(
107-
summary=self.summary,
108-
vulnerability_id=self.vulnerability_id,
109-
affected_package_urls=affected_package_urls,
110-
fixed_package_urls=fixed_package_urls,
111-
references=references,
112-
)
123+
...
124+
125+
def serializable(self, o):
126+
if isinstance(o, AffectedPackage):
127+
return o.toJson()
128+
if isinstance(o, Reference):
129+
return vars(o)
130+
if isinstance(o, datetime):
131+
return o.isoformat()
132+
133+
return json.JSONEncoder.default(self, o)
134+
135+
def toJson(self):
136+
return json.dumps(vars(self), default=self.serializable)
137+
138+
@staticmethod
139+
def fromJson(advisory_data_json: str):
140+
obj = json.loads(advisory_data_json)
141+
advisory_data = AdvisoryData(**obj)
142+
advisory_data.affected_packages = [
143+
AffectedPackage.fromJson(p) for p in advisory_data.affected_packages
144+
]
145+
advisory_data.fixed_packages = [
146+
AffectedPackage.fromJson(p) for p in advisory_data.fixed_packages
147+
]
148+
advisory_data.references = [Reference(**ref) for ref in advisory_data.references]
149+
return advisory_data
150+
151+
152+
class Advisory:
153+
# TODO: Get rid of this after migration
154+
...
113155

114156

115157
class InvalidConfigurationError(Exception):
@@ -192,23 +234,19 @@ def validate_configuration(self) -> None:
192234
"""
193235
pass
194236

195-
def updated_advisories(self) -> Set[Advisory]:
237+
def advisory_data(self) -> Set[AdvisoryData]:
196238
"""
197-
Subclasses return Advisory objects that have been modified since
198-
the last run or self.cutoff_date.
199-
200-
NOTE: Data sources that do not enable detection of changes to existing records vs added
201-
records must only implement this method, not added_advisories(). The ImportRunner
202-
relies on this contract to decide between insert and update operations.
239+
Subclasses return AdvisoryData objects
203240
"""
204-
return set()
241+
raise NotImplementedError
205242

206243
def error(self, msg: str) -> None:
207244
"""
208245
Helper method for raising InvalidConfigurationError with the class name in the message.
209246
"""
210247
raise InvalidConfigurationError(f"{type(self).__name__}: {msg}")
211248

249+
212250
@dataclasses.dataclass
213251
class GitDataSourceConfiguration(DataSourceConfiguration):
214252
repository_url: str
@@ -451,7 +489,7 @@ def _fetch(self) -> Tuple[Mapping, Iterable[ET.ElementTree]]:
451489
# TODO: enforce that we receive the proper data here
452490
raise NotImplementedError
453491

454-
def updated_advisories(self) -> List[Advisory]:
492+
def advisory_data(self) -> List[AdvisoryData]:
455493
for metadata, oval_file in self._fetch():
456494
try:
457495
oval_data = self.get_data_from_xml_doc(oval_file, metadata)
@@ -476,7 +514,7 @@ def set_api(self, all_pkgs: Iterable[str]):
476514
"""
477515
raise NotImplementedError
478516

479-
def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[Advisory]:
517+
def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[AdvisoryData]:
480518
"""
481519
The orchestration method of the OvalDataSource. This method breaks an
482520
OVAL xml ElementTree into a list of `Advisory`.
@@ -488,65 +526,67 @@ def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> Lis
488526
{"type":"deb","qualifiers":{"distro":"buster"} }
489527
"""
490528

491-
all_adv = []
492-
oval_doc = OvalParser(self.translations, xml_doc)
493-
raw_data = oval_doc.get_data()
494-
all_pkgs = self._collect_pkgs(raw_data)
495-
self.set_api(all_pkgs)
496-
497-
# convert definition_data to Advisory objects
498-
for definition_data in raw_data:
499-
# These fields are definition level, i.e common for all elements
500-
# connected/linked to an OvalDefinition
501-
vuln_id = definition_data["vuln_id"]
502-
description = definition_data["description"]
503-
references = [Reference(url=url) for url in definition_data["reference_urls"]]
504-
affected_packages = []
505-
for test_data in definition_data["test_data"]:
506-
for package_name in test_data["package_list"]:
507-
if package_name and len(package_name) >= 50:
508-
continue
509-
510-
affected_version_range = test_data["version_ranges"] or set()
511-
version_class = version_class_by_package_type[pkg_metadata["type"]]
512-
version_scheme = version_class.scheme
513-
514-
affected_version_range = VersionSpecifier.from_scheme_version_spec_string(
515-
version_scheme, affected_version_range
516-
)
517-
all_versions = self.pkg_manager_api.get(package_name).valid_versions
518-
519-
# FIXME: what is this 50 DB limit? that's too small for versions
520-
# FIXME: we should not drop data this way
521-
# This filter is for filtering out long versions.
522-
# 50 is limit because that's what db permits atm.
523-
all_versions = [version for version in all_versions if len(version) < 50]
524-
if not all_versions:
525-
continue
526-
527-
affected_purls = []
528-
safe_purls = []
529-
for version in all_versions:
530-
purl = self.create_purl(
531-
pkg_name=package_name,
532-
pkg_version=version,
533-
pkg_data=pkg_metadata,
534-
)
535-
if version_class(version) in affected_version_range:
536-
affected_purls.append(purl)
537-
else:
538-
safe_purls.append(purl)
539-
540-
affected_packages.extend(
541-
nearest_patched_package(affected_purls, safe_purls),
542-
)
543-
544-
all_adv.append(
545-
Advisory(
546-
summary=description,
547-
affected_packages=affected_packages,
548-
vulnerability_id=vuln_id,
549-
references=references,
550-
)
551-
)
552-
return all_adv
529+
# TODO: Make this compatible to new model
530+
531+
# all_adv = []
532+
# oval_doc = OvalParser(self.translations, xml_doc)
533+
# raw_data = oval_doc.get_data()
534+
# all_pkgs = self._collect_pkgs(raw_data)
535+
# self.set_api(all_pkgs)
536+
537+
# # convert definition_data to Advisory objects
538+
# for definition_data in raw_data:
539+
# # These fields are definition level, i.e common for all elements
540+
# # connected/linked to an OvalDefinition
541+
# vuln_id = definition_data["vuln_id"]
542+
# description = definition_data["description"]
543+
# references = [Reference(url=url) for url in definition_data["reference_urls"]]
544+
# affected_packages = []
545+
# for test_data in definition_data["test_data"]:
546+
# for package_name in test_data["package_list"]:
547+
# if package_name and len(package_name) >= 50:
548+
# continue
549+
550+
# affected_version_range = test_data["version_ranges"] or set()
551+
# version_class = version_class_by_package_type[pkg_metadata["type"]]
552+
# version_scheme = version_class.scheme
553+
554+
# affected_version_range = VersionSpecifier.from_scheme_version_spec_string(
555+
# version_scheme, affected_version_range
556+
# )
557+
# all_versions = self.pkg_manager_api.get(package_name).valid_versions
558+
559+
# # FIXME: what is this 50 DB limit? that's too small for versions
560+
# # FIXME: we should not drop data this way
561+
# # This filter is for filtering out long versions.
562+
# # 50 is limit because that's what db permits atm.
563+
# all_versions = [version for version in all_versions if len(version) < 50]
564+
# if not all_versions:
565+
# continue
566+
567+
# affected_purls = []
568+
# safe_purls = []
569+
# for version in all_versions:
570+
# purl = self.create_purl(
571+
# pkg_name=package_name,
572+
# pkg_version=version,
573+
# pkg_data=pkg_metadata,
574+
# )
575+
# if version_class(version) in affected_version_range:
576+
# affected_purls.append(purl)
577+
# else:
578+
# safe_purls.append(purl)
579+
580+
# affected_packages.extend(
581+
# nearest_patched_package(affected_purls, safe_purls),
582+
# )
583+
584+
# all_adv.append(
585+
# Advisory(
586+
# summary=description,
587+
# affected_packages=affected_packages,
588+
# vulnerability_id=vuln_id,
589+
# references=references,
590+
# )
591+
# )
592+
# return all_adv

0 commit comments

Comments
 (0)