2121# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
2323import dataclasses
24+ import json
2425import logging
2526import os
2627import shutil
4748from vulnerabilities .severity_systems import ScoringSystem
4849from vulnerabilities .helpers import is_cve
4950from vulnerabilities .helpers import nearest_patched_package
50- from vulnerabilities .helpers import AffectedPackage
5151
5252logger = 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
115157class 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
213251class 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