Skip to content

Commit 918f85b

Browse files
committed
Add Grafana security advisory importer
Adds GrafanaImporterPipeline using the GitHub Security Advisory REST API. Covers grafana/grafana, grafana/loki, credativ/plutono and credativ/vali. - Parses GHSA IDs, CVE aliases, CVSS v3.1 scores, CWE weaknesses and version ranges from the API response - Normalizes space-separated version constraints to comma-separated format before passing to build_range_from_github_advisory_constraint - Skips advisories without a parseable version range rather than raising - Registers GrafanaImporterPipeline in IMPORTERS_REGISTRY - Includes JSON test fixtures and unit tests for parse_advisory_data Closes #1462 Signed-off-by: newklei <magmacicada@proton.me>
1 parent 2dbbd38 commit 918f85b

File tree

7 files changed

+569
-0
lines changed

7 files changed

+569
-0
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from vulnerabilities.pipelines.v2_importers import epss_importer_v2
5757
from vulnerabilities.pipelines.v2_importers import fireeye_importer_v2
5858
from vulnerabilities.pipelines.v2_importers import gentoo_importer as gentoo_importer_v2
59+
from vulnerabilities.pipelines.v2_importers import grafana_importer as grafana_importer_v2
5960
from vulnerabilities.pipelines.v2_importers import github_osv_importer as github_osv_importer_v2
6061
from vulnerabilities.pipelines.v2_importers import gitlab_importer as gitlab_importer_v2
6162
from vulnerabilities.pipelines.v2_importers import istio_importer as istio_importer_v2
@@ -110,6 +111,7 @@
110111
ruby_importer_v2.RubyImporterPipeline,
111112
epss_importer_v2.EPSSImporterPipeline,
112113
gentoo_importer_v2.GentooImporterPipeline,
114+
grafana_importer_v2.GrafanaImporterPipeline,
113115
nginx_importer_v2.NginxImporterPipeline,
114116
debian_importer_v2.DebianImporterPipeline,
115117
mattermost_importer_v2.MattermostImporterPipeline,
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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+
import re
13+
from datetime import datetime
14+
from datetime import timezone
15+
from typing import Iterable
16+
17+
from packageurl import PackageURL
18+
from univers.version_range import build_range_from_github_advisory_constraint
19+
20+
from vulnerabilities.importer import AdvisoryDataV2
21+
from vulnerabilities.importer import AffectedPackageV2
22+
from vulnerabilities.importer import ReferenceV2
23+
from vulnerabilities.importer import VulnerabilitySeverity
24+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
25+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
26+
from vulnerabilities.utils import fetch_response
27+
from vulnerabilities.utils import get_cwe_id
28+
29+
logger = logging.getLogger(__name__)
30+
31+
# Repos tracked per issue #1462: grafana/grafana, grafana/loki,
32+
# credativ/plutono (Grafana fork), credativ/vali (Loki fork).
33+
GRAFANA_REPOS = [
34+
("grafana", "grafana", "golang", "github.com/grafana/grafana"),
35+
("grafana", "loki", "golang", "github.com/grafana/loki"),
36+
("credativ", "plutono", "golang", "github.com/credativ/plutono"),
37+
("credativ", "vali", "golang", "github.com/credativ/vali"),
38+
]
39+
40+
GITHUB_ADVISORY_API = (
41+
"https://api.github.com/repos/{owner}/{repo}/security-advisories?per_page=100&page={page}"
42+
)
43+
44+
45+
class GrafanaImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
46+
"""
47+
Pipeline-based importer for Grafana security advisories from the GitHub
48+
Security Advisory REST API. Covers grafana/grafana, grafana/loki,
49+
credativ/plutono, and credativ/vali.
50+
"""
51+
52+
pipeline_id = "grafana_importer"
53+
spdx_license_expression = "Apache-2.0"
54+
license_url = "https://github.com/grafana/grafana/blob/main/LICENSE"
55+
repo_url = "https://github.com/grafana/grafana"
56+
precedence = 200
57+
58+
@classmethod
59+
def steps(cls):
60+
return (cls.collect_and_store_advisories,)
61+
62+
def advisories_count(self) -> int:
63+
count = 0
64+
for owner, repo, _, _ in GRAFANA_REPOS:
65+
page = 1
66+
while True:
67+
url = GITHUB_ADVISORY_API.format(owner=owner, repo=repo, page=page)
68+
try:
69+
advisories = fetch_response(url).json()
70+
except Exception as e:
71+
logger.error("Failed to fetch advisories from %s: %s", url, e)
72+
break
73+
if not advisories:
74+
break
75+
count += sum(1 for a in advisories if a.get("state") == "published")
76+
if len(advisories) < 100:
77+
break
78+
page += 1
79+
return count
80+
81+
def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
82+
for owner, repo, purl_type, purl_namespace in GRAFANA_REPOS:
83+
yield from fetch_grafana_advisories(
84+
owner=owner,
85+
repo=repo,
86+
purl_type=purl_type,
87+
purl_namespace=purl_namespace,
88+
)
89+
90+
91+
def fetch_grafana_advisories(
92+
owner: str,
93+
repo: str,
94+
purl_type: str,
95+
purl_namespace: str,
96+
) -> Iterable[AdvisoryDataV2]:
97+
"""
98+
Paginate through the GitHub Security Advisory REST API for the given
99+
owner/repo and yield parsed AdvisoryDataV2 objects.
100+
"""
101+
page = 1
102+
while True:
103+
url = GITHUB_ADVISORY_API.format(owner=owner, repo=repo, page=page)
104+
try:
105+
advisories = fetch_response(url).json()
106+
except Exception as e:
107+
logger.error("Failed to fetch advisories from %s: %s", url, e)
108+
break
109+
if not advisories:
110+
break
111+
for advisory in advisories:
112+
if advisory.get("state") != "published":
113+
continue
114+
parsed = parse_advisory_data(
115+
advisory=advisory,
116+
purl_type=purl_type,
117+
purl_namespace=purl_namespace,
118+
)
119+
if parsed:
120+
yield parsed
121+
if len(advisories) < 100:
122+
break
123+
page += 1
124+
125+
126+
def parse_advisory_data(advisory: dict, purl_type: str, purl_namespace: str):
127+
"""
128+
Parse a GitHub Security Advisory REST API response for a Grafana repo and
129+
return an AdvisoryDataV2 object, or None if parsing fails.
130+
131+
``advisory_id`` is set to the GHSA ID; any CVE ID goes into ``aliases``.
132+
Version ranges from the API (space-separated constraints) are normalized to
133+
comma-separated format before being passed to
134+
``build_range_from_github_advisory_constraint``.
135+
136+
>>> advisory = {
137+
... "ghsa_id": "GHSA-7rqg-hjwc-6mjf",
138+
... "cve_id": "CVE-2023-22462",
139+
... "html_url": "https://github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjf",
140+
... "summary": "Stored XSS in Text plugin",
141+
... "description": "An attacker needs Editor role.",
142+
... "severity": "medium",
143+
... "state": "published",
144+
... "published_at": "2023-03-01T08:59:53Z",
145+
... "vulnerabilities": [
146+
... {
147+
... "package": {"ecosystem": "", "name": "github.com/grafana/grafana"},
148+
... "vulnerable_version_range": ">=9.2.0 <9.2.10",
149+
... "patched_versions": "9.2.10",
150+
... "vulnerable_functions": []
151+
... }
152+
... ],
153+
... "cvss_severities": {
154+
... "cvss_v3": {"vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:N", "score": 6.4},
155+
... "cvss_v4": {"vector_string": None, "score": None}
156+
... },
157+
... "cwes": [{"cwe_id": "CWE-79", "name": "Cross-site Scripting"}],
158+
... "identifiers": [
159+
... {"value": "GHSA-7rqg-hjwc-6mjf", "type": "GHSA"},
160+
... {"value": "CVE-2023-22462", "type": "CVE"}
161+
... ]
162+
... }
163+
>>> result = parse_advisory_data(advisory, "golang", "github.com/grafana/grafana")
164+
>>> result.advisory_id
165+
'GHSA-7rqg-hjwc-6mjf'
166+
>>> result.aliases
167+
['CVE-2023-22462']
168+
>>> result.summary
169+
'Stored XSS in Text plugin'
170+
"""
171+
ghsa_id = advisory.get("ghsa_id") or ""
172+
cve_id = advisory.get("cve_id") or ""
173+
html_url = advisory.get("html_url") or ""
174+
summary = advisory.get("summary") or ""
175+
published_at = advisory.get("published_at") or ""
176+
177+
if not ghsa_id:
178+
logger.error("Advisory has no GHSA ID, skipping.")
179+
return None
180+
181+
aliases = []
182+
if cve_id:
183+
aliases.append(cve_id)
184+
185+
date_published = None
186+
if published_at:
187+
try:
188+
date_published = datetime.strptime(published_at, "%Y-%m-%dT%H:%M:%SZ").replace(
189+
tzinfo=timezone.utc
190+
)
191+
except ValueError:
192+
logger.error("Cannot parse date %r for %s", published_at, ghsa_id)
193+
194+
cvss_v3 = (advisory.get("cvss_severities") or {}).get("cvss_v3") or {}
195+
cvss_vector = cvss_v3.get("vector_string") or ""
196+
cvss_score = cvss_v3.get("score")
197+
198+
severities = []
199+
if cvss_vector:
200+
severities.append(
201+
VulnerabilitySeverity(
202+
system=SCORING_SYSTEMS["cvssv3.1"],
203+
value=str(cvss_score) if cvss_score is not None else "",
204+
scoring_elements=cvss_vector,
205+
)
206+
)
207+
208+
references = []
209+
if html_url:
210+
references.append(ReferenceV2(url=html_url))
211+
212+
weaknesses = []
213+
for cwe in advisory.get("cwes") or []:
214+
cwe_string = cwe.get("cwe_id") or ""
215+
if cwe_string:
216+
cwe_int = get_cwe_id(cwe_string)
217+
if cwe_int:
218+
weaknesses.append(cwe_int)
219+
220+
affected_packages = []
221+
for vuln in advisory.get("vulnerabilities") or []:
222+
pkg_name = (vuln.get("package") or {}).get("name") or purl_namespace
223+
if not pkg_name:
224+
pkg_name = purl_namespace
225+
226+
raw_range = vuln.get("vulnerable_version_range") or ""
227+
version_range = None
228+
if raw_range:
229+
# Normalize space-separated constraints to comma-separated format.
230+
# Example: ">=9.2.0 <9.2.10 >=9.3.0 <9.3.4" -> ">=9.2.0, <9.2.10, >=9.3.0, <9.3.4"
231+
normalized = re.sub(r"\s+(?=[<>!=])", ", ", raw_range.strip())
232+
try:
233+
version_range = build_range_from_github_advisory_constraint(
234+
purl_type, normalized
235+
)
236+
except Exception as e:
237+
logger.error(
238+
"Cannot parse version range %r for %s: %s", raw_range, ghsa_id, e
239+
)
240+
241+
if version_range is None:
242+
continue
243+
244+
purl = PackageURL(type=purl_type, namespace="", name=pkg_name)
245+
try:
246+
affected_packages.append(
247+
AffectedPackageV2(
248+
package=purl,
249+
affected_version_range=version_range,
250+
)
251+
)
252+
except ValueError as e:
253+
logger.error("Cannot create AffectedPackageV2 for %s: %s", ghsa_id, e)
254+
255+
return AdvisoryDataV2(
256+
advisory_id=ghsa_id,
257+
aliases=aliases,
258+
summary=summary,
259+
affected_packages=affected_packages,
260+
references=references,
261+
date_published=date_published,
262+
weaknesses=weaknesses,
263+
severities=severities,
264+
url=html_url,
265+
original_advisory_text=json.dumps(advisory, indent=2, ensure_ascii=False),
266+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"advisory_id": "GHSA-7rqg-hjwc-6mjf",
3+
"aliases": [
4+
"CVE-2023-22462"
5+
],
6+
"summary": "Stored XSS in Text plugin",
7+
"affected_packages": [
8+
{
9+
"package": {
10+
"type": "golang",
11+
"namespace": "",
12+
"name": "github.com/grafana/grafana",
13+
"version": "",
14+
"qualifiers": "",
15+
"subpath": ""
16+
},
17+
"affected_version_range": "vers:golang/>=9.2.0|<9.2.10",
18+
"fixed_version_range": null,
19+
"introduced_by_commit_patches": [],
20+
"fixed_by_commit_patches": []
21+
},
22+
{
23+
"package": {
24+
"type": "golang",
25+
"namespace": "",
26+
"name": "github.com/grafana/grafana",
27+
"version": "",
28+
"qualifiers": "",
29+
"subpath": ""
30+
},
31+
"affected_version_range": "vers:golang/>=9.3.0|<9.3.4",
32+
"fixed_version_range": null,
33+
"introduced_by_commit_patches": [],
34+
"fixed_by_commit_patches": []
35+
}
36+
],
37+
"references": [
38+
{
39+
"reference_id": "",
40+
"reference_type": "",
41+
"url": "https://github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjf"
42+
}
43+
],
44+
"patches": [],
45+
"severities": [
46+
{
47+
"system": "cvssv3.1",
48+
"value": "6.4",
49+
"scoring_elements": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:N"
50+
}
51+
],
52+
"date_published": "2023-03-01T08:59:53+00:00",
53+
"weaknesses": [
54+
79
55+
],
56+
"url": "https://github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjf"
57+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"advisory_id": "GHSA-cvm3-pp85-m5vm",
3+
"aliases": [],
4+
"summary": "Loki unintentionally exposes Grafana Labs error tracking endpoint",
5+
"affected_packages": [
6+
{
7+
"package": {
8+
"type": "golang",
9+
"namespace": "",
10+
"name": "github.com/grafana/loki",
11+
"version": "",
12+
"qualifiers": "",
13+
"subpath": ""
14+
},
15+
"affected_version_range": "vers:golang/>=2.6.0|<2.6.6",
16+
"fixed_version_range": null,
17+
"introduced_by_commit_patches": [],
18+
"fixed_by_commit_patches": []
19+
}
20+
],
21+
"references": [
22+
{
23+
"reference_id": "",
24+
"reference_type": "",
25+
"url": "https://github.com/grafana/loki/security/advisories/GHSA-cvm3-pp85-m5vm"
26+
}
27+
],
28+
"patches": [],
29+
"severities": [],
30+
"date_published": "2023-01-15T12:00:00+00:00",
31+
"weaknesses": [],
32+
"url": "https://github.com/grafana/loki/security/advisories/GHSA-cvm3-pp85-m5vm"
33+
}

0 commit comments

Comments
 (0)