From eddb6addbd2efab18c8c93f7ebce7f3bb1630939 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Mon, 5 Jan 2026 02:11:18 +0530 Subject: [PATCH 1/9] 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 6d5e0eff8..72c686366 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -75,6 +75,7 @@ from vulnerabilities.pipelines.v2_importers import ruby_importer as ruby_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( @@ -104,6 +105,7 @@ epss_importer_v2.EPSSImporterPipeline, nginx_importer_v2.NginxImporterPipeline, mattermost_importer_v2.MattermostImporterPipeline, + tuxcare_importer_v2.TuxCareImporterPipeline, apache_tomcat_v2.ApacheTomcatImporterPipeline, nvd_importer.NVDImporterPipeline, github_importer.GitHubAPIImporterPipeline, 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 70bbeac58bb266dd953d8fbad32dab9ac683b138 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Thu, 8 Jan 2026 02:34:53 +0530 Subject: [PATCH 2/9] 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 be1fba5ef1914c4f2e28aa0fed69a8176a77712c Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Sat, 10 Jan 2026 05:10:57 +0530 Subject: [PATCH 3/9] 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 550c40872db857c93196f7752f2d4c8abace5d02 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Sat, 10 Jan 2026 18:44:30 +0530 Subject: [PATCH 4/9] 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 dfc1da7154064c86e0664eeb9f0732a20e571360 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Mon, 12 Jan 2026 19:49:39 +0530 Subject: [PATCH 5/9] 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 e05d5ac7df7a5f9267ceaa9cd13401968958762b Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Tue, 13 Jan 2026 18:42:05 +0530 Subject: [PATCH 6/9] 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 72c686366..89b7d5910 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -73,9 +73,9 @@ from vulnerabilities.pipelines.v2_importers import pysec_importer as pysec_importer_v2 from vulnerabilities.pipelines.v2_importers import redhat_importer as redhat_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 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 124f7977d000c858c6196d2b00fe8448111fcbcd Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Wed, 21 Jan 2026 17:46:38 +0530 Subject: [PATCH 7/9] 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 5ea3d09572b9dca5d79403f8c0e338ad62bd3bc0 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Tue, 27 Jan 2026 00:49:58 +0530 Subject: [PATCH 8/9] 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 66be49134c3a0d7027d4ef7904a3318df6e88264 Mon Sep 17 00:00:00 2001 From: Sampurna Pyne Date: Tue, 3 Feb 2026 03:43:24 +0530 Subject: [PATCH 9/9] 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(