1313from timeit import default_timer as timer
1414from traceback import format_exc as traceback_format_exc
1515from typing import Iterable
16+ from typing import List
17+ from typing import Optional
1618
1719from aboutcode .pipeline import BasePipeline
1820from aboutcode .pipeline import LoopProgress
1921from aboutcode .pipeline import humanize_time
22+ from fetchcode import package_versions
23+ from packageurl import PackageURL
2024
2125from vulnerabilities .importer import AdvisoryData
26+ from vulnerabilities .importer import AffectedPackage
27+ from vulnerabilities .importer import UnMergeablePackageError
2228from vulnerabilities .improver import MAX_CONFIDENCE
2329from vulnerabilities .models import Advisory
2430from vulnerabilities .models import PackageV2
2531from vulnerabilities .pipes .advisory import import_advisory
2632from vulnerabilities .pipes .advisory import insert_advisory
2733from vulnerabilities .pipes .advisory import insert_advisory_v2
34+ from vulnerabilities .utils import AffectedPackage as LegacyAffectedPackage
2835from vulnerabilities .utils import classproperty
36+ from vulnerabilities .utils import get_affected_packages_by_patched_package
37+ from vulnerabilities .utils import nearest_patched_package
38+ from vulnerabilities .utils import resolve_version_range
2939
3040module_logger = logging .getLogger (__name__ )
3141
@@ -210,13 +220,12 @@ class VulnerableCodeBaseImporterPipelineV2(VulnerableCodePipeline):
210220 repo_url = None
211221 importer_name = None
212222 advisory_confidence = MAX_CONFIDENCE
223+ ignorable_versions = []
224+ unfurl_version_ranges = False
213225
214226 @classmethod
215227 def steps (cls ):
216- return (
217- cls .collect_and_store_advisories ,
218- cls .import_new_advisories ,
219- )
228+ return (cls .collect_and_store_advisories ,)
220229
221230 def collect_advisories (self ) -> Iterable [AdvisoryData ]:
222231 """
@@ -270,6 +279,15 @@ def get_advisory_packages(self, advisory_data: AdvisoryData) -> list:
270279 affected_purls .extend (package_affected_purls )
271280 fixed_purls .extend (package_fixed_purls )
272281
282+
283+ if self .unfurl_version_ranges :
284+ vulnerable_pvs , fixed_pvs = self .get_impacted_packages (
285+ affected_packages = advisory_data .affected_packages ,
286+ advisory_date_published = advisory_data .date_published ,
287+ )
288+ affected_purls .extend (vulnerable_pvs )
289+ fixed_purls .extend (fixed_pvs )
290+
273291 vulnerable_packages = []
274292 fixed_packages = []
275293
@@ -282,3 +300,143 @@ def get_advisory_packages(self, advisory_data: AdvisoryData) -> list:
282300 fixed_packages .append (fixed_package )
283301
284302 return vulnerable_packages , fixed_packages
303+
304+ def get_published_package_versions (
305+ self , package_url : PackageURL , until : Optional [datetime ] = None
306+ ) -> List [str ]:
307+ """
308+ Return a list of versions published before `until` for the `package_url`
309+ """
310+ versions = package_versions .versions (str (package_url ))
311+ versions_before_until = []
312+ for version in versions or []:
313+ if until and version .release_date and version .release_date > until :
314+ continue
315+ versions_before_until .append (version .value )
316+
317+ return versions_before_until
318+
319+ def get_impacted_packages (self , affected_packages , advisory_date_published ):
320+ """
321+ Return a tuple of lists of affected and fixed PackageURLs
322+ """
323+ if not affected_packages :
324+ return [], []
325+
326+ mergable = True
327+
328+ # TODO: We should never had the exception in first place
329+ try :
330+ purl , affected_version_ranges , fixed_versions = AffectedPackage .merge (affected_packages )
331+ except UnMergeablePackageError :
332+ self .log (f"Cannot merge with different purls { affected_packages !r} " , logging .ERROR )
333+ mergable = False
334+
335+ if not mergable :
336+ for affected_package in affected_packages :
337+ purl = affected_package .package
338+ affected_version_range = affected_package .affected_version_range
339+ fixed_version = affected_package .fixed_version
340+ pkg_type = purl .type
341+ pkg_namespace = purl .namespace
342+ pkg_name = purl .name
343+ if not affected_version_range and fixed_version :
344+ # FIXME: Handle the receving end to address the concern of looping the data
345+ return [], [
346+ PackageURL (
347+ type = pkg_type ,
348+ namespace = pkg_namespace ,
349+ name = pkg_name ,
350+ version = str (fixed_version ),
351+ )
352+ ]
353+ else :
354+ # FIXME: Handle the receving end to address the concern of looping the data
355+ valid_versions = self .get_published_package_versions (
356+ package_url = purl , until = advisory_date_published
357+ )
358+ return self .resolve_package_versions (
359+ affected_version_range = affected_version_range ,
360+ pkg_type = pkg_type ,
361+ pkg_namespace = pkg_namespace ,
362+ pkg_name = pkg_name ,
363+ valid_versions = valid_versions ,
364+ )
365+
366+ else :
367+ pkg_type = purl .type
368+ pkg_namespace = purl .namespace
369+ pkg_name = purl .name
370+ pkg_qualifiers = purl .qualifiers
371+ fixed_purls = [
372+ PackageURL (
373+ type = pkg_type ,
374+ namespace = pkg_namespace ,
375+ name = pkg_name ,
376+ version = str (version ),
377+ qualifiers = pkg_qualifiers ,
378+ )
379+ for version in fixed_versions
380+ ]
381+ if not affected_version_ranges :
382+ return [], fixed_purls
383+ else :
384+ valid_versions = self .get_published_package_versions (
385+ package_url = purl , until = advisory_date_published
386+ )
387+ for affected_version_range in affected_version_ranges :
388+ return self .resolve_package_versions (
389+ affected_version_range = affected_version_range ,
390+ pkg_type = pkg_type ,
391+ pkg_namespace = pkg_namespace ,
392+ pkg_name = pkg_name ,
393+ valid_versions = valid_versions ,
394+ )
395+
396+ def resolve_package_versions (
397+ self ,
398+ affected_version_range ,
399+ pkg_type ,
400+ pkg_namespace ,
401+ pkg_name ,
402+ valid_versions ,
403+ ):
404+ """
405+ Return a tuple of lists of ``affected_packages`` and ``fixed_packages`` PackageURL for the given `affected_version_range` and `valid_versions`.
406+
407+ ``valid_versions`` are the valid version listed on the package registry for that package
408+
409+ """
410+ aff_vers , unaff_vers = resolve_version_range (
411+ affected_version_range = affected_version_range ,
412+ ignorable_versions = self .ignorable_versions ,
413+ package_versions = valid_versions ,
414+ )
415+
416+ affected_purls = list (
417+ self .expand_verion_range_to_purls (pkg_type , pkg_namespace , pkg_name , aff_vers )
418+ )
419+
420+ unaffected_purls = list (
421+ self .expand_verion_range_to_purls (pkg_type , pkg_namespace , pkg_name , unaff_vers )
422+ )
423+
424+ fixed_packages = []
425+ affected_packages = []
426+
427+ patched_packages = nearest_patched_package (
428+ vulnerable_packages = affected_purls , resolved_packages = unaffected_purls
429+ )
430+
431+ for (fixed_package , affected_purls ,) in get_affected_packages_by_patched_package (
432+ patched_packages
433+ ).items ():
434+ if fixed_package :
435+ fixed_packages .append (fixed_package )
436+ affected_packages .extend (affected_purls )
437+
438+ return affected_packages , fixed_packages
439+
440+ def expand_verion_range_to_purls (self , pkg_type , pkg_namespace , pkg_name , versions ):
441+ for version in versions :
442+ yield PackageURL (type = pkg_type , namespace = pkg_namespace , name = pkg_name , version = version )
0 commit comments