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 @@ -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
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
179 changes: 179 additions & 0 deletions vulnerabilities/pipelines/v2_importers/tuxcare_importer.py
Original file line number Diff line number Diff line change
@@ -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
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

@Samk1710 I think this purl is incorrect, sorry for the confusion.

I noticed that TuxCare provides security patches for Endless Lifecycle Support (ELS), delivering qualified security and selected bug-fix errata advisories across all architectures.

So, I think the correct way to do this is that we should create a mapping.

cve    "CVE-2024-50006"
os_name    "Ubuntu 16.04 ELS"
project_name    "linux-hwe"
version    "4.15.0"
score    "4.7"
severity    "MEDIUM"
status    "Needs Triage"
pkg:deb/ubuntu/linux-hwe@4.15.0?distro=ubuntu-16.04-els

For unknown OS, use generic.

Copy link
Author

Choose a reason for hiding this comment

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

Implemented purl as:

pkg:deb/ubuntu/linux-hwe?distro=ubuntu-16.04-els

This is as per
https://github.com/Samk1710/vulnerablecode/blob/05b26dd356f5afd8347cf33b6a5690e9e0cf48f0/vulnerabilities/importer.py#L442


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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Advisory_id should be unique, so drop the old database and run the importer again to ensure there is no duplication

from vulnerabilities.models import AdvisoryV2
from django.db.models import Count
duplicates = (
    AdvisoryV2.objects
    .values('avid')
    .annotate(count=Count('id'))
    .filter(count__gt=1)
)
len(duplicates)

Copy link
Author

@Samk1710 Samk1710 Jan 17, 2026

Choose a reason for hiding this comment

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

Hey @ziadhany!

I've analyzed the data and found duplicate advisory IDs in the database. This happens because TuxCare structures their data with the same CVE ID across different OS distributions, packages, and versions - each record represents the same CVE but for a specific OS+package+version combination.

For example, CVE-2023-52922 appears in 17 different records:

{
    "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-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"
  }

Workaround
-- use a composite advisory_id like {cve_id}-{normalized_os}-{project_name}-{version}
-- CVE-2023-52922-cloudlinux-7-els-squid-3.5.20 and CVE-2023-52922-oracle-linux-7-els-squid-3.5.20
-- add CVE_ID to aliases: ["CVE-2023-52922"]

This way each OS+package+version combination gets a unique advisory, the CVE remains searchable via aliases, and we avoid duplicates.

Does this approach work for you? Do share your suggestions.

Also, just a heads up - the TuxCare endpoint is currently returning a 500 error, it might take some time before I can run the importer and verify everything works as expected.

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