|
| 1 | +# |
| 2 | +# Copyright (c) nexB Inc. and others. All rights reserved. |
| 3 | +# VulnerableCode is a trademark of nexB Inc. |
| 4 | +# SPDX-License-Identifier: Apache-2.0 |
| 5 | +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. |
| 6 | +# See https://github.com/aboutcode-org/vulnerablecode for support or download. |
| 7 | +# See https://aboutcode.org for more information about nexB OSS projects. |
| 8 | +# |
| 9 | + |
| 10 | +import logging |
| 11 | +from traceback import format_exc as traceback_format_exc |
| 12 | + |
| 13 | +from aboutcode.pipeline import LoopProgress |
| 14 | +from fetchcode.package_versions import SUPPORTED_ECOSYSTEMS as FETCHCODE_SUPPORTED_ECOSYSTEMS |
| 15 | +from packageurl import PackageURL |
| 16 | +from univers.version_range import RANGE_CLASS_BY_SCHEMES |
| 17 | +from univers.version_range import VersionRange |
| 18 | + |
| 19 | +from vulnerabilities.models import ImpactedPackage |
| 20 | +from vulnerabilities.models import PackageV2 |
| 21 | +from vulnerabilities.pipelines import VulnerableCodePipeline |
| 22 | +from vulnerabilities.pipes.fetchcode_utils import get_versions |
| 23 | +from vulnerabilities.utils import update_purl_version |
| 24 | + |
| 25 | + |
| 26 | +class UnfurlVersionRangePipeline(VulnerableCodePipeline): |
| 27 | + |
| 28 | + pipeline_id = "unfurl_version_range_v2" |
| 29 | + |
| 30 | + @classmethod |
| 31 | + def steps(cls): |
| 32 | + return (cls.unfurl_version_range,) |
| 33 | + |
| 34 | + def unfurl_version_range(self): |
| 35 | + impacted_packages = ImpactedPackage.objects.all().order_by("-created_at") |
| 36 | + impacted_packages_count = impacted_packages.count() |
| 37 | + |
| 38 | + processed_impacted_packages_count = 0 |
| 39 | + processed_affected_packages_count = 0 |
| 40 | + cached_versions = {} |
| 41 | + self.log(f"Unfurl affected vers range for {impacted_packages_count:,d} ImpactedPackage.") |
| 42 | + progress = LoopProgress(total_iterations=impacted_packages_count, logger=self.log) |
| 43 | + for impact in progress.iter(impacted_packages): |
| 44 | + purl = PackageURL.from_string(impact.base_purl) |
| 45 | + if not impact.affecting_vers or not any( |
| 46 | + c in impact.affecting_vers for c in ("<", ">", "!") |
| 47 | + ): |
| 48 | + continue |
| 49 | + if purl.type not in FETCHCODE_SUPPORTED_ECOSYSTEMS: |
| 50 | + continue |
| 51 | + if purl.type not in RANGE_CLASS_BY_SCHEMES: |
| 52 | + continue |
| 53 | + |
| 54 | + versions = get_purl_versions(purl, cached_versions) |
| 55 | + affected_purls = get_affected_purls( |
| 56 | + versions=versions, |
| 57 | + affecting_vers=impact.affecting_vers, |
| 58 | + base_purl=purl, |
| 59 | + logger=self.log, |
| 60 | + ) |
| 61 | + if not affected_purls: |
| 62 | + continue |
| 63 | + |
| 64 | + processed_affected_packages_count += bulk_create_with_m2m( |
| 65 | + purls=affected_purls, |
| 66 | + impact=impact, |
| 67 | + relation=ImpactedPackage.affecting_packages.through, |
| 68 | + logger=self.log, |
| 69 | + ) |
| 70 | + processed_impacted_packages_count += 1 |
| 71 | + |
| 72 | + self.log(f"Successfully processed {processed_impacted_packages_count:,d} ImpactedPackage.") |
| 73 | + self.log(f"{processed_affected_packages_count:,d} new Impact-Package relation created.") |
| 74 | + |
| 75 | + |
| 76 | +def get_affected_purls(versions, affecting_vers, base_purl, logger): |
| 77 | + affecting_version_range = VersionRange.from_string(affecting_vers) |
| 78 | + version_class = affecting_version_range.version_class |
| 79 | + |
| 80 | + try: |
| 81 | + versions = [version_class(v) for v in versions] |
| 82 | + except Exception as e: |
| 83 | + logger( |
| 84 | + f"Error while parsing versions for {base_purl!s}: {e!r} \n {traceback_format_exc()}", |
| 85 | + level=logging.ERROR, |
| 86 | + ) |
| 87 | + return |
| 88 | + |
| 89 | + affected_purls = [] |
| 90 | + for version in versions: |
| 91 | + try: |
| 92 | + if version in affecting_version_range: |
| 93 | + affected_purls.append( |
| 94 | + update_purl_version( |
| 95 | + purl=base_purl, |
| 96 | + version=str(version), |
| 97 | + ) |
| 98 | + ) |
| 99 | + except Exception as e: |
| 100 | + logger( |
| 101 | + f"Error while checking {version!s} in {affecting_version_range!s}: {e!r} \n {traceback_format_exc()}", |
| 102 | + level=logging.ERROR, |
| 103 | + ) |
| 104 | + return affected_purls |
| 105 | + |
| 106 | + |
| 107 | +def get_purl_versions(purl, cached_versions={}): |
| 108 | + if not purl in cached_versions: |
| 109 | + cached_versions[purl] = get_versions(purl) |
| 110 | + return cached_versions[purl] |
| 111 | + |
| 112 | + |
| 113 | +def bulk_create_with_m2m(purls, impact, relation, logger): |
| 114 | + """Bulk create PackageV2 and also bulk populate M2M Impact and Package relationships.""" |
| 115 | + if not purls: |
| 116 | + return 0 |
| 117 | + |
| 118 | + affected_packages_v2 = PackageV2.objects.bulk_get_or_create_from_purls(purls=purls) |
| 119 | + |
| 120 | + relations = [ |
| 121 | + relation(impactedpackage=impact, packagev2=package) for package in affected_packages_v2 |
| 122 | + ] |
| 123 | + |
| 124 | + try: |
| 125 | + relation.objects.bulk_create(relations, ignore_conflicts=True) |
| 126 | + except Exception as e: |
| 127 | + logger(f"Error creating ImpactedPackage {relation}: {e!r} \n {traceback_format_exc()}") |
| 128 | + return 0 |
| 129 | + |
| 130 | + return len(relations) |
0 commit comments