Skip to content

Commit 5f0cd4c

Browse files
committed
Refactor parse_version_range to univers
Signed-off-by: Sampurna Pyne <sampurnapyne1710@gmail.com>
1 parent 72f4e87 commit 5f0cd4c

File tree

4 files changed

+34
-87
lines changed

4 files changed

+34
-87
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ toml==0.10.2
118118
tomli==2.0.1
119119
traitlets==5.1.1
120120
typing_extensions==4.1.1
121-
univers==30.12.0
121+
git+https://github.com/aboutcode-org/univers.git@fa06361292798d09fbb088799e9f4b275a422aa5#egg=univers
122122
urllib3==1.26.19
123123
wcwidth==0.2.5
124124
websocket-client==0.59.0

vulnerabilities/pipelines/v2_importers/ossa_importer_v2.py

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

10-
import re
1110
from pathlib import Path
1211
from typing import Iterable
1312
from typing import Tuple
@@ -16,11 +15,10 @@
1615
from fetchcode.vcs import fetch_via_vcs
1716
from packageurl import PackageURL
1817
from pytz import UTC
19-
from univers.version_constraint import VersionConstraint
18+
from univers.version_range import InvalidVersionRange
2019
from univers.version_range import PypiVersionRange
21-
from univers.versions import PypiVersion
2220

23-
from vulnerabilities.importer import AdvisoryData
21+
from vulnerabilities.importer import AdvisoryDataV2
2422
from vulnerabilities.importer import AffectedPackageV2
2523
from vulnerabilities.importer import ReferenceV2
2624
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
@@ -42,7 +40,6 @@ class OSSAImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
4240
def steps(cls):
4341
return (
4442
cls.clone,
45-
cls.fetch,
4643
cls.collect_and_store_advisories,
4744
cls.clean_downloads,
4845
)
@@ -51,41 +48,33 @@ def clone(self):
5148
self.log(f"Cloning `{self.repo_url}`")
5249
self.vcs_response = fetch_via_vcs(self.repo_url)
5350

54-
def fetch(self):
51+
def get_processable_files(self) -> Iterable[Path]:
52+
"""
53+
Returns a list of OSSA YAML files that are eligible for processing based on the cutoff year.
54+
"""
5555
ossa_dir = Path(self.vcs_response.dest_dir) / "ossa"
56-
self.processable_advisories = []
57-
skipped_old = 0
58-
5956
for file_path in sorted(ossa_dir.glob("OSSA-*.yaml")):
60-
data = load_yaml(str(file_path))
61-
62-
date_str = data.get("date")
63-
date_published = dateparser.parse(str(date_str)).replace(tzinfo=UTC)
64-
if date_published.year < self.cutoff_year:
65-
skipped_old += 1
66-
continue
67-
68-
self.processable_advisories.append(file_path)
69-
70-
if skipped_old > 0:
71-
self.log(f"Skipped {skipped_old} advisories older than {self.cutoff_year}")
72-
self.log(f"Fetched {len(self.processable_advisories)} processable advisories")
57+
filename = file_path.stem
58+
year = int(filename.split("-")[1])
59+
if year >= self.cutoff_year:
60+
yield file_path
7361

7462
def advisories_count(self) -> int:
75-
return len(self.processable_advisories)
63+
return sum(1 for _ in self.get_processable_files())
7664

77-
def collect_advisories(self) -> Iterable[AdvisoryData]:
78-
for file_path in self.processable_advisories:
65+
def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
66+
for file_path in self.get_processable_files():
7967
advisory = self.process_file(file_path)
8068
yield advisory
8169

82-
def process_file(self, file_path) -> AdvisoryData:
70+
def process_file(self, file_path) -> AdvisoryDataV2:
8371
"""Parse a single OSSA YAML file and extract advisory data"""
8472
data = load_yaml(str(file_path))
8573
ossa_id = data.get("id")
8674

8775
date_str = data.get("date")
88-
date_published = dateparser.parse(str(date_str)).replace(tzinfo=UTC)
76+
if date_str:
77+
date_published = dateparser.parse(str(date_str)).replace(tzinfo=UTC)
8978

9079
aliases = []
9180
for vulnerability in data.get("vulnerabilities"):
@@ -106,7 +95,8 @@ def process_file(self, file_path) -> AdvisoryData:
10695
)
10796

10897
references = []
109-
for link in (data.get("issues")).get("links"):
98+
issues = data.get("issues")
99+
for link in issues.get("links", []):
110100
references.append(ReferenceV2(url=str(link)))
111101
reviews = data.get("reviews")
112102
for branch, links in reviews.items():
@@ -120,12 +110,12 @@ def process_file(self, file_path) -> AdvisoryData:
120110
description = data.get("description")
121111
summary = f"{title}\n\n{description}"
122112
url = f"https://security.openstack.org/ossa/{ossa_id}.html"
123-
return AdvisoryData(
113+
return AdvisoryDataV2(
124114
advisory_id=ossa_id,
125115
aliases=aliases,
126116
summary=summary,
127117
affected_packages=affected_packages,
128-
references_v2=references,
118+
references=references,
129119
date_published=date_published,
130120
url=url,
131121
)
@@ -158,56 +148,14 @@ def expand_products(self, product_str, version_str) -> Iterable[Tuple[str, str]]
158148
yield product_str, version_str
159149

160150
def parse_version_range(self, version_str: str) -> PypiVersionRange:
161-
"""
162-
Normalizes the version string and extracts individual constraints to create a PypiVersionRange object.
163-
"""
164-
original_version_str = version_str
151+
"""Parse a version string from OSSA advisories into a PypiVersionRange object."""
165152

166-
if version_str.lower() == "all versions":
167-
self.log(f"Skipping 'all versions' - cannot parse to specific range")
153+
try:
154+
return PypiVersionRange.from_ossa_native(version_str)
155+
except InvalidVersionRange as e:
156+
self.log(f"Failed to parse version range {version_str!r}: {e}")
168157
return None
169158

170-
# Normalize "and" to comma
171-
# "<=5.0.3, >=6.0.0 <=6.1.0 and ==7.0.0" -> "<=5.0.3, >=6.0.0 <=6.1.0, ==7.0.0"
172-
version_str = version_str.lower().replace(" and ", ",")
173-
174-
# Remove spaces around operators
175-
# "<=5.0.3, >=6.0.0 <=6.1.0, ==7.0.0" -> "<=5.0.3,>=6.0.0<=6.1.0,==7.0.0"
176-
version_str = re.sub(r"\s+([<>=!]+)", r"\1", version_str)
177-
version_str = re.sub(r"([<>=!]+)\s+", r"\1", version_str)
178-
179-
# Insert comma between consecutive constraints
180-
# "<=5.0.3,>=6.0.0<=6.1.0,==7.0.0" -> "<=5.0.3,>=6.0.0,<=6.1.0,==7.0.0"
181-
version_str = re.sub(r"(\d)([<>=!])", r"\1,\2", version_str)
182-
183-
constraints = []
184-
for part in version_str.split(","):
185-
comparator = None
186-
version = part
187-
188-
for op in ["==", "!=", "<=", ">=", "<", ">", "="]:
189-
if part.startswith(op):
190-
comparator = op
191-
version = part[len(op) :].strip()
192-
break
193-
194-
# Default to "=" if no comparator is found
195-
# "1.16.0" -> "=1.16.0"
196-
if comparator is None:
197-
comparator = "="
198-
# "==27.0.0" -> "=27.0.0"
199-
if comparator == "==":
200-
comparator = "="
201-
try:
202-
constraints.append(
203-
VersionConstraint(comparator=comparator, version=PypiVersion(version))
204-
)
205-
except ValueError as e:
206-
self.log(f"Failed to parse version '{version}' from '{original_version_str}' : {e}")
207-
continue
208-
209-
return PypiVersionRange(constraints=constraints) if constraints else None
210-
211159
def clean_downloads(self):
212160
if self.vcs_response:
213161
self.log("Removing cloned repository")

vulnerabilities/tests/pipelines/v2_importers/test_ossa_importer_v2.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ def mock_fetch_via_vcs(mock_vcs_response):
3737
def test_collect_advisories(mock_fetch_via_vcs):
3838
pipeline = OSSAImporterPipeline()
3939
pipeline.clone()
40-
pipeline.fetch()
4140
advisories = [adv.to_dict() for adv in pipeline.collect_advisories()]
4241
expected_file = TEST_DATA / "expected.json"
4342
util_tests.check_results_against_json(advisories, expected_file)

vulnerabilities/tests/test_data/ossa/expected.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"fixed_by_commit_patches": []
2222
}
2323
],
24-
"references_v2": [
24+
"references": [
2525
{
2626
"reference_id": "",
2727
"reference_type": "",
@@ -96,7 +96,7 @@
9696
"fixed_by_commit_patches": []
9797
}
9898
],
99-
"references_v2": [
99+
"references": [
100100
{
101101
"reference_id": "",
102102
"reference_type": "",
@@ -161,7 +161,7 @@
161161
"fixed_by_commit_patches": []
162162
}
163163
],
164-
"references_v2": [
164+
"references": [
165165
{
166166
"reference_id": "",
167167
"reference_type": "",
@@ -231,7 +231,7 @@
231231
"fixed_by_commit_patches": []
232232
}
233233
],
234-
"references_v2": [
234+
"references": [
235235
{
236236
"reference_id": "",
237237
"reference_type": "",
@@ -363,7 +363,7 @@
363363
"fixed_by_commit_patches": []
364364
}
365365
],
366-
"references_v2": [
366+
"references": [
367367
{
368368
"reference_id": "",
369369
"reference_type": "",
@@ -566,7 +566,7 @@
566566
"fixed_by_commit_patches": []
567567
}
568568
],
569-
"references_v2": [
569+
"references": [
570570
{
571571
"reference_id": "",
572572
"reference_type": "",
@@ -841,7 +841,7 @@
841841
"fixed_by_commit_patches": []
842842
}
843843
],
844-
"references_v2": [
844+
"references": [
845845
{
846846
"reference_id": "",
847847
"reference_type": "",
@@ -926,7 +926,7 @@
926926
"fixed_by_commit_patches": []
927927
}
928928
],
929-
"references_v2": [
929+
"references": [
930930
{
931931
"reference_id": "",
932932
"reference_type": "",

0 commit comments

Comments
 (0)