From b20822be64ba4e14c5c816d92f0d63b85acabdd9 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 17 May 2024 16:03:57 +0400 Subject: [PATCH] Add support for CycloneDX SBOM component properties from external tools Signed-off-by: tdruez --- CHANGELOG.rst | 4 ++++ scanpipe/pipes/cyclonedx.py | 26 ++++++++++++++++++++------ scanpipe/tests/pipes/test_cyclonedx.py | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1f9cc94a4..0a3968b06 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -57,6 +57,10 @@ v34.5.0 (unreleased) - Remove the ``scancode_license_score`` option from the Project configuration. https://github.com/nexB/scancode.io/issues/1231 +- Add support for CycloneDX SBOM component properties as generated by external tools. + For example, the ``ResolvedUrl`` generated by cdxgen is now imported as the package + ``download_url``. + v34.4.0 (2024-04-22) -------------------- diff --git a/scanpipe/pipes/cyclonedx.py b/scanpipe/pipes/cyclonedx.py index 782fe4bc5..9df1736ba 100644 --- a/scanpipe/pipes/cyclonedx.py +++ b/scanpipe/pipes/cyclonedx.py @@ -88,14 +88,28 @@ def get_properties_data(component): """Return the properties as dict, extracted from `component.properties`.""" prefix = "aboutcode:" properties_data = {} - properties = component.properties or [] + properties_dict = { + component_property.name: component_property.value + for component_property in component.properties + } - for component_property in properties: - property_name = component_property.name - property_value = component_property.value + for property_name, property_value in properties_dict.items(): if property_name.startswith(prefix) and property_value not in EMPTY_VALUES: - field_name = property_name.replace(prefix, "", 1) - properties_data[field_name] = property_value + package_field_name = property_name.replace(prefix, "", 1) + properties_data[package_field_name] = property_value + + # Mapping of few other properties found in SBOM generated by external tools. + property_mapping = { + "ResolvedUrl": "download_url", # generated by "CycloneDX/cdxgen" + } + + for property_name, package_field_name in property_mapping.items(): + if properties_data.get(package_field_name) not in EMPTY_VALUES: + continue # Skip if a value is already set for that field + + property_value = properties_dict.get(property_name) + if property_value not in EMPTY_VALUES: + properties_data[package_field_name] = property_value return properties_data diff --git a/scanpipe/tests/pipes/test_cyclonedx.py b/scanpipe/tests/pipes/test_cyclonedx.py index af607520a..1dc2e7ef4 100644 --- a/scanpipe/tests/pipes/test_cyclonedx.py +++ b/scanpipe/tests/pipes/test_cyclonedx.py @@ -25,6 +25,7 @@ from django.test import TestCase +from cyclonedx.model import Property from cyclonedx.model import license as cdx_license_model from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component @@ -133,6 +134,21 @@ def test_scanpipe_cyclonedx_get_properties_data(self): } self.assertEqual(expected, properties_data) + resolved_url_property = Property( + name="ResolvedUrl", value="https://download.url/resolved.tar.gz" + ) + self.component3.properties.add(resolved_url_property) + properties_data = cyclonedx.get_properties_data(self.component3) + # Same result as the "aboutcode:download_url" takes precedence. + self.assertEqual(expected, properties_data) + + self.component3.properties = {resolved_url_property} + properties_data = cyclonedx.get_properties_data(self.component3) + expected = { + "download_url": "https://download.url/resolved.tar.gz", + } + self.assertEqual(expected, properties_data) + def test_scanpipe_cyclonedx_validate_document(self): error = cyclonedx.validate_document(document="{}") self.assertIsInstance(error, ValidationError)