diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fabe28e91..7dcf23094 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,10 @@ v34.5.0 (unreleased) symbol, string and comments using Pygments. https://github.com/nexB/scancode.io/pull/1179 +- Workaround an issue with the cyclonedx-python-lib that does not allow to load + SBOMs that contains properties with no values. + https://github.com/nexB/scancode.io/issues/1185 + v34.4.0 (2024-04-22) -------------------- diff --git a/scanpipe/pipes/cyclonedx.py b/scanpipe/pipes/cyclonedx.py index 5e2530c7a..3663894bc 100644 --- a/scanpipe/pipes/cyclonedx.py +++ b/scanpipe/pipes/cyclonedx.py @@ -189,6 +189,7 @@ def delete_tools(cyclonedx_document_json): The new structure is not yet supported by the cyclonedx-python-lib, neither for serialization (output) nor deserialization (input). + https://github.com/CycloneDX/cyclonedx-python-lib/issues/578 The tools are not used anyway in the context of loading the SBOM component data as packages. @@ -199,6 +200,30 @@ def delete_tools(cyclonedx_document_json): return cyclonedx_document_json +def delete_empty_dict_property(cyclonedx_document_json): + """ + Remove dict entry where keys are defined but no values are set, such as + ``{"name": ""}``. + + Class like cyclonedx.model.contact.OrganizationalEntity raise a + NoPropertiesProvidedException while it is not enforced in the spec. + + See https://github.com/CycloneDX/cyclonedx-python-lib/issues/600 + """ + entries_to_delete = [] + + for component in cyclonedx_document_json["components"]: + for property_name, property_value in component.items(): + if isinstance(property_value, dict) and not any(property_value.values()): + entries_to_delete.append((component, property_name)) + + # Now delete the keys outside the loop + for component, property_name in entries_to_delete: + del component[property_name] + + return cyclonedx_document_json + + def resolve_cyclonedx_packages(input_location): """Resolve the packages from the `input_location` CycloneDX document file.""" input_path = Path(input_location) @@ -215,7 +240,9 @@ def resolve_cyclonedx_packages(input_location): f'CycloneDX document "{input_path.name}" is not valid:\n{errors}' ) raise ValueError(error_msg) + cyclonedx_document = delete_tools(cyclonedx_document) + cyclonedx_document = delete_empty_dict_property(cyclonedx_document) cyclonedx_bom = Bom.from_json(data=cyclonedx_document) else: diff --git a/scanpipe/pipes/resolve.py b/scanpipe/pipes/resolve.py index d09718fbd..46c6e71e9 100644 --- a/scanpipe/pipes/resolve.py +++ b/scanpipe/pipes/resolve.py @@ -42,6 +42,16 @@ """ +def resolve_manifest_resources(resource, package_registry): + """Get package data from resource.""" + packages = get_packages_from_manifest(resource.location, package_registry) or [] + + for package_data in packages: + package_data["codebase_resources"] = [resource] + + return packages + + def get_packages(project, package_registry, manifest_resources, model=None): """ Get package data from package manifests/lockfiles/SBOMs or @@ -51,15 +61,13 @@ def get_packages(project, package_registry, manifest_resources, model=None): if not manifest_resources.exists(): project.add_warning( - description="No resources found with package data", + description="No resources containing package data found in codebase.", model=model, ) - return + return [] for resource in manifest_resources: - if packages := get_packages_from_manifest(resource.location, package_registry): - for package_data in packages: - package_data["codebase_resources"] = [resource] + if packages := resolve_manifest_resources(resource, package_registry): resolved_packages.extend(packages) else: project.add_error( diff --git a/scanpipe/tests/test_pipelines.py b/scanpipe/tests/test_pipelines.py index 988be16a9..6abe883ff 100644 --- a/scanpipe/tests/test_pipelines.py +++ b/scanpipe/tests/test_pipelines.py @@ -900,7 +900,7 @@ def test_scanpipe_resolve_dependencies_pipeline_integration(self): self.assertEqual(1, project1.projectmessages.count()) message = project1.projectmessages.get() self.assertEqual("get_packages_from_manifest", message.model) - expected = "No resources found with package data" + expected = "No resources containing package data found in codebase." self.assertIn(expected, message.description) def test_scanpipe_resolve_dependencies_pipeline_integration_empty_manifest(self):