diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index dd3e7a6d8..ae3f41141 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -69,6 +69,7 @@ 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.utils import 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..118ec48c4 --- /dev/null +++ b/vulnerabilities/pipelines/v2_importers/tuxcare_importer.py @@ -0,0 +1,179 @@ +# +# 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 + +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) -> None: + 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: + 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 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() + + if not all([os_name, project_name, version, status]): + logger.warning(f"Skipping {cve_id} - missing required fields") + 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 + + if status not in affected_statuses and status not in fixed_statuses: + logger.warning(f"Skipping {cve_id} - unknown status: {status}") + continue + + 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: + 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], + 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..731fe0052 --- /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() == 13 diff --git a/vulnerabilities/tests/test_data/tuxcare/data.json b/vulnerabilities/tests/test_data/tuxcare/data.json new file mode 100644 index 000000000..8a2e5268d --- /dev/null +++ b/vulnerabilities/tests/test_data/tuxcare/data.json @@ -0,0 +1,132 @@ +[ + { + "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" + }, + { + "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 new file mode 100644 index 000000000..0878f6f55 --- /dev/null +++ b/vulnerabilities/tests/test_data/tuxcare/expected.json @@ -0,0 +1,422 @@ +[ + { + "advisory_id": "CVE-2023-52922-cloudlinux-7-els-squid-3.5.20", + "aliases": [ + "CVE-2023-52922" + ], + "summary": "", + "affected_packages": [ + { + "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": [], + "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", + "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": [], + "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-rhel-7-els-java-11-openjdk-11.0.23", + "aliases": [ + "CVE-2023-48161" + ], + "summary": "", + "affected_packages": [ + { + "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": [], + "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-rhel-7-els-java-11-openjdk-11.0.23", + "aliases": [ + "CVE-2024-21147" + ], + "summary": "", + "affected_packages": [ + { + "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": [], + "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-rhel-7-els-java-11-openjdk-11.0.23", + "aliases": [ + "CVE-2025-21587" + ], + "summary": "", + "affected_packages": [ + { + "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": [], + "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" + }, + { + "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