Skip to content

Commit 66c914b

Browse files
authored
Merge pull request #2145 from aboutcode-org/ubuntu-v2
Add v2 pipeline to collect Ubuntu OSV advisories
2 parents 93888f1 + ecb6914 commit 66c914b

16 files changed

Lines changed: 1588 additions & 33 deletions

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
from vulnerabilities.pipelines.v2_importers import pysec_importer as pysec_importer_v2
7575
from vulnerabilities.pipelines.v2_importers import redhat_importer as redhat_importer_v2
7676
from vulnerabilities.pipelines.v2_importers import ruby_importer as ruby_importer_v2
77+
from vulnerabilities.pipelines.v2_importers import ubuntu_osv_importer as ubuntu_osv_importer_v2
7778
from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2
7879
from vulnerabilities.pipelines.v2_importers import xen_importer as xen_importer_v2
7980
from vulnerabilities.utils import create_registry
@@ -107,6 +108,7 @@
107108
debian_importer_v2.DebianImporterPipeline,
108109
mattermost_importer_v2.MattermostImporterPipeline,
109110
apache_tomcat_v2.ApacheTomcatImporterPipeline,
111+
ubuntu_osv_importer_v2.UbuntuOSVImporterPipeline,
110112
nvd_importer.NVDImporterPipeline,
111113
github_importer.GitHubAPIImporterPipeline,
112114
gitlab_importer.GitLabImporterPipeline,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Generated by Django 4.2.25 on 2026-02-05 10:10
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0111_alter_advisoryseverity_scoring_system_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="advisoryseverity",
15+
name="scoring_system",
16+
field=models.CharField(
17+
choices=[
18+
("cvssv2", "CVSSv2 Base Score"),
19+
("cvssv3", "CVSSv3 Base Score"),
20+
("cvssv3.1", "CVSSv3.1 Base Score"),
21+
("cvssv4", "CVSSv4 Base Score"),
22+
("rhbs", "RedHat Bugzilla severity"),
23+
("rhas", "RedHat Aggregate severity"),
24+
("archlinux", "Archlinux Vulnerability Group Severity"),
25+
("cvssv3.1_qr", "CVSSv3.1 Qualitative Severity Rating"),
26+
("generic_textual", "Generic textual severity rating"),
27+
("apache_httpd", "Apache Httpd Severity"),
28+
("apache_tomcat", "Apache Tomcat Severity"),
29+
("epss", "Exploit Prediction Scoring System"),
30+
("ssvc", "Stakeholder-Specific Vulnerability Categorization"),
31+
("openssl", "OpenSSL Severity"),
32+
("ubuntu-priority", "Ubuntu Priority"),
33+
],
34+
help_text="Identifier for the scoring system used. Available choices are: cvssv2: CVSSv2 Base Score,\ncvssv3: CVSSv3 Base Score,\ncvssv3.1: CVSSv3.1 Base Score,\ncvssv4: CVSSv4 Base Score,\nrhbs: RedHat Bugzilla severity,\nrhas: RedHat Aggregate severity,\narchlinux: Archlinux Vulnerability Group Severity,\ncvssv3.1_qr: CVSSv3.1 Qualitative Severity Rating,\ngeneric_textual: Generic textual severity rating,\napache_httpd: Apache Httpd Severity,\napache_tomcat: Apache Tomcat Severity,\nepss: Exploit Prediction Scoring System,\nssvc: Stakeholder-Specific Vulnerability Categorization,\nopenssl: OpenSSL Severity,\nubuntu-priority: Ubuntu Priority ",
35+
max_length=50,
36+
),
37+
),
38+
migrations.AlterField(
39+
model_name="vulnerabilityseverity",
40+
name="scoring_system",
41+
field=models.CharField(
42+
choices=[
43+
("cvssv2", "CVSSv2 Base Score"),
44+
("cvssv3", "CVSSv3 Base Score"),
45+
("cvssv3.1", "CVSSv3.1 Base Score"),
46+
("cvssv4", "CVSSv4 Base Score"),
47+
("rhbs", "RedHat Bugzilla severity"),
48+
("rhas", "RedHat Aggregate severity"),
49+
("archlinux", "Archlinux Vulnerability Group Severity"),
50+
("cvssv3.1_qr", "CVSSv3.1 Qualitative Severity Rating"),
51+
("generic_textual", "Generic textual severity rating"),
52+
("apache_httpd", "Apache Httpd Severity"),
53+
("apache_tomcat", "Apache Tomcat Severity"),
54+
("epss", "Exploit Prediction Scoring System"),
55+
("ssvc", "Stakeholder-Specific Vulnerability Categorization"),
56+
("openssl", "OpenSSL Severity"),
57+
("ubuntu-priority", "Ubuntu Priority"),
58+
],
59+
help_text="Identifier for the scoring system used. Available choices are: cvssv2: CVSSv2 Base Score,\ncvssv3: CVSSv3 Base Score,\ncvssv3.1: CVSSv3.1 Base Score,\ncvssv4: CVSSv4 Base Score,\nrhbs: RedHat Bugzilla severity,\nrhas: RedHat Aggregate severity,\narchlinux: Archlinux Vulnerability Group Severity,\ncvssv3.1_qr: CVSSv3.1 Qualitative Severity Rating,\ngeneric_textual: Generic textual severity rating,\napache_httpd: Apache Httpd Severity,\napache_tomcat: Apache Tomcat Severity,\nepss: Exploit Prediction Scoring System,\nssvc: Stakeholder-Specific Vulnerability Categorization,\nopenssl: OpenSSL Severity,\nubuntu-priority: Ubuntu Priority ",
60+
max_length=50,
61+
),
62+
),
63+
]

vulnerabilities/pipelines/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ class VulnerableCodeBaseImporterPipelineV2(VulnerableCodePipeline):
266266
repo_url = None
267267
ignorable_versions = []
268268

269+
# Control how often progress log is shown (range: 1–100, higher value = less frequent log)
270+
progress_step = 10
271+
269272
# When set to true pipeline is run only once.
270273
# To rerun onetime pipeline reset is_active field to True via migration.
271274
run_once = False
@@ -301,7 +304,11 @@ def collect_and_store_advisories(self):
301304
if estimated_advisory_count > 0:
302305
self.log(f"Collecting {estimated_advisory_count:,d} advisories")
303306

304-
progress = LoopProgress(total_iterations=estimated_advisory_count, logger=self.log)
307+
progress = LoopProgress(
308+
total_iterations=estimated_advisory_count,
309+
logger=self.log,
310+
progress_step=self.progress_step,
311+
)
305312
for advisory in progress.iter(self.collect_advisories()):
306313
if advisory is None:
307314
self.log("Advisory is None, skipping")
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
from pathlib import Path
11+
from typing import Iterable
12+
13+
from fetchcode.vcs import fetch_via_vcs
14+
15+
from vulnerabilities.importer import AdvisoryData
16+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
17+
from vulnerabilities.pipes.osv_v2 import parse_advisory_data_v3
18+
from vulnerabilities.utils import get_advisory_url
19+
from vulnerabilities.utils import load_json
20+
21+
22+
class UbuntuOSVImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
23+
"""
24+
Collect Ubuntu OSV format advisories.
25+
26+
Collect advisories from the GitHub Ubuntu Vulnerability Data repository.
27+
"""
28+
29+
pipeline_id = "ubuntu_osv_importer_v2"
30+
spdx_license_expression = "CC-BY-4.0"
31+
license_url = "https://github.com/canonical/ubuntu-security-notices/blob/main/LICENSE"
32+
repo_url = "git+https://github.com/canonical/ubuntu-security-notices/"
33+
34+
progress_step = 1
35+
36+
@classmethod
37+
def steps(cls):
38+
return (
39+
cls.clone,
40+
cls.collect_and_store_advisories,
41+
cls.clean_downloads,
42+
)
43+
44+
def clone(self):
45+
self.log(f"Cloning `{self.repo_url}`")
46+
self.vcs_response = fetch_via_vcs(self.repo_url)
47+
self.advisories_path = Path(self.vcs_response.dest_dir)
48+
49+
def advisories_count(self):
50+
cve_directory = self.advisories_path / "osv" / "cve"
51+
return sum(1 for _ in cve_directory.rglob("*.json"))
52+
53+
def collect_advisories(self) -> Iterable[AdvisoryData]:
54+
supported_ecosystems = ["deb"]
55+
cve_directory = self.advisories_path / "osv" / "cve"
56+
57+
for file in cve_directory.rglob("*.json"):
58+
advisory_url = get_advisory_url(
59+
file=file,
60+
base_path=self.advisories_path,
61+
url="https://github.com/canonical/ubuntu-security-notices/blob/main/",
62+
)
63+
raw_data = load_json(file)
64+
advisory_text = file.read_text()
65+
66+
yield parse_advisory_data_v3(
67+
raw_data=raw_data,
68+
supported_ecosystems=supported_ecosystems,
69+
advisory_url=advisory_url,
70+
advisory_text=advisory_text,
71+
)
72+
73+
def clean_downloads(self):
74+
if self.vcs_response:
75+
self.log("Removing cloned repository")
76+
self.vcs_response.delete()
77+
78+
def on_failure(self):
79+
self.clean_downloads()

vulnerabilities/pipes/advisory.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,14 +360,14 @@ def insert_advisory_v2(
360360
affected_package=affected_pkg,
361361
logger=logger,
362362
)
363-
affected_packages_v2 = [
364-
PackageV2.objects.get_or_create_from_purl(purl=purl)[0]
365-
for purl in package_affected_purls
366-
]
367-
fixed_packages_v2 = [
368-
PackageV2.objects.get_or_create_from_purl(purl=purl)[0]
369-
for purl in package_fixed_purls
370-
]
363+
364+
affected_packages_v2 = PackageV2.objects.bulk_get_or_create_from_purls(
365+
purls=package_affected_purls
366+
)
367+
fixed_packages_v2 = PackageV2.objects.bulk_get_or_create_from_purls(
368+
purls=package_fixed_purls
369+
)
370+
371371
impact.affecting_packages.add(*affected_packages_v2)
372372
impact.fixed_by_packages.add(*fixed_packages_v2)
373373

vulnerabilities/pipes/osv_v2.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
"crates.io": "cargo",
5252
}
5353

54+
OSV_TO_VCIO_SEVERITY_MAP = {
55+
"cvss_v3": "cvssv3.1",
56+
"cvss_v4": "cvssv4",
57+
"ubuntu": "ubuntu-priority",
58+
}
59+
5460

5561
def parse_advisory_data_v3(
5662
raw_data: dict, supported_ecosystems, advisory_url: str, advisory_text: str
@@ -67,9 +73,10 @@ def parse_advisory_data_v3(
6773
details = raw_data.get("details") or ""
6874
summary = build_description(summary=summary, description=details)
6975
aliases = raw_data.get("aliases") or []
76+
aliases.extend(raw_data.get("upstream", []))
7077

7178
date_published = get_published_date(raw_data=raw_data)
72-
severities = list(get_severities(raw_data=raw_data))
79+
severities = list(get_severities(raw_data=raw_data, url=advisory_url))
7380
references = get_references_v2(raw_data=raw_data)
7481

7582
patches = []
@@ -236,29 +243,38 @@ def get_published_date(raw_data):
236243
return published and dateparser.parse(date_string=published)
237244

238245

239-
def get_severities(raw_data) -> Iterable[VulnerabilitySeverity]:
240-
"""
241-
Yield VulnerabilitySeverity extracted from a mapping of OSV ``raw_data``
242-
"""
246+
def get_severities(raw_data, url) -> Iterable[VulnerabilitySeverity]:
247+
"""Yield VulnerabilitySeverity extracted from a mapping of OSV ``raw_data``"""
243248
try:
244249
for severity in raw_data.get("severity") or []:
245-
vector = severity.get("score")
246-
valid_vector = vector[:-1] if vector and vector.endswith("/") else vector
247-
248-
if severity.get("type") == "CVSS_V3":
249-
system = SCORING_SYSTEMS["cvssv3.1"]
250-
score = system.compute(valid_vector)
251-
yield VulnerabilitySeverity(system=system, value=score, scoring_elements=vector)
252-
253-
elif severity.get("type") == "CVSS_V4":
254-
system = SCORING_SYSTEMS["cvssv4"]
255-
score = system.compute(valid_vector)
256-
yield VulnerabilitySeverity(system=system, value=score, scoring_elements=vector)
257-
258-
else:
250+
severity_type = severity.get("type")
251+
value = severity.get("score")
252+
severity_type = severity_type.lower()
253+
scoring_element = None
254+
255+
if (
256+
severity_type not in SCORING_SYSTEMS
257+
and severity_type not in OSV_TO_VCIO_SEVERITY_MAP
258+
):
259259
logger.error(
260260
f"Unsupported severity type: {severity!r} for OSV id: {raw_data.get('id')!r}"
261261
)
262+
continue
263+
264+
severity_type = OSV_TO_VCIO_SEVERITY_MAP.get(severity_type, severity_type)
265+
system = SCORING_SYSTEMS[severity_type]
266+
267+
if severity_type in ["cvssv3.1", "cvssv4"]:
268+
scoring_element = value
269+
valid_vector = value[:-1] if value and value.endswith("/") else value
270+
value = system.compute(valid_vector)
271+
272+
yield VulnerabilitySeverity(
273+
system=system,
274+
value=value,
275+
scoring_elements=scoring_element,
276+
url=url,
277+
)
262278
except (CVSS3MalformedError, CVSS4MalformedError) as e:
263279
logger.error(f"Invalid severity {e}")
264280

@@ -302,10 +318,11 @@ def get_affected_purl(affected_pkg, raw_id):
302318
data and a ``raw_id``.
303319
"""
304320
package = affected_pkg.get("package") or {}
305-
purl = package.get("purl")
306-
if purl:
321+
if purl := package.get("purl"):
307322
try:
308-
purl = PackageURL.from_string(purl)
323+
purl_dict = PackageURL.from_string(purl).to_dict()
324+
del purl_dict["version"]
325+
purl = PackageURL(**purl_dict)
309326
except ValueError:
310327
logger.error(
311328
f"Invalid PackageURL: {purl!r} for OSV "
@@ -314,12 +331,17 @@ def get_affected_purl(affected_pkg, raw_id):
314331
else:
315332
ecosys = package.get("ecosystem")
316333
name = package.get("name")
334+
namespace = ""
335+
317336
if ecosys and name:
318337
ecosys = ecosys.lower()
319338
purl_type = PURL_TYPE_BY_OSV_ECOSYSTEM.get(ecosys)
339+
if ecosys.startswith("ubuntu"):
340+
purl_type = "deb"
341+
namespace = "ubuntu"
342+
320343
if not purl_type:
321344
return
322-
namespace = ""
323345
if purl_type == "maven":
324346
namespace, _, name = name.partition(":")
325347

vulnerabilities/severity_systems.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ def get(self, scoring_elements: str) -> dict:
196196
"Low",
197197
]
198198

199+
UBUNTU_PRIORITY = ScoringSystem(
200+
identifier="ubuntu-priority",
201+
name="Ubuntu Priority",
202+
url="https://ubuntu.com/security/cves/about#priority",
203+
)
204+
UBUNTU_PRIORITY.choices = [
205+
"Critical",
206+
"High",
207+
"Medium",
208+
"Low",
209+
"Negligible",
210+
]
211+
199212

200213
@dataclasses.dataclass(order=True)
201214
class EPSSScoringSystem(ScoringSystem):
@@ -239,5 +252,6 @@ def get(self, scoring_elements: str):
239252
EPSS,
240253
SSVC,
241254
OPENSSL,
255+
UBUNTU_PRIORITY,
242256
)
243257
}

vulnerabilities/tests/pipelines/v2_importers/test_openssl_importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def setUp(self):
2626
self.logger = TestLogger()
2727

2828
@patch("vulnerabilities.pipelines.v2_importers.openssl_importer.OpenSSLImporterPipeline.clone")
29-
def test_redhat_advisories_v2(self, mock_clone):
29+
def test_openssl_advisories_v2(self, mock_clone):
3030
mock_clone.__name__ = "clone"
3131
pipeline = OpenSSLImporterPipeline()
3232
pipeline.advisory_path = TEST_DATA

0 commit comments

Comments
 (0)