Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions vulnerabilities/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
135 changes: 135 additions & 0 deletions vulnerabilities/pipelines/v2_importers/tuxcare_importer.py
Original file line number Diff line number Diff line change
@@ -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:
Comment on lines +86 to +89
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is wrong. The OS name should be considered a qualifier.
Please look at https://github.com/package-url/purl-spec

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran a small local script on the data source JSON file to find the unique OS names.

CentOS: 23778
Ubuntu: 13934
Oracle: 8190
AlmaLinux: 6562
Unknown: 4526
CloudLinux: 1934
RHEL: 1524
Debian: 944
TuxCare: 792
Alpine: 119

Idea is to map these with the appropriate PURLs. Will be implementing this and pushing the changes shortly.
Do let me know if any other approach is more suitable. Thanks for the correction.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ziadhany
I had a quick question: while looking through the importers I came across some parity with the qualifier field in PURL. I went with qualifiers["os"] = os_name. Was a little confused if it should be distro though. If you could kindly guide me on this!

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),
)
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions vulnerabilities/tests/test_data/tuxcare/data.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
102 changes: 102 additions & 0 deletions vulnerabilities/tests/test_data/tuxcare/expected.json
Original file line number Diff line number Diff line change
@@ -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"
}
]