Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any examples of this? If yes, please log this, if no, please remove it.

Suggested change
if not advisory_id:
return None

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it since I did not find any instances in API data


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 ""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something like this is simpler.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used entry.get("summary") or {} instead of the suggested entry.get("summary", {}) because the API actually returns "summary": null for some entries (CVE-2024-2212 in the sample data). When the key exists but its value is null, .get("summary", {}) still returns None, which makes None.get("content") blow up. The or {} handles both the missing key and the null case cleanly.


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:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if cvss is not None:
if cvss:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

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):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change this to test against the file instead of running parse_advisory for every attribute.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the whole class with a single test_parse_advisories() that runs all three sample entries and checks against an expected JSON file using util_tests.check_results_against_json. Also dropped test_collect_advisories_skips_on_http_error as it had assert not hasattr(...) or True which is always true and wasn't actually testing anything.

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"
}
]
Loading