Skip to content

Commit 07d3691

Browse files
authored
Merge pull request #133 from scanoss/feature/mdaloia/add-vulns-to-cdx-hfh-output
feat: add vulnerabilities into cdx output when using folder hashing command
2 parents 93b2475 + ec19a2e commit 07d3691

File tree

5 files changed

+96
-2
lines changed

5 files changed

+96
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Upcoming changes...
1111

12+
## [1.28.0] - 2025-07-10
13+
### Added
14+
- Add vulnerabilities response to `folder-scan` CycloneDX output
15+
1216
## [1.27.1] - 2025-07-09
1317
### Fixed
1418
- Fixed when running `folder-scan` with `--format cyclonedx` the output was not writing to file

src/scanoss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
__version__ = '1.27.1'
25+
__version__ = '1.28.0'

src/scanoss/cyclonedx.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,87 @@ def produce_from_str(self, json_str: str, output_file: str = None) -> bool:
287287
return False
288288
return self.produce_from_json(data, output_file)
289289

290+
def _normalize_vulnerability_id(self, vuln: dict) -> tuple[str, str]:
291+
"""
292+
Normalize vulnerability ID and CVE from different possible field names.
293+
Returns tuple of (vuln_id, vuln_cve).
294+
"""
295+
vuln_id = vuln.get('ID', '') or vuln.get('id', '')
296+
vuln_cve = vuln.get('CVE', '') or vuln.get('cve', '')
297+
298+
# Skip CPE entries, use CVE if available
299+
if vuln_id.upper().startswith('CPE:') and vuln_cve:
300+
vuln_id = vuln_cve
301+
302+
return vuln_id, vuln_cve
303+
304+
def _create_vulnerability_entry(self, vuln_id: str, vuln: dict, vuln_cve: str, purl: str) -> dict:
305+
"""
306+
Create a new vulnerability entry for CycloneDX format.
307+
"""
308+
vuln_source = vuln.get('source', '').lower()
309+
return {
310+
'id': vuln_id,
311+
'source': {
312+
'name': 'NVD' if vuln_source == 'nvd' else 'GitHub Advisories',
313+
'url': f'https://nvd.nist.gov/vuln/detail/{vuln_cve}'
314+
if vuln_source == 'nvd'
315+
else f'https://github.com/advisories/{vuln_id}'
316+
},
317+
'ratings': [{'severity': self._sev_lookup(vuln.get('severity', 'unknown').lower())}],
318+
'affects': [{'ref': purl}]
319+
}
320+
321+
def append_vulnerabilities(self, cdx_dict: dict, vulnerabilities_data: dict, purl: str) -> dict:
322+
"""
323+
Append vulnerabilities to an existing CycloneDX dictionary
324+
325+
Args:
326+
cdx_dict (dict): The existing CycloneDX dictionary
327+
vulnerabilities_data (dict): The vulnerabilities data from get_vulnerabilities_json
328+
purl (str): The PURL of the component these vulnerabilities affect
329+
330+
Returns:
331+
dict: The updated CycloneDX dictionary with vulnerabilities appended
332+
"""
333+
if not cdx_dict or not vulnerabilities_data:
334+
return cdx_dict
335+
336+
if 'vulnerabilities' not in cdx_dict:
337+
cdx_dict['vulnerabilities'] = []
338+
339+
# Extract vulnerabilities from the response
340+
vulns_list = vulnerabilities_data.get('purls', [])
341+
if not vulns_list:
342+
return cdx_dict
343+
344+
vuln_items = vulns_list[0].get('vulnerabilities', [])
345+
346+
for vuln in vuln_items:
347+
vuln_id, vuln_cve = self._normalize_vulnerability_id(vuln)
348+
349+
# Skip empty IDs or CPE-only entries
350+
if not vuln_id or vuln_id.upper().startswith('CPE:'):
351+
continue
352+
353+
# Check if vulnerability already exists
354+
existing_vuln = next(
355+
(v for v in cdx_dict['vulnerabilities'] if v.get('id') == vuln_id),
356+
None
357+
)
358+
359+
if existing_vuln:
360+
# Add this PURL to the affects list if not already present
361+
if not any(ref.get('ref') == purl for ref in existing_vuln.get('affects', [])):
362+
existing_vuln['affects'].append({'ref': purl})
363+
else:
364+
# Create new vulnerability entry
365+
cdx_dict['vulnerabilities'].append(
366+
self._create_vulnerability_entry(vuln_id, vuln, vuln_cve, purl)
367+
)
368+
369+
return cdx_dict
370+
290371
@staticmethod
291372
def _sev_lookup(value: str):
292373
"""

src/scanoss/scanners/scanner_hfh.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,13 @@ def _format_cyclonedx_output(self) -> str: # noqa: PLR0911
193193
}
194194
]
195195
}
196+
197+
get_vulnerabilities_json_request = {
198+
'purls': [{'purl': purl, 'requirement': best_match_version['version']}],
199+
}
196200

197201
decorated_scan_results = self.scanner.client.get_dependencies(get_dependencies_json_request)
202+
vulnerabilities = self.scanner.client.get_vulnerabilities_json(get_vulnerabilities_json_request)
198203

199204
cdx = CycloneDx(self.base.debug)
200205
scan_results = {}
@@ -205,6 +210,10 @@ def _format_cyclonedx_output(self) -> str: # noqa: PLR0911
205210
error_msg = 'ERROR: Failed to produce CycloneDX output'
206211
self.base.print_stderr(error_msg)
207212
return None
213+
214+
if vulnerabilities:
215+
cdx_output = cdx.append_vulnerabilities(cdx_output, vulnerabilities, purl)
216+
208217
return json.dumps(cdx_output, indent=2)
209218
except Exception as e:
210219
self.base.print_stderr(f'ERROR: Failed to get license information: {e}')

src/scanoss/scanossgrpc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def get_vulnerabilities_json(self, purls: dict) -> dict:
326326
request = ParseDict(purls, PurlRequest()) # Parse the JSON/Dict into the purl request object
327327
metadata = self.metadata[:]
328328
metadata.append(('x-request-id', request_id)) # Set a Request ID
329-
self.print_debug(f'Sending crypto data for decoration (rqId: {request_id})...')
329+
self.print_debug(f'Sending vulnerability data for decoration (rqId: {request_id})...')
330330
resp = self.vuln_stub.GetVulnerabilities(request, metadata=metadata, timeout=self.timeout)
331331
except Exception as e:
332332
self.print_stderr(

0 commit comments

Comments
 (0)