diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index dd3e7a6d8..14c8fa43e 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -71,6 +71,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( @@ -98,6 +99,7 @@ ruby_importer_v2.RubyImporterPipeline, epss_importer_v2.EPSSImporterPipeline, mattermost_importer_v2.MattermostImporterPipeline, + tuxcare_importer_v2.TuxCareImporterPipeline, nvd_importer.NVDImporterPipeline, github_importer.GitHubAPIImporterPipeline, gitlab_importer.GitLabImporterPipeline, diff --git a/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py new file mode 100644 index 000000000..6ecd08997 --- /dev/null +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -0,0 +1,135 @@ +import json +import logging +from typing import Iterable +from typing import Mapping + +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 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" + + @classmethod + def steps(cls): + 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: + 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() + 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() + last_updated = record.get("last_updated", "").strip() + + 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) + + 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}") + + affected_packages.append( + AffectedPackageV2( + package=purl, + affected_version_range=affected_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, + summary=summary, + 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), + ) 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..a0ca0218a --- /dev/null +++ b/vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py @@ -0,0 +1,38 @@ +# +# 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): + 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) + + assert pipeline.advisories_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..9ab7ebdd3 --- /dev/null +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -0,0 +1,102 @@ +[ + { + "advisory_id": "CVE-2023-52922", + "aliases": [], + "summary": "TuxCare advisory for CVE-2023-52922 in squid on CloudLinux 7 ELS", + "affected_packages": [ + { + "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": [], + "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", + "aliases": [], + "summary": "TuxCare advisory for CVE-2023-52922 in squid on Oracle Linux 7 ELS", + "affected_packages": [ + { + "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": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "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": [], + "summary": "TuxCare advisory for CVE-2023-48161 in java-11-openjdk on RHEL 7 ELS", + "affected_packages": [ + { + "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": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "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": [], + "summary": "TuxCare advisory for CVE-2024-21147 in java-11-openjdk on RHEL 7 ELS", + "affected_packages": [ + { + "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": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "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": [], + "summary": "TuxCare advisory for CVE-2025-21587 in java-11-openjdk on RHEL 7 ELS", + "affected_packages": [ + { + "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": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "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" + } +]