Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions vulnerabilities/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from vulnerabilities.pipelines.v2_importers import gentoo_importer as gentoo_importer_v2
from vulnerabilities.pipelines.v2_importers import github_osv_importer as github_osv_importer_v2
from vulnerabilities.pipelines.v2_importers import gitlab_importer as gitlab_importer_v2
from vulnerabilities.pipelines.v2_importers import grafana_importer as grafana_importer_v2
from vulnerabilities.pipelines.v2_importers import istio_importer as istio_importer_v2
from vulnerabilities.pipelines.v2_importers import mattermost_importer as mattermost_importer_v2
from vulnerabilities.pipelines.v2_importers import mozilla_importer as mozilla_importer_v2
Expand Down Expand Up @@ -110,6 +111,7 @@
ruby_importer_v2.RubyImporterPipeline,
epss_importer_v2.EPSSImporterPipeline,
gentoo_importer_v2.GentooImporterPipeline,
grafana_importer_v2.GrafanaImporterPipeline,
nginx_importer_v2.NginxImporterPipeline,
debian_importer_v2.DebianImporterPipeline,
mattermost_importer_v2.MattermostImporterPipeline,
Expand Down
192 changes: 192 additions & 0 deletions vulnerabilities/pipelines/v2_importers/grafana_importer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# VulnerableCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#

import json
import logging
import re
from typing import Iterable

import dateparser
from packageurl import PackageURL
from univers.version_range import build_range_from_github_advisory_constraint

from vulnerabilities.importer import AdvisoryDataV2
from vulnerabilities.importer import AffectedPackageV2
from vulnerabilities.importer import ReferenceV2
from vulnerabilities.importer import VulnerabilitySeverity
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
from vulnerabilities.severity_systems import SCORING_SYSTEMS
from vulnerabilities.utils import fetch_response
from vulnerabilities.utils import get_cwe_id

logger = logging.getLogger(__name__)

# repos from issue #1462: grafana, loki, plutono (fork), vali (fork)
GRAFANA_REPOS = [
("grafana", "grafana", "golang", "github.com/grafana/grafana"),
("grafana", "loki", "golang", "github.com/grafana/loki"),
("credativ", "plutono", "golang", "github.com/credativ/plutono"),
("credativ", "vali", "golang", "github.com/credativ/vali"),
]

GITHUB_ADVISORY_API = (
"https://api.github.com/repos/{owner}/{repo}/security-advisories?per_page=100&page={page}"
)


class GrafanaImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
"""Collect Grafana security advisories from the GitHub Security Advisory API."""

pipeline_id = "grafana_importer"
spdx_license_expression = "Apache-2.0"
license_url = "https://github.com/grafana/grafana/blob/main/LICENSE"
precedence = 200

@classmethod
def steps(cls):
return (cls.collect_and_store_advisories,)

def advisories_count(self) -> int:
return 0

def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
for owner, repo, purl_type, purl_namespace in GRAFANA_REPOS:
yield from fetch_grafana_advisories(
owner=owner,
repo=repo,
purl_type=purl_type,
purl_namespace=purl_namespace,
)


def fetch_grafana_advisories(
owner: str,
repo: str,
purl_type: str,
purl_namespace: str,
) -> Iterable[AdvisoryDataV2]:
"""Paginate GitHub advisory API for the given repo and yield parsed advisories."""
page = 1
while True:
url = GITHUB_ADVISORY_API.format(owner=owner, repo=repo, page=page)
try:
advisories = fetch_response(url).json()
except Exception as e:
logger.error("Failed to fetch advisories from %s: %s", url, e)
break
if not advisories:
break
for advisory in advisories:
if advisory.get("state") != "published":
continue
parsed = parse_advisory_data(
advisory=advisory,
purl_type=purl_type,
purl_namespace=purl_namespace,
)
if parsed:
yield parsed
if len(advisories) < 100:
break
page += 1


def parse_advisory_data(advisory: dict, purl_type: str, purl_namespace: str):
"""Parse a GitHub advisory dict; return None if GHSA ID is missing."""
ghsa_id = advisory.get("ghsa_id") or ""
cve_id = advisory.get("cve_id") or ""
html_url = advisory.get("html_url") or ""
summary = advisory.get("summary") or ""
published_at = advisory.get("published_at") or ""

if not ghsa_id:
logger.error("Advisory has no GHSA ID, skipping.")
return None

aliases = []
if cve_id:
aliases.append(cve_id)

date_published = None
if published_at:
date_published = dateparser.parse(published_at)
if date_published is None:
logger.warning("Could not parse date %r for advisory %s", published_at, ghsa_id)

cvss_v3 = (advisory.get("cvss_severities") or {}).get("cvss_v3") or {}
cvss_vector = cvss_v3.get("vector_string") or ""
cvss_score = cvss_v3.get("score")

severities = []
if cvss_vector:
system = (
SCORING_SYSTEMS["cvssv3.1"]
if cvss_vector.startswith("CVSS:3.1/")
else SCORING_SYSTEMS["cvssv3"]
)
severities.append(
VulnerabilitySeverity(
system=system,
value=str(cvss_score) if cvss_score is not None else "",
scoring_elements=cvss_vector,
)
)

references = []
if html_url:
references.append(ReferenceV2(url=html_url))

weaknesses = []
for cwe in advisory.get("cwes") or []:
cwe_string = cwe.get("cwe_id") or ""
if cwe_string:
cwe_int = get_cwe_id(cwe_string)
if cwe_int:
weaknesses.append(cwe_int)

affected_packages = []
for vuln in advisory.get("vulnerabilities") or []:
pkg_name = (vuln.get("package") or {}).get("name") or purl_namespace

raw_range = vuln.get("vulnerable_version_range") or ""
version_range = None
if raw_range:
# space-separated API constraints must be comma-separated for range parsing
normalized = re.sub(r"\s+(?=[<>!=])", ", ", raw_range.strip())
try:
version_range = build_range_from_github_advisory_constraint(purl_type, normalized)
except Exception as e:
logger.error("Cannot parse version range %r for %s: %s", raw_range, ghsa_id, e)

if version_range is None:
continue

purl = PackageURL(type=purl_type, namespace="", name=pkg_name)
try:
affected_packages.append(
AffectedPackageV2(
package=purl,
affected_version_range=version_range,
)
)
except ValueError as e:
logger.error("Cannot create AffectedPackageV2 for %s: %s", ghsa_id, e)

return AdvisoryDataV2(
advisory_id=ghsa_id,
aliases=aliases,
summary=summary,
affected_packages=affected_packages,
references=references,
date_published=date_published,
weaknesses=weaknesses,
severities=severities,
url=html_url,
original_advisory_text=json.dumps(advisory, indent=2, ensure_ascii=False),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"advisory_id": "GHSA-7rqg-hjwc-6mjf",
"aliases": [
"CVE-2023-22462"
],
"summary": "Stored XSS in Text plugin",
"affected_packages": [
{
"package": {
"type": "golang",
"namespace": "",
"name": "github.com/grafana/grafana",
"version": "",
"qualifiers": "",
"subpath": ""
},
"affected_version_range": "vers:golang/>=9.2.0|<9.2.10",
"fixed_version_range": null,
"introduced_by_commit_patches": [],
"fixed_by_commit_patches": []
},
{
"package": {
"type": "golang",
"namespace": "",
"name": "github.com/grafana/grafana",
"version": "",
"qualifiers": "",
"subpath": ""
},
"affected_version_range": "vers:golang/>=9.3.0|<9.3.4",
"fixed_version_range": null,
"introduced_by_commit_patches": [],
"fixed_by_commit_patches": []
}
],
"references": [
{
"reference_id": "",
"reference_type": "",
"url": "https://github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjf"
}
],
"patches": [],
"severities": [
{
"system": "cvssv3.1",
"value": "6.4",
"scoring_elements": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:N"
}
],
"date_published": "2023-03-01T08:59:53+00:00",
"weaknesses": [
79
],
"url": "https://github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjf"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"advisory_id": "GHSA-cvm3-pp85-m5vm",
"aliases": [],
"summary": "Loki unintentionally exposes Grafana Labs error tracking endpoint",
"affected_packages": [
{
"package": {
"type": "golang",
"namespace": "",
"name": "github.com/grafana/loki",
"version": "",
"qualifiers": "",
"subpath": ""
},
"affected_version_range": "vers:golang/>=2.6.0|<2.6.6",
"fixed_version_range": null,
"introduced_by_commit_patches": [],
"fixed_by_commit_patches": []
}
],
"references": [
{
"reference_id": "",
"reference_type": "",
"url": "https://github.com/grafana/loki/security/advisories/GHSA-cvm3-pp85-m5vm"
}
],
"patches": [],
"severities": [],
"date_published": "2023-01-15T12:00:00+00:00",
"weaknesses": [],
"url": "https://github.com/grafana/loki/security/advisories/GHSA-cvm3-pp85-m5vm"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"ghsa_id": "GHSA-7rqg-hjwc-6mjf",
"cve_id": "CVE-2023-22462",
"url": "https://api.github.com/repos/grafana/grafana/security-advisories/GHSA-7rqg-hjwc-6mjf",
"html_url": "https://github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjf",
"summary": "Stored XSS in Text plugin",
"description": "An attacker needs Editor role in order to change a text panel to include a script that will execute malicious code.",
"severity": "medium",
"author": null,
"publisher": null,
"identifiers": [
{
"value": "GHSA-7rqg-hjwc-6mjf",
"type": "GHSA"
},
{
"value": "CVE-2023-22462",
"type": "CVE"
}
],
"state": "published",
"created_at": "2023-03-01T08:45:00Z",
"updated_at": "2023-05-09T18:31:28Z",
"published_at": "2023-03-01T08:59:53Z",
"closed_at": null,
"withdrawn_at": null,
"submission": null,
"vulnerabilities": [
{
"package": {
"ecosystem": "",
"name": "github.com/grafana/grafana"
},
"vulnerable_version_range": ">=9.2.0 <9.2.10",
"patched_versions": "9.2.10",
"vulnerable_functions": []
},
{
"package": {
"ecosystem": "",
"name": "github.com/grafana/grafana"
},
"vulnerable_version_range": ">=9.3.0 <9.3.4",
"patched_versions": "9.3.4",
"vulnerable_functions": []
}
],
"cvss_severities": {
"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
},
"cvss_v4": {
"vector_string": null,
"score": null
}
},
"cwes": [
{
"cwe_id": "CWE-79",
"name": "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
}
],
"credits": [],
"permalink": "https://github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjf",
"cve_id_url": "https://www.cve.org/CVERecord?id=CVE-2023-22462",
"private_fork": null
}
Loading