From 0ba7a3be0c4218eca4b6337563018c5d9ae362f4 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Mon, 5 Jan 2026 02:11:18 +0530 Subject: [PATCH 01/10] Add V2 Importer for Tuxcare Signed-off-by: Sampurna Pyne --- vulnerabilities/importers/__init__.py | 2 + .../v2_importers/tuxcare_importer.py | 114 ++++++++++++++++++ .../v2_importers/test_tuxcare_importer_v2.py | 48 ++++++++ .../tests/test_data/tuxcare/data.json | 52 ++++++++ .../tests/test_data/tuxcare/expected.json | 102 ++++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 vulnerabilities/pipelines/v2_importers/tuxcare_importer.py create mode 100644 vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py create mode 100644 vulnerabilities/tests/test_data/tuxcare/data.json create mode 100644 vulnerabilities/tests/test_data/tuxcare/expected.json diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index 594021092..e7a03aa8f 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -82,6 +82,7 @@ from vulnerabilities.pipelines.v2_importers import ubuntu_osv_importer as ubuntu_osv_importer_v2 from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2 from vulnerabilities.pipelines.v2_importers import xen_importer as xen_importer_v2 +from vulnerabilities.pipelines.v2_importers import tuxcare_importer as tuxcare_importer_v2 from vulnerabilities.utils import create_registry IMPORTERS_REGISTRY = create_registry( @@ -113,6 +114,7 @@ nginx_importer_v2.NginxImporterPipeline, debian_importer_v2.DebianImporterPipeline, mattermost_importer_v2.MattermostImporterPipeline, + tuxcare_importer_v2.TuxCareImporterPipeline, apache_tomcat_v2.ApacheTomcatImporterPipeline, suse_score_importer_v2.SUSESeverityScoreImporterPipeline, retiredotnet_importer_v2.RetireDotnetImporterPipeline, diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py new file mode 100644 index 000000000..a49d642e0 --- /dev/null +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -0,0 +1,114 @@ +import json +import logging +from typing import Iterable + +from dateutil import parser as date_parser +from django.utils import timezone +from packageurl import PackageURL +from univers.version_range import GenericVersionRange + +from vulnerabilities.importer import AdvisoryData +from vulnerabilities.importer import AffectedPackageV2 +from vulnerabilities.importer import ReferenceV2 +from vulnerabilities.importer import VulnerabilitySeverity +from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2 +from vulnerabilities.severity_systems import GENERIC +from vulnerabilities.utils import fetch_response + +logger = logging.getLogger(__name__) + + +class TuxCareImporterPipeline(VulnerableCodeBaseImporterPipelineV2): + pipeline_id = "tuxcare_importer_v2" + spdx_license_expression = "Apache-2.0" + license_url = "https://tuxcare.com/legal" + url = "https://cve.tuxcare.com/els/download-json?orderBy=updated-desc" + + @classmethod + def steps(cls): + return (cls.collect_and_store_advisories,) + + def advisories_count(self) -> int: + response = fetch_response(self.url) + data = response.json() if response else [] + return len(data) + + def collect_advisories(self) -> Iterable[AdvisoryData]: + response = fetch_response(self.url) + if not response: + return + + data = response.json() + if not data: + return + + for record in data: + cve_id = record.get("cve", "").strip() + if not cve_id or not cve_id.startswith("CVE-"): + continue + + os_name = record.get("os_name", "").strip() + project_name = record.get("project_name", "").strip() + version = record.get("version", "").strip() + score = record.get("score", "").strip() + severity = record.get("severity", "").strip() + status = record.get("status", "").strip() + last_updated = record.get("last_updated", "").strip() + + safe_os = os_name.replace(" ", "_") if os_name else "unknown" + advisory_id = f"TUXCARE-{cve_id}-{safe_os}-{project_name}" + + summary = f"TuxCare advisory for {cve_id}" + if project_name: + summary += f" in {project_name}" + if os_name: + summary += f" on {os_name}" + + affected_packages = [] + if project_name: + purl = PackageURL(type="generic", name=project_name) + + affected_version_range = None + if version: + try: + affected_version_range = GenericVersionRange.from_versions([version]) + except Exception: + pass + + affected_packages.append( + AffectedPackageV2( + package=purl, + affected_version_range=affected_version_range, + ) + ) + + severities = [] + if severity and score: + severities.append( + VulnerabilitySeverity( + system=GENERIC, + value=f"{severity} ({score})", + scoring_elements=f"score={score},severity={severity}", + ) + ) + + date_published = None + if last_updated: + try: + date_published = date_parser.parse(last_updated) + if timezone.is_naive(date_published): + date_published = timezone.make_aware(date_published, timezone=timezone.utc) + except Exception: + pass + + yield AdvisoryData( + advisory_id=advisory_id, + aliases=[cve_id], + summary=summary, + affected_packages=affected_packages, + references_v2=[ReferenceV2(url="https://cve.tuxcare.com/")], + severities=severities, + date_published=date_published, + url="https://cve.tuxcare.com/", + original_advisory_text=json.dumps(record, indent=2, ensure_ascii=False), + ) diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py new file mode 100644 index 000000000..98d5dc5af --- /dev/null +++ b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py @@ -0,0 +1,48 @@ +# +# 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.pipelines.v2_importers.tuxcare_importer import TuxCareImporterPipeline +from vulnerabilities.tests import util_tests + +TEST_DATA = Path(__file__).parent.parent.parent / "test_data" / "tuxcare" + + +class TestTuxCareImporterPipeline(TestCase): + @patch("vulnerabilities.pipelines.v2_importers.tuxcare_importer.fetch_response") + def test_collect_advisories(self, mock_fetch): + """Test collecting and parsing advisories from test data""" + sample_path = TEST_DATA / "data.json" + sample_data = json.loads(sample_path.read_text(encoding="utf-8")) + + mock_fetch.return_value = Mock(json=lambda: sample_data) + + pipeline = TuxCareImporterPipeline() + advisories = [data.to_dict() for data in list(pipeline.collect_advisories())] + + expected_file = TEST_DATA / "expected.json" + util_tests.check_results_against_json(advisories, expected_file) + + @patch("vulnerabilities.pipelines.v2_importers.tuxcare_importer.fetch_response") + def test_advisories_count(self, mock_fetch): + """Test counting advisories""" + sample_path = TEST_DATA / "data.json" + sample_data = json.loads(sample_path.read_text(encoding="utf-8")) + + mock_fetch.return_value = Mock(json=lambda: sample_data) + + pipeline = TuxCareImporterPipeline() + count = pipeline.advisories_count() + + assert count == 5 diff --git a/vulnerabilities/tests/test_data/tuxcare/data.json b/vulnerabilities/tests/test_data/tuxcare/data.json new file mode 100644 index 000000000..0bb656622 --- /dev/null +++ b/vulnerabilities/tests/test_data/tuxcare/data.json @@ -0,0 +1,52 @@ +[ + { + "cve": "CVE-2023-52922", + "os_name": "CloudLinux 7 ELS", + "project_name": "squid", + "version": "3.5.20", + "score": "7.8", + "severity": "HIGH", + "status": "In Testing", + "last_updated": "2025-12-23 10:08:36.423446" + }, + { + "cve": "CVE-2023-52922", + "os_name": "Oracle Linux 7 ELS", + "project_name": "squid", + "version": "3.5.20", + "score": "7.8", + "severity": "HIGH", + "status": "In Testing", + "last_updated": "2025-12-23 10:08:35.944749" + }, + { + "cve": "CVE-2023-48161", + "os_name": "RHEL 7 ELS", + "project_name": "java-11-openjdk", + "version": "11.0.23", + "score": "7.1", + "severity": "HIGH", + "status": "In Progress", + "last_updated": "2025-12-23 08:55:12.096092" + }, + { + "cve": "CVE-2024-21147", + "os_name": "RHEL 7 ELS", + "project_name": "java-11-openjdk", + "version": "11.0.23", + "score": "7.4", + "severity": "HIGH", + "status": "In Progress", + "last_updated": "2025-12-23 08:55:07.139188" + }, + { + "cve": "CVE-2025-21587", + "os_name": "RHEL 7 ELS", + "project_name": "java-11-openjdk", + "version": "11.0.23", + "score": "7.4", + "severity": "HIGH", + "status": "In Progress", + "last_updated": "2025-12-23 08:55:06.706873" + } +] diff --git a/vulnerabilities/tests/test_data/tuxcare/expected.json b/vulnerabilities/tests/test_data/tuxcare/expected.json new file mode 100644 index 000000000..e7488c57e --- /dev/null +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -0,0 +1,102 @@ +[ + { + "advisory_id": "TUXCARE-CVE-2023-52922-CloudLinux_7_ELS-squid", + "aliases": ["CVE-2023-52922"], + "summary": "TuxCare advisory for CVE-2023-52922 in squid on CloudLinux 7 ELS", + "affected_packages": [ + { + "package": {"type": "generic", "namespace": "", "name": "squid", "version": "", "qualifiers": "", "subpath": ""}, + "affected_version_range": "vers:generic/3.5.20", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "patches": [], + "severities": [{"system": "generic_textual", "value": "HIGH (7.8)", "scoring_elements": "score=7.8,severity=HIGH"}], + "date_published": "2025-12-23T10:08:36.423446+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/" + }, + { + "advisory_id": "TUXCARE-CVE-2023-52922-Oracle_Linux_7_ELS-squid", + "aliases": ["CVE-2023-52922"], + "summary": "TuxCare advisory for CVE-2023-52922 in squid on Oracle Linux 7 ELS", + "affected_packages": [ + { + "package": {"type": "generic", "namespace": "", "name": "squid", "version": "", "qualifiers": "", "subpath": ""}, + "affected_version_range": "vers:generic/3.5.20", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "patches": [], + "severities": [{"system": "generic_textual", "value": "HIGH (7.8)", "scoring_elements": "score=7.8,severity=HIGH"}], + "date_published": "2025-12-23T10:08:35.944749+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/" + }, + { + "advisory_id": "TUXCARE-CVE-2023-48161-RHEL_7_ELS-java-11-openjdk", + "aliases": ["CVE-2023-48161"], + "summary": "TuxCare advisory for CVE-2023-48161 in java-11-openjdk on RHEL 7 ELS", + "affected_packages": [ + { + "package": {"type": "generic", "namespace": "", "name": "java-11-openjdk", "version": "", "qualifiers": "", "subpath": ""}, + "affected_version_range": "vers:generic/11.0.23", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "patches": [], + "severities": [{"system": "generic_textual", "value": "HIGH (7.1)", "scoring_elements": "score=7.1,severity=HIGH"}], + "date_published": "2025-12-23T08:55:12.096092+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/" + }, + { + "advisory_id": "TUXCARE-CVE-2024-21147-RHEL_7_ELS-java-11-openjdk", + "aliases": ["CVE-2024-21147"], + "summary": "TuxCare advisory for CVE-2024-21147 in java-11-openjdk on RHEL 7 ELS", + "affected_packages": [ + { + "package": {"type": "generic", "namespace": "", "name": "java-11-openjdk", "version": "", "qualifiers": "", "subpath": ""}, + "affected_version_range": "vers:generic/11.0.23", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "patches": [], + "severities": [{"system": "generic_textual", "value": "HIGH (7.4)", "scoring_elements": "score=7.4,severity=HIGH"}], + "date_published": "2025-12-23T08:55:07.139188+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/" + }, + { + "advisory_id": "TUXCARE-CVE-2025-21587-RHEL_7_ELS-java-11-openjdk", + "aliases": ["CVE-2025-21587"], + "summary": "TuxCare advisory for CVE-2025-21587 in java-11-openjdk on RHEL 7 ELS", + "affected_packages": [ + { + "package": {"type": "generic", "namespace": "", "name": "java-11-openjdk", "version": "", "qualifiers": "", "subpath": ""}, + "affected_version_range": "vers:generic/11.0.23", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "patches": [], + "severities": [{"system": "generic_textual", "value": "HIGH (7.4)", "scoring_elements": "score=7.4,severity=HIGH"}], + "date_published": "2025-12-23T08:55:06.706873+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/" + } +] From 8bdd80a29317b2ea51bbf53975ec28c32bb5015d Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Thu, 8 Jan 2026 02:34:53 +0530 Subject: [PATCH 02/10] Refactor as per review Signed-off-by: Sampurna Pyne --- .../v2_importers/tuxcare_importer.py | 58 ++++++++----------- .../v2_importers/test_tuxcare_importer_v2.py | 16 +---- .../tests/test_data/tuxcare/expected.json | 50 ++++++++-------- 3 files changed, 53 insertions(+), 71 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index a49d642e0..e93e448d0 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -1,15 +1,15 @@ import json import logging from typing import Iterable +from typing import Mapping -from dateutil import parser as date_parser -from django.utils import timezone +from dateutil.parser import parse from packageurl import PackageURL +from pytz import UTC from univers.version_range import GenericVersionRange from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackageV2 -from vulnerabilities.importer import ReferenceV2 from vulnerabilities.importer import VulnerabilitySeverity from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2 from vulnerabilities.severity_systems import GENERIC @@ -22,27 +22,25 @@ class TuxCareImporterPipeline(VulnerableCodeBaseImporterPipelineV2): pipeline_id = "tuxcare_importer_v2" spdx_license_expression = "Apache-2.0" license_url = "https://tuxcare.com/legal" - url = "https://cve.tuxcare.com/els/download-json?orderBy=updated-desc" @classmethod def steps(cls): - return (cls.collect_and_store_advisories,) + return ( + cls.fetch, + cls.collect_and_store_advisories, + ) + + def fetch(self) -> Iterable[Mapping]: + url = "https://cve.tuxcare.com/els/download-json?orderBy=updated-desc" + self.log(f"Fetching `{url}`") + response = fetch_response(url) + self.response = response.json() if response else [] def advisories_count(self) -> int: - response = fetch_response(self.url) - data = response.json() if response else [] - return len(data) + return len(self.response) def collect_advisories(self) -> Iterable[AdvisoryData]: - response = fetch_response(self.url) - if not response: - return - - data = response.json() - if not data: - return - - for record in data: + for record in self.response: cve_id = record.get("cve", "").strip() if not cve_id or not cve_id.startswith("CVE-"): continue @@ -52,11 +50,9 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: version = record.get("version", "").strip() score = record.get("score", "").strip() severity = record.get("severity", "").strip() - status = record.get("status", "").strip() last_updated = record.get("last_updated", "").strip() - safe_os = os_name.replace(" ", "_") if os_name else "unknown" - advisory_id = f"TUXCARE-{cve_id}-{safe_os}-{project_name}" + advisory_id = cve_id summary = f"TuxCare advisory for {cve_id}" if project_name: @@ -67,13 +63,13 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: affected_packages = [] if project_name: purl = PackageURL(type="generic", name=project_name) - + affected_version_range = None if version: try: affected_version_range = GenericVersionRange.from_versions([version]) - except Exception: - pass + except ValueError as e: + logger.warning(f"Failed to parse version {version} for {cve_id}: {e}") affected_packages.append( AffectedPackageV2( @@ -87,28 +83,24 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: severities.append( VulnerabilitySeverity( system=GENERIC, - value=f"{severity} ({score})", - scoring_elements=f"score={score},severity={severity}", + value=score, + scoring_elements=severity, ) ) date_published = None if last_updated: try: - date_published = date_parser.parse(last_updated) - if timezone.is_naive(date_published): - date_published = timezone.make_aware(date_published, timezone=timezone.utc) - except Exception: - pass + date_published = parse(last_updated).replace(tzinfo=UTC) + except ValueError as e: + logger.warning(f"Failed to parse date {last_updated} for {cve_id}: {e}") yield AdvisoryData( advisory_id=advisory_id, - aliases=[cve_id], summary=summary, affected_packages=affected_packages, - references_v2=[ReferenceV2(url="https://cve.tuxcare.com/")], severities=severities, date_published=date_published, - url="https://cve.tuxcare.com/", + url=f"https://cve.tuxcare.com/els/cve/{cve_id}", original_advisory_text=json.dumps(record, indent=2, ensure_ascii=False), ) diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py index 98d5dc5af..a0ca0218a 100644 --- a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py +++ b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py @@ -22,27 +22,17 @@ class TestTuxCareImporterPipeline(TestCase): @patch("vulnerabilities.pipelines.v2_importers.tuxcare_importer.fetch_response") def test_collect_advisories(self, mock_fetch): - """Test collecting and parsing advisories from test data""" sample_path = TEST_DATA / "data.json" sample_data = json.loads(sample_path.read_text(encoding="utf-8")) mock_fetch.return_value = Mock(json=lambda: sample_data) pipeline = TuxCareImporterPipeline() + pipeline.fetch() + advisories = [data.to_dict() for data in list(pipeline.collect_advisories())] expected_file = TEST_DATA / "expected.json" util_tests.check_results_against_json(advisories, expected_file) - @patch("vulnerabilities.pipelines.v2_importers.tuxcare_importer.fetch_response") - def test_advisories_count(self, mock_fetch): - """Test counting advisories""" - sample_path = TEST_DATA / "data.json" - sample_data = json.loads(sample_path.read_text(encoding="utf-8")) - - mock_fetch.return_value = Mock(json=lambda: sample_data) - - pipeline = TuxCareImporterPipeline() - count = pipeline.advisories_count() - - assert count == 5 + assert pipeline.advisories_count() == 5 diff --git a/vulnerabilities/tests/test_data/tuxcare/expected.json b/vulnerabilities/tests/test_data/tuxcare/expected.json index e7488c57e..96287039a 100644 --- a/vulnerabilities/tests/test_data/tuxcare/expected.json +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -1,7 +1,7 @@ [ { - "advisory_id": "TUXCARE-CVE-2023-52922-CloudLinux_7_ELS-squid", - "aliases": ["CVE-2023-52922"], + "advisory_id": "CVE-2023-52922", + "aliases": [], "summary": "TuxCare advisory for CVE-2023-52922 in squid on CloudLinux 7 ELS", "affected_packages": [ { @@ -12,16 +12,16 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "HIGH (7.8)", "scoring_elements": "score=7.8,severity=HIGH"}], + "severities": [{"system": "generic_textual", "value": "7.8", "scoring_elements": "HIGH"}], "date_published": "2025-12-23T10:08:36.423446+00:00", "weaknesses": [], - "url": "https://cve.tuxcare.com/" + "url": "https://cve.tuxcare.com/els/cve/CVE-2023-52922" }, { - "advisory_id": "TUXCARE-CVE-2023-52922-Oracle_Linux_7_ELS-squid", - "aliases": ["CVE-2023-52922"], + "advisory_id": "CVE-2023-52922", + "aliases": [], "summary": "TuxCare advisory for CVE-2023-52922 in squid on Oracle Linux 7 ELS", "affected_packages": [ { @@ -32,16 +32,16 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "HIGH (7.8)", "scoring_elements": "score=7.8,severity=HIGH"}], + "severities": [{"system": "generic_textual", "value": "7.8", "scoring_elements": "HIGH"}], "date_published": "2025-12-23T10:08:35.944749+00:00", "weaknesses": [], - "url": "https://cve.tuxcare.com/" + "url": "https://cve.tuxcare.com/els/cve/CVE-2023-52922" }, { - "advisory_id": "TUXCARE-CVE-2023-48161-RHEL_7_ELS-java-11-openjdk", - "aliases": ["CVE-2023-48161"], + "advisory_id": "CVE-2023-48161", + "aliases": [], "summary": "TuxCare advisory for CVE-2023-48161 in java-11-openjdk on RHEL 7 ELS", "affected_packages": [ { @@ -52,16 +52,16 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "HIGH (7.1)", "scoring_elements": "score=7.1,severity=HIGH"}], + "severities": [{"system": "generic_textual", "value": "7.1", "scoring_elements": "HIGH"}], "date_published": "2025-12-23T08:55:12.096092+00:00", "weaknesses": [], - "url": "https://cve.tuxcare.com/" + "url": "https://cve.tuxcare.com/els/cve/CVE-2023-48161" }, { - "advisory_id": "TUXCARE-CVE-2024-21147-RHEL_7_ELS-java-11-openjdk", - "aliases": ["CVE-2024-21147"], + "advisory_id": "CVE-2024-21147", + "aliases": [], "summary": "TuxCare advisory for CVE-2024-21147 in java-11-openjdk on RHEL 7 ELS", "affected_packages": [ { @@ -72,16 +72,16 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "HIGH (7.4)", "scoring_elements": "score=7.4,severity=HIGH"}], + "severities": [{"system": "generic_textual", "value": "7.4", "scoring_elements": "HIGH"}], "date_published": "2025-12-23T08:55:07.139188+00:00", "weaknesses": [], - "url": "https://cve.tuxcare.com/" + "url": "https://cve.tuxcare.com/els/cve/CVE-2024-21147" }, { - "advisory_id": "TUXCARE-CVE-2025-21587-RHEL_7_ELS-java-11-openjdk", - "aliases": ["CVE-2025-21587"], + "advisory_id": "CVE-2025-21587", + "aliases": [], "summary": "TuxCare advisory for CVE-2025-21587 in java-11-openjdk on RHEL 7 ELS", "affected_packages": [ { @@ -92,11 +92,11 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [{"reference_id": "", "reference_type": "", "url": "https://cve.tuxcare.com/"}], + "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "HIGH (7.4)", "scoring_elements": "score=7.4,severity=HIGH"}], + "severities": [{"system": "generic_textual", "value": "7.4", "scoring_elements": "HIGH"}], "date_published": "2025-12-23T08:55:06.706873+00:00", "weaknesses": [], - "url": "https://cve.tuxcare.com/" + "url": "https://cve.tuxcare.com/els/cve/CVE-2025-21587" } ] From 2c0239a78ef1c691e206ab55491376877b708be5 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Sat, 10 Jan 2026 05:10:57 +0530 Subject: [PATCH 03/10] Refactor to PURL qualifier Signed-off-by: Sampurna Pyne --- .../v2_importers/tuxcare_importer.py | 34 +++++++++++++++++-- .../tests/test_data/tuxcare/expected.json | 10 +++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index e93e448d0..dee36f461 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -1,7 +1,6 @@ import json import logging -from typing import Iterable -from typing import Mapping +from typing import Iterable, Mapping from dateutil.parser import parse from packageurl import PackageURL @@ -39,6 +38,35 @@ def fetch(self) -> Iterable[Mapping]: def advisories_count(self) -> int: return len(self.response) + def _create_purl(self, project_name: str, os_name: str) -> PackageURL: + os_mapping = { + "ubuntu": ("deb", "ubuntu"), + "debian": ("deb", "debian"), + "centos": ("rpm", "centos"), + "almalinux": ("rpm", "almalinux"), + "rhel": ("rpm", "redhat"), + "red hat": ("rpm", "redhat"), + "oracle": ("rpm", "oracle"), + "cloudlinux": ("rpm", "cloudlinux"), + "alpine": ("apk", "alpine"), + } + + qualifiers = {} + if os_name: + qualifiers["os"] = os_name + + if not os_name: + return PackageURL(type="generic", name=project_name) + + os_lower = os_name.lower() + for keyword, (pkg_type, namespace) in os_mapping.items(): + if keyword in os_lower: + return PackageURL( + type=pkg_type, namespace=namespace, name=project_name, qualifiers=qualifiers + ) + + return PackageURL(type="generic", name=project_name, qualifiers=qualifiers) + def collect_advisories(self) -> Iterable[AdvisoryData]: for record in self.response: cve_id = record.get("cve", "").strip() @@ -62,7 +90,7 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: affected_packages = [] if project_name: - purl = PackageURL(type="generic", name=project_name) + purl = self._create_purl(project_name, os_name) affected_version_range = None if version: diff --git a/vulnerabilities/tests/test_data/tuxcare/expected.json b/vulnerabilities/tests/test_data/tuxcare/expected.json index 96287039a..9ab7ebdd3 100644 --- a/vulnerabilities/tests/test_data/tuxcare/expected.json +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -5,7 +5,7 @@ "summary": "TuxCare advisory for CVE-2023-52922 in squid on CloudLinux 7 ELS", "affected_packages": [ { - "package": {"type": "generic", "namespace": "", "name": "squid", "version": "", "qualifiers": "", "subpath": ""}, + "package": {"type": "rpm", "namespace": "cloudlinux", "name": "squid", "version": "", "qualifiers": "os=CloudLinux%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/3.5.20", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -25,7 +25,7 @@ "summary": "TuxCare advisory for CVE-2023-52922 in squid on Oracle Linux 7 ELS", "affected_packages": [ { - "package": {"type": "generic", "namespace": "", "name": "squid", "version": "", "qualifiers": "", "subpath": ""}, + "package": {"type": "rpm", "namespace": "oracle", "name": "squid", "version": "", "qualifiers": "os=Oracle%20Linux%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/3.5.20", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -45,7 +45,7 @@ "summary": "TuxCare advisory for CVE-2023-48161 in java-11-openjdk on RHEL 7 ELS", "affected_packages": [ { - "package": {"type": "generic", "namespace": "", "name": "java-11-openjdk", "version": "", "qualifiers": "", "subpath": ""}, + "package": {"type": "rpm", "namespace": "redhat", "name": "java-11-openjdk", "version": "", "qualifiers": "os=RHEL%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -65,7 +65,7 @@ "summary": "TuxCare advisory for CVE-2024-21147 in java-11-openjdk on RHEL 7 ELS", "affected_packages": [ { - "package": {"type": "generic", "namespace": "", "name": "java-11-openjdk", "version": "", "qualifiers": "", "subpath": ""}, + "package": {"type": "rpm", "namespace": "redhat", "name": "java-11-openjdk", "version": "", "qualifiers": "os=RHEL%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -85,7 +85,7 @@ "summary": "TuxCare advisory for CVE-2025-21587 in java-11-openjdk on RHEL 7 ELS", "affected_packages": [ { - "package": {"type": "generic", "namespace": "", "name": "java-11-openjdk", "version": "", "qualifiers": "", "subpath": ""}, + "package": {"type": "rpm", "namespace": "redhat", "name": "java-11-openjdk", "version": "", "qualifiers": "os=RHEL%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], From cb6b7c21b65d87a843df1952526fedeca27dc4bb Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Sat, 10 Jan 2026 18:44:30 +0530 Subject: [PATCH 04/10] Codestyle fix Signed-off-by: Sampurna Pyne --- vulnerabilities/pipelines/v2_importers/tuxcare_importer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index dee36f461..6ecd08997 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -1,6 +1,7 @@ import json import logging -from typing import Iterable, Mapping +from typing import Iterable +from typing import Mapping from dateutil.parser import parse from packageurl import PackageURL From 917678803d7b8ee79f29e687a66a124a0129eed6 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Mon, 12 Jan 2026 19:49:39 +0530 Subject: [PATCH 05/10] Refactor PURL and fix type-hinting Signed-off-by: Sampurna Pyne --- .../v2_importers/tuxcare_importer.py | 38 +++---------------- .../tests/test_data/tuxcare/expected.json | 20 +++++----- 2 files changed, 15 insertions(+), 43 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index 6ecd08997..dd162a480 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -1,7 +1,6 @@ import json import logging from typing import Iterable -from typing import Mapping from dateutil.parser import parse from packageurl import PackageURL @@ -30,7 +29,7 @@ def steps(cls): cls.collect_and_store_advisories, ) - def fetch(self) -> Iterable[Mapping]: + def fetch(self) -> None: url = "https://cve.tuxcare.com/els/download-json?orderBy=updated-desc" self.log(f"Fetching `{url}`") response = fetch_response(url) @@ -40,33 +39,13 @@ def advisories_count(self) -> int: return len(self.response) def _create_purl(self, project_name: str, os_name: str) -> PackageURL: - os_mapping = { - "ubuntu": ("deb", "ubuntu"), - "debian": ("deb", "debian"), - "centos": ("rpm", "centos"), - "almalinux": ("rpm", "almalinux"), - "rhel": ("rpm", "redhat"), - "red hat": ("rpm", "redhat"), - "oracle": ("rpm", "oracle"), - "cloudlinux": ("rpm", "cloudlinux"), - "alpine": ("apk", "alpine"), - } - qualifiers = {} if os_name: - qualifiers["os"] = os_name - - if not os_name: - return PackageURL(type="generic", name=project_name) - - os_lower = os_name.lower() - for keyword, (pkg_type, namespace) in os_mapping.items(): - if keyword in os_lower: - return PackageURL( - type=pkg_type, namespace=namespace, name=project_name, qualifiers=qualifiers - ) + qualifiers["distro"] = os_name - return PackageURL(type="generic", name=project_name, qualifiers=qualifiers) + return PackageURL( + type="generic", namespace="tuxcare", name=project_name, qualifiers=qualifiers + ) def collect_advisories(self) -> Iterable[AdvisoryData]: for record in self.response: @@ -83,12 +62,6 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: advisory_id = cve_id - summary = f"TuxCare advisory for {cve_id}" - if project_name: - summary += f" in {project_name}" - if os_name: - summary += f" on {os_name}" - affected_packages = [] if project_name: purl = self._create_purl(project_name, os_name) @@ -126,7 +99,6 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: yield AdvisoryData( advisory_id=advisory_id, - summary=summary, affected_packages=affected_packages, severities=severities, date_published=date_published, diff --git a/vulnerabilities/tests/test_data/tuxcare/expected.json b/vulnerabilities/tests/test_data/tuxcare/expected.json index 9ab7ebdd3..a4497fde9 100644 --- a/vulnerabilities/tests/test_data/tuxcare/expected.json +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -2,10 +2,10 @@ { "advisory_id": "CVE-2023-52922", "aliases": [], - "summary": "TuxCare advisory for CVE-2023-52922 in squid on CloudLinux 7 ELS", + "summary": "", "affected_packages": [ { - "package": {"type": "rpm", "namespace": "cloudlinux", "name": "squid", "version": "", "qualifiers": "os=CloudLinux%207%20ELS", "subpath": ""}, + "package": {"type": "generic", "namespace": "tuxcare", "name": "squid", "version": "", "qualifiers": "distro=CloudLinux%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/3.5.20", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -22,10 +22,10 @@ { "advisory_id": "CVE-2023-52922", "aliases": [], - "summary": "TuxCare advisory for CVE-2023-52922 in squid on Oracle Linux 7 ELS", + "summary": "", "affected_packages": [ { - "package": {"type": "rpm", "namespace": "oracle", "name": "squid", "version": "", "qualifiers": "os=Oracle%20Linux%207%20ELS", "subpath": ""}, + "package": {"type": "generic", "namespace": "tuxcare", "name": "squid", "version": "", "qualifiers": "distro=Oracle%20Linux%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/3.5.20", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -42,10 +42,10 @@ { "advisory_id": "CVE-2023-48161", "aliases": [], - "summary": "TuxCare advisory for CVE-2023-48161 in java-11-openjdk on RHEL 7 ELS", + "summary": "", "affected_packages": [ { - "package": {"type": "rpm", "namespace": "redhat", "name": "java-11-openjdk", "version": "", "qualifiers": "os=RHEL%207%20ELS", "subpath": ""}, + "package": {"type": "generic", "namespace": "tuxcare", "name": "java-11-openjdk", "version": "", "qualifiers": "distro=RHEL%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -62,10 +62,10 @@ { "advisory_id": "CVE-2024-21147", "aliases": [], - "summary": "TuxCare advisory for CVE-2024-21147 in java-11-openjdk on RHEL 7 ELS", + "summary": "", "affected_packages": [ { - "package": {"type": "rpm", "namespace": "redhat", "name": "java-11-openjdk", "version": "", "qualifiers": "os=RHEL%207%20ELS", "subpath": ""}, + "package": {"type": "generic", "namespace": "tuxcare", "name": "java-11-openjdk", "version": "", "qualifiers": "distro=RHEL%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -82,10 +82,10 @@ { "advisory_id": "CVE-2025-21587", "aliases": [], - "summary": "TuxCare advisory for CVE-2025-21587 in java-11-openjdk on RHEL 7 ELS", + "summary": "", "affected_packages": [ { - "package": {"type": "rpm", "namespace": "redhat", "name": "java-11-openjdk", "version": "", "qualifiers": "os=RHEL%207%20ELS", "subpath": ""}, + "package": {"type": "generic", "namespace": "tuxcare", "name": "java-11-openjdk", "version": "", "qualifiers": "distro=RHEL%207%20ELS", "subpath": ""}, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], From d9b3f2c6a8165abfbcc6352a7c260c732b202948 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Tue, 13 Jan 2026 18:42:05 +0530 Subject: [PATCH 06/10] Fix import ordering Signed-off-by: Sampurna Pyne --- vulnerabilities/importers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index e7a03aa8f..514f1d087 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -78,11 +78,11 @@ from vulnerabilities.pipelines.v2_importers import redhat_importer as redhat_importer_v2 from vulnerabilities.pipelines.v2_importers import retiredotnet_importer as retiredotnet_importer_v2 from vulnerabilities.pipelines.v2_importers import ruby_importer as ruby_importer_v2 +from vulnerabilities.pipelines.v2_importers import tuxcare_importer as tuxcare_importer_v2 from vulnerabilities.pipelines.v2_importers import suse_score_importer as suse_score_importer_v2 from vulnerabilities.pipelines.v2_importers import ubuntu_osv_importer as ubuntu_osv_importer_v2 from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2 from vulnerabilities.pipelines.v2_importers import xen_importer as xen_importer_v2 -from vulnerabilities.pipelines.v2_importers import tuxcare_importer as tuxcare_importer_v2 from vulnerabilities.utils import create_registry IMPORTERS_REGISTRY = create_registry( From f0a3ee832f69a77264bb7e4a8bb1dab400e5acf4 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Wed, 21 Jan 2026 17:46:38 +0530 Subject: [PATCH 07/10] Refactor PURL and status handling Signed-off-by: Sampurna Pyne --- .../v2_importers/tuxcare_importer.py | 108 +++++- .../v2_importers/test_tuxcare_importer_v2.py | 2 +- .../tests/test_data/tuxcare/data.json | 80 ++++ .../tests/test_data/tuxcare/expected.json | 362 +++++++++++++++++- 4 files changed, 512 insertions(+), 40 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index dd162a480..118ec48c4 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -1,3 +1,12 @@ +# +# 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 @@ -39,18 +48,46 @@ def advisories_count(self) -> int: return len(self.response) def _create_purl(self, project_name: str, os_name: str) -> PackageURL: + normalized_os = os_name.lower().replace(" ", "-") + os_lower = os_name.lower() + + os_mapping = { + "ubuntu": ("deb", "ubuntu"), + "debian": ("deb", "debian"), + "centos": ("rpm", "centos"), + "almalinux": ("rpm", "almalinux"), + "rhel": ("rpm", "rhel"), + "oracle": ("rpm", "oracle"), + "cloudlinux": ("rpm", "cloudlinux"), + "alpine": ("apk", "alpine"), + "unknown": ("generic", "tuxcare"), + "tuxcare": ("generic", "tuxcare"), + } + + pkg_type = "generic" + namespace = "tuxcare" + + for keyword, (ptype, pns) in os_mapping.items(): + if keyword in os_lower: + pkg_type = ptype + namespace = pns + break + else: + return None + qualifiers = {} - if os_name: - qualifiers["distro"] = os_name + if normalized_os: + qualifiers["distro"] = normalized_os return PackageURL( - type="generic", namespace="tuxcare", name=project_name, qualifiers=qualifiers + type=pkg_type, namespace=namespace, name=project_name, qualifiers=qualifiers ) def collect_advisories(self) -> Iterable[AdvisoryData]: for record in self.response: cve_id = record.get("cve", "").strip() if not cve_id or not cve_id.startswith("CVE-"): + logger.warning(f"Skipping record with invalid CVE ID: {cve_id}") continue os_name = record.get("os_name", "").strip() @@ -58,27 +95,61 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: version = record.get("version", "").strip() score = record.get("score", "").strip() severity = record.get("severity", "").strip() + status = record.get("status", "").strip() last_updated = record.get("last_updated", "").strip() - advisory_id = cve_id + if not all([os_name, project_name, version, status]): + logger.warning(f"Skipping {cve_id} - missing required fields") + continue - affected_packages = [] - if project_name: - purl = self._create_purl(project_name, os_name) + # See https://docs.tuxcare.com/els-for-os/#cve-status-definition + non_affected_statuses = ["Not Vulnerable"] + affected_statuses = [ + "Ignored", + "Needs Triage", + "In Testing", + "In Progress", + "In Rollout", + ] + fixed_statuses = ["Released", "Already Fixed"] + + # Skip CVEs that are not vulnerable + if status in non_affected_statuses: + continue - affected_version_range = None - if version: - try: - affected_version_range = GenericVersionRange.from_versions([version]) - except ValueError as e: - logger.warning(f"Failed to parse version {version} for {cve_id}: {e}") + if status not in affected_statuses and status not in fixed_statuses: + logger.warning(f"Skipping {cve_id} - unknown status: {status}") + continue - affected_packages.append( - AffectedPackageV2( - package=purl, - affected_version_range=affected_version_range, - ) + normalized_os = os_name.lower().replace(" ", "-") + advisory_id = f"{cve_id}-{normalized_os}-{project_name.lower()}-{version}" + + purl = self._create_purl(project_name, os_name) + if not purl: + logger.warning(f"Skipping {cve_id} - unexpected OS type: '{os_name}'") + continue + + try: + version_range = GenericVersionRange.from_versions([version]) + except ValueError as e: + logger.warning(f"Failed to parse version {version} for {cve_id}: {e}") + continue + + affected_version_range = None + fixed_version_range = None + + if status in affected_statuses: + affected_version_range = version_range + elif status in fixed_statuses: + fixed_version_range = version_range + + affected_packages = [ + AffectedPackageV2( + package=purl, + affected_version_range=affected_version_range, + fixed_version_range=fixed_version_range, ) + ] severities = [] if severity and score: @@ -99,6 +170,7 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: yield AdvisoryData( advisory_id=advisory_id, + aliases=[cve_id], affected_packages=affected_packages, severities=severities, date_published=date_published, diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py index a0ca0218a..731fe0052 100644 --- a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py +++ b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py @@ -35,4 +35,4 @@ def test_collect_advisories(self, mock_fetch): expected_file = TEST_DATA / "expected.json" util_tests.check_results_against_json(advisories, expected_file) - assert pipeline.advisories_count() == 5 + assert pipeline.advisories_count() == 13 diff --git a/vulnerabilities/tests/test_data/tuxcare/data.json b/vulnerabilities/tests/test_data/tuxcare/data.json index 0bb656622..8a2e5268d 100644 --- a/vulnerabilities/tests/test_data/tuxcare/data.json +++ b/vulnerabilities/tests/test_data/tuxcare/data.json @@ -48,5 +48,85 @@ "severity": "HIGH", "status": "In Progress", "last_updated": "2025-12-23 08:55:06.706873" + }, + { + "cve": "CVE-2024-39502", + "os_name": "Unknown OS", + "project_name": "kernel", + "version": "2.6.32", + "score": "7.8", + "severity": "HIGH", + "status": "Needs Triage", + "last_updated": "2025-09-20 06:03:30.551756" + }, + { + "cve": "CVE-2024-40927", + "os_name": "Unknown OS", + "project_name": "kernel", + "version": "2.6.32", + "score": "7.8", + "severity": "HIGH", + "status": "Needs Triage", + "last_updated": "2025-09-20 06:03:26.132106" + }, + { + "cve": "CVE-2025-4517", + "os_name": "CentOS 8.4 ELS", + "project_name": "python2", + "version": "2.7.18", + "score": "7.6", + "severity": "HIGH", + "status": "Not Vulnerable", + "last_updated": "2025-12-22 16:43:49.287021" + }, + { + "cve": "CVE-2025-43392", + "os_name": "TuxCare 9.6 ESU", + "project_name": "webkit2gtk3", + "version": "2.50.1", + "score": "6.5", + "severity": "MEDIUM", + "status": "In Testing", + "last_updated": "2025-12-20 04:26:48.737089" + }, + { + "cve": "CVE-2023-50868", + "os_name": "CloudLinux 7 ELS", + "project_name": "dhcp", + "version": "4.2.5", + "score": "7.5", + "severity": "HIGH", + "status": "Already Fixed", + "last_updated": "2025-12-23 01:56:28.627699" + }, + { + "cve": "CVE-2021-33193", + "os_name": "Unknown OS", + "project_name": "httpd", + "version": "2.2.15", + "score": "7.5", + "severity": "HIGH", + "status": "Ignored", + "last_updated": "2025-09-19 21:21:01.425783" + }, + { + "cve": "CVE-2025-50093", + "os_name": "AlmaLinux 9.2 ESU", + "project_name": "mysql", + "version": "8.0.32", + "score": "4.9", + "severity": "MEDIUM", + "status": "Released", + "last_updated": "2025-12-22 17:11:15.409148" + }, + { + "cve": "CVE-2025-64505", + "os_name": "CentOS 7 ELS", + "project_name": "libpng", + "version": "1.5.13", + "score": "4.4", + "severity": "MEDIUM", + "status": "In Rollout", + "last_updated": "2025-12-20 04:34:51.112485" } ] diff --git a/vulnerabilities/tests/test_data/tuxcare/expected.json b/vulnerabilities/tests/test_data/tuxcare/expected.json index a4497fde9..0878f6f55 100644 --- a/vulnerabilities/tests/test_data/tuxcare/expected.json +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -1,11 +1,20 @@ [ { - "advisory_id": "CVE-2023-52922", - "aliases": [], + "advisory_id": "CVE-2023-52922-cloudlinux-7-els-squid-3.5.20", + "aliases": [ + "CVE-2023-52922" + ], "summary": "", "affected_packages": [ { - "package": {"type": "generic", "namespace": "tuxcare", "name": "squid", "version": "", "qualifiers": "distro=CloudLinux%207%20ELS", "subpath": ""}, + "package": { + "type": "rpm", + "namespace": "cloudlinux", + "name": "squid", + "version": "", + "qualifiers": "distro=cloudlinux-7-els", + "subpath": "" + }, "affected_version_range": "vers:generic/3.5.20", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -14,18 +23,33 @@ ], "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "7.8", "scoring_elements": "HIGH"}], + "severities": [ + { + "system": "generic_textual", + "value": "7.8", + "scoring_elements": "HIGH" + } + ], "date_published": "2025-12-23T10:08:36.423446+00:00", "weaknesses": [], "url": "https://cve.tuxcare.com/els/cve/CVE-2023-52922" }, { - "advisory_id": "CVE-2023-52922", - "aliases": [], + "advisory_id": "CVE-2023-52922-oracle-linux-7-els-squid-3.5.20", + "aliases": [ + "CVE-2023-52922" + ], "summary": "", "affected_packages": [ { - "package": {"type": "generic", "namespace": "tuxcare", "name": "squid", "version": "", "qualifiers": "distro=Oracle%20Linux%207%20ELS", "subpath": ""}, + "package": { + "type": "rpm", + "namespace": "oracle", + "name": "squid", + "version": "", + "qualifiers": "distro=oracle-linux-7-els", + "subpath": "" + }, "affected_version_range": "vers:generic/3.5.20", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -34,18 +58,33 @@ ], "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "7.8", "scoring_elements": "HIGH"}], + "severities": [ + { + "system": "generic_textual", + "value": "7.8", + "scoring_elements": "HIGH" + } + ], "date_published": "2025-12-23T10:08:35.944749+00:00", "weaknesses": [], "url": "https://cve.tuxcare.com/els/cve/CVE-2023-52922" }, { - "advisory_id": "CVE-2023-48161", - "aliases": [], + "advisory_id": "CVE-2023-48161-rhel-7-els-java-11-openjdk-11.0.23", + "aliases": [ + "CVE-2023-48161" + ], "summary": "", "affected_packages": [ { - "package": {"type": "generic", "namespace": "tuxcare", "name": "java-11-openjdk", "version": "", "qualifiers": "distro=RHEL%207%20ELS", "subpath": ""}, + "package": { + "type": "rpm", + "namespace": "rhel", + "name": "java-11-openjdk", + "version": "", + "qualifiers": "distro=rhel-7-els", + "subpath": "" + }, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -54,18 +93,33 @@ ], "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "7.1", "scoring_elements": "HIGH"}], + "severities": [ + { + "system": "generic_textual", + "value": "7.1", + "scoring_elements": "HIGH" + } + ], "date_published": "2025-12-23T08:55:12.096092+00:00", "weaknesses": [], "url": "https://cve.tuxcare.com/els/cve/CVE-2023-48161" }, { - "advisory_id": "CVE-2024-21147", - "aliases": [], + "advisory_id": "CVE-2024-21147-rhel-7-els-java-11-openjdk-11.0.23", + "aliases": [ + "CVE-2024-21147" + ], "summary": "", "affected_packages": [ { - "package": {"type": "generic", "namespace": "tuxcare", "name": "java-11-openjdk", "version": "", "qualifiers": "distro=RHEL%207%20ELS", "subpath": ""}, + "package": { + "type": "rpm", + "namespace": "rhel", + "name": "java-11-openjdk", + "version": "", + "qualifiers": "distro=rhel-7-els", + "subpath": "" + }, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -74,18 +128,33 @@ ], "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "7.4", "scoring_elements": "HIGH"}], + "severities": [ + { + "system": "generic_textual", + "value": "7.4", + "scoring_elements": "HIGH" + } + ], "date_published": "2025-12-23T08:55:07.139188+00:00", "weaknesses": [], "url": "https://cve.tuxcare.com/els/cve/CVE-2024-21147" }, { - "advisory_id": "CVE-2025-21587", - "aliases": [], + "advisory_id": "CVE-2025-21587-rhel-7-els-java-11-openjdk-11.0.23", + "aliases": [ + "CVE-2025-21587" + ], "summary": "", "affected_packages": [ { - "package": {"type": "generic", "namespace": "tuxcare", "name": "java-11-openjdk", "version": "", "qualifiers": "distro=RHEL%207%20ELS", "subpath": ""}, + "package": { + "type": "rpm", + "namespace": "rhel", + "name": "java-11-openjdk", + "version": "", + "qualifiers": "distro=rhel-7-els", + "subpath": "" + }, "affected_version_range": "vers:generic/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], @@ -94,9 +163,260 @@ ], "references_v2": [], "patches": [], - "severities": [{"system": "generic_textual", "value": "7.4", "scoring_elements": "HIGH"}], + "severities": [ + { + "system": "generic_textual", + "value": "7.4", + "scoring_elements": "HIGH" + } + ], "date_published": "2025-12-23T08:55:06.706873+00:00", "weaknesses": [], "url": "https://cve.tuxcare.com/els/cve/CVE-2025-21587" + }, + { + "advisory_id": "CVE-2024-39502-unknown-os-kernel-2.6.32", + "aliases": [ + "CVE-2024-39502" + ], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "generic", + "namespace": "tuxcare", + "name": "kernel", + "version": "", + "qualifiers": "distro=unknown-os", + "subpath": "" + }, + "affected_version_range": "vers:generic/2.6.32", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "7.8", + "scoring_elements": "HIGH" + } + ], + "date_published": "2025-09-20T06:03:30.551756+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2024-39502" + }, + { + "advisory_id": "CVE-2024-40927-unknown-os-kernel-2.6.32", + "aliases": [ + "CVE-2024-40927" + ], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "generic", + "namespace": "tuxcare", + "name": "kernel", + "version": "", + "qualifiers": "distro=unknown-os", + "subpath": "" + }, + "affected_version_range": "vers:generic/2.6.32", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "7.8", + "scoring_elements": "HIGH" + } + ], + "date_published": "2025-09-20T06:03:26.132106+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2024-40927" + }, + { + "advisory_id": "CVE-2025-43392-tuxcare-9.6-esu-webkit2gtk3-2.50.1", + "aliases": [ + "CVE-2025-43392" + ], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "generic", + "namespace": "tuxcare", + "name": "webkit2gtk3", + "version": "", + "qualifiers": "distro=tuxcare-9.6-esu", + "subpath": "" + }, + "affected_version_range": "vers:generic/2.50.1", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "6.5", + "scoring_elements": "MEDIUM" + } + ], + "date_published": "2025-12-20T04:26:48.737089+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2025-43392" + }, + { + "advisory_id": "CVE-2023-50868-cloudlinux-7-els-dhcp-4.2.5", + "aliases": [ + "CVE-2023-50868" + ], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "rpm", + "namespace": "cloudlinux", + "name": "dhcp", + "version": "", + "qualifiers": "distro=cloudlinux-7-els", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:generic/4.2.5", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "7.5", + "scoring_elements": "HIGH" + } + ], + "date_published": "2025-12-23T01:56:28.627699+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2023-50868" + }, + { + "advisory_id": "CVE-2021-33193-unknown-os-httpd-2.2.15", + "aliases": [ + "CVE-2021-33193" + ], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "generic", + "namespace": "tuxcare", + "name": "httpd", + "version": "", + "qualifiers": "distro=unknown-os", + "subpath": "" + }, + "affected_version_range": "vers:generic/2.2.15", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "7.5", + "scoring_elements": "HIGH" + } + ], + "date_published": "2025-09-19T21:21:01.425783+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2021-33193" + }, + { + "advisory_id": "CVE-2025-50093-almalinux-9.2-esu-mysql-8.0.32", + "aliases": [ + "CVE-2025-50093" + ], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "rpm", + "namespace": "almalinux", + "name": "mysql", + "version": "", + "qualifiers": "distro=almalinux-9.2-esu", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:generic/8.0.32", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "4.9", + "scoring_elements": "MEDIUM" + } + ], + "date_published": "2025-12-22T17:11:15.409148+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2025-50093" + }, + { + "advisory_id": "CVE-2025-64505-centos-7-els-libpng-1.5.13", + "aliases": [ + "CVE-2025-64505" + ], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "rpm", + "namespace": "centos", + "name": "libpng", + "version": "", + "qualifiers": "distro=centos-7-els", + "subpath": "" + }, + "affected_version_range": "vers:generic/1.5.13", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "4.4", + "scoring_elements": "MEDIUM" + } + ], + "date_published": "2025-12-20T04:34:51.112485+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2025-64505" } -] +] \ No newline at end of file From 70b0250bd496afa8fd213fe287d431e1cb7cc7cb Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Tue, 27 Jan 2026 00:49:58 +0530 Subject: [PATCH 08/10] Implement impact packages and specific version with univers Signed-off-by: Sampurna Pyne --- .../v2_importers/tuxcare_importer.py | 214 ++++++++++------- .../v2_importers/test_tuxcare_importer_v2.py | 2 +- .../tests/test_data/tuxcare/data.json | 70 ++++++ .../tests/test_data/tuxcare/expected.json | 222 ++++++++++++------ 4 files changed, 346 insertions(+), 162 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index 118ec48c4..c91b454e4 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -14,7 +14,10 @@ from dateutil.parser import parse from packageurl import PackageURL from pytz import UTC +from univers.version_range import AlpineLinuxVersionRange +from univers.version_range import DebianVersionRange from univers.version_range import GenericVersionRange +from univers.version_range import RpmVersionRange from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackageV2 @@ -25,6 +28,18 @@ logger = logging.getLogger(__name__) +# See https://docs.tuxcare.com/els-for-os/#cve-status-definition +NON_AFFECTED_STATUSES = ["Not Vulnerable"] +AFFECTED_STATUSES = ["Ignored", "Needs Triage", "In Testing", "In Progress", "In Rollout"] +FIXED_STATUSES = ["Released", "Already Fixed"] + +VERSION_RANGE_BY_PURL_TYPE = { + "rpm": RpmVersionRange, + "deb": DebianVersionRange, + "apk": AlpineLinuxVersionRange, + "generic": GenericVersionRange, +} + class TuxCareImporterPipeline(VulnerableCodeBaseImporterPipelineV2): pipeline_id = "tuxcare_importer_v2" @@ -43,9 +58,54 @@ def fetch(self) -> None: self.log(f"Fetching `{url}`") response = fetch_response(url) self.response = response.json() if response else [] + self._grouped = self._group_records_by_cve() + + def _group_records_by_cve(self) -> dict: + grouped = {} + skipped_invalid = 0 + skipped_non_affected = 0 + + for record in self.response: + cve_id = record.get("cve", "").strip() + if not cve_id or not cve_id.startswith("CVE-"): + logger.warning(f"Skipping invalid CVE ID: {cve_id}") + skipped_invalid += 1 + continue + + os_name = record.get("os_name", "").strip() + project_name = record.get("project_name", "").strip() + version = record.get("version", "").strip() + status = record.get("status", "").strip() + + if not all([os_name, project_name, version, status]): + logger.warning(f"Skipping {cve_id}: missing required fields") + skipped_invalid += 1 + continue + + # Skip records with non-affected statuses + if status in NON_AFFECTED_STATUSES: + skipped_non_affected += 1 + continue + + if status not in AFFECTED_STATUSES and status not in FIXED_STATUSES: + logger.warning(f"Skipping {cve_id}: unrecognized status '{status}'") + skipped_invalid += 1 + continue + + if cve_id not in grouped: + grouped[cve_id] = [] + grouped[cve_id].append(record) + + total_skipped = skipped_invalid + skipped_non_affected + self.log( + f"Grouped {len(self.response):,d} records into {len(grouped):,d} unique CVEs " + f"(skipped {total_skipped:,d}: {skipped_invalid:,d} invalid, " + f"{skipped_non_affected:,d} non-affected)" + ) + return grouped def advisories_count(self) -> int: - return len(self.response) + return len(self._grouped) def _create_purl(self, project_name: str, os_name: str) -> PackageURL: normalized_os = os_name.lower().replace(" ", "-") @@ -64,9 +124,6 @@ def _create_purl(self, project_name: str, os_name: str) -> PackageURL: "tuxcare": ("generic", "tuxcare"), } - pkg_type = "generic" - namespace = "tuxcare" - for keyword, (ptype, pns) in os_mapping.items(): if keyword in os_lower: pkg_type = ptype @@ -75,105 +132,90 @@ def _create_purl(self, project_name: str, os_name: str) -> PackageURL: else: return None - qualifiers = {} - if normalized_os: - qualifiers["distro"] = normalized_os + qualifiers = {"distro": normalized_os} return PackageURL( type=pkg_type, namespace=namespace, name=project_name, qualifiers=qualifiers ) def collect_advisories(self) -> Iterable[AdvisoryData]: - for record in self.response: - cve_id = record.get("cve", "").strip() - if not cve_id or not cve_id.startswith("CVE-"): - logger.warning(f"Skipping record with invalid CVE ID: {cve_id}") - continue - - os_name = record.get("os_name", "").strip() - project_name = record.get("project_name", "").strip() - version = record.get("version", "").strip() - score = record.get("score", "").strip() - severity = record.get("severity", "").strip() - status = record.get("status", "").strip() - last_updated = record.get("last_updated", "").strip() + grouped_by_cve = self._grouped - if not all([os_name, project_name, version, status]): - logger.warning(f"Skipping {cve_id} - missing required fields") - continue + for cve_id, records in grouped_by_cve.items(): + affected_packages = [] + severities = [] + date_published = None + all_records = [] + severity_added = False + + for record in records: + os_name = record.get("os_name", "").strip() + project_name = record.get("project_name", "").strip() + version = record.get("version", "").strip() + score = record.get("score", "").strip() + severity = record.get("severity", "").strip() + status = record.get("status", "").strip() + last_updated = record.get("last_updated", "").strip() + + purl = self._create_purl(project_name, os_name) + if not purl: + logger.warning( + f"Skipping package {project_name} on {os_name} for {cve_id} - unexpected OS type" + ) + continue - # See https://docs.tuxcare.com/els-for-os/#cve-status-definition - non_affected_statuses = ["Not Vulnerable"] - affected_statuses = [ - "Ignored", - "Needs Triage", - "In Testing", - "In Progress", - "In Rollout", - ] - fixed_statuses = ["Released", "Already Fixed"] - - # Skip CVEs that are not vulnerable - if status in non_affected_statuses: - continue + version_range_class = VERSION_RANGE_BY_PURL_TYPE.get(purl.type, GenericVersionRange) + try: + version_range = version_range_class.from_versions([version]) + except ValueError as e: + logger.warning(f"Failed to parse version {version} for {cve_id}: {e}") + continue + + affected_version_range = None + fixed_version_range = None + + if status in AFFECTED_STATUSES: + affected_version_range = version_range + elif status in FIXED_STATUSES: + fixed_version_range = version_range + + affected_packages.append( + AffectedPackageV2( + package=purl, + affected_version_range=affected_version_range, + fixed_version_range=fixed_version_range, + ) + ) - if status not in affected_statuses and status not in fixed_statuses: - logger.warning(f"Skipping {cve_id} - unknown status: {status}") - continue + if severity and score and not severity_added: + severities.append( + VulnerabilitySeverity( + system=GENERIC, + value=score, + scoring_elements=severity, + ) + ) + severity_added = True - normalized_os = os_name.lower().replace(" ", "-") - advisory_id = f"{cve_id}-{normalized_os}-{project_name.lower()}-{version}" + if last_updated: + try: + current_date = parse(last_updated).replace(tzinfo=UTC) + if date_published is None or current_date > date_published: + date_published = current_date + except ValueError as e: + logger.warning(f"Failed to parse date {last_updated} for {cve_id}: {e}") - purl = self._create_purl(project_name, os_name) - if not purl: - logger.warning(f"Skipping {cve_id} - unexpected OS type: '{os_name}'") - continue + all_records.append(record) - try: - version_range = GenericVersionRange.from_versions([version]) - except ValueError as e: - logger.warning(f"Failed to parse version {version} for {cve_id}: {e}") + if not affected_packages: + logger.warning(f"Skipping {cve_id} - no valid affected packages") continue - affected_version_range = None - fixed_version_range = None - - if status in affected_statuses: - affected_version_range = version_range - elif status in fixed_statuses: - fixed_version_range = version_range - - affected_packages = [ - AffectedPackageV2( - package=purl, - affected_version_range=affected_version_range, - fixed_version_range=fixed_version_range, - ) - ] - - severities = [] - if severity and score: - severities.append( - VulnerabilitySeverity( - system=GENERIC, - value=score, - scoring_elements=severity, - ) - ) - - date_published = None - if last_updated: - try: - date_published = parse(last_updated).replace(tzinfo=UTC) - except ValueError as e: - logger.warning(f"Failed to parse date {last_updated} for {cve_id}: {e}") - yield AdvisoryData( - advisory_id=advisory_id, - aliases=[cve_id], + advisory_id=cve_id, affected_packages=affected_packages, severities=severities, date_published=date_published, url=f"https://cve.tuxcare.com/els/cve/{cve_id}", - original_advisory_text=json.dumps(record, indent=2, ensure_ascii=False), + original_advisory_text=json.dumps(all_records, indent=2, ensure_ascii=False), ) diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py index 731fe0052..fa31117eb 100644 --- a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py +++ b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py @@ -35,4 +35,4 @@ def test_collect_advisories(self, mock_fetch): expected_file = TEST_DATA / "expected.json" util_tests.check_results_against_json(advisories, expected_file) - assert pipeline.advisories_count() == 13 + assert len(advisories) == 14 diff --git a/vulnerabilities/tests/test_data/tuxcare/data.json b/vulnerabilities/tests/test_data/tuxcare/data.json index 8a2e5268d..4c5a53a90 100644 --- a/vulnerabilities/tests/test_data/tuxcare/data.json +++ b/vulnerabilities/tests/test_data/tuxcare/data.json @@ -19,6 +19,46 @@ "status": "In Testing", "last_updated": "2025-12-23 10:08:35.944749" }, + { + "cve": "CVE-2023-52922", + "os_name": "CentOS 8.5 ELS", + "project_name": "kernel", + "version": "4.18.0", + "score": "7.8", + "severity": "HIGH", + "status": "Released", + "last_updated": "2025-05-21 01:43:28.677045" + }, + { + "cve": "CVE-2023-52922", + "os_name": "AlmaLinux 9.2 ESU", + "project_name": "squid", + "version": "5.5", + "score": "7.8", + "severity": "HIGH", + "status": "Not Vulnerable", + "last_updated": "2025-08-28 00:52:25.579518" + }, + { + "cve": "CVE-2023-52922", + "os_name": "RHEL 7 ELS", + "project_name": "squid", + "version": "3.5.20", + "score": "7.8", + "severity": "HIGH", + "status": "Not Vulnerable", + "last_updated": "2025-11-27 10:32:13.088814" + }, + { + "cve": "CVE-2023-52922", + "os_name": "CentOS Stream 8 ELS", + "project_name": "squid", + "version": "4.15", + "score": "7.8", + "severity": "HIGH", + "status": "Ignored", + "last_updated": "2025-08-28 22:56:12.357818" + }, { "cve": "CVE-2023-48161", "os_name": "RHEL 7 ELS", @@ -128,5 +168,35 @@ "severity": "MEDIUM", "status": "In Rollout", "last_updated": "2025-12-20 04:34:51.112485" + }, + { + "cve": "CVE-2022-50268", + "os_name": "Ubuntu 16.04 ELS", + "project_name": "linux", + "version": "4.4.0", + "score": "5.5", + "severity": "MEDIUM", + "status": "Needs Triage", + "last_updated": "2025-12-23 07:31:24.920518" + }, + { + "cve": "CVE-2020-1472", + "os_name": "Debian 10 ELS", + "project_name": "samba", + "version": "4.9.5", + "score": "0.0", + "severity": "", + "status": "In Testing", + "last_updated": "2025-12-22 16:58:13.189255" + }, + { + "cve": "CVE-2025-6297", + "os_name": "Alpine Linux 3.18 ELS", + "project_name": "dpkg", + "version": "1.21.21", + "score": "0.0", + "severity": "", + "status": "Released", + "last_updated": "2025-12-19 05:00:40.874276" } ] diff --git a/vulnerabilities/tests/test_data/tuxcare/expected.json b/vulnerabilities/tests/test_data/tuxcare/expected.json index 0878f6f55..eec9eb05a 100644 --- a/vulnerabilities/tests/test_data/tuxcare/expected.json +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -1,9 +1,7 @@ [ { - "advisory_id": "CVE-2023-52922-cloudlinux-7-els-squid-3.5.20", - "aliases": [ - "CVE-2023-52922" - ], + "advisory_id": "CVE-2023-52922", + "aliases": [], "summary": "", "affected_packages": [ { @@ -15,32 +13,11 @@ "qualifiers": "distro=cloudlinux-7-els", "subpath": "" }, - "affected_version_range": "vers:generic/3.5.20", + "affected_version_range": "vers:rpm/3.5.20", "fixed_version_range": null, "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] - } - ], - "references_v2": [], - "patches": [], - "severities": [ - { - "system": "generic_textual", - "value": "7.8", - "scoring_elements": "HIGH" - } - ], - "date_published": "2025-12-23T10:08:36.423446+00:00", - "weaknesses": [], - "url": "https://cve.tuxcare.com/els/cve/CVE-2023-52922" - }, - { - "advisory_id": "CVE-2023-52922-oracle-linux-7-els-squid-3.5.20", - "aliases": [ - "CVE-2023-52922" - ], - "summary": "", - "affected_packages": [ + }, { "package": { "type": "rpm", @@ -50,7 +27,35 @@ "qualifiers": "distro=oracle-linux-7-els", "subpath": "" }, - "affected_version_range": "vers:generic/3.5.20", + "affected_version_range": "vers:rpm/3.5.20", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + }, + { + "package": { + "type": "rpm", + "namespace": "centos", + "name": "kernel", + "version": "", + "qualifiers": "distro=centos-8.5-els", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:rpm/4.18.0", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + }, + { + "package": { + "type": "rpm", + "namespace": "centos", + "name": "squid", + "version": "", + "qualifiers": "distro=centos-stream-8-els", + "subpath": "" + }, + "affected_version_range": "vers:rpm/4.15", "fixed_version_range": null, "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] @@ -65,15 +70,13 @@ "scoring_elements": "HIGH" } ], - "date_published": "2025-12-23T10:08:35.944749+00:00", + "date_published": "2025-12-23T10:08:36.423446+00:00", "weaknesses": [], "url": "https://cve.tuxcare.com/els/cve/CVE-2023-52922" }, { - "advisory_id": "CVE-2023-48161-rhel-7-els-java-11-openjdk-11.0.23", - "aliases": [ - "CVE-2023-48161" - ], + "advisory_id": "CVE-2023-48161", + "aliases": [], "summary": "", "affected_packages": [ { @@ -85,7 +88,7 @@ "qualifiers": "distro=rhel-7-els", "subpath": "" }, - "affected_version_range": "vers:generic/11.0.23", + "affected_version_range": "vers:rpm/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] @@ -105,10 +108,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2023-48161" }, { - "advisory_id": "CVE-2024-21147-rhel-7-els-java-11-openjdk-11.0.23", - "aliases": [ - "CVE-2024-21147" - ], + "advisory_id": "CVE-2024-21147", + "aliases": [], "summary": "", "affected_packages": [ { @@ -120,7 +121,7 @@ "qualifiers": "distro=rhel-7-els", "subpath": "" }, - "affected_version_range": "vers:generic/11.0.23", + "affected_version_range": "vers:rpm/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] @@ -140,10 +141,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2024-21147" }, { - "advisory_id": "CVE-2025-21587-rhel-7-els-java-11-openjdk-11.0.23", - "aliases": [ - "CVE-2025-21587" - ], + "advisory_id": "CVE-2025-21587", + "aliases": [], "summary": "", "affected_packages": [ { @@ -155,7 +154,7 @@ "qualifiers": "distro=rhel-7-els", "subpath": "" }, - "affected_version_range": "vers:generic/11.0.23", + "affected_version_range": "vers:rpm/11.0.23", "fixed_version_range": null, "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] @@ -175,10 +174,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2025-21587" }, { - "advisory_id": "CVE-2024-39502-unknown-os-kernel-2.6.32", - "aliases": [ - "CVE-2024-39502" - ], + "advisory_id": "CVE-2024-39502", + "aliases": [], "summary": "", "affected_packages": [ { @@ -210,10 +207,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2024-39502" }, { - "advisory_id": "CVE-2024-40927-unknown-os-kernel-2.6.32", - "aliases": [ - "CVE-2024-40927" - ], + "advisory_id": "CVE-2024-40927", + "aliases": [], "summary": "", "affected_packages": [ { @@ -245,10 +240,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2024-40927" }, { - "advisory_id": "CVE-2025-43392-tuxcare-9.6-esu-webkit2gtk3-2.50.1", - "aliases": [ - "CVE-2025-43392" - ], + "advisory_id": "CVE-2025-43392", + "aliases": [], "summary": "", "affected_packages": [ { @@ -280,10 +273,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2025-43392" }, { - "advisory_id": "CVE-2023-50868-cloudlinux-7-els-dhcp-4.2.5", - "aliases": [ - "CVE-2023-50868" - ], + "advisory_id": "CVE-2023-50868", + "aliases": [], "summary": "", "affected_packages": [ { @@ -296,7 +287,7 @@ "subpath": "" }, "affected_version_range": null, - "fixed_version_range": "vers:generic/4.2.5", + "fixed_version_range": "vers:rpm/4.2.5", "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] } @@ -315,10 +306,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2023-50868" }, { - "advisory_id": "CVE-2021-33193-unknown-os-httpd-2.2.15", - "aliases": [ - "CVE-2021-33193" - ], + "advisory_id": "CVE-2021-33193", + "aliases": [], "summary": "", "affected_packages": [ { @@ -350,10 +339,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2021-33193" }, { - "advisory_id": "CVE-2025-50093-almalinux-9.2-esu-mysql-8.0.32", - "aliases": [ - "CVE-2025-50093" - ], + "advisory_id": "CVE-2025-50093", + "aliases": [], "summary": "", "affected_packages": [ { @@ -366,7 +353,7 @@ "subpath": "" }, "affected_version_range": null, - "fixed_version_range": "vers:generic/8.0.32", + "fixed_version_range": "vers:rpm/8.0.32", "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] } @@ -385,10 +372,8 @@ "url": "https://cve.tuxcare.com/els/cve/CVE-2025-50093" }, { - "advisory_id": "CVE-2025-64505-centos-7-els-libpng-1.5.13", - "aliases": [ - "CVE-2025-64505" - ], + "advisory_id": "CVE-2025-64505", + "aliases": [], "summary": "", "affected_packages": [ { @@ -400,7 +385,7 @@ "qualifiers": "distro=centos-7-els", "subpath": "" }, - "affected_version_range": "vers:generic/1.5.13", + "affected_version_range": "vers:rpm/1.5.13", "fixed_version_range": null, "introduced_by_commit_patches": [], "fixed_by_commit_patches": [] @@ -418,5 +403,92 @@ "date_published": "2025-12-20T04:34:51.112485+00:00", "weaknesses": [], "url": "https://cve.tuxcare.com/els/cve/CVE-2025-64505" + }, + { + "advisory_id": "CVE-2022-50268", + "aliases": [], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "deb", + "namespace": "ubuntu", + "name": "linux", + "version": "", + "qualifiers": "distro=ubuntu-16.04-els", + "subpath": "" + }, + "affected_version_range": "vers:deb/4.4.0", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "generic_textual", + "value": "5.5", + "scoring_elements": "MEDIUM" + } + ], + "date_published": "2025-12-23T07:31:24.920518+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2022-50268" + }, + { + "advisory_id": "CVE-2020-1472", + "aliases": [], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "deb", + "namespace": "debian", + "name": "samba", + "version": "", + "qualifiers": "distro=debian-10-els", + "subpath": "" + }, + "affected_version_range": "vers:deb/4.9.5", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [], + "date_published": "2025-12-22T16:58:13.189255+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2020-1472" + }, + { + "advisory_id": "CVE-2025-6297", + "aliases": [], + "summary": "", + "affected_packages": [ + { + "package": { + "type": "apk", + "namespace": "alpine", + "name": "dpkg", + "version": "", + "qualifiers": "distro=alpine-linux-3.18-els", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:alpine/1.21.21", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [], + "date_published": "2025-12-19T05:00:40.874276+00:00", + "weaknesses": [], + "url": "https://cve.tuxcare.com/els/cve/CVE-2025-6297" } ] \ No newline at end of file From bf3f3216f3c7815ed4a3563ae02c59b06ae88372 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Tue, 3 Feb 2026 03:43:24 +0530 Subject: [PATCH 09/10] Add docstrings and use pipeline logger Signed-off-by: Sampurna Pyne --- .../v2_importers/tuxcare_importer.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index c91b454e4..881b4cd65 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -8,16 +8,13 @@ # import json -import logging from typing import Iterable from dateutil.parser import parse from packageurl import PackageURL from pytz import UTC +from univers.version_range import RANGE_CLASS_BY_SCHEMES from univers.version_range import AlpineLinuxVersionRange -from univers.version_range import DebianVersionRange -from univers.version_range import GenericVersionRange -from univers.version_range import RpmVersionRange from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackageV2 @@ -26,18 +23,16 @@ from vulnerabilities.severity_systems import GENERIC from vulnerabilities.utils import fetch_response -logger = logging.getLogger(__name__) - # See https://docs.tuxcare.com/els-for-os/#cve-status-definition NON_AFFECTED_STATUSES = ["Not Vulnerable"] AFFECTED_STATUSES = ["Ignored", "Needs Triage", "In Testing", "In Progress", "In Rollout"] FIXED_STATUSES = ["Released", "Already Fixed"] VERSION_RANGE_BY_PURL_TYPE = { - "rpm": RpmVersionRange, - "deb": DebianVersionRange, + "rpm": RANGE_CLASS_BY_SCHEMES["rpm"], + "deb": RANGE_CLASS_BY_SCHEMES["deb"], "apk": AlpineLinuxVersionRange, - "generic": GenericVersionRange, + "generic": RANGE_CLASS_BY_SCHEMES["generic"], } @@ -61,14 +56,17 @@ def fetch(self) -> None: self._grouped = self._group_records_by_cve() def _group_records_by_cve(self) -> dict: + """ + A single CVE can appear in multiple records across different operating systems, distributions, or package versions. This method groups all records with the same CVE together and skips entries that are invalid or marked as not affected. The result is a dictionary keyed by CVE ID, with each value containing the related records. + """ grouped = {} skipped_invalid = 0 skipped_non_affected = 0 for record in self.response: cve_id = record.get("cve", "").strip() - if not cve_id or not cve_id.startswith("CVE-"): - logger.warning(f"Skipping invalid CVE ID: {cve_id}") + if not cve_id: + self.log(f"Skipping record with empty CVE ID") skipped_invalid += 1 continue @@ -78,7 +76,7 @@ def _group_records_by_cve(self) -> dict: status = record.get("status", "").strip() if not all([os_name, project_name, version, status]): - logger.warning(f"Skipping {cve_id}: missing required fields") + self.log(f"Skipping {cve_id}: missing required fields") skipped_invalid += 1 continue @@ -88,7 +86,7 @@ def _group_records_by_cve(self) -> dict: continue if status not in AFFECTED_STATUSES and status not in FIXED_STATUSES: - logger.warning(f"Skipping {cve_id}: unrecognized status '{status}'") + self.log(f"Skipping {cve_id}: unrecognized status '{status}'") skipped_invalid += 1 continue @@ -146,7 +144,6 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: severities = [] date_published = None all_records = [] - severity_added = False for record in records: os_name = record.get("os_name", "").strip() @@ -159,16 +156,17 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: purl = self._create_purl(project_name, os_name) if not purl: - logger.warning( + self.log( f"Skipping package {project_name} on {os_name} for {cve_id} - unexpected OS type" ) continue - version_range_class = VERSION_RANGE_BY_PURL_TYPE.get(purl.type, GenericVersionRange) + version_range_class = VERSION_RANGE_BY_PURL_TYPE.get(purl.type) + try: version_range = version_range_class.from_versions([version]) except ValueError as e: - logger.warning(f"Failed to parse version {version} for {cve_id}: {e}") + self.log(f"Failed to parse version {version} for {cve_id}: {e}") continue affected_version_range = None @@ -187,7 +185,8 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: ) ) - if severity and score and not severity_added: + # Severity is per-CVE hence we add it only once + if severity and score and not severities: severities.append( VulnerabilitySeverity( system=GENERIC, @@ -195,7 +194,6 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: scoring_elements=severity, ) ) - severity_added = True if last_updated: try: @@ -203,12 +201,12 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: if date_published is None or current_date > date_published: date_published = current_date except ValueError as e: - logger.warning(f"Failed to parse date {last_updated} for {cve_id}: {e}") + self.log(f"Failed to parse date {last_updated} for {cve_id}: {e}") all_records.append(record) if not affected_packages: - logger.warning(f"Skipping {cve_id} - no valid affected packages") + self.log(f"Skipping {cve_id} - no valid affected packages") continue yield AdvisoryData( From 07a5af2ee1c31785af9c115041c56f94461d4584 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Wed, 25 Feb 2026 00:10:22 +0530 Subject: [PATCH 10/10] Add tests for purl creation and use AdvisoryV2 Signed-off-by: Sampurna Pyne --- requirements.txt | 2 +- vulnerabilities/importers/__init__.py | 2 +- .../v2_importers/tuxcare_importer.py | 37 +++++++------------ .../v2_importers/test_tuxcare_importer_v2.py | 29 +++++++++++++++ .../tests/test_data/tuxcare/expected.json | 28 +++++++------- 5 files changed, 58 insertions(+), 40 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6788a34a5..81648f65e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -118,7 +118,7 @@ toml==0.10.2 tomli==2.0.1 traitlets==5.1.1 typing_extensions==4.1.1 -univers==30.12.0 +git+https://github.com/aboutcode-org/univers.git@5e28f2cd0fbee81a2b1268e88916a52a208bb672#egg=univers urllib3==1.26.19 wcwidth==0.2.5 websocket-client==0.59.0 diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index 514f1d087..3978b7e1e 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -78,8 +78,8 @@ from vulnerabilities.pipelines.v2_importers import redhat_importer as redhat_importer_v2 from vulnerabilities.pipelines.v2_importers import retiredotnet_importer as retiredotnet_importer_v2 from vulnerabilities.pipelines.v2_importers import ruby_importer as ruby_importer_v2 -from vulnerabilities.pipelines.v2_importers import tuxcare_importer as tuxcare_importer_v2 from vulnerabilities.pipelines.v2_importers import suse_score_importer as suse_score_importer_v2 +from vulnerabilities.pipelines.v2_importers import tuxcare_importer as tuxcare_importer_v2 from vulnerabilities.pipelines.v2_importers import ubuntu_osv_importer as ubuntu_osv_importer_v2 from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2 from vulnerabilities.pipelines.v2_importers import xen_importer as xen_importer_v2 diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py index 881b4cd65..2ccfc34ce 100644 --- a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -14,9 +14,8 @@ from packageurl import PackageURL from pytz import UTC from univers.version_range import RANGE_CLASS_BY_SCHEMES -from univers.version_range import AlpineLinuxVersionRange -from vulnerabilities.importer import AdvisoryData +from vulnerabilities.importer import AdvisoryDataV2 from vulnerabilities.importer import AffectedPackageV2 from vulnerabilities.importer import VulnerabilitySeverity from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2 @@ -28,13 +27,6 @@ AFFECTED_STATUSES = ["Ignored", "Needs Triage", "In Testing", "In Progress", "In Rollout"] FIXED_STATUSES = ["Released", "Already Fixed"] -VERSION_RANGE_BY_PURL_TYPE = { - "rpm": RANGE_CLASS_BY_SCHEMES["rpm"], - "deb": RANGE_CLASS_BY_SCHEMES["deb"], - "apk": AlpineLinuxVersionRange, - "generic": RANGE_CLASS_BY_SCHEMES["generic"], -} - class TuxCareImporterPipeline(VulnerableCodeBaseImporterPipelineV2): pipeline_id = "tuxcare_importer_v2" @@ -45,6 +37,7 @@ class TuxCareImporterPipeline(VulnerableCodeBaseImporterPipelineV2): def steps(cls): return ( cls.fetch, + cls.group_records_by_cve, cls.collect_and_store_advisories, ) @@ -53,13 +46,12 @@ def fetch(self) -> None: self.log(f"Fetching `{url}`") response = fetch_response(url) self.response = response.json() if response else [] - self._grouped = self._group_records_by_cve() - def _group_records_by_cve(self) -> dict: + def group_records_by_cve(self): """ A single CVE can appear in multiple records across different operating systems, distributions, or package versions. This method groups all records with the same CVE together and skips entries that are invalid or marked as not affected. The result is a dictionary keyed by CVE ID, with each value containing the related records. """ - grouped = {} + self.cve_to_records = {} skipped_invalid = 0 skipped_non_affected = 0 @@ -90,20 +82,19 @@ def _group_records_by_cve(self) -> dict: skipped_invalid += 1 continue - if cve_id not in grouped: - grouped[cve_id] = [] - grouped[cve_id].append(record) + if cve_id not in self.cve_to_records: + self.cve_to_records[cve_id] = [] + self.cve_to_records[cve_id].append(record) total_skipped = skipped_invalid + skipped_non_affected self.log( - f"Grouped {len(self.response):,d} records into {len(grouped):,d} unique CVEs " + f"Grouped {len(self.response):,d} records into {len(self.cve_to_records):,d} unique CVEs " f"(skipped {total_skipped:,d}: {skipped_invalid:,d} invalid, " f"{skipped_non_affected:,d} non-affected)" ) - return grouped def advisories_count(self) -> int: - return len(self._grouped) + return len(self.cve_to_records) def _create_purl(self, project_name: str, os_name: str) -> PackageURL: normalized_os = os_name.lower().replace(" ", "-") @@ -136,10 +127,8 @@ def _create_purl(self, project_name: str, os_name: str) -> PackageURL: type=pkg_type, namespace=namespace, name=project_name, qualifiers=qualifiers ) - def collect_advisories(self) -> Iterable[AdvisoryData]: - grouped_by_cve = self._grouped - - for cve_id, records in grouped_by_cve.items(): + def collect_advisories(self) -> Iterable[AdvisoryDataV2]: + for cve_id, records in self.cve_to_records.items(): affected_packages = [] severities = [] date_published = None @@ -161,7 +150,7 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: ) continue - version_range_class = VERSION_RANGE_BY_PURL_TYPE.get(purl.type) + version_range_class = RANGE_CLASS_BY_SCHEMES.get(purl.type) try: version_range = version_range_class.from_versions([version]) @@ -209,7 +198,7 @@ def collect_advisories(self) -> Iterable[AdvisoryData]: self.log(f"Skipping {cve_id} - no valid affected packages") continue - yield AdvisoryData( + yield AdvisoryDataV2( advisory_id=cve_id, affected_packages=affected_packages, severities=severities, diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py index fa31117eb..9c7a752c7 100644 --- a/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py +++ b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py @@ -29,6 +29,7 @@ def test_collect_advisories(self, mock_fetch): pipeline = TuxCareImporterPipeline() pipeline.fetch() + pipeline.group_records_by_cve() advisories = [data.to_dict() for data in list(pipeline.collect_advisories())] @@ -36,3 +37,31 @@ def test_collect_advisories(self, mock_fetch): util_tests.check_results_against_json(advisories, expected_file) assert len(advisories) == 14 + + def test_create_purl(self): + pipeline = TuxCareImporterPipeline() + + cases = [ + ("squid", "CloudLinux 7 ELS", "rpm", "cloudlinux", "cloudlinux-7-els"), + ("squid", "Oracle Linux 7 ELS", "rpm", "oracle", "oracle-linux-7-els"), + ("kernel", "CentOS 8.5 ELS", "rpm", "centos", "centos-8.5-els"), + ("squid", "CentOS Stream 8 ELS", "rpm", "centos", "centos-stream-8-els"), + ("libpng", "CentOS 7 ELS", "rpm", "centos", "centos-7-els"), + ("java-11-openjdk", "RHEL 7 ELS", "rpm", "rhel", "rhel-7-els"), + ("mysql", "AlmaLinux 9.2 ESU", "rpm", "almalinux", "almalinux-9.2-esu"), + ("linux", "Ubuntu 16.04 ELS", "deb", "ubuntu", "ubuntu-16.04-els"), + ("samba", "Debian 10 ELS", "deb", "debian", "debian-10-els"), + ("dpkg", "Alpine Linux 3.18 ELS", "apk", "alpine", "alpine-linux-3.18-els"), + ("kernel", "Unknown OS", "generic", "tuxcare", "unknown-os"), + ("webkit2gtk3", "TuxCare 9.6 ESU", "generic", "tuxcare", "tuxcare-9.6-esu"), + ] + + for name, os_name, expected_type, expected_ns, expected_distro in cases: + purl = pipeline._create_purl(name, os_name) + assert purl is not None, f"Expected purl for os_name={os_name!r}" + assert purl.type == expected_type + assert purl.namespace == expected_ns + assert purl.qualifiers == {"distro": expected_distro} + + #Invalid PURL + assert pipeline._create_purl("foo", "Foo 123") is None diff --git a/vulnerabilities/tests/test_data/tuxcare/expected.json b/vulnerabilities/tests/test_data/tuxcare/expected.json index eec9eb05a..e4480eec6 100644 --- a/vulnerabilities/tests/test_data/tuxcare/expected.json +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -61,7 +61,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -94,7 +94,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -127,7 +127,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -160,7 +160,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -193,7 +193,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -226,7 +226,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -259,7 +259,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -292,7 +292,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -325,7 +325,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -358,7 +358,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -391,7 +391,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -424,7 +424,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [ { @@ -457,7 +457,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [], "date_published": "2025-12-22T16:58:13.189255+00:00", @@ -484,7 +484,7 @@ "fixed_by_commit_patches": [] } ], - "references_v2": [], + "references": [], "patches": [], "severities": [], "date_published": "2025-12-19T05:00:40.874276+00:00",