-
-
Notifications
You must be signed in to change notification settings - Fork 304
Add Eclipse Foundation security advisories importer #2219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 "" | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think something like this is simpler.
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used |
||||||||||
|
|
||||||||||
| 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: | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||||||||||
| ) | ||||||||||
| 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): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please change this to test against the file instead of running
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced the whole class with a single |
||
| 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 | ||
| 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" | ||
| } | ||
| ] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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