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 :
@@ -256,7 +259,8 @@ def file_changes(
256259
257260 return {str (p ) for p in path .glob (glob ) if p .is_file ()}, set ()
258261
259- return self ._collect_file_changes (subdir = subdir , recursive = recursive , file_ext = file_ext )
262+ return self ._collect_file_changes (
263+ subdir = subdir , recursive = recursive , file_ext = file_ext )
260264
261265 def _collect_file_changes (
262266 self ,
@@ -268,7 +272,8 @@ def _collect_file_changes(
268272 previous_commit = None
269273 added_files , updated_files = set (), set ()
270274
271- for commit in self ._repo .walk (self ._repo .head .target , pygit2 .GIT_SORT_TIME ):
275+ for commit in self ._repo .walk (
276+ self ._repo .head .target , pygit2 .GIT_SORT_TIME ):
272277 commit_time = commit .commit_time + commit .commit_time_offset # convert to UTC
273278
274279 if commit_time < self .cutoff_timestamp :
@@ -279,13 +284,16 @@ def _collect_file_changes(
279284 continue
280285
281286 for d in commit .tree .diff_to_tree (previous_commit .tree ).deltas :
282- if not _include_file (d .new_file .path , subdir , recursive , file_ext ) or d .is_binary :
287+ if not _include_file (
288+ d .new_file .path , subdir , recursive , file_ext ) or d .is_binary :
283289 continue
284290
285- abspath = os .path .join (self .config .working_directory , d .new_file .path )
291+ abspath = os .path .join (
292+ self .config .working_directory , d .new_file .path )
286293 # TODO
287294 # Just filtering on the two status values for "added" and "modified" is too
288- # simplistic. This does not cover file renames, copies & deletions.
295+ # simplistic. This does not cover file renames, copies &
296+ # deletions.
289297 if d .status == pygit2 .GIT_DELTA_ADDED :
290298 added_files .add (abspath )
291299 elif d .status == pygit2 .GIT_DELTA_MODIFIED :
@@ -380,3 +388,103 @@ def _include_file(
380388 match = match and path .endswith (f'.{ file_ext } ' )
381389
382390 return match
391+
392+
393+ class OvalDataSource (DataSource ):
394+
395+ @staticmethod
396+ def create_purl (pkg_name : str , pkg_version : str , pkg_data : Mapping ):
397+ """
398+ Note: pkg_data must include 'type' of package
399+ """
400+ return PackageURL (name = pkg_name , version = pkg_version , ** pkg_data )
401+
402+ @staticmethod
403+ def _collect_pkgs (parsed_oval_data : Mapping ) -> Set :
404+ """
405+ Helper method, used for loading the API. It expects data from
406+ OvalParser.get_data() .
407+ """
408+ all_pkgs = set ()
409+ for definition_data in parsed_oval_data :
410+ for test_data in definition_data ['test_data' ]:
411+ for package in test_data ['package_list' ]:
412+ all_pkgs .add (package )
413+
414+ return all_pkgs
415+
416+ def _fetch () -> Tuple [Mapping , Iterable [ET .ElementTree ]]:
417+ """
418+ This method contains logic to fetch OVAL files and yield them into
419+ a tuple of file's metadata and it's ET.ElementTree.
420+ Subclasses must implement this method.
421+ """
422+ raise NotImplementedError
423+
424+ def added_advisories (self ) -> List [Advisory ]:
425+ advisories = []
426+ for metadata , oval_file in self ._fetch ():
427+ advisories .extend (self .get_data_from_xml_doc (oval_file , metadata ))
428+ return advisories
429+
430+ def set_api (self , all_pkgs : Iterable [str ]):
431+ """
432+ This method loads the self.pkg_manager_api with the specified packages. It fetches
433+ and caches the data about these packages exposes them through
434+ self.pkg_manager_api.get(<package_name>)
435+ """
436+ raise NotImplementedError
437+
438+ def get_data_from_xml_doc (self , xml_doc : ET .ElementTree , pkg_metadata = {}) -> List [Advisory ]:
439+ """
440+ The orchestration method of the OvalDataSource. Breaks an OVAL xml
441+ ElementTree into a list of Advisory.
442+ """
443+ all_adv = []
444+ oval_doc = OvalParser (self .translations , xml_doc )
445+ raw_data = oval_doc .get_data ()
446+ all_pkgs = self ._collect_pkgs (raw_data )
447+ self .set_api (all_pkgs )
448+ for definition_data in raw_data : # definition_data -> Advisory
449+ vuln_id = definition_data ['vuln_id' ]
450+ description = definition_data ['description' ]
451+ affected_purls = set ()
452+ safe_purls = set ()
453+ urls = definition_data ['reference_urls' ]
454+ for test_data in definition_data ['test_data' ]:
455+ for package in test_data ['package_list' ]:
456+ pkg_name = package
457+ aff_ver_range = test_data ['version_ranges' ]
458+ all_versions = self .pkg_manager_api .get (package )
459+ # This filter is to filter out long versions.
460+ # 50 is limit because that's what db permits atm
461+ all_versions = set (
462+ filter (
463+ lambda x : len (x ) < 50 ,
464+ all_versions ))
465+ if not all_versions :
466+ continue
467+ affected_versions = set (
468+ filter (
469+ lambda x : x in aff_ver_range ,
470+ all_versions ))
471+ safe_versions = all_versions - affected_versions
472+
473+ for version in affected_versions :
474+ pkg_url = self .create_purl (
475+ pkg_name = pkg_name , pkg_version = version , pkg_data = pkg_metadata )
476+ affected_purls .add (pkg_url )
477+
478+ for version in safe_versions :
479+ pkg_url = self .create_purl (
480+ pkg_name = pkg_name , pkg_version = version , pkg_data = pkg_metadata )
481+ safe_purls .add (pkg_url )
482+
483+ all_adv .append (
484+ Advisory (
485+ summary = description ,
486+ impacted_package_urls = affected_purls ,
487+ resolved_package_urls = safe_purls ,
488+ cve_id = vuln_id ,
489+ reference_urls = urls ))
490+ return all_adv
0 commit comments