From 19d4d456fc74573dad257deb9a59832c67856bbd Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Wed, 26 Nov 2025 16:20:21 +0530 Subject: [PATCH 1/5] Add V2_importer to collect advisories from EUVD Signed-off-by: Sampurna Pyne --- vulnerabilities/importers/__init__.py | 2 + .../pipelines/v2_importers/euvd_importer.py | 206 ++++++++++++++++++ .../v2_importers/test_euvd_importer_v2.py | 125 +++++++++++ .../tests/test_data/euvd/euvd_sample1.json | 34 +++ .../tests/test_data/euvd/euvd_sample2.json | 24 ++ 5 files changed, 391 insertions(+) create mode 100644 vulnerabilities/pipelines/v2_importers/euvd_importer.py create mode 100644 vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py create mode 100644 vulnerabilities/tests/test_data/euvd/euvd_sample1.json create mode 100644 vulnerabilities/tests/test_data/euvd/euvd_sample2.json diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index 82ee4525a..54b97adc2 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -47,6 +47,7 @@ from vulnerabilities.pipelines.v2_importers import ( elixir_security_importer as elixir_security_importer_v2, ) +from vulnerabilities.pipelines.v2_importers import euvd_importer as euvd_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 istio_importer as istio_importer_v2 @@ -75,6 +76,7 @@ pysec_importer_v2.PyPIImporterPipeline, xen_importer_v2.XenImporterPipeline, curl_importer_v2.CurlImporterPipeline, + euvd_importer_v2.EUVDImporterPipeline, oss_fuzz_v2.OSSFuzzImporterPipeline, istio_importer_v2.IstioImporterPipeline, postgresql_importer_v2.PostgreSQLImporterPipeline, diff --git a/vulnerabilities/pipelines/v2_importers/euvd_importer.py b/vulnerabilities/pipelines/v2_importers/euvd_importer.py new file mode 100644 index 000000000..9b488186c --- /dev/null +++ b/vulnerabilities/pipelines/v2_importers/euvd_importer.py @@ -0,0 +1,206 @@ +# +# 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 requests +import time +from datetime import datetime +from http import HTTPStatus +from typing import Iterable + +from dateutil import parser as dateparser + +from vulnerabilities.importer import AdvisoryData +from vulnerabilities.importer import ReferenceV2 +from vulnerabilities.importer import VulnerabilitySeverity +from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2 +from vulnerabilities.severity_systems import SCORING_SYSTEMS + +logger = logging.getLogger(__name__) + + +class EUVDImporterPipeline(VulnerableCodeBaseImporterPipelineV2): + """ + EUVD (EU Vulnerability Database) Importer Pipeline + + This pipeline imports security advisories from the European Union Vulnerability Database (EUVD). + """ + + pipeline_id = "euvd_importer_v2" + spdx_license_expression = "LicenseRef-scancode-other-permissive" + license_url = "https://www.enisa.europa.eu/about-enisa/legal-notice/" + url = "https://euvdservices.enisa.europa.eu/api/search" + + def __init__(self): + super().__init__() + self._cached_data = None + + @classmethod + def steps(cls): + return (cls.collect_and_store_advisories,) + + def fetch_data(self): + # Return cached data if already fetched + if self._cached_data is not None: + logger.info(f"Using cached data: {len(self._cached_data)} items") + return self._cached_data + + headers = {"User-Agent": "VulnerableCode"} + all_items = [] + page = 0 + size = 100 + max_retries = 100 + + logger.info(f"Fetching data from EUVD API: {self.url}") + + while True: + + retry_count = 0 + success = False + + while retry_count < max_retries and not success: + try: + params = {"size": size, "page": page} + response = requests.get(self.url, headers=headers, params=params, timeout=30) + + if response.status_code != HTTPStatus.OK: + logger.error(f"API returned status {response.status_code} for page {page}") + retry_count += 1 + if retry_count < max_retries: + sleep_time = min(10 * (2 ** min(retry_count - 1, 5)), 60) + logger.info(f"Retrying page {page} in {sleep_time}s (attempt {retry_count}/{max_retries})") + time.sleep(sleep_time) + continue + else: + logger.error(f"Max retries reached for page {page}") + return all_items + + data = response.json() + items = data.get("items", []) + + if not items: + logger.info(f"No items in response for page {page}; stopping fetch.") + logger.info(f"Fetch completed successfully. Total items collected: {len(all_items)}") + + # Cache the fetched data for reuse + self._cached_data = all_items + logger.info(f"Cached {len(all_items)} items for reuse") + + return all_items + + all_items.extend(items) + logger.info(f"Fetched page {page}: {len(items)} items (total: {len(all_items)})") + success = True + page += 1 + + except requests.exceptions.Timeout as e: + retry_count += 1 + if retry_count < max_retries: + logger.warning(f"Timeout on page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})") + time.sleep(10) + else: + logger.error(f"Max retries reached for page {page} after timeout") + return all_items + + except Exception as e: + retry_count += 1 + if retry_count < max_retries: + logger.error(f"Error fetching page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})") + time.sleep(10) + else: + logger.error(f"Max retries reached for page {page}") + return all_items + + def advisories_count(self) -> int: + return len(self.fetch_data()) + + def collect_advisories(self) -> Iterable[AdvisoryData]: + for raw_data in self.fetch_data(): + try: + advisory = self.parse_advisory(raw_data) + if advisory: + yield advisory + except Exception as e: + logger.error(f"Failed to parse advisory: {e}") + logger.debug(f"Raw data: {raw_data}") + continue + + def parse_advisory(self, raw_data: dict) -> AdvisoryData: + advisory_id = raw_data.get("id", "") + + aliases = [advisory_id] if advisory_id else [] + aliases_str = raw_data.get("aliases", "") + if aliases_str: + cve_aliases = [alias.strip() for alias in aliases_str.split("\n") if alias.strip()] + aliases.extend(cve_aliases) + + summary = raw_data.get("description", "") + + date_published = None + date_str = raw_data.get("datePublished", "") + if date_str: + try: + date_published = dateparser.parse(date_str) + if date_published and date_published.tzinfo is None: + date_published = date_published.replace(tzinfo=datetime.now().astimezone().tzinfo) + except Exception as e: + logger.warning(f"Failed to parse date '{date_str}': {e}") + + references = [] + references_str = raw_data.get("references", "") + if references_str: + urls = [url.strip() for url in references_str.split("\n") if url.strip()] + for url in urls: + references.append(ReferenceV2(url=url)) + + if advisory_id: + advisory_url = f"https://euvd.enisa.europa.eu/vulnerability/{advisory_id}" + references.append(ReferenceV2(url=advisory_url)) + + severities = [] + base_score = raw_data.get("baseScore") + base_score_version = raw_data.get("baseScoreVersion") + base_score_vector = raw_data.get("baseScoreVector") + + if base_score and base_score_version: + scoring_system = self.get_scoring_system(base_score_version) + if scoring_system: + severity = VulnerabilitySeverity( + system=scoring_system, + value=str(base_score), + scoring_elements=base_score_vector or "", + ) + severities.append(severity) + + return AdvisoryData( + advisory_id=advisory_id, + aliases=aliases, + summary=summary, + references_v2=references, + affected_packages=[], + date_published=date_published, + url=advisory_url if advisory_id else "", + severities=severities, + original_advisory_text=json.dumps(raw_data, indent=2, ensure_ascii=False), + ) + + @staticmethod + def get_scoring_system(version: str): + version_map = { + "4.0": "cvssv4", + "3.1": "cvssv3.1", + "3.0": "cvssv3", + "2.0": "cvssv2", + } + system_key = version_map.get(version) + if system_key: + return SCORING_SYSTEMS.get(system_key) + logger.warning(f"Unknown CVSS version: {version}") + return None diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py new file mode 100644 index 000000000..555ac7b8d --- /dev/null +++ b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py @@ -0,0 +1,125 @@ +# +# 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 Mock +from unittest.mock import patch + +from vulnerabilities.importer import AdvisoryData +from vulnerabilities.pipelines.v2_importers.euvd_importer import EUVDImporterPipeline + +TEST_DATA = Path(__file__).parent.parent.parent / "test_data" / "euvd" + + +class TestEUVDImporterPipeline(TestCase): + @patch("vulnerabilities.pipelines.v2_importers.euvd_importer.requests.get") + def test_collect_advisories(self, mock_get): + """Test collecting and parsing advisories from test data""" + sample1_path = TEST_DATA / "euvd_sample1.json" + sample2_path = TEST_DATA / "euvd_sample2.json" + + sample1 = json.loads(sample1_path.read_text(encoding="utf-8")) + sample2 = json.loads(sample2_path.read_text(encoding="utf-8")) + + mock_responses = [ + Mock(status_code=200, json=lambda: sample1), + Mock(status_code=200, json=lambda: sample2), + Mock(status_code=200, json=lambda: {"items": []}), + ] + mock_get.side_effect = mock_responses + + pipeline = EUVDImporterPipeline() + advisories = list(pipeline.collect_advisories()) + + assert len(advisories) == 5 + + first = advisories[0] + assert isinstance(first, AdvisoryData) + assert first.advisory_id == "EUVD-2025-197757" + assert "EUVD-2025-197757" in first.aliases + assert "CVE-2025-13284" in first.aliases + assert ( + first.summary == "ThinPLUS vulnerability that allows remote code execution" + ) + assert first.date_published is not None + assert len(first.severities) == 1 + assert first.severities[0].system.identifier == "cvssv3.1" + assert first.severities[0].value == "9.8" + assert first.severities[0].scoring_elements == "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + + urls = [ref.url for ref in first.references_v2] + assert "https://nvd.nist.gov/vuln/detail/CVE-2025-13284" in urls + assert "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-197757" in urls + + second = advisories[1] + assert second.advisory_id == "EUVD-2024-123456" + assert "CVE-2024-12345" in second.aliases + assert "CVE-2024-67890" in second.aliases + assert len([a for a in second.aliases if a.startswith("CVE-")]) == 2 + + urls = [ref.url for ref in second.references_v2] + assert "https://example.com/advisory1" in urls + assert "https://example.com/advisory2" in urls + + third = advisories[2] + assert third.advisory_id == "EUVD-2023-999999" + assert third.severities[0].system.identifier == "cvssv3" + assert third.severities[0].value == "5.3" + + fourth = advisories[3] + assert fourth.advisory_id == "EUVD-2022-555555" + assert fourth.summary == "" + assert fourth.severities[0].system.identifier == "cvssv2" + assert fourth.severities[0].value == "4.3" + + fifth = advisories[4] + assert fifth.advisory_id == "EUVD-2021-111111" + assert len([a for a in fifth.aliases if a.startswith("CVE-")]) == 0 + assert fifth.summary == "Advisory without CVE alias but with EUVD ID" + + def test_get_scoring_system(self): + """Test CVSS version to scoring system mapping""" + pipeline = EUVDImporterPipeline() + + system_v4 = pipeline.get_scoring_system("4.0") + assert system_v4 is not None + assert system_v4.identifier == "cvssv4" + + system_v31 = pipeline.get_scoring_system("3.1") + assert system_v31 is not None + assert system_v31.identifier == "cvssv3.1" + + system_v3 = pipeline.get_scoring_system("3.0") + assert system_v3 is not None + assert system_v3.identifier == "cvssv3" + + system_v2 = pipeline.get_scoring_system("2.0") + assert system_v2 is not None + assert system_v2.identifier == "cvssv2" + + system_unknown = pipeline.get_scoring_system("unknown") + assert system_unknown is None + + @patch("vulnerabilities.pipelines.v2_importers.euvd_importer.requests.get") + def test_advisories_count(self, mock_get): + """Test counting advisories""" + sample_data = {"items": [{"id": "1"}, {"id": "2"}, {"id": "3"}]} + mock_responses = [ + Mock(status_code=200, json=lambda: sample_data), + Mock(status_code=200, json=lambda: {"items": []}), + ] + mock_get.side_effect = mock_responses + + pipeline = EUVDImporterPipeline() + count = pipeline.advisories_count() + + assert count == 3 + diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample1.json b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json new file mode 100644 index 000000000..5ac4b56fc --- /dev/null +++ b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json @@ -0,0 +1,34 @@ +{ + "items": [ + { + "id": "EUVD-2025-197757", + "aliases": "CVE-2025-13284", + "description": "ThinPLUS vulnerability that allows remote code execution", + "datePublished": "2025-01-09T01:00:00.000Z", + "baseScore": "9.8", + "baseScoreVersion": "3.1", + "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "references": "https://nvd.nist.gov/vuln/detail/CVE-2025-13284" + }, + { + "id": "EUVD-2024-123456", + "aliases": "CVE-2024-12345\nCVE-2024-67890", + "description": "Multiple vulnerabilities in authentication system", + "datePublished": "2024-12-15T10:30:00.000Z", + "baseScore": "7.5", + "baseScoreVersion": "3.1", + "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "references": "https://example.com/advisory1\nhttps://example.com/advisory2" + }, + { + "id": "EUVD-2023-999999", + "aliases": "CVE-2023-99999", + "description": "Denial of service vulnerability", + "datePublished": "2023-06-20T14:22:00.000Z", + "baseScore": "5.3", + "baseScoreVersion": "3.0", + "baseScoreVector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "references": "https://security.example.org/2023-999999" + } + ] +} diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample2.json b/vulnerabilities/tests/test_data/euvd/euvd_sample2.json new file mode 100644 index 000000000..67347e35a --- /dev/null +++ b/vulnerabilities/tests/test_data/euvd/euvd_sample2.json @@ -0,0 +1,24 @@ +{ + "items": [ + { + "id": "EUVD-2022-555555", + "aliases": "CVE-2022-55555", + "description": "", + "datePublished": "2022-03-10T08:15:00.000Z", + "baseScore": "4.3", + "baseScoreVersion": "2.0", + "baseScoreVector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "references": "" + }, + { + "id": "EUVD-2021-111111", + "aliases": "", + "description": "Advisory without CVE alias but with EUVD ID", + "datePublished": "2021-11-05T16:45:00.000Z", + "baseScore": "6.5", + "baseScoreVersion": "3.1", + "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", + "references": "https://euvd.example.org/2021-111111" + } + ] +} From a3a5c36fddd50f9b7d606773adca841bb18f20cc Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Wed, 26 Nov 2025 22:14:50 +0530 Subject: [PATCH 2/5] Fix code formatting with black and isort Signed-off-by: Sampurna Pyne --- .../pipelines/v2_importers/euvd_importer.py | 50 ++++++++++++------- .../v2_importers/test_euvd_importer_v2.py | 21 ++++---- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/euvd_importer.py b/vulnerabilities/pipelines/v2_importers/euvd_importer.py index 9b488186c..c1ac7549d 100644 --- a/vulnerabilities/pipelines/v2_importers/euvd_importer.py +++ b/vulnerabilities/pipelines/v2_importers/euvd_importer.py @@ -9,12 +9,12 @@ import json import logging -import requests import time from datetime import datetime from http import HTTPStatus from typing import Iterable +import requests from dateutil import parser as dateparser from vulnerabilities.importer import AdvisoryData @@ -51,7 +51,7 @@ def fetch_data(self): if self._cached_data is not None: logger.info(f"Using cached data: {len(self._cached_data)} items") return self._cached_data - + headers = {"User-Agent": "VulnerableCode"} all_items = [] page = 0 @@ -61,7 +61,7 @@ def fetch_data(self): logger.info(f"Fetching data from EUVD API: {self.url}") while True: - + retry_count = 0 success = False @@ -75,7 +75,9 @@ def fetch_data(self): retry_count += 1 if retry_count < max_retries: sleep_time = min(10 * (2 ** min(retry_count - 1, 5)), 60) - logger.info(f"Retrying page {page} in {sleep_time}s (attempt {retry_count}/{max_retries})") + logger.info( + f"Retrying page {page} in {sleep_time}s (attempt {retry_count}/{max_retries})" + ) time.sleep(sleep_time) continue else: @@ -87,23 +89,29 @@ def fetch_data(self): if not items: logger.info(f"No items in response for page {page}; stopping fetch.") - logger.info(f"Fetch completed successfully. Total items collected: {len(all_items)}") - + logger.info( + f"Fetch completed successfully. Total items collected: {len(all_items)}" + ) + # Cache the fetched data for reuse self._cached_data = all_items logger.info(f"Cached {len(all_items)} items for reuse") - + return all_items all_items.extend(items) - logger.info(f"Fetched page {page}: {len(items)} items (total: {len(all_items)})") + logger.info( + f"Fetched page {page}: {len(items)} items (total: {len(all_items)})" + ) success = True page += 1 except requests.exceptions.Timeout as e: retry_count += 1 if retry_count < max_retries: - logger.warning(f"Timeout on page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})") + logger.warning( + f"Timeout on page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})" + ) time.sleep(10) else: logger.error(f"Max retries reached for page {page} after timeout") @@ -112,7 +120,9 @@ def fetch_data(self): except Exception as e: retry_count += 1 if retry_count < max_retries: - logger.error(f"Error fetching page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})") + logger.error( + f"Error fetching page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})" + ) time.sleep(10) else: logger.error(f"Max retries reached for page {page}") @@ -134,41 +144,43 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: def parse_advisory(self, raw_data: dict) -> AdvisoryData: advisory_id = raw_data.get("id", "") - + aliases = [advisory_id] if advisory_id else [] aliases_str = raw_data.get("aliases", "") if aliases_str: cve_aliases = [alias.strip() for alias in aliases_str.split("\n") if alias.strip()] aliases.extend(cve_aliases) - + summary = raw_data.get("description", "") - + date_published = None date_str = raw_data.get("datePublished", "") if date_str: try: date_published = dateparser.parse(date_str) if date_published and date_published.tzinfo is None: - date_published = date_published.replace(tzinfo=datetime.now().astimezone().tzinfo) + date_published = date_published.replace( + tzinfo=datetime.now().astimezone().tzinfo + ) except Exception as e: logger.warning(f"Failed to parse date '{date_str}': {e}") - + references = [] references_str = raw_data.get("references", "") if references_str: urls = [url.strip() for url in references_str.split("\n") if url.strip()] for url in urls: references.append(ReferenceV2(url=url)) - + if advisory_id: advisory_url = f"https://euvd.enisa.europa.eu/vulnerability/{advisory_id}" references.append(ReferenceV2(url=advisory_url)) - + severities = [] base_score = raw_data.get("baseScore") base_score_version = raw_data.get("baseScoreVersion") base_score_vector = raw_data.get("baseScoreVector") - + if base_score and base_score_version: scoring_system = self.get_scoring_system(base_score_version) if scoring_system: @@ -178,7 +190,7 @@ def parse_advisory(self, raw_data: dict) -> AdvisoryData: scoring_elements=base_score_vector or "", ) severities.append(severity) - + return AdvisoryData( advisory_id=advisory_id, aliases=aliases, diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py index 555ac7b8d..02c472ad3 100644 --- a/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py +++ b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py @@ -46,15 +46,15 @@ def test_collect_advisories(self, mock_get): assert first.advisory_id == "EUVD-2025-197757" assert "EUVD-2025-197757" in first.aliases assert "CVE-2025-13284" in first.aliases - assert ( - first.summary == "ThinPLUS vulnerability that allows remote code execution" - ) + assert first.summary == "ThinPLUS vulnerability that allows remote code execution" assert first.date_published is not None assert len(first.severities) == 1 assert first.severities[0].system.identifier == "cvssv3.1" assert first.severities[0].value == "9.8" - assert first.severities[0].scoring_elements == "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" - + assert ( + first.severities[0].scoring_elements == "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + ) + urls = [ref.url for ref in first.references_v2] assert "https://nvd.nist.gov/vuln/detail/CVE-2025-13284" in urls assert "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-197757" in urls @@ -88,23 +88,23 @@ def test_collect_advisories(self, mock_get): def test_get_scoring_system(self): """Test CVSS version to scoring system mapping""" pipeline = EUVDImporterPipeline() - + system_v4 = pipeline.get_scoring_system("4.0") assert system_v4 is not None assert system_v4.identifier == "cvssv4" - + system_v31 = pipeline.get_scoring_system("3.1") assert system_v31 is not None assert system_v31.identifier == "cvssv3.1" - + system_v3 = pipeline.get_scoring_system("3.0") assert system_v3 is not None assert system_v3.identifier == "cvssv3" - + system_v2 = pipeline.get_scoring_system("2.0") assert system_v2 is not None assert system_v2.identifier == "cvssv2" - + system_unknown = pipeline.get_scoring_system("unknown") assert system_unknown is None @@ -122,4 +122,3 @@ def test_advisories_count(self, mock_get): count = pipeline.advisories_count() assert count == 3 - From 8b21b2fc3624f816ff6f6640200a4d6de5631bd2 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Sat, 29 Nov 2025 01:17:44 +0530 Subject: [PATCH 3/5] Address review feedback for EUVD importer and tests Signed-off-by: Sampurna Pyne --- .../pipelines/v2_importers/euvd_importer.py | 172 ++++++++++-------- .../v2_importers/test_euvd_importer_v2.py | 59 +----- .../tests/test_data/euvd/euvd-expected.json | 157 ++++++++++++++++ .../tests/test_data/euvd/euvd_sample1.json | 3 +- .../tests/test_data/euvd/euvd_sample2.json | 3 +- 5 files changed, 268 insertions(+), 126 deletions(-) create mode 100644 vulnerabilities/tests/test_data/euvd/euvd-expected.json diff --git a/vulnerabilities/pipelines/v2_importers/euvd_importer.py b/vulnerabilities/pipelines/v2_importers/euvd_importer.py index c1ac7549d..cac0919f4 100644 --- a/vulnerabilities/pipelines/v2_importers/euvd_importer.py +++ b/vulnerabilities/pipelines/v2_importers/euvd_importer.py @@ -9,6 +9,7 @@ import json import logging +import math import time from datetime import datetime from http import HTTPStatus @@ -47,86 +48,111 @@ def steps(cls): return (cls.collect_and_store_advisories,) def fetch_data(self): - # Return cached data if already fetched if self._cached_data is not None: logger.info(f"Using cached data: {len(self._cached_data)} items") return self._cached_data - headers = {"User-Agent": "VulnerableCode"} all_items = [] - page = 0 size = 100 - max_retries = 100 + max_retries = 2 logger.info(f"Fetching data from EUVD API: {self.url}") - while True: - - retry_count = 0 - success = False - - while retry_count < max_retries and not success: - try: - params = {"size": size, "page": page} - response = requests.get(self.url, headers=headers, params=params, timeout=30) - - if response.status_code != HTTPStatus.OK: - logger.error(f"API returned status {response.status_code} for page {page}") - retry_count += 1 - if retry_count < max_retries: - sleep_time = min(10 * (2 ** min(retry_count - 1, 5)), 60) - logger.info( - f"Retrying page {page} in {sleep_time}s (attempt {retry_count}/{max_retries})" - ) - time.sleep(sleep_time) - continue - else: - logger.error(f"Max retries reached for page {page}") - return all_items - - data = response.json() - items = data.get("items", []) - - if not items: - logger.info(f"No items in response for page {page}; stopping fetch.") - logger.info( - f"Fetch completed successfully. Total items collected: {len(all_items)}" - ) - - # Cache the fetched data for reuse - self._cached_data = all_items - logger.info(f"Cached {len(all_items)} items for reuse") - - return all_items - - all_items.extend(items) - logger.info( - f"Fetched page {page}: {len(items)} items (total: {len(all_items)})" - ) - success = True - page += 1 - - except requests.exceptions.Timeout as e: - retry_count += 1 - if retry_count < max_retries: - logger.warning( - f"Timeout on page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})" - ) - time.sleep(10) - else: - logger.error(f"Max retries reached for page {page} after timeout") - return all_items - - except Exception as e: - retry_count += 1 - if retry_count < max_retries: - logger.error( - f"Error fetching page {page}: {e}. Retrying in 10s (attempt {retry_count}/{max_retries})" - ) - time.sleep(10) - else: - logger.error(f"Max retries reached for page {page}") - return all_items + total_count = self._fetch_total_count(size, max_retries) + if total_count is None: + logger.error("Failed to fetch total count from API") + return all_items + + total_pages = math.ceil(total_count / size) + logger.info(f"Total advisories: {total_count}, Total pages: {total_pages}") + + first_page_data = self._fetch_page(0, size, max_retries) + if first_page_data: + all_items.extend(first_page_data) + logger.info(f"Fetched page 0: {len(first_page_data)} items (total: {len(all_items)})") + + for page in range(1, total_pages): + page_data = self._fetch_page(page, size, max_retries) + if page_data is None: + logger.warning(f"Skipping page {page} after failed retries") + continue + + if not page_data: + logger.info(f"No items in response for page {page}; stopping fetch.") + break + + all_items.extend(page_data) + logger.info(f"Fetched page {page}: {len(page_data)} items (total: {len(all_items)})") + + logger.info(f"Fetch completed successfully. Total items collected: {len(all_items)}") + + self._cached_data = all_items + logger.info(f"Cached {len(all_items)} items for reuse") + + return all_items + + def _make_request_with_retry(self, params, max_retries, context): + headers = {"User-Agent": "VulnerableCode"} + + for attempt in range(max_retries): + try: + response = requests.get(self.url, headers=headers, params=params, timeout=30) + + if response.status_code != HTTPStatus.OK: + logger.error(f"API returned status {response.status_code} for {context}") + if attempt < max_retries - 1: + logger.info(f"Retrying {context} (attempt {attempt + 1}/{max_retries})") + time.sleep(3) + continue + return None + + return response.json() + + except requests.exceptions.Timeout: + logger.warning(f"Timeout on {context} (attempt {attempt + 1}/{max_retries})") + if attempt < max_retries - 1: + time.sleep(3) + continue + return None + + except requests.exceptions.RequestException as e: + logger.error( + f"Network error on {context}: {e} (attempt {attempt + 1}/{max_retries})" + ) + if attempt < max_retries - 1: + time.sleep(3) + continue + return None + + except (ValueError, KeyError) as e: + logger.error(f"Error parsing response for {context}: {e}") + return None + + return None + + def _fetch_total_count(self, size, max_retries): + """Fetch the total count of advisories from the API.""" + params = {"size": size, "page": 0} + data = self._make_request_with_retry(params, max_retries, "total count") + + if data is None: + return None + + total = data.get("total") + if total is None: + logger.error("No 'total' field in API response") + + return total + + def _fetch_page(self, page, size, max_retries): + """Fetch a single page of advisories from the API.""" + params = {"size": size, "page": page} + data = self._make_request_with_retry(params, max_retries, f"page {page}") + + if data is None: + return None + + return data.get("items", []) def advisories_count(self) -> int: return len(self.fetch_data()) @@ -137,7 +163,7 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: advisory = self.parse_advisory(raw_data) if advisory: yield advisory - except Exception as e: + except (ValueError, KeyError, TypeError) as e: logger.error(f"Failed to parse advisory: {e}") logger.debug(f"Raw data: {raw_data}") continue @@ -162,7 +188,7 @@ def parse_advisory(self, raw_data: dict) -> AdvisoryData: date_published = date_published.replace( tzinfo=datetime.now().astimezone().tzinfo ) - except Exception as e: + except (ValueError, TypeError) as e: logger.warning(f"Failed to parse date '{date_str}': {e}") references = [] diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py index 02c472ad3..88a0e0cea 100644 --- a/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py +++ b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py @@ -13,8 +13,8 @@ from unittest.mock import Mock from unittest.mock import patch -from vulnerabilities.importer import AdvisoryData from vulnerabilities.pipelines.v2_importers.euvd_importer import EUVDImporterPipeline +from vulnerabilities.tests import util_tests TEST_DATA = Path(__file__).parent.parent.parent / "test_data" / "euvd" @@ -30,60 +30,17 @@ def test_collect_advisories(self, mock_get): sample2 = json.loads(sample2_path.read_text(encoding="utf-8")) mock_responses = [ + Mock(status_code=200, json=lambda: sample1), Mock(status_code=200, json=lambda: sample1), Mock(status_code=200, json=lambda: sample2), - Mock(status_code=200, json=lambda: {"items": []}), ] mock_get.side_effect = mock_responses pipeline = EUVDImporterPipeline() - advisories = list(pipeline.collect_advisories()) - - assert len(advisories) == 5 - - first = advisories[0] - assert isinstance(first, AdvisoryData) - assert first.advisory_id == "EUVD-2025-197757" - assert "EUVD-2025-197757" in first.aliases - assert "CVE-2025-13284" in first.aliases - assert first.summary == "ThinPLUS vulnerability that allows remote code execution" - assert first.date_published is not None - assert len(first.severities) == 1 - assert first.severities[0].system.identifier == "cvssv3.1" - assert first.severities[0].value == "9.8" - assert ( - first.severities[0].scoring_elements == "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" - ) - - urls = [ref.url for ref in first.references_v2] - assert "https://nvd.nist.gov/vuln/detail/CVE-2025-13284" in urls - assert "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-197757" in urls - - second = advisories[1] - assert second.advisory_id == "EUVD-2024-123456" - assert "CVE-2024-12345" in second.aliases - assert "CVE-2024-67890" in second.aliases - assert len([a for a in second.aliases if a.startswith("CVE-")]) == 2 - - urls = [ref.url for ref in second.references_v2] - assert "https://example.com/advisory1" in urls - assert "https://example.com/advisory2" in urls - - third = advisories[2] - assert third.advisory_id == "EUVD-2023-999999" - assert third.severities[0].system.identifier == "cvssv3" - assert third.severities[0].value == "5.3" - - fourth = advisories[3] - assert fourth.advisory_id == "EUVD-2022-555555" - assert fourth.summary == "" - assert fourth.severities[0].system.identifier == "cvssv2" - assert fourth.severities[0].value == "4.3" - - fifth = advisories[4] - assert fifth.advisory_id == "EUVD-2021-111111" - assert len([a for a in fifth.aliases if a.startswith("CVE-")]) == 0 - assert fifth.summary == "Advisory without CVE alias but with EUVD ID" + advisories = [data.to_dict() for data in list(pipeline.collect_advisories())] + + expected_file = TEST_DATA / "euvd-expected.json" + util_tests.check_results_against_json(advisories, expected_file) def test_get_scoring_system(self): """Test CVSS version to scoring system mapping""" @@ -111,10 +68,10 @@ def test_get_scoring_system(self): @patch("vulnerabilities.pipelines.v2_importers.euvd_importer.requests.get") def test_advisories_count(self, mock_get): """Test counting advisories""" - sample_data = {"items": [{"id": "1"}, {"id": "2"}, {"id": "3"}]} + sample_data = {"items": [{"id": "1"}, {"id": "2"}, {"id": "3"}], "total": 3} mock_responses = [ Mock(status_code=200, json=lambda: sample_data), - Mock(status_code=200, json=lambda: {"items": []}), + Mock(status_code=200, json=lambda: sample_data), ] mock_get.side_effect = mock_responses diff --git a/vulnerabilities/tests/test_data/euvd/euvd-expected.json b/vulnerabilities/tests/test_data/euvd/euvd-expected.json new file mode 100644 index 000000000..0f8275b91 --- /dev/null +++ b/vulnerabilities/tests/test_data/euvd/euvd-expected.json @@ -0,0 +1,157 @@ +[ + { + "advisory_id": "EUVD-2025-197757", + "aliases": [ + "EUVD-2025-197757", + "CVE-2025-13284" + ], + "summary": "ThinPLUS vulnerability that allows remote code execution", + "affected_packages": [], + "references_v2": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-13284" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-197757" + } + ], + "severities": [ + { + "system": "cvssv3.1", + "value": "9.8", + "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "date_published": "2025-01-09T01:00:00+00:00", + "weaknesses": [], + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-197757" + }, + { + "advisory_id": "EUVD-2024-123456", + "aliases": [ + "EUVD-2024-123456", + "CVE-2024-12345", + "CVE-2024-67890" + ], + "summary": "Multiple vulnerabilities in authentication system", + "affected_packages": [], + "references_v2": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://example.com/advisory1" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://example.com/advisory2" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2024-123456" + } + ], + "severities": [ + { + "system": "cvssv3.1", + "value": "7.5", + "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" + } + ], + "date_published": "2024-12-15T10:30:00+00:00", + "weaknesses": [], + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2024-123456" + }, + { + "advisory_id": "EUVD-2023-999999", + "aliases": [ + "EUVD-2023-999999", + "CVE-2023-99999" + ], + "summary": "Denial of service vulnerability", + "affected_packages": [], + "references_v2": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://security.example.org/2023-999999" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2023-999999" + } + ], + "severities": [ + { + "system": "cvssv3", + "value": "5.3", + "scoring_elements": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + ], + "date_published": "2023-06-20T14:22:00+00:00", + "weaknesses": [], + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2023-999999" + }, + { + "advisory_id": "EUVD-2022-555555", + "aliases": [ + "EUVD-2022-555555", + "CVE-2022-55555" + ], + "summary": "", + "affected_packages": [], + "references_v2": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2022-555555" + } + ], + "severities": [ + { + "system": "cvssv2", + "value": "4.3", + "scoring_elements": "AV:N/AC:M/Au:N/C:N/I:P/A:N" + } + ], + "date_published": "2022-03-10T08:15:00+00:00", + "weaknesses": [], + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2022-555555" + }, + { + "advisory_id": "EUVD-2021-111111", + "aliases": [ + "EUVD-2021-111111" + ], + "summary": "Advisory without CVE alias but with EUVD ID", + "affected_packages": [], + "references_v2": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.example.org/2021-111111" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2021-111111" + } + ], + "severities": [ + { + "system": "cvssv3.1", + "value": "6.5", + "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N" + } + ], + "date_published": "2021-11-05T16:45:00+00:00", + "weaknesses": [], + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2021-111111" + } +] diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample1.json b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json index 5ac4b56fc..8f92c0183 100644 --- a/vulnerabilities/tests/test_data/euvd/euvd_sample1.json +++ b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json @@ -30,5 +30,6 @@ "baseScoreVector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", "references": "https://security.example.org/2023-999999" } - ] + ], + "total": 200 } diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample2.json b/vulnerabilities/tests/test_data/euvd/euvd_sample2.json index 67347e35a..bb69d9b29 100644 --- a/vulnerabilities/tests/test_data/euvd/euvd_sample2.json +++ b/vulnerabilities/tests/test_data/euvd/euvd_sample2.json @@ -20,5 +20,6 @@ "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", "references": "https://euvd.example.org/2021-111111" } - ] + ], + "total": 200 } From 4fb6ec53f379761185806029d2cd778f309d5372 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Mon, 1 Dec 2025 21:48:45 +0530 Subject: [PATCH 4/5] Update License Expression and Sample Test Data Signed-off-by: Sampurna Pyne --- .../pipelines/v2_importers/euvd_importer.py | 2 +- .../tests/test_data/euvd/euvd-expected.json | 137 +++++++++++------- .../tests/test_data/euvd/euvd_sample1.json | 60 +++++--- .../tests/test_data/euvd/euvd_sample2.json | 74 ++++++++-- 4 files changed, 179 insertions(+), 94 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/euvd_importer.py b/vulnerabilities/pipelines/v2_importers/euvd_importer.py index cac0919f4..0babfe060 100644 --- a/vulnerabilities/pipelines/v2_importers/euvd_importer.py +++ b/vulnerabilities/pipelines/v2_importers/euvd_importer.py @@ -35,7 +35,7 @@ class EUVDImporterPipeline(VulnerableCodeBaseImporterPipelineV2): """ pipeline_id = "euvd_importer_v2" - spdx_license_expression = "LicenseRef-scancode-other-permissive" + spdx_license_expression = "CC-BY-4.0" license_url = "https://www.enisa.europa.eu/about-enisa/legal-notice/" url = "https://euvdservices.enisa.europa.eu/api/search" diff --git a/vulnerabilities/tests/test_data/euvd/euvd-expected.json b/vulnerabilities/tests/test_data/euvd/euvd-expected.json index 0f8275b91..c1f38b7a5 100644 --- a/vulnerabilities/tests/test_data/euvd/euvd-expected.json +++ b/vulnerabilities/tests/test_data/euvd/euvd-expected.json @@ -1,157 +1,188 @@ [ { - "advisory_id": "EUVD-2025-197757", + "advisory_id": "EUVD-2022-0569", "aliases": [ - "EUVD-2025-197757", - "CVE-2025-13284" + "EUVD-2022-0569", + "CVE-2018-1109", + "GHSA-cwfw-4gq5-mrqx" ], - "summary": "ThinPLUS vulnerability that allows remote code execution", + "summary": "A vulnerability was found in Braces versions 2.2.0 and above, prior to 2.3.1. Affected versions of this package are vulnerable to Regular Expression Denial of Service (ReDoS) attacks.", "affected_packages": [], "references_v2": [ { "reference_id": "", "reference_type": "", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-13284" + "url": "https://nvd.nist.gov/vuln/detail/CVE-2018-1109" }, { "reference_id": "", "reference_type": "", - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-197757" + "url": "https://github.com/micromatch/braces/commit/abdafb0cae1e0c00f184abbadc692f4eaa98f451" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1547272" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2022-0569" } ], "severities": [ { "system": "cvssv3.1", - "value": "9.8", - "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + "value": "5.3", + "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" } ], - "date_published": "2025-01-09T01:00:00+00:00", + "date_published": "2021-03-30T01:52:55+00:00", "weaknesses": [], - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-197757" + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2022-0569" }, { - "advisory_id": "EUVD-2024-123456", + "advisory_id": "EUVD-2025-199883", "aliases": [ - "EUVD-2024-123456", - "CVE-2024-12345", - "CVE-2024-67890" + "EUVD-2025-199883", + "CVE-2025-66027" ], - "summary": "Multiple vulnerabilities in authentication system", + "summary": "Rallly is an open-source scheduling and collaboration tool. Prior to version 4.5.6, an information disclosure vulnerability exposes participant details, including names and email addresses through the /api/trpc/polls.get,polls.participants.list endpoint, even when Pro privacy features are enabled. This bypasses intended privacy controls that should prevent participants from viewing other users' personal information. This issue has been patched in version 4.5.6.", "affected_packages": [], "references_v2": [ { "reference_id": "", "reference_type": "", - "url": "https://example.com/advisory1" + "url": "https://github.com/lukevella/rallly/security/advisories/GHSA-65wg-8xgw-f3fg" }, { "reference_id": "", "reference_type": "", - "url": "https://example.com/advisory2" + "url": "https://github.com/lukevella/rallly/commit/59738c04f9a8ec25f0af5ce20ad0eab6cf134963" }, { "reference_id": "", "reference_type": "", - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2024-123456" + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199883" } ], "severities": [ { - "system": "cvssv3.1", - "value": "7.5", - "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" + "system": "cvssv4", + "value": "7.1", + "scoring_elements": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:L/SI:N/SA:N" } ], - "date_published": "2024-12-15T10:30:00+00:00", + "date_published": "2025-11-29T00:43:02+00:00", "weaknesses": [], - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2024-123456" + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199883" }, { - "advisory_id": "EUVD-2023-999999", + "advisory_id": "EUVD-2025-199882", "aliases": [ - "EUVD-2023-999999", - "CVE-2023-99999" + "EUVD-2025-199882", + "CVE-2025-66034" ], - "summary": "Denial of service vulnerability", + "summary": "fontTools is a library for manipulating fonts, written in Python. In versions from 4.33.0 to before 4.60.2, the fonttools varLib (or python3 -m fontTools.varLib) script has an arbitrary file write vulnerability that leads to remote code execution when a malicious .designspace file is processed. The vulnerability affects the main() code path of fontTools.varLib, used by the fonttools varLib CLI and any code that invokes fontTools.varLib.main(). This issue has been patched in version 4.60.2.", "affected_packages": [], "references_v2": [ { "reference_id": "", "reference_type": "", - "url": "https://security.example.org/2023-999999" + "url": "https://github.com/fonttools/fonttools/security/advisories/GHSA-768j-98cg-p3fv" }, { "reference_id": "", "reference_type": "", - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2023-999999" + "url": "https://github.com/fonttools/fonttools/commit/a696d5ba93270d5954f98e7cab5ddca8a02c1e32" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199882" } ], "severities": [ { - "system": "cvssv3", - "value": "5.3", - "scoring_elements": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + "system": "cvssv3.1", + "value": "6.3", + "scoring_elements": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:L" } ], - "date_published": "2023-06-20T14:22:00+00:00", + "date_published": "2025-11-29T01:07:12+00:00", "weaknesses": [], - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2023-999999" + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199882" }, { - "advisory_id": "EUVD-2022-555555", + "advisory_id": "EUVD-2025-199921", "aliases": [ - "EUVD-2022-555555", - "CVE-2022-55555" + "EUVD-2025-199921", + "CVE-2025-66420" ], - "summary": "", + "summary": "Tryton sao (aka tryton-sao) before 7.6.9 allows XSS via an HTML attachment. This is fixed in 7.6.9, 7.4.19, 7.0.38, and 6.0.67.", "affected_packages": [], "references_v2": [ { "reference_id": "", "reference_type": "", - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2022-555555" + "url": "https://discuss.tryton.org/t/security-release-for-issue-14290/8895" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://foss.heptapod.net/tryton/tryton/-/issues/14290" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-66420" + }, + { + "reference_id": "", + "reference_type": "", + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199921" } ], "severities": [ { - "system": "cvssv2", - "value": "4.3", - "scoring_elements": "AV:N/AC:M/Au:N/C:N/I:P/A:N" + "system": "cvssv3.1", + "value": "5.4", + "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N" } ], - "date_published": "2022-03-10T08:15:00+00:00", + "date_published": "2025-11-30T00:00:00+00:00", "weaknesses": [], - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2022-555555" + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199921" }, { - "advisory_id": "EUVD-2021-111111", + "advisory_id": "EUVD-2025-199889", "aliases": [ - "EUVD-2021-111111" + "EUVD-2025-199889", + "CVE-2025-66036" ], - "summary": "Advisory without CVE alias but with EUVD ID", + "summary": "Retro is an online platform providing items of vintage collections. Prior to version 2.4.7, Retro is vulnerable to a cross-site scripting (XSS) in the input handling component. This issue has been patched in version 2.4.7.", "affected_packages": [], "references_v2": [ { "reference_id": "", "reference_type": "", - "url": "https://euvd.example.org/2021-111111" + "url": "https://github.com/Anjaliavv51/Retro/security/advisories/GHSA-gvv6-p6h6-2vj2" }, { "reference_id": "", "reference_type": "", - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2021-111111" + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199889" } ], "severities": [ { "system": "cvssv3.1", - "value": "6.5", - "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N" + "value": "6.1", + "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N" } ], - "date_published": "2021-11-05T16:45:00+00:00", + "date_published": "2025-11-29T01:14:38+00:00", "weaknesses": [], - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2021-111111" + "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199889" } ] diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample1.json b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json index 8f92c0183..4c54bddb9 100644 --- a/vulnerabilities/tests/test_data/euvd/euvd_sample1.json +++ b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json @@ -1,35 +1,47 @@ { "items": [ { - "id": "EUVD-2025-197757", - "aliases": "CVE-2025-13284", - "description": "ThinPLUS vulnerability that allows remote code execution", - "datePublished": "2025-01-09T01:00:00.000Z", - "baseScore": "9.8", + "id": "EUVD-2022-0569", + "enisaUuid": "ff8d275e-fded-32e0-b1dd-74cbae780c34", + "description": "A vulnerability was found in Braces versions 2.2.0 and above, prior to 2.3.1. Affected versions of this package are vulnerable to Regular Expression Denial of Service (ReDoS) attacks.", + "datePublished": "Mar 30, 2021, 1:52:55 AM", + "dateUpdated": "Dec 1, 2025, 2:18:10 PM", + "baseScore": 5.3, "baseScoreVersion": "3.1", - "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", - "references": "https://nvd.nist.gov/vuln/detail/CVE-2025-13284" + "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "references": "https://nvd.nist.gov/vuln/detail/CVE-2018-1109\nhttps://github.com/micromatch/braces/commit/abdafb0cae1e0c00f184abbadc692f4eaa98f451\nhttps://bugzilla.redhat.com/show_bug.cgi?id=1547272\n", + "aliases": "CVE-2018-1109\nGHSA-cwfw-4gq5-mrqx\n", + "assigner": "redhat", + "epss": 0.27 }, { - "id": "EUVD-2024-123456", - "aliases": "CVE-2024-12345\nCVE-2024-67890", - "description": "Multiple vulnerabilities in authentication system", - "datePublished": "2024-12-15T10:30:00.000Z", - "baseScore": "7.5", - "baseScoreVersion": "3.1", - "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", - "references": "https://example.com/advisory1\nhttps://example.com/advisory2" + "id": "EUVD-2025-199883", + "enisaUuid": "227b1294-1b89-32c6-acd8-2cc50af9ea1c", + "description": "Rallly is an open-source scheduling and collaboration tool. Prior to version 4.5.6, an information disclosure vulnerability exposes participant details, including names and email addresses through the /api/trpc/polls.get,polls.participants.list endpoint, even when Pro privacy features are enabled. This bypasses intended privacy controls that should prevent participants from viewing other users' personal information. This issue has been patched in version 4.5.6.", + "datePublished": "Nov 29, 2025, 12:43:02 AM", + "dateUpdated": "Dec 1, 2025, 2:11:22 PM", + "baseScore": 7.1, + "baseScoreVersion": "4.0", + "baseScoreVector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:L/SI:N/SA:N", + "references": "https://github.com/lukevella/rallly/security/advisories/GHSA-65wg-8xgw-f3fg\nhttps://github.com/lukevella/rallly/commit/59738c04f9a8ec25f0af5ce20ad0eab6cf134963\n", + "aliases": "CVE-2025-66027\n", + "assigner": "GitHub_M", + "epss": 0.04 }, { - "id": "EUVD-2023-999999", - "aliases": "CVE-2023-99999", - "description": "Denial of service vulnerability", - "datePublished": "2023-06-20T14:22:00.000Z", - "baseScore": "5.3", - "baseScoreVersion": "3.0", - "baseScoreVector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", - "references": "https://security.example.org/2023-999999" + "id": "EUVD-2025-199882", + "enisaUuid": "911c0f6a-52a9-3013-a8e2-50ff32772b3f", + "description": "fontTools is a library for manipulating fonts, written in Python. In versions from 4.33.0 to before 4.60.2, the fonttools varLib (or python3 -m fontTools.varLib) script has an arbitrary file write vulnerability that leads to remote code execution when a malicious .designspace file is processed. The vulnerability affects the main() code path of fontTools.varLib, used by the fonttools varLib CLI and any code that invokes fontTools.varLib.main(). This issue has been patched in version 4.60.2.", + "datePublished": "Nov 29, 2025, 1:07:12 AM", + "dateUpdated": "Dec 1, 2025, 2:11:17 PM", + "baseScore": 6.3, + "baseScoreVersion": "3.1", + "baseScoreVector": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:L", + "references": "https://github.com/fonttools/fonttools/security/advisories/GHSA-768j-98cg-p3fv\nhttps://github.com/fonttools/fonttools/commit/a696d5ba93270d5954f98e7cab5ddca8a02c1e32\n", + "aliases": "CVE-2025-66034\n", + "assigner": "GitHub_M", + "epss": 0.09 } ], - "total": 200 + "total": 452826 } diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample2.json b/vulnerabilities/tests/test_data/euvd/euvd_sample2.json index bb69d9b29..2b2ec8101 100644 --- a/vulnerabilities/tests/test_data/euvd/euvd_sample2.json +++ b/vulnerabilities/tests/test_data/euvd/euvd_sample2.json @@ -1,25 +1,67 @@ { "items": [ { - "id": "EUVD-2022-555555", - "aliases": "CVE-2022-55555", - "description": "", - "datePublished": "2022-03-10T08:15:00.000Z", - "baseScore": "4.3", - "baseScoreVersion": "2.0", - "baseScoreVector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", - "references": "" + "id": "EUVD-2025-199882", + "enisaUuid": "911c0f6a-52a9-3013-a8e2-50ff32772b3f", + "description": "fontTools is a library for manipulating fonts, written in Python. In versions from 4.33.0 to before 4.60.2, the fonttools varLib (or python3 -m fontTools.varLib) script has an arbitrary file write vulnerability that leads to remote code execution when a malicious .designspace file is processed. The vulnerability affects the main() code path of fontTools.varLib, used by the fonttools varLib CLI and any code that invokes fontTools.varLib.main(). This issue has been patched in version 4.60.2.", + "datePublished": "Nov 29, 2025, 1:07:12 AM", + "dateUpdated": "Dec 1, 2025, 2:11:17 PM", + "baseScore": 6.3, + "baseScoreVersion": "3.1", + "baseScoreVector": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:L", + "references": "https://github.com/fonttools/fonttools/security/advisories/GHSA-768j-98cg-p3fv\nhttps://github.com/fonttools/fonttools/commit/a696d5ba93270d5954f98e7cab5ddca8a02c1e32\n", + "aliases": "CVE-2025-66034\n", + "assigner": "GitHub_M", + "epss": 0.09, + "enisaIdProduct": [ + { + "id": "2ffeef7e-ff67-3e85-a9ac-983195786663", + "product": { + "name": "fonttools" + }, + "product_version": "4.33.0, < 4.60.2" + } + ], + "enisaIdVendor": [ + { + "id": "d709fba0-867b-3831-965e-05b3b019d930", + "vendor": { + "name": "fonttools" + } + } + ] }, { - "id": "EUVD-2021-111111", - "aliases": "", - "description": "Advisory without CVE alias but with EUVD ID", - "datePublished": "2021-11-05T16:45:00.000Z", - "baseScore": "6.5", + "id": "EUVD-2025-199921", + "enisaUuid": "6ac6d459-6393-3e2e-ad31-122cf95d732e", + "description": "Tryton sao (aka tryton-sao) before 7.6.9 allows XSS via an HTML attachment. This is fixed in 7.6.9, 7.4.19, 7.0.38, and 6.0.67.", + "datePublished": "Nov 30, 2025, 12:00:00 AM", + "dateUpdated": "Dec 1, 2025, 2:10:50 PM", + "baseScore": 5.4, "baseScoreVersion": "3.1", - "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", - "references": "https://euvd.example.org/2021-111111" + "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N", + "references": "https://discuss.tryton.org/t/security-release-for-issue-14290/8895\nhttps://foss.heptapod.net/tryton/tryton/-/issues/14290\nhttps://nvd.nist.gov/vuln/detail/CVE-2025-66420\n", + "aliases": "CVE-2025-66420\n", + "assigner": "mitre", + "epss": 0.03, + "enisaIdProduct": [ + { + "id": "3cc4305f-3f05-39df-bf41-6fe760513270", + "product": { + "name": "sao" + }, + "product_version": "7.0.0 <7.0.38" + } + ], + "enisaIdVendor": [ + { + "id": "2cb8be18-e27b-3cc9-aa3f-e68b7f308d98", + "vendor": { + "name": "tryton" + } + } + ] } ], - "total": 200 + "total": 452826 } From 506d03f6351e772b0b3b013e35eb00f33914a5e4 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Mon, 1 Dec 2025 23:42:21 +0530 Subject: [PATCH 5/5] Update test_importer and sample data Signed-off-by: Sampurna Pyne --- .../v2_importers/test_euvd_importer_v2.py | 3 - .../tests/test_data/euvd/euvd-expected.json | 72 ------------------- .../tests/test_data/euvd/euvd_sample1.json | 2 +- .../tests/test_data/euvd/euvd_sample2.json | 67 ----------------- 4 files changed, 1 insertion(+), 143 deletions(-) delete mode 100644 vulnerabilities/tests/test_data/euvd/euvd_sample2.json diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py index 88a0e0cea..106b37cf0 100644 --- a/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py +++ b/vulnerabilities/tests/pipelines/v2_importers/test_euvd_importer_v2.py @@ -24,15 +24,12 @@ class TestEUVDImporterPipeline(TestCase): def test_collect_advisories(self, mock_get): """Test collecting and parsing advisories from test data""" sample1_path = TEST_DATA / "euvd_sample1.json" - sample2_path = TEST_DATA / "euvd_sample2.json" sample1 = json.loads(sample1_path.read_text(encoding="utf-8")) - sample2 = json.loads(sample2_path.read_text(encoding="utf-8")) mock_responses = [ Mock(status_code=200, json=lambda: sample1), Mock(status_code=200, json=lambda: sample1), - Mock(status_code=200, json=lambda: sample2), ] mock_get.side_effect = mock_responses diff --git a/vulnerabilities/tests/test_data/euvd/euvd-expected.json b/vulnerabilities/tests/test_data/euvd/euvd-expected.json index c1f38b7a5..3a6dab3a8 100644 --- a/vulnerabilities/tests/test_data/euvd/euvd-expected.json +++ b/vulnerabilities/tests/test_data/euvd/euvd-expected.json @@ -112,77 +112,5 @@ "date_published": "2025-11-29T01:07:12+00:00", "weaknesses": [], "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199882" - }, - { - "advisory_id": "EUVD-2025-199921", - "aliases": [ - "EUVD-2025-199921", - "CVE-2025-66420" - ], - "summary": "Tryton sao (aka tryton-sao) before 7.6.9 allows XSS via an HTML attachment. This is fixed in 7.6.9, 7.4.19, 7.0.38, and 6.0.67.", - "affected_packages": [], - "references_v2": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://discuss.tryton.org/t/security-release-for-issue-14290/8895" - }, - { - "reference_id": "", - "reference_type": "", - "url": "https://foss.heptapod.net/tryton/tryton/-/issues/14290" - }, - { - "reference_id": "", - "reference_type": "", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-66420" - }, - { - "reference_id": "", - "reference_type": "", - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199921" - } - ], - "severities": [ - { - "system": "cvssv3.1", - "value": "5.4", - "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N" - } - ], - "date_published": "2025-11-30T00:00:00+00:00", - "weaknesses": [], - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199921" - }, - { - "advisory_id": "EUVD-2025-199889", - "aliases": [ - "EUVD-2025-199889", - "CVE-2025-66036" - ], - "summary": "Retro is an online platform providing items of vintage collections. Prior to version 2.4.7, Retro is vulnerable to a cross-site scripting (XSS) in the input handling component. This issue has been patched in version 2.4.7.", - "affected_packages": [], - "references_v2": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://github.com/Anjaliavv51/Retro/security/advisories/GHSA-gvv6-p6h6-2vj2" - }, - { - "reference_id": "", - "reference_type": "", - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199889" - } - ], - "severities": [ - { - "system": "cvssv3.1", - "value": "6.1", - "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N" - } - ], - "date_published": "2025-11-29T01:14:38+00:00", - "weaknesses": [], - "url": "https://euvd.enisa.europa.eu/vulnerability/EUVD-2025-199889" } ] diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample1.json b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json index 4c54bddb9..87b493436 100644 --- a/vulnerabilities/tests/test_data/euvd/euvd_sample1.json +++ b/vulnerabilities/tests/test_data/euvd/euvd_sample1.json @@ -43,5 +43,5 @@ "epss": 0.09 } ], - "total": 452826 + "total": 5 } diff --git a/vulnerabilities/tests/test_data/euvd/euvd_sample2.json b/vulnerabilities/tests/test_data/euvd/euvd_sample2.json deleted file mode 100644 index 2b2ec8101..000000000 --- a/vulnerabilities/tests/test_data/euvd/euvd_sample2.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "items": [ - { - "id": "EUVD-2025-199882", - "enisaUuid": "911c0f6a-52a9-3013-a8e2-50ff32772b3f", - "description": "fontTools is a library for manipulating fonts, written in Python. In versions from 4.33.0 to before 4.60.2, the fonttools varLib (or python3 -m fontTools.varLib) script has an arbitrary file write vulnerability that leads to remote code execution when a malicious .designspace file is processed. The vulnerability affects the main() code path of fontTools.varLib, used by the fonttools varLib CLI and any code that invokes fontTools.varLib.main(). This issue has been patched in version 4.60.2.", - "datePublished": "Nov 29, 2025, 1:07:12 AM", - "dateUpdated": "Dec 1, 2025, 2:11:17 PM", - "baseScore": 6.3, - "baseScoreVersion": "3.1", - "baseScoreVector": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:L", - "references": "https://github.com/fonttools/fonttools/security/advisories/GHSA-768j-98cg-p3fv\nhttps://github.com/fonttools/fonttools/commit/a696d5ba93270d5954f98e7cab5ddca8a02c1e32\n", - "aliases": "CVE-2025-66034\n", - "assigner": "GitHub_M", - "epss": 0.09, - "enisaIdProduct": [ - { - "id": "2ffeef7e-ff67-3e85-a9ac-983195786663", - "product": { - "name": "fonttools" - }, - "product_version": "4.33.0, < 4.60.2" - } - ], - "enisaIdVendor": [ - { - "id": "d709fba0-867b-3831-965e-05b3b019d930", - "vendor": { - "name": "fonttools" - } - } - ] - }, - { - "id": "EUVD-2025-199921", - "enisaUuid": "6ac6d459-6393-3e2e-ad31-122cf95d732e", - "description": "Tryton sao (aka tryton-sao) before 7.6.9 allows XSS via an HTML attachment. This is fixed in 7.6.9, 7.4.19, 7.0.38, and 6.0.67.", - "datePublished": "Nov 30, 2025, 12:00:00 AM", - "dateUpdated": "Dec 1, 2025, 2:10:50 PM", - "baseScore": 5.4, - "baseScoreVersion": "3.1", - "baseScoreVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N", - "references": "https://discuss.tryton.org/t/security-release-for-issue-14290/8895\nhttps://foss.heptapod.net/tryton/tryton/-/issues/14290\nhttps://nvd.nist.gov/vuln/detail/CVE-2025-66420\n", - "aliases": "CVE-2025-66420\n", - "assigner": "mitre", - "epss": 0.03, - "enisaIdProduct": [ - { - "id": "3cc4305f-3f05-39df-bf41-6fe760513270", - "product": { - "name": "sao" - }, - "product_version": "7.0.0 <7.0.38" - } - ], - "enisaIdVendor": [ - { - "id": "2cb8be18-e27b-3cc9-aa3f-e68b7f308d98", - "vendor": { - "name": "tryton" - } - } - ] - } - ], - "total": 452826 -}