Skip to content

Commit 0e0f78b

Browse files
committed
feat: add Collabora Online security advisory importer for issue #1899
Adds CollaboraImporterPipeline to collect published security advisories from the GitHub Security Advisory REST API for CollaboraOnline/online. Parses GHSA id, CVE alias, CVSS 3.x severity (version detected from vector prefix), CWE weaknesses, date, and reference URL. Pagination is handled via the Link header cursor returned by the GitHub API. Includes 5 unit tests with real API fixture data covering CVSS 3.1, CVSS 3.0 with CWE, missing GHSA id, missing CVE id, and missing CVSS. Signed-off-by: newklei <magmacicada@proton.me>
1 parent 2dbbd38 commit 0e0f78b

File tree

5 files changed

+430
-0
lines changed

5 files changed

+430
-0
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from vulnerabilities.pipelines.v2_importers import apache_kafka_importer as apache_kafka_importer_v2
4848
from vulnerabilities.pipelines.v2_importers import apache_tomcat_importer as apache_tomcat_v2
4949
from vulnerabilities.pipelines.v2_importers import archlinux_importer as archlinux_importer_v2
50+
from vulnerabilities.pipelines.v2_importers import collabora_importer as collabora_importer_v2
5051
from vulnerabilities.pipelines.v2_importers import collect_fix_commits as collect_fix_commits_v2
5152
from vulnerabilities.pipelines.v2_importers import curl_importer as curl_importer_v2
5253
from vulnerabilities.pipelines.v2_importers import debian_importer as debian_importer_v2
@@ -118,6 +119,7 @@
118119
retiredotnet_importer_v2.RetireDotnetImporterPipeline,
119120
ubuntu_osv_importer_v2.UbuntuOSVImporterPipeline,
120121
alpine_linux_importer_v2.AlpineLinuxImporterPipeline,
122+
collabora_importer_v2.CollaboraImporterPipeline,
121123
nvd_importer.NVDImporterPipeline,
122124
github_importer.GitHubAPIImporterPipeline,
123125
gitlab_importer.GitLabImporterPipeline,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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 json
11+
import logging
12+
from typing import Iterable
13+
14+
import dateparser
15+
import requests
16+
17+
from vulnerabilities.importer import AdvisoryDataV2
18+
from vulnerabilities.importer import ReferenceV2
19+
from vulnerabilities.importer import VulnerabilitySeverity
20+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
21+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
22+
23+
logger = logging.getLogger(__name__)
24+
25+
COLLABORA_URL = "https://api.github.com/repos/CollaboraOnline/online/security-advisories"
26+
27+
28+
class CollaboraImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
29+
"""Collect Collabora Online security advisories from the GitHub Security Advisory API."""
30+
31+
pipeline_id = "collabora_importer"
32+
spdx_license_expression = "LicenseRef-scancode-proprietary-license"
33+
license_url = "https://github.com/CollaboraOnline/online/security/advisories"
34+
precedence = 200
35+
36+
@classmethod
37+
def steps(cls):
38+
return (cls.collect_and_store_advisories,)
39+
40+
def advisories_count(self) -> int:
41+
return 0
42+
43+
def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
44+
url = COLLABORA_URL
45+
params = {"state": "published", "per_page": 100}
46+
while url:
47+
try:
48+
resp = requests.get(url, params=params, timeout=30)
49+
resp.raise_for_status()
50+
except Exception as e:
51+
logger.error("Failed to fetch Collabora advisories from %s: %s", url, e)
52+
break
53+
for item in resp.json():
54+
advisory = parse_advisory(item)
55+
if advisory:
56+
yield advisory
57+
# cursor is already embedded in the next URL
58+
url = resp.links.get("next", {}).get("url")
59+
params = None
60+
61+
62+
def parse_advisory(data: dict):
63+
"""Parse a GitHub security advisory object; return None if the GHSA ID is missing."""
64+
ghsa_id = data.get("ghsa_id") or ""
65+
if not ghsa_id:
66+
return None
67+
68+
cve_id = data.get("cve_id") or ""
69+
aliases = [cve_id] if cve_id else []
70+
71+
summary = data.get("summary") or ""
72+
html_url = data.get("html_url") or ""
73+
references = [ReferenceV2(url=html_url)] if html_url else []
74+
75+
date_published = None
76+
published_at = data.get("published_at") or ""
77+
if published_at:
78+
date_published = dateparser.parse(published_at)
79+
if date_published is None:
80+
logger.warning("Could not parse date %r for %s", published_at, ghsa_id)
81+
82+
severities = []
83+
cvss_v3 = (data.get("cvss_severities") or {}).get("cvss_v3") or {}
84+
cvss_vector = cvss_v3.get("vector_string") or ""
85+
cvss_score = cvss_v3.get("score")
86+
if cvss_vector and cvss_score:
87+
system = (
88+
SCORING_SYSTEMS["cvssv3.1"]
89+
if cvss_vector.startswith("CVSS:3.1/")
90+
else SCORING_SYSTEMS["cvssv3"]
91+
)
92+
severities.append(
93+
VulnerabilitySeverity(
94+
system=system,
95+
value=str(cvss_score),
96+
scoring_elements=cvss_vector,
97+
)
98+
)
99+
100+
weaknesses = []
101+
for cwe_str in data.get("cwe_ids") or []:
102+
# cwe_ids entries are like "CWE-79"; extract the integer part
103+
suffix = cwe_str[4:] if cwe_str.upper().startswith("CWE-") else ""
104+
if suffix.isdigit():
105+
weaknesses.append(int(suffix))
106+
107+
return AdvisoryDataV2(
108+
advisory_id=ghsa_id,
109+
aliases=aliases,
110+
summary=summary,
111+
affected_packages=[],
112+
references=references,
113+
date_published=date_published,
114+
severities=severities,
115+
weaknesses=weaknesses,
116+
url=html_url,
117+
original_advisory_text=json.dumps(data, indent=2, ensure_ascii=False),
118+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 json
11+
import os
12+
from unittest import TestCase
13+
14+
from vulnerabilities.pipelines.v2_importers.collabora_importer import parse_advisory
15+
16+
TEST_DATA = os.path.join(os.path.dirname(__file__), "test_data", "collabora")
17+
18+
19+
def load_json(filename):
20+
with open(os.path.join(TEST_DATA, filename), encoding="utf-8") as f:
21+
return json.load(f)
22+
23+
24+
class TestCollaboraImporter(TestCase):
25+
def test_parse_advisory_with_cvss31(self):
26+
# mock1: GHSA-68v6-r6qq-mmq2, CVSS 3.1 score 5.3, no CWEs
27+
data = load_json("collabora_mock1.json")
28+
advisory = parse_advisory(data)
29+
self.assertIsNotNone(advisory)
30+
self.assertEqual(advisory.advisory_id, "GHSA-68v6-r6qq-mmq2")
31+
self.assertIn("CVE-2026-23623", advisory.aliases)
32+
self.assertEqual(len(advisory.severities), 1)
33+
self.assertEqual(advisory.severities[0].value, "5.3")
34+
self.assertIn("CVSS:3.1/", advisory.severities[0].scoring_elements)
35+
self.assertEqual(advisory.weaknesses, [])
36+
self.assertEqual(len(advisory.references), 1)
37+
self.assertIsNotNone(advisory.date_published)
38+
39+
def test_parse_advisory_with_cvss30_and_cwe(self):
40+
# mock2: GHSA-7582-pwfh-3pwr, CVSS 3.0 score 9.0, CWE-79
41+
data = load_json("collabora_mock2.json")
42+
advisory = parse_advisory(data)
43+
self.assertIsNotNone(advisory)
44+
self.assertEqual(advisory.advisory_id, "GHSA-7582-pwfh-3pwr")
45+
self.assertIn("CVE-2023-34088", advisory.aliases)
46+
self.assertEqual(len(advisory.severities), 1)
47+
self.assertEqual(advisory.severities[0].value, "9.0")
48+
self.assertIn("CVSS:3.0/", advisory.severities[0].scoring_elements)
49+
self.assertEqual(advisory.weaknesses, [79])
50+
51+
def test_parse_advisory_missing_ghsa_id_returns_none(self):
52+
advisory = parse_advisory({"cve_id": "CVE-2024-0001", "summary": "test"})
53+
self.assertIsNone(advisory)
54+
55+
def test_parse_advisory_no_cve_id_has_empty_aliases(self):
56+
data = load_json("collabora_mock1.json")
57+
data = dict(data)
58+
data["cve_id"] = None
59+
advisory = parse_advisory(data)
60+
self.assertIsNotNone(advisory)
61+
self.assertEqual(advisory.aliases, [])
62+
63+
def test_parse_advisory_no_cvss_has_empty_severities(self):
64+
data = load_json("collabora_mock1.json")
65+
data = dict(data)
66+
data["cvss_severities"] = {
67+
"cvss_v3": {"vector_string": None, "score": None},
68+
"cvss_v4": None,
69+
}
70+
advisory = parse_advisory(data)
71+
self.assertIsNotNone(advisory)
72+
self.assertEqual(advisory.severities, [])
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
{
2+
"ghsa_id": "GHSA-68v6-r6qq-mmq2",
3+
"cve_id": "CVE-2026-23623",
4+
"url": "https://api.github.com/repos/CollaboraOnline/online/security-advisories/GHSA-68v6-r6qq-mmq2",
5+
"html_url": "https://github.com/CollaboraOnline/online/security/advisories/GHSA-68v6-r6qq-mmq2",
6+
"summary": "CVE-2026-23623 Authorization Bypass: ability to download read-only files in Collabora Online",
7+
"description": "### Summary\r\n\r\nA user with view-only rights and no download privileges can obtain a local copy of a shared file. Although there are no corresponding buttons in the interface, pressing Ctrl+Shift+S initiates the file download process. This allows the user to bypass the access restrictions and leads to unauthorized data retrieval.\r\n\r\n### Details\r\n\r\nNextcloud 31\r\nCollabora Online Development Edition 25.04.08.1\r\n\r\n### PoC\r\n\r\nIn the Nextcloud environment with integrated Collabora Online, UserA grants access to file A (format .xlsx) to UserB with view-only rights and an explicit prohibition on downloading.\r\n\r\nFor UserB:\r\n\r\n- there is no option to download the file in the Nextcloud web interface;\r\n- there are no “Download”, “Save as” or “Print” buttons in the Collabora Online web interface;\r\n- the file is available for viewing only, as specified in the access settings.\r\n\r\nHowever, using the Ctrl + Shift + S key combination in the Collabora Online web interface initiates the process of saving (downloading) the file. As a result, UserB receives a local copy of the original file, despite not having the appropriate access rights.\r\n\r\n### Impact\r\n\r\n- Violation of access control models.\r\n- Unauthorized distribution of confidential documents.\r\n- Risk of data leakage in corporate and regulated environments.\r\n- False sense of security for file owners who rely on “view only” mode.",
8+
"severity": "medium",
9+
"author": null,
10+
"publisher": {
11+
"login": "caolanm",
12+
"id": 833656,
13+
"node_id": "MDQ6VXNlcjgzMzY1Ng==",
14+
"avatar_url": "https://avatars.githubusercontent.com/u/833656?v=4",
15+
"gravatar_id": "",
16+
"url": "https://api.github.com/users/caolanm",
17+
"html_url": "https://github.com/caolanm",
18+
"followers_url": "https://api.github.com/users/caolanm/followers",
19+
"following_url": "https://api.github.com/users/caolanm/following{/other_user}",
20+
"gists_url": "https://api.github.com/users/caolanm/gists{/gist_id}",
21+
"starred_url": "https://api.github.com/users/caolanm/starred{/owner}{/repo}",
22+
"subscriptions_url": "https://api.github.com/users/caolanm/subscriptions",
23+
"organizations_url": "https://api.github.com/users/caolanm/orgs",
24+
"repos_url": "https://api.github.com/users/caolanm/repos",
25+
"events_url": "https://api.github.com/users/caolanm/events{/privacy}",
26+
"received_events_url": "https://api.github.com/users/caolanm/received_events",
27+
"type": "User",
28+
"user_view_type": "public",
29+
"site_admin": false
30+
},
31+
"identifiers": [
32+
{
33+
"value": "GHSA-68v6-r6qq-mmq2",
34+
"type": "GHSA"
35+
},
36+
{
37+
"value": "CVE-2026-23623",
38+
"type": "CVE"
39+
}
40+
],
41+
"state": "published",
42+
"created_at": null,
43+
"updated_at": "2026-02-05T11:20:00Z",
44+
"published_at": "2026-02-05T11:20:00Z",
45+
"closed_at": null,
46+
"withdrawn_at": null,
47+
"submission": null,
48+
"vulnerabilities": [
49+
{
50+
"package": {
51+
"ecosystem": "",
52+
"name": "Collabora Online Development Edition"
53+
},
54+
"vulnerable_version_range": "< 25.04.08.2",
55+
"patched_versions": "25.04.08.2",
56+
"vulnerable_functions": [
57+
58+
]
59+
},
60+
{
61+
"package": {
62+
"ecosystem": "",
63+
"name": "Collabora Online"
64+
},
65+
"vulnerable_version_range": "< 25.04.7.5",
66+
"patched_versions": "25.04.7.5",
67+
"vulnerable_functions": [
68+
69+
]
70+
},
71+
{
72+
"package": {
73+
"ecosystem": "",
74+
"name": "Collabora Online"
75+
},
76+
"vulnerable_version_range": "< 24.04.17.3",
77+
"patched_versions": "24.04.17.3",
78+
"vulnerable_functions": [
79+
80+
]
81+
},
82+
{
83+
"package": {
84+
"ecosystem": "",
85+
"name": "Collabora Online"
86+
},
87+
"vulnerable_version_range": "< 23.05.20.1",
88+
"patched_versions": "23.05.20.1",
89+
"vulnerable_functions": [
90+
91+
]
92+
}
93+
],
94+
"cvss_severities": {
95+
"cvss_v3": {
96+
"vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
97+
"score": 5.3
98+
},
99+
"cvss_v4": {
100+
"vector_string": null,
101+
"score": null
102+
}
103+
},
104+
"cwes": [
105+
106+
],
107+
"cwe_ids": [
108+
109+
],
110+
"credits": [
111+
112+
],
113+
"credits_detailed": [
114+
115+
],
116+
"collaborating_users": null,
117+
"collaborating_teams": null,
118+
"private_fork": null,
119+
"cvss": {
120+
"vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
121+
"score": 5.3
122+
}
123+
}

0 commit comments

Comments
 (0)