Skip to content

Commit 53b4efc

Browse files
committed
Add pipeline to unfurl affected VERS range in V2 impacts
Signed-off-by: Keshav Priyadarshi <[email protected]>
1 parent 7633991 commit 53b4efc

File tree

5 files changed

+185
-4
lines changed

5 files changed

+185
-4
lines changed

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
enhance_with_metasploit as enhance_with_metasploit_v2,
3131
)
3232
from vulnerabilities.pipelines.v2_improvers import flag_ghost_packages as flag_ghost_packages_v2
33+
from vulnerabilities.pipelines.v2_improvers import unfurl_version_range as unfurl_version_range_v2
3334
from vulnerabilities.utils import create_registry
3435

3536
IMPROVERS_REGISTRY = create_registry(
@@ -67,6 +68,7 @@
6768
compute_package_risk_v2.ComputePackageRiskPipeline,
6869
compute_version_rank_v2.ComputeVersionRankPipeline,
6970
compute_advisory_todo_v2.ComputeToDo,
71+
unfurl_version_range_v2.UnfurlVersionRangePipeline,
7072
compute_advisory_todo.ComputeToDo,
7173
]
7274
)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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)

vulnerabilities/pipes/fetchcode_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
1010
import logging
1111
from traceback import format_exc as traceback_format_exc
1212
from typing import Callable
13+
from typing import Union
1314

1415
from fetchcode.package_versions import SUPPORTED_ECOSYSTEMS as FETCHCODE_SUPPORTED_ECOSYSTEMS
1516
from fetchcode.package_versions import versions
1617
from packageurl import PackageURL
1718

1819

19-
def get_versions(purl: PackageURL, logger: Callable = None):
20+
def get_versions(purl: Union[PackageURL, str], logger: Callable = None):
2021
"""Return set of known versions for the given purl."""
22+
if isinstance(purl, str):
23+
purl = PackageURL.from_string(purl)
24+
2125
if purl.type not in FETCHCODE_SUPPORTED_ECOSYSTEMS:
2226
return
2327

vulnerabilities/tests/pipelines/v2_importers/test_redhat_importer_v2.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10-
import json
11-
import os
10+
1211
from pathlib import Path
13-
from unittest.mock import Mock
1412
from unittest.mock import patch
1513

1614
from django.test import TestCase
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
11+
from unittest.mock import patch
12+
13+
from django.test import TestCase
14+
15+
from vulnerabilities.models import AdvisoryV2
16+
from vulnerabilities.models import ImpactedPackage
17+
from vulnerabilities.models import PackageV2
18+
from vulnerabilities.pipelines.v2_improvers.unfurl_version_range import UnfurlVersionRangePipeline
19+
20+
21+
class TestUnfurlVersionRangePipeline(TestCase):
22+
def setUp(self):
23+
self.advisory1 = AdvisoryV2.objects.create(
24+
datasource_id="ghsa",
25+
advisory_id="GHSA-1234",
26+
avid="ghsa/GHSA-1234",
27+
unique_content_id="f" * 64,
28+
url="https://example.com/advisory",
29+
date_collected="2025-07-01T00:00:00Z",
30+
)
31+
32+
self.impact1 = ImpactedPackage.objects.create(
33+
advisory=self.advisory1,
34+
base_purl="pkg:npm/foobar",
35+
affecting_vers="vers:npm/>3.2.1|<4.0.0",
36+
fixed_vers=None,
37+
)
38+
39+
@patch("vulnerabilities.pipelines.v2_improvers.unfurl_version_range.get_purl_versions")
40+
def test_affecting_version_range_unfurl(self, mock_fetch):
41+
self.assertEqual(0, PackageV2.objects.count())
42+
mock_fetch.return_value = {"3.4.1", "3.9.0", "2.1.0", "4.0.0", "4.1.0"}
43+
pipeline = UnfurlVersionRangePipeline()
44+
pipeline.execute()
45+
46+
self.assertEqual(2, PackageV2.objects.count())
47+
self.assertEqual(2, self.impact1.affecting_packages.count())

0 commit comments

Comments
 (0)