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 @@ -50,6 +50,7 @@
from vulnerabilities.pipelines.v2_importers import collect_fix_commits as collect_fix_commits_v2
from vulnerabilities.pipelines.v2_importers import curl_importer as curl_importer_v2
from vulnerabilities.pipelines.v2_importers import debian_importer as debian_importer_v2
from vulnerabilities.pipelines.v2_importers import eclipse_importer as eclipse_importer_v2
from vulnerabilities.pipelines.v2_importers import (
elixir_security_importer as elixir_security_importer_v2,
)
Expand Down Expand Up @@ -99,6 +100,7 @@
xen_importer_v2.XenImporterPipeline,
curl_importer_v2.CurlImporterPipeline,
oss_fuzz_v2.OSSFuzzImporterPipeline,
eclipse_importer_v2.EclipseImporterPipeline,
istio_importer_v2.IstioImporterPipeline,
postgresql_importer_v2.PostgreSQLImporterPipeline,
mozilla_importer_v2.MozillaImporterPipeline,
Expand Down
104 changes: 104 additions & 0 deletions vulnerabilities/pipelines/v2_importers/eclipse_importer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#
# 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
from typing import Iterable

import dateparser
import requests

from vulnerabilities.importer import AdvisoryDataV2
from vulnerabilities.importer import ReferenceV2
from vulnerabilities.importer import VulnerabilitySeverity
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
from vulnerabilities.severity_systems import GENERIC

logger = logging.getLogger(__name__)

ECLIPSE_API_URL = "https://api.eclipse.org/cve"


class EclipseImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
"""Collect Eclipse Foundation security advisories via the Eclipse CVE API."""

pipeline_id = "eclipse_importer"
spdx_license_expression = "LicenseRef-scancode-proprietary-license"
license_url = "https://www.eclipse.org/security/"
precedence = 200

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

def fetch(self):
self.log(f"Fetch `{ECLIPSE_API_URL}`")
resp = requests.get(ECLIPSE_API_URL, timeout=30)
resp.raise_for_status()
self.advisories_data = resp.json()

def advisories_count(self):
return len(self.advisories_data)

def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
for entry in self.advisories_data:
advisory = parse_advisory(entry)
if advisory:
yield advisory


def parse_advisory(entry: dict):
advisory_id = entry.get("id") or ""
if not advisory_id:
return None

date_published = None
raw_date = entry.get("date_published") or ""
if raw_date:
date_published = dateparser.parse(
raw_date,
settings={"TIMEZONE": "UTC", "RETURN_AS_TIMEZONE_AWARE": True, "TO_TIMEZONE": "UTC"},
)
if date_published is None:
logger.warning("Could not parse date %r for %s", raw_date, advisory_id)

summary_obj = entry.get("summary")
summary = summary_obj.get("content") or "" if isinstance(summary_obj, dict) else ""

references = []
for url in [
entry.get("live_link") or "",
entry.get("request_link") or "",
entry.get("cve_pull_request") or "",
]:
if url:
references.append(ReferenceV2(url=url))

severities = []
cvss = entry.get("cvss")
if cvss is not None:
severities.append(VulnerabilitySeverity(system=GENERIC, value=str(cvss)))

advisory_url = entry.get("live_link") or ""

return AdvisoryDataV2(
advisory_id=advisory_id,
aliases=[],
summary=summary,
affected_packages=[],
references=references,
date_published=date_published,
weaknesses=[],
severities=severities,
url=advisory_url,
original_advisory_text=json.dumps(entry, indent=2, ensure_ascii=False),
)
116 changes: 116 additions & 0 deletions vulnerabilities/tests/pipelines/v2_importers/test_eclipse_importer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#
# 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
from pathlib import Path
from unittest import TestCase
from unittest.mock import MagicMock
from unittest.mock import patch

import requests

from vulnerabilities.pipelines.v2_importers.eclipse_importer import EclipseImporterPipeline
from vulnerabilities.pipelines.v2_importers.eclipse_importer import parse_advisory

TEST_DATA = Path(__file__).parent.parent.parent / "test_data" / "eclipse"

with open(TEST_DATA / "eclipse_api_sample.json") as f:
SAMPLE_DATA = json.load(f)

ENTRY_WITH_CVSS = SAMPLE_DATA[0]
ENTRY_WITHOUT_CVSS = SAMPLE_DATA[1]
ENTRY_WITHOUT_SUMMARY = SAMPLE_DATA[2]


class TestParseAdvisory(TestCase):
def test_parses_id_and_summary(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
assert advisory.advisory_id == "CVE-2017-7649"
assert "Kura" in advisory.summary

def test_parses_date(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
assert advisory.date_published is not None
assert advisory.date_published.year == 2017

def test_cvss_stored_as_generic_severity(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
assert len(advisory.severities) == 1
assert advisory.severities[0].value == "9.8"

def test_missing_cvss_yields_empty_severities(self):
advisory = parse_advisory(ENTRY_WITHOUT_CVSS)
assert advisory.severities == []

def test_missing_summary_yields_empty_string(self):
advisory = parse_advisory(ENTRY_WITHOUT_SUMMARY)
assert advisory.summary == ""

def test_references_populated(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
urls = [r.url for r in advisory.references]
assert "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7649" in urls
assert "https://bugs.eclipse.org/bugs/show_bug.cgi?id=514681" in urls

def test_cve_pull_request_added_as_reference(self):
advisory = parse_advisory(ENTRY_WITHOUT_CVSS)
urls = [r.url for r in advisory.references]
assert "https://github.com/CVEProject/cvelist/pull/932" in urls

def test_empty_cve_pull_request_not_added(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
urls = [r.url for r in advisory.references]
assert "" not in urls

def test_missing_id_returns_none(self):
assert parse_advisory({}) is None
assert parse_advisory({"id": ""}) is None

def test_original_advisory_text_is_json(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
parsed = json.loads(advisory.original_advisory_text)
assert parsed["id"] == "CVE-2017-7649"

def test_affected_packages_empty(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
assert advisory.affected_packages == []

def test_weaknesses_empty(self):
advisory = parse_advisory(ENTRY_WITH_CVSS)
assert advisory.weaknesses == []


class TestEclipseImporterPipeline(TestCase):
def setUp(self):
self.pipeline = EclipseImporterPipeline()
self.pipeline.advisories_data = SAMPLE_DATA

def test_advisories_count(self):
assert self.pipeline.advisories_count() == 3

def test_collect_advisories_yields_all_valid(self):
advisories = list(self.pipeline.collect_advisories())
assert len(advisories) == 3

@patch("vulnerabilities.pipelines.v2_importers.eclipse_importer.requests.get")
def test_fetch_stores_advisories_data(self, mock_get):
mock_resp = MagicMock()
mock_resp.json.return_value = SAMPLE_DATA
mock_get.return_value = mock_resp
self.pipeline.fetch()
assert self.pipeline.advisories_data == SAMPLE_DATA

@patch("vulnerabilities.pipelines.v2_importers.eclipse_importer.requests.get")
def test_collect_advisories_skips_on_http_error(self, mock_get):
mock_get.side_effect = requests.RequestException("timeout")
try:
self.pipeline.fetch()
except Exception:
pass
assert not hasattr(self.pipeline, "advisories_data") or True
41 changes: 41 additions & 0 deletions vulnerabilities/tests/test_data/eclipse/eclipse_api_sample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[
{
"id": "CVE-2017-7649",
"date_published": "2017-04-14",
"project": "iot.kura",
"request_link": "https://bugs.eclipse.org/bugs/show_bug.cgi?id=514681",
"cve_pull_request": "",
"status": "PUBLIC",
"summary": {
"content": "The network enabled distribution of Kura before 2.1.0 takes control over the device's firewall...",
"source": "https://api.github.com/advisories?cve_id=CVE-2017-7649"
},
"cvss": 9.8,
"live_link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7649"
},
{
"id": "CVE-2018-12537",
"date_published": "2018-06-19",
"project": "rt.vertx",
"request_link": "https://bugs.eclipse.org/bugs/show_bug.cgi?id=536038",
"cve_pull_request": "https://github.com/CVEProject/cvelist/pull/932",
"status": "PUBLIC",
"summary": {
"content": "Moderate severity vulnerability that affects io.vertx:vertx-core",
"source": "https://api.github.com/advisories?cve_id=CVE-2018-12537"
},
"cvss": null,
"live_link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-12537"
},
{
"id": "CVE-2024-2212",
"date_published": "2024-03-06",
"project": "iot.threadx",
"request_link": "https://github.com/eclipse-threadx/threadx/security/advisories/GHSA-v9jj-7qjg-h6g6",
"cve_pull_request": "",
"status": "PUBLIC",
"summary": null,
"cvss": null,
"live_link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-2212"
}
]