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+
110import json
211import logging
312from 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 ,
0 commit comments