Skip to content

Commit

Permalink
feat(SBOMER-297): rebuild purls using Syft information
Browse files Browse the repository at this point in the history
  • Loading branch information
vibe13 committed Jan 14, 2025
1 parent c881a52 commit 41f0690
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,9 @@ private void filterComponents(List<Component> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Component> copyOfComponents = new ArrayList<>(bom.getComponents());
for (Component c : copyOfComponents) { // We modify bom.components, so need to iterate over a copy
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -323,4 +323,26 @@ void shouldSanitizeBogusPurls() throws IOException {
"pkg:rpm/redhat/[email protected]_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()));
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<Property> 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("/");
Expand All @@ -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("/");
Expand All @@ -157,7 +157,7 @@ private static String sanitizeSubpath(String subpath) {
return String.join("/", segments);
}

private static TreeMap<String, String> sanitizeQualifiers(TreeMap<String, String> qualifiers) {
public static TreeMap<String, String> sanitizeQualifiers(TreeMap<String, String> qualifiers) {
if (qualifiers == null)
return null;
TreeMap<String, String> sanitized = new TreeMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

0 comments on commit 41f0690

Please sign in to comment.