Skip to content

Commit f0a3ee8

Browse files
committed
Refactor PURL and status handling
Signed-off-by: Sampurna Pyne <sampurnapyne1710@gmail.com>
1 parent d9b3f2c commit f0a3ee8

File tree

4 files changed

+512
-40
lines changed

4 files changed

+512
-40
lines changed

vulnerabilities/pipelines/v2_importers/tuxcare_importer.py

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
110
import json
211
import logging
312
from typing import Iterable
@@ -39,46 +48,108 @@ def advisories_count(self) -> int:
3948
return len(self.response)
4049

4150
def _create_purl(self, project_name: str, os_name: str) -> PackageURL:
51+
normalized_os = os_name.lower().replace(" ", "-")
52+
os_lower = os_name.lower()
53+
54+
os_mapping = {
55+
"ubuntu": ("deb", "ubuntu"),
56+
"debian": ("deb", "debian"),
57+
"centos": ("rpm", "centos"),
58+
"almalinux": ("rpm", "almalinux"),
59+
"rhel": ("rpm", "rhel"),
60+
"oracle": ("rpm", "oracle"),
61+
"cloudlinux": ("rpm", "cloudlinux"),
62+
"alpine": ("apk", "alpine"),
63+
"unknown": ("generic", "tuxcare"),
64+
"tuxcare": ("generic", "tuxcare"),
65+
}
66+
67+
pkg_type = "generic"
68+
namespace = "tuxcare"
69+
70+
for keyword, (ptype, pns) in os_mapping.items():
71+
if keyword in os_lower:
72+
pkg_type = ptype
73+
namespace = pns
74+
break
75+
else:
76+
return None
77+
4278
qualifiers = {}
43-
if os_name:
44-
qualifiers["distro"] = os_name
79+
if normalized_os:
80+
qualifiers["distro"] = normalized_os
4581

4682
return PackageURL(
47-
type="generic", namespace="tuxcare", name=project_name, qualifiers=qualifiers
83+
type=pkg_type, namespace=namespace, name=project_name, qualifiers=qualifiers
4884
)
4985

5086
def collect_advisories(self) -> Iterable[AdvisoryData]:
5187
for record in self.response:
5288
cve_id = record.get("cve", "").strip()
5389
if not cve_id or not cve_id.startswith("CVE-"):
90+
logger.warning(f"Skipping record with invalid CVE ID: {cve_id}")
5491
continue
5592

5693
os_name = record.get("os_name", "").strip()
5794
project_name = record.get("project_name", "").strip()
5895
version = record.get("version", "").strip()
5996
score = record.get("score", "").strip()
6097
severity = record.get("severity", "").strip()
98+
status = record.get("status", "").strip()
6199
last_updated = record.get("last_updated", "").strip()
62100

63-
advisory_id = cve_id
101+
if not all([os_name, project_name, version, status]):
102+
logger.warning(f"Skipping {cve_id} - missing required fields")
103+
continue
64104

65-
affected_packages = []
66-
if project_name:
67-
purl = self._create_purl(project_name, os_name)
105+
# See https://docs.tuxcare.com/els-for-os/#cve-status-definition
106+
non_affected_statuses = ["Not Vulnerable"]
107+
affected_statuses = [
108+
"Ignored",
109+
"Needs Triage",
110+
"In Testing",
111+
"In Progress",
112+
"In Rollout",
113+
]
114+
fixed_statuses = ["Released", "Already Fixed"]
115+
116+
# Skip CVEs that are not vulnerable
117+
if status in non_affected_statuses:
118+
continue
68119

69-
affected_version_range = None
70-
if version:
71-
try:
72-
affected_version_range = GenericVersionRange.from_versions([version])
73-
except ValueError as e:
74-
logger.warning(f"Failed to parse version {version} for {cve_id}: {e}")
120+
if status not in affected_statuses and status not in fixed_statuses:
121+
logger.warning(f"Skipping {cve_id} - unknown status: {status}")
122+
continue
75123

76-
affected_packages.append(
77-
AffectedPackageV2(
78-
package=purl,
79-
affected_version_range=affected_version_range,
80-
)
124+
normalized_os = os_name.lower().replace(" ", "-")
125+
advisory_id = f"{cve_id}-{normalized_os}-{project_name.lower()}-{version}"
126+
127+
purl = self._create_purl(project_name, os_name)
128+
if not purl:
129+
logger.warning(f"Skipping {cve_id} - unexpected OS type: '{os_name}'")
130+
continue
131+
132+
try:
133+
version_range = GenericVersionRange.from_versions([version])
134+
except ValueError as e:
135+
logger.warning(f"Failed to parse version {version} for {cve_id}: {e}")
136+
continue
137+
138+
affected_version_range = None
139+
fixed_version_range = None
140+
141+
if status in affected_statuses:
142+
affected_version_range = version_range
143+
elif status in fixed_statuses:
144+
fixed_version_range = version_range
145+
146+
affected_packages = [
147+
AffectedPackageV2(
148+
package=purl,
149+
affected_version_range=affected_version_range,
150+
fixed_version_range=fixed_version_range,
81151
)
152+
]
82153

83154
severities = []
84155
if severity and score:
@@ -99,6 +170,7 @@ def collect_advisories(self) -> Iterable[AdvisoryData]:
99170

100171
yield AdvisoryData(
101172
advisory_id=advisory_id,
173+
aliases=[cve_id],
102174
affected_packages=affected_packages,
103175
severities=severities,
104176
date_published=date_published,

vulnerabilities/tests/pipelines/v2_importers/test_tuxcare_importer_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ def test_collect_advisories(self, mock_fetch):
3535
expected_file = TEST_DATA / "expected.json"
3636
util_tests.check_results_against_json(advisories, expected_file)
3737

38-
assert pipeline.advisories_count() == 5
38+
assert pipeline.advisories_count() == 13

vulnerabilities/tests/test_data/tuxcare/data.json

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,85 @@
4848
"severity": "HIGH",
4949
"status": "In Progress",
5050
"last_updated": "2025-12-23 08:55:06.706873"
51+
},
52+
{
53+
"cve": "CVE-2024-39502",
54+
"os_name": "Unknown OS",
55+
"project_name": "kernel",
56+
"version": "2.6.32",
57+
"score": "7.8",
58+
"severity": "HIGH",
59+
"status": "Needs Triage",
60+
"last_updated": "2025-09-20 06:03:30.551756"
61+
},
62+
{
63+
"cve": "CVE-2024-40927",
64+
"os_name": "Unknown OS",
65+
"project_name": "kernel",
66+
"version": "2.6.32",
67+
"score": "7.8",
68+
"severity": "HIGH",
69+
"status": "Needs Triage",
70+
"last_updated": "2025-09-20 06:03:26.132106"
71+
},
72+
{
73+
"cve": "CVE-2025-4517",
74+
"os_name": "CentOS 8.4 ELS",
75+
"project_name": "python2",
76+
"version": "2.7.18",
77+
"score": "7.6",
78+
"severity": "HIGH",
79+
"status": "Not Vulnerable",
80+
"last_updated": "2025-12-22 16:43:49.287021"
81+
},
82+
{
83+
"cve": "CVE-2025-43392",
84+
"os_name": "TuxCare 9.6 ESU",
85+
"project_name": "webkit2gtk3",
86+
"version": "2.50.1",
87+
"score": "6.5",
88+
"severity": "MEDIUM",
89+
"status": "In Testing",
90+
"last_updated": "2025-12-20 04:26:48.737089"
91+
},
92+
{
93+
"cve": "CVE-2023-50868",
94+
"os_name": "CloudLinux 7 ELS",
95+
"project_name": "dhcp",
96+
"version": "4.2.5",
97+
"score": "7.5",
98+
"severity": "HIGH",
99+
"status": "Already Fixed",
100+
"last_updated": "2025-12-23 01:56:28.627699"
101+
},
102+
{
103+
"cve": "CVE-2021-33193",
104+
"os_name": "Unknown OS",
105+
"project_name": "httpd",
106+
"version": "2.2.15",
107+
"score": "7.5",
108+
"severity": "HIGH",
109+
"status": "Ignored",
110+
"last_updated": "2025-09-19 21:21:01.425783"
111+
},
112+
{
113+
"cve": "CVE-2025-50093",
114+
"os_name": "AlmaLinux 9.2 ESU",
115+
"project_name": "mysql",
116+
"version": "8.0.32",
117+
"score": "4.9",
118+
"severity": "MEDIUM",
119+
"status": "Released",
120+
"last_updated": "2025-12-22 17:11:15.409148"
121+
},
122+
{
123+
"cve": "CVE-2025-64505",
124+
"os_name": "CentOS 7 ELS",
125+
"project_name": "libpng",
126+
"version": "1.5.13",
127+
"score": "4.4",
128+
"severity": "MEDIUM",
129+
"status": "In Rollout",
130+
"last_updated": "2025-12-20 04:34:51.112485"
51131
}
52132
]

0 commit comments

Comments
 (0)