3535from typing import Sequence
3636from typing import Set
3737from typing import Tuple
38+ import xml .etree .ElementTree as ET
3839
3940import pygit2
4041from packageurl import PackageURL
4142
43+ from vulnerabilities .oval_parser import OvalParser
44+
4245
4346@dataclasses .dataclass
4447class Advisory :
@@ -257,7 +260,8 @@ def file_changes(
257260
258261 return {str (p ) for p in path .glob (glob ) if p .is_file ()}, set ()
259262
260- return self ._collect_file_changes (subdir = subdir , recursive = recursive , file_ext = file_ext )
263+ return self ._collect_file_changes (
264+ subdir = subdir , recursive = recursive , file_ext = file_ext )
261265
262266 def _collect_file_changes (
263267 self ,
@@ -269,7 +273,8 @@ def _collect_file_changes(
269273 previous_commit = None
270274 added_files , updated_files = set (), set ()
271275
272- for commit in self ._repo .walk (self ._repo .head .target , pygit2 .GIT_SORT_TIME ):
276+ for commit in self ._repo .walk (
277+ self ._repo .head .target , pygit2 .GIT_SORT_TIME ):
273278 commit_time = commit .commit_time + commit .commit_time_offset # convert to UTC
274279
275280 if commit_time < self .cutoff_timestamp :
@@ -280,13 +285,16 @@ def _collect_file_changes(
280285 continue
281286
282287 for d in commit .tree .diff_to_tree (previous_commit .tree ).deltas :
283- if not _include_file (d .new_file .path , subdir , recursive , file_ext ) or d .is_binary :
288+ if not _include_file (
289+ d .new_file .path , subdir , recursive , file_ext ) or d .is_binary :
284290 continue
285291
286- abspath = os .path .join (self .config .working_directory , d .new_file .path )
292+ abspath = os .path .join (
293+ self .config .working_directory , d .new_file .path )
287294 # TODO
288295 # Just filtering on the two status values for "added" and "modified" is too
289- # simplistic. This does not cover file renames, copies & deletions.
296+ # simplistic. This does not cover file renames, copies &
297+ # deletions.
290298 if d .status == pygit2 .GIT_DELTA_ADDED :
291299 added_files .add (abspath )
292300 elif d .status == pygit2 .GIT_DELTA_MODIFIED :
@@ -381,3 +389,127 @@ def _include_file(
381389 match = match and path .endswith (f'.{ file_ext } ' )
382390
383391 return match
392+
393+
394+ class OvalDataSource (DataSource ):
395+ """
396+ All data sources which collect data from OVAL files must inherit from this
397+ `OvalDataSource` class. Subclasses must implement the methods `_fetch` and `set_api`.
398+ """
399+ @staticmethod
400+ def create_purl (pkg_name : str , pkg_version : str , pkg_data : Mapping ) -> PackageURL :
401+ """
402+ Helper method for creating different purls for subclasses without them reimplementing
403+ get_data_from_xml_doc method
404+ Note: pkg_data must include 'type' of package
405+ """
406+ return PackageURL (name = pkg_name , version = pkg_version , ** pkg_data )
407+
408+ @staticmethod
409+ def _collect_pkgs (parsed_oval_data : Mapping ) -> Set :
410+ """
411+ Helper method, used for loading the API. It expects data from
412+ OvalParser.get_data().
413+ """
414+ all_pkgs = set ()
415+ for definition_data in parsed_oval_data :
416+ for test_data in definition_data ['test_data' ]:
417+ for package in test_data ['package_list' ]:
418+ all_pkgs .add (package )
419+
420+ return all_pkgs
421+
422+ def _fetch () -> Tuple [Mapping , Iterable [ET .ElementTree ]]:
423+ """
424+ This method contains logic to fetch OVAL files and yield them into
425+ a tuple of file's metadata and it's ET.ElementTree.
426+ Subclasses must implement this method.
427+
428+ Note: Mapping MUST INCLUDE "type" key. Example values of Mapping
429+ {"type":"deb","qualifiers":{"distro":"buster"} }
430+
431+ """
432+ raise NotImplementedError
433+
434+ def updated_advisories (self ) -> List [Advisory ]:
435+ """
436+ Note: metadata MUST INCLUDE "type" key, implement _fetch accordingly.
437+ """
438+ advisories = []
439+ for metadata , oval_file in self ._fetch ():
440+ advisories .extend (self .get_data_from_xml_doc (oval_file , metadata ))
441+ return self .batch_advisories (advisories )
442+
443+ def set_api (self , all_pkgs : Iterable [str ]):
444+ """
445+ This method loads the self.pkg_manager_api with the specified packages. It fetches
446+ and caches all the versions of these packages and exposes them through
447+ self.pkg_manager_api.get(<package_name>). Example
448+
449+ >>> self.set_api(['electron'])
450+ Assume 'electron' has only versions 1.0.0 and 1.2.0
451+ >>> assert self.pkg_manager_api.get('electron') == {'1.0.0','1.2.0'}
452+
453+ """
454+ raise NotImplementedError
455+
456+ def get_data_from_xml_doc (self , xml_doc : ET .ElementTree , pkg_metadata = {}) -> List [Advisory ]:
457+ """
458+ The orchestration method of the OvalDataSource. This method breaks an OVAL xml
459+ ElementTree into a list of `Advisory`.
460+
461+ Note: pkg_metadata MUST INCLUDE "type" key. Example value of pkg_metadata,
462+ {"type":"deb","qualifiers":{"distro":"buster"} }
463+ """
464+ all_adv = []
465+ oval_doc = OvalParser (self .translations , xml_doc )
466+ raw_data = oval_doc .get_data ()
467+ all_pkgs = self ._collect_pkgs (raw_data )
468+ self .set_api (all_pkgs )
469+ for definition_data in raw_data : # definition_data -> Advisory
470+
471+ # These fields are definition level, i.e common for all
472+ # elements connected/linked to an OvalDefinition
473+ vuln_id = definition_data ['vuln_id' ]
474+ description = definition_data ['description' ]
475+ affected_purls = set ()
476+ safe_purls = set ()
477+ urls = definition_data ['reference_urls' ]
478+
479+ for test_data in definition_data ['test_data' ]:
480+ for package in test_data ['package_list' ]:
481+ pkg_name = package
482+ aff_ver_range = test_data ['version_ranges' ]
483+ all_versions = self .pkg_manager_api .get (package )
484+ # This filter is for filtering out long versions.
485+ # 50 is limit because that's what db permits atm.
486+ all_versions = set (
487+ filter (
488+ lambda x : len (x ) < 50 ,
489+ all_versions ))
490+ if not all_versions :
491+ continue
492+ affected_versions = set (
493+ filter (
494+ lambda x : x in aff_ver_range ,
495+ all_versions ))
496+ safe_versions = all_versions - affected_versions
497+
498+ for version in affected_versions :
499+ pkg_url = self .create_purl (
500+ pkg_name = pkg_name , pkg_version = version , pkg_data = pkg_metadata )
501+ affected_purls .add (pkg_url )
502+
503+ for version in safe_versions :
504+ pkg_url = self .create_purl (
505+ pkg_name = pkg_name , pkg_version = version , pkg_data = pkg_metadata )
506+ safe_purls .add (pkg_url )
507+
508+ all_adv .append (
509+ Advisory (
510+ summary = description ,
511+ impacted_package_urls = affected_purls ,
512+ resolved_package_urls = safe_purls ,
513+ cve_id = vuln_id ,
514+ reference_urls = urls ))
515+ return all_adv
0 commit comments