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