From 41f06901b5a542f4ea9cd7068a67c44fb6f2feb5 Mon Sep 17 00:00:00 2001 From: Andrea Vibelli Date: Tue, 14 Jan 2025 14:29:55 +0100 Subject: [PATCH] feat(SBOMER-297): rebuild purls using Syft information --- .../sbom/adjuster/SyftImageAdjuster.java | 10 +- .../sbom/processor/DefaultProcessor.java | 9 +- .../sbom/adjust/SyftImageAdjusterTest.java | 24 ++- .../features/sbom/utils/PurlRebuilder.java | 138 ++++++++++++++++++ .../features/sbom/utils/PurlSanitizer.java | 16 +- .../core/features/sbom/utils/SbomUtils.java | 60 ++++++++ 6 files changed, 235 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlRebuilder.java diff --git a/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/adjuster/SyftImageAdjuster.java b/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/adjuster/SyftImageAdjuster.java index 9efa0eb90..fe6a81b76 100644 --- a/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/adjuster/SyftImageAdjuster.java +++ b/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/adjuster/SyftImageAdjuster.java @@ -170,13 +170,9 @@ private void filterComponents(List components) { return true; } - try { - // Validate the PURL - new PackageURL(c.getPurl()); - } catch (MalformedPackageURLException e) { - String sanitizedPurl = PurlSanitizer.sanitizePurl(c.getPurl()); - log.debug("Sanitized purl {} to {}", c.getPurl(), sanitizedPurl); - c.setPurl(sanitizedPurl); + if (!SbomUtils.hasValidOrSanitizablePurl(c)) { + log.debug("Component has a purl ({}) which cannot be made valid!", c.getPurl()); + return true; } log.debug("Handling component '{}'", c.getPurl()); diff --git a/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/processor/DefaultProcessor.java b/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/processor/DefaultProcessor.java index 1093a15b8..8b2f2f13e 100644 --- a/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/processor/DefaultProcessor.java +++ b/cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/processor/DefaultProcessor.java @@ -277,12 +277,12 @@ public Bom process(Bom bom) { // If there are any purl relcoations, process these. purlRelocations.forEach((oldPurl, newPurl) -> updatePurl(bom, oldPurl, newPurl)); - if (bom.getMetadata() != null && bom.getMetadata().getComponent() != null) { Component mainComponent = bom.getMetadata().getComponent(); addMissingNpmDependencies(bom, mainComponent); // Add missing NPM Depenencies for CycloneDxGenerateOperationComand manifest - if (mainComponent.getDescription() != null && mainComponent.getDescription().contains(SBOM_REPRESENTING_THE_DELIVERABLE)) { + if (mainComponent.getDescription() != null + && mainComponent.getDescription().contains(SBOM_REPRESENTING_THE_DELIVERABLE)) { if (bom.getComponents() != null) { ArrayList copyOfComponents = new ArrayList<>(bom.getComponents()); for (Component c : copyOfComponents) { // We modify bom.components, so need to iterate over a copy @@ -354,10 +354,7 @@ private void addMissingNpmDependencies(Bom bom, Component component, Collection< if (listedPurls.contains(coordinates)) { continue; } - Component newComponent = createComponent( - artifact, - Component.Scope.REQUIRED, - Component.Type.LIBRARY); + Component newComponent = createComponent(artifact, Component.Scope.REQUIRED, Component.Type.LIBRARY); setArtifactMetadata(newComponent, artifact, pncService.getApiUrl()); setPncBuildMetadata(newComponent, artifact.getBuild(), pncService.getApiUrl()); bom.addComponent(newComponent); diff --git a/cli/src/test/java/org/jboss/sbomer/cli/test/unit/feature/sbom/adjust/SyftImageAdjusterTest.java b/cli/src/test/java/org/jboss/sbomer/cli/test/unit/feature/sbom/adjust/SyftImageAdjusterTest.java index e222b5cb3..aaa827d9e 100644 --- a/cli/src/test/java/org/jboss/sbomer/cli/test/unit/feature/sbom/adjust/SyftImageAdjusterTest.java +++ b/cli/src/test/java/org/jboss/sbomer/cli/test/unit/feature/sbom/adjust/SyftImageAdjusterTest.java @@ -18,11 +18,11 @@ import org.cyclonedx.model.Bom; import org.cyclonedx.model.Component; +import org.cyclonedx.model.Component.Type; import org.cyclonedx.model.Dependency; import org.cyclonedx.model.ExternalReference; import org.cyclonedx.model.Property; import org.jboss.sbomer.cli.feature.sbom.adjuster.SyftImageAdjuster; -import org.jboss.sbomer.core.features.sbom.Constants; import org.jboss.sbomer.core.features.sbom.utils.PurlSanitizer; import org.jboss.sbomer.core.features.sbom.utils.SbomUtils; import org.jboss.sbomer.core.test.TestResources; @@ -323,4 +323,26 @@ void shouldSanitizeBogusPurls() throws IOException { "pkg:rpm/redhat/passt@0-20230222.g4ddbcb9-4.el9_2?arch=x86_64&distro=rhel-9.2&upstream=passt-0-20230222.g4ddbcb9-4.el9_2.src.rpm", sanitizedPurl); } + + @Test + void shouldRebuildBogusPurls() throws IOException { + // Initialize the bogus component generated from Syft and verify it's not fixable, and remains unchanged + Component component = SbomUtils + .createComponent(null, "../", "(devel)", null, "pkg:golang/../@(devel)", Component.Type.LIBRARY); + component.setBomRef("a02ebe2f06983d18"); + SbomUtils.addPropertyIfMissing(component, "syft:package:type", "go-module"); + + // Verify that the purl is not valid + boolean isValid = SbomUtils.isValidPurl(component.getPurl()); + assertFalse(isValid); + + // Verify that the purl cannot be sanitized + String sanitizedPurl = SbomUtils.sanitizePurl(component.getPurl()); + assertNull(sanitizedPurl); + + // Verify that the purl can be rebuilt + boolean isValidAfterRebuilding = SbomUtils.hasValidOrSanitizablePurl(component); + assertTrue(isValidAfterRebuilding); + assertTrue(SbomUtils.isValidPurl(component.getPurl())); + } } \ No newline at end of file diff --git a/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlRebuilder.java b/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlRebuilder.java new file mode 100644 index 000000000..70df98d9b --- /dev/null +++ b/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlRebuilder.java @@ -0,0 +1,138 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.sbomer.core.features.sbom.utils; + +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; + +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import org.cyclonedx.model.Component; +import org.cyclonedx.model.Property; + +@Slf4j +public class PurlRebuilder { + + // List taken from https://github.com/anchore/syft/blob/main/syft/pkg/type.go + private final static String SYFT_ALPMPKG = "alpm"; + private final static String SYFT_APKPKG = "apk"; + private final static String SYFT_BINARYPKG = "binary"; + private final static String SYFT_COCOAPODSPKG = "pod"; + private final static String SYFT_CONANPKG = "conan"; + private final static String SYFT_DARTPUBPKG = "dart-pub"; + private final static String SYFT_DEBPKG = "deb"; + private final static String SYFT_DOTNETPKG = "dotnet"; + private final static String SYFT_ERLANGOTPPKG = "erlang-otp"; + private final static String SYFT_GEMPKG = "gem"; + private final static String SYFT_GITHUBACTIONPKG = "github-action"; + private final static String SYFT_GITHUBACTIONWORKFLOWPKG = "github-action-workflow"; + private final static String SYFT_GOMODULEPKG = "go-module"; + private final static String SYFT_GRAALVMNATIVEIMAGEPKG = "graalvm-native-image"; + private final static String SYFT_HACKAGEPKG = "hackage"; + private final static String SYFT_HEXPKG = "hex"; + private final static String SYFT_JAVAPKG = "java-archive"; + private final static String SYFT_JENKINSPACKAGE = "jenkins-plugin"; + private final static String SYFT_KBPKG = "msrc-kb"; + private final static String SYFT_LINUXKERNELPKG = "linux-kernel"; + private final static String SYFT_LINUXKERNELMODULEPKG = "linux-kernel-module"; + private final static String SYFT_NIXPKG = "nix"; + private final static String SYFT_NPMPKG = "npm"; + private final static String SYFT_PHPCOMPOSERPKG = "php-composer"; + private final static String SYFT_PHPPECLPKG = "php-pecl"; + private final static String SYFT_PORTAGEPKG = "portage"; + private final static String SYFT_PYTHONPKG = "python"; + private final static String SYFT_RPKG = "R-package"; + private final static String SYFT_LUAROCKSPKG = "lua-rocks"; + private final static String SYFT_RPMPKG = "rpm"; + private final static String SYFT_RUSTPKG = "rust-crate"; + private final static String SYFT_SWIFTPKG = "swift"; + private final static String SYFT_SWIPLPACKPKG = "swiplpack"; + private final static String SYFT_OPAMPKG = "opam"; + private final static String SYFT_WORDPRESSPLUGINPKG = "wordpress-plugin"; + + // Associations taken from https://github.com/anchore/syft/blob/main/syft/pkg/type.go#L91 + private static final Map SYFT_PACKAGE_2_PURL_TYPE_MAP = new HashMap<>(); + static { + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_ALPMPKG, "alpm"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_APKPKG, "apk"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_COCOAPODSPKG, "cocoapods"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_CONANPKG, "conan"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_DARTPUBPKG, "pub"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_DEBPKG, PackageURL.StandardTypes.DEBIAN); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_DOTNETPKG, "dotnet"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_ERLANGOTPPKG, "otp"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GEMPKG, PackageURL.StandardTypes.GEM); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GITHUBACTIONPKG, PackageURL.StandardTypes.GITHUB); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GITHUBACTIONWORKFLOWPKG, PackageURL.StandardTypes.GITHUB); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GOMODULEPKG, PackageURL.StandardTypes.GOLANG); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_HACKAGEPKG, "hackage"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_HEXPKG, PackageURL.StandardTypes.HEX); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_JAVAPKG, PackageURL.StandardTypes.MAVEN); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_JENKINSPACKAGE, PackageURL.StandardTypes.MAVEN); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_LINUXKERNELPKG, "generic/linux-kernel"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_LINUXKERNELMODULEPKG, PackageURL.StandardTypes.GENERIC); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_NIXPKG, "nix"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_NPMPKG, PackageURL.StandardTypes.NPM); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PHPCOMPOSERPKG, PackageURL.StandardTypes.COMPOSER); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PHPPECLPKG, "pecl"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PORTAGEPKG, "portage"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PYTHONPKG, PackageURL.StandardTypes.PYPI); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_RPKG, "cran"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_LUAROCKSPKG, "luarocks"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_RPMPKG, PackageURL.StandardTypes.RPM); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_RUSTPKG, PackageURL.StandardTypes.CARGO); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_SWIFTPKG, "swift"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_SWIPLPACKPKG, "swiplpack"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_OPAMPKG, "opam"); + SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_WORDPRESSPLUGINPKG, "wordpress-plugin"); + } + + /** + * Given a component, tries to create a valid purl using the Syft information (if available) and the component + * properties + * + * @param component + * @return a valid rebuilt purl + */ + public static String rebuildPurlFromSyftComponent(Component component) throws MalformedPackageURLException { + + Optional syftPackageType = SbomUtils.findPropertyWithNameInComponent("syft:package:type", component); + if (!syftPackageType.isPresent()) { + return null; + } + + String type = SYFT_PACKAGE_2_PURL_TYPE_MAP.get(syftPackageType.get().getValue()); + if (type == null) { + return null; + } + + // Use all the data we have without overthinking about the type of PURL + String namespace = PurlSanitizer.sanitizeNamespace(component.getGroup()); + String name = PurlSanitizer.sanitizeName(component.getName()); + String version = PurlSanitizer.sanitizeVersion(component.getVersion()); + + PackageURL purl = new PackageURL(type, namespace, name, version, null, null); + return purl.canonicalize(); + } + +} diff --git a/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlSanitizer.java b/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlSanitizer.java index 21f3e9aba..1f771690e 100644 --- a/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlSanitizer.java +++ b/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlSanitizer.java @@ -50,7 +50,7 @@ public static String sanitizePurl(String purl) { return parsedPurl.canonicalize(); } catch (MalformedPackageURLException e) { // If parsing fails, proceed to manual sanitization - log.error("Malformed PURL detected, attempting to sanitize: {}", e.getMessage()); + log.error("Malformed PURL detected, attempting to sanitize: {}", purl, e.getMessage()); } // Manually parse and sanitize the PURL components @@ -119,15 +119,15 @@ public static String sanitizePurl(String purl) { return sanitizedPurl.canonicalize(); } catch (Exception ex) { - throw new IllegalArgumentException("Failed to sanitize PURL: " + ex.getMessage(), ex); + throw new IllegalArgumentException("Failed to sanitize PURL: " + purl, ex); } } - private static String sanitizeType(String type) { + public static String sanitizeType(String type) { return type.replaceAll(TYPE_INVALID_CHARS, "-").toLowerCase(); } - private static String sanitizeNamespace(String namespace) { + public static String sanitizeNamespace(String namespace) { if (namespace == null) return null; String[] segments = namespace.split("/"); @@ -137,17 +137,17 @@ private static String sanitizeNamespace(String namespace) { return String.join("/", segments); } - private static String sanitizeName(String name) { + public static String sanitizeName(String name) { return name.replaceAll(NAME_VERSION_QKEY_QVALUE, "-"); } - private static String sanitizeVersion(String version) { + public static String sanitizeVersion(String version) { if (version == null) return null; return version.replaceAll(NAME_VERSION_QKEY_QVALUE, "-"); } - private static String sanitizeSubpath(String subpath) { + public static String sanitizeSubpath(String subpath) { if (subpath == null) return null; String[] segments = subpath.split("/"); @@ -157,7 +157,7 @@ private static String sanitizeSubpath(String subpath) { return String.join("/", segments); } - private static TreeMap sanitizeQualifiers(TreeMap qualifiers) { + public static TreeMap sanitizeQualifiers(TreeMap qualifiers) { if (qualifiers == null) return null; TreeMap sanitized = new TreeMap<>(); diff --git a/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/SbomUtils.java b/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/SbomUtils.java index a44f7a53c..dabcdacb6 100644 --- a/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/SbomUtils.java +++ b/core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/SbomUtils.java @@ -1006,4 +1006,64 @@ public static void addMissingSerialNumber(Bom bom) { } } } + + /** + * + * @param component the component whose purl needs to be analyzed + * @return true if the component has a valid purl, false if the purl is not valid even after a sanitization + */ + public static boolean hasValidOrSanitizablePurl(Component component) { + String purl = component.getPurl(); + + // Try to validate the PURL first + if (isValidPurl(purl)) { + return true; + } + + // Try to sanitize the PURL if invalid + String sanitizedPurl = sanitizePurl(purl); + if (sanitizedPurl != null) { + component.setPurl(sanitizedPurl); + log.debug("Sanitized purl {} to {}", purl, sanitizedPurl); + return true; + } + + // Attempt to rebuild the PURL if sanitization failed + String rebuiltPurl = rebuildPurl(component); + if (rebuiltPurl != null) { + component.setPurl(rebuiltPurl); + log.debug("Rebuilt purl {} to {}", purl, rebuiltPurl); + return true; + } + + return false; + } + + public static boolean isValidPurl(String purl) { + try { + new PackageURL(purl); + return true; + } catch (MalformedPackageURLException e) { + return false; + } + } + + public static String sanitizePurl(String purl) { + try { + return PurlSanitizer.sanitizePurl(purl); + } catch (Exception e) { + log.debug("Failed to sanitize purl {}", purl, e); + return null; + } + } + + private static String rebuildPurl(Component component) { + try { + log.debug("Purl was not valid and could not be sanitized, trying to rebuild it!"); + return PurlRebuilder.rebuildPurlFromSyftComponent(component); + } catch (MalformedPackageURLException e) { + log.debug("Purl {} could not be rebuilt!", component.getPurl()); + return null; + } + } }