Skip to content

Commit 7373899

Browse files
committed
feat(SBOMER-297): rebuild purls using Syft information
1 parent c881a52 commit 7373899

File tree

6 files changed

+235
-22
lines changed

6 files changed

+235
-22
lines changed

cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/adjuster/SyftImageAdjuster.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,9 @@ private void filterComponents(List<Component> components) {
170170
return true;
171171
}
172172

173-
try {
174-
// Validate the PURL
175-
new PackageURL(c.getPurl());
176-
} catch (MalformedPackageURLException e) {
177-
String sanitizedPurl = PurlSanitizer.sanitizePurl(c.getPurl());
178-
log.debug("Sanitized purl {} to {}", c.getPurl(), sanitizedPurl);
179-
c.setPurl(sanitizedPurl);
173+
if (!SbomUtils.hasValidOrSanitizablePurl(c)) {
174+
log.debug("Component has a purl ({}) which cannot be made valid!", c.getPurl());
175+
return true;
180176
}
181177

182178
log.debug("Handling component '{}'", c.getPurl());

cli/src/main/java/org/jboss/sbomer/cli/feature/sbom/processor/DefaultProcessor.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,12 @@ public Bom process(Bom bom) {
277277
// If there are any purl relcoations, process these.
278278
purlRelocations.forEach((oldPurl, newPurl) -> updatePurl(bom, oldPurl, newPurl));
279279

280-
281280
if (bom.getMetadata() != null && bom.getMetadata().getComponent() != null) {
282281
Component mainComponent = bom.getMetadata().getComponent();
283282
addMissingNpmDependencies(bom, mainComponent);
284283
// Add missing NPM Depenencies for CycloneDxGenerateOperationComand manifest
285-
if (mainComponent.getDescription() != null && mainComponent.getDescription().contains(SBOM_REPRESENTING_THE_DELIVERABLE)) {
284+
if (mainComponent.getDescription() != null
285+
&& mainComponent.getDescription().contains(SBOM_REPRESENTING_THE_DELIVERABLE)) {
286286
if (bom.getComponents() != null) {
287287
ArrayList<Component> copyOfComponents = new ArrayList<>(bom.getComponents());
288288
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<
354354
if (listedPurls.contains(coordinates)) {
355355
continue;
356356
}
357-
Component newComponent = createComponent(
358-
artifact,
359-
Component.Scope.REQUIRED,
360-
Component.Type.LIBRARY);
357+
Component newComponent = createComponent(artifact, Component.Scope.REQUIRED, Component.Type.LIBRARY);
361358
setArtifactMetadata(newComponent, artifact, pncService.getApiUrl());
362359
setPncBuildMetadata(newComponent, artifact.getBuild(), pncService.getApiUrl());
363360
bom.addComponent(newComponent);

cli/src/test/java/org/jboss/sbomer/cli/test/unit/feature/sbom/adjust/SyftImageAdjusterTest.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818

1919
import org.cyclonedx.model.Bom;
2020
import org.cyclonedx.model.Component;
21+
import org.cyclonedx.model.Component.Type;
2122
import org.cyclonedx.model.Dependency;
2223
import org.cyclonedx.model.ExternalReference;
2324
import org.cyclonedx.model.Property;
2425
import org.jboss.sbomer.cli.feature.sbom.adjuster.SyftImageAdjuster;
25-
import org.jboss.sbomer.core.features.sbom.Constants;
2626
import org.jboss.sbomer.core.features.sbom.utils.PurlSanitizer;
2727
import org.jboss.sbomer.core.features.sbom.utils.SbomUtils;
2828
import org.jboss.sbomer.core.test.TestResources;
@@ -323,4 +323,26 @@ void shouldSanitizeBogusPurls() throws IOException {
323323
"pkg:rpm/redhat/[email protected]_2?arch=x86_64&distro=rhel-9.2&upstream=passt-0-20230222.g4ddbcb9-4.el9_2.src.rpm",
324324
sanitizedPurl);
325325
}
326+
327+
@Test
328+
void shouldRebuildBogusPurls() throws IOException {
329+
// Initialize the bogus component generated from Syft and verify it's not fixable, and remains unchanged
330+
Component component = SbomUtils
331+
.createComponent(null, "../", "(devel)", null, "pkg:golang/../@(devel)", Component.Type.LIBRARY);
332+
component.setBomRef("a02ebe2f06983d18");
333+
SbomUtils.addPropertyIfMissing(component, "syft:package:type", "go-module");
334+
335+
// Verify that the purl is not valid
336+
boolean isValid = SbomUtils.isValidPurl(component.getPurl());
337+
assertFalse(isValid);
338+
339+
// Verify that the purl cannot be sanitized
340+
String sanitizedPurl = SbomUtils.sanitizePurl(component.getPurl());
341+
assertNull(sanitizedPurl);
342+
343+
// Verify that the purl can be rebuilt
344+
boolean isValidAfterRebuilding = SbomUtils.hasValidOrSanitizablePurl(component);
345+
assertTrue(isValidAfterRebuilding);
346+
assertTrue(SbomUtils.isValidPurl(component.getPurl()));
347+
}
326348
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
* Copyright 2023 Red Hat, Inc., and individual contributors
4+
* as indicated by the @author tags.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.jboss.sbomer.core.features.sbom.utils;
19+
20+
import com.github.packageurl.MalformedPackageURLException;
21+
import com.github.packageurl.PackageURL;
22+
23+
import lombok.extern.slf4j.Slf4j;
24+
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
import java.util.Optional;
28+
import java.util.TreeMap;
29+
30+
import org.cyclonedx.model.Component;
31+
import org.cyclonedx.model.Property;
32+
33+
@Slf4j
34+
public class PurlRebuilder {
35+
36+
// List taken from https://github.com/anchore/syft/blob/main/syft/pkg/type.go
37+
private final static String SYFT_ALPMPKG = "alpm";
38+
private final static String SYFT_APKPKG = "apk";
39+
private final static String SYFT_BINARYPKG = "binary";
40+
private final static String SYFT_COCOAPODSPKG = "pod";
41+
private final static String SYFT_CONANPKG = "conan";
42+
private final static String SYFT_DARTPUBPKG = "dart-pub";
43+
private final static String SYFT_DEBPKG = "deb";
44+
private final static String SYFT_DOTNETPKG = "dotnet";
45+
private final static String SYFT_ERLANGOTPPKG = "erlang-otp";
46+
private final static String SYFT_GEMPKG = "gem";
47+
private final static String SYFT_GITHUBACTIONPKG = "github-action";
48+
private final static String SYFT_GITHUBACTIONWORKFLOWPKG = "github-action-workflow";
49+
private final static String SYFT_GOMODULEPKG = "go-module";
50+
private final static String SYFT_GRAALVMNATIVEIMAGEPKG = "graalvm-native-image";
51+
private final static String SYFT_HACKAGEPKG = "hackage";
52+
private final static String SYFT_HEXPKG = "hex";
53+
private final static String SYFT_JAVAPKG = "java-archive";
54+
private final static String SYFT_JENKINSPACKAGE = "jenkins-plugin";
55+
private final static String SYFT_KBPKG = "msrc-kb";
56+
private final static String SYFT_LINUXKERNELPKG = "linux-kernel";
57+
private final static String SYFT_LINUXKERNELMODULEPKG = "linux-kernel-module";
58+
private final static String SYFT_NIXPKG = "nix";
59+
private final static String SYFT_NPMPKG = "npm";
60+
private final static String SYFT_PHPCOMPOSERPKG = "php-composer";
61+
private final static String SYFT_PHPPECLPKG = "php-pecl";
62+
private final static String SYFT_PORTAGEPKG = "portage";
63+
private final static String SYFT_PYTHONPKG = "python";
64+
private final static String SYFT_RPKG = "R-package";
65+
private final static String SYFT_LUAROCKSPKG = "lua-rocks";
66+
private final static String SYFT_RPMPKG = "rpm";
67+
private final static String SYFT_RUSTPKG = "rust-crate";
68+
private final static String SYFT_SWIFTPKG = "swift";
69+
private final static String SYFT_SWIPLPACKPKG = "swiplpack";
70+
private final static String SYFT_OPAMPKG = "opam";
71+
private final static String SYFT_WORDPRESSPLUGINPKG = "wordpress-plugin";
72+
73+
// Associations taken from https://github.com/anchore/syft/blob/main/syft/pkg/type.go#L91
74+
private static final Map<String, String> SYFT_PACKAGE_2_PURL_TYPE_MAP = new HashMap<>();
75+
static {
76+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_ALPMPKG, "alpm");
77+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_APKPKG, "apk");
78+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_COCOAPODSPKG, "cocoapods");
79+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_CONANPKG, "conan");
80+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_DARTPUBPKG, "pub");
81+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_DEBPKG, PackageURL.StandardTypes.DEBIAN);
82+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_DOTNETPKG, "dotnet");
83+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_ERLANGOTPPKG, "otp");
84+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GEMPKG, PackageURL.StandardTypes.GEM);
85+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GITHUBACTIONPKG, PackageURL.StandardTypes.GITHUB);
86+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GITHUBACTIONWORKFLOWPKG, PackageURL.StandardTypes.GITHUB);
87+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_GOMODULEPKG, PackageURL.StandardTypes.GOLANG);
88+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_HACKAGEPKG, "hackage");
89+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_HEXPKG, PackageURL.StandardTypes.HEX);
90+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_JAVAPKG, PackageURL.StandardTypes.MAVEN);
91+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_JENKINSPACKAGE, PackageURL.StandardTypes.MAVEN);
92+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_LINUXKERNELPKG, "generic/linux-kernel");
93+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_LINUXKERNELMODULEPKG, PackageURL.StandardTypes.GENERIC);
94+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_NIXPKG, "nix");
95+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_NPMPKG, PackageURL.StandardTypes.NPM);
96+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PHPCOMPOSERPKG, PackageURL.StandardTypes.COMPOSER);
97+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PHPPECLPKG, "pecl");
98+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PORTAGEPKG, "portage");
99+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_PYTHONPKG, PackageURL.StandardTypes.PYPI);
100+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_RPKG, "cran");
101+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_LUAROCKSPKG, "luarocks");
102+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_RPMPKG, PackageURL.StandardTypes.RPM);
103+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_RUSTPKG, PackageURL.StandardTypes.CARGO);
104+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_SWIFTPKG, "swift");
105+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_SWIPLPACKPKG, "swiplpack");
106+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_OPAMPKG, "opam");
107+
SYFT_PACKAGE_2_PURL_TYPE_MAP.put(SYFT_WORDPRESSPLUGINPKG, "wordpress-plugin");
108+
}
109+
110+
/**
111+
* Given a component, tries to create a valid purl using the Syft information (if available) and the component
112+
* properties
113+
*
114+
* @param component
115+
* @return a valid rebuilt purl
116+
*/
117+
public static String rebuildPurlFromSyftComponent(Component component) throws MalformedPackageURLException {
118+
119+
Optional<Property> syftPackageType = SbomUtils.findPropertyWithNameInComponent("syft:package:type", component);
120+
if (!syftPackageType.isPresent()) {
121+
return null;
122+
}
123+
124+
String type = SYFT_PACKAGE_2_PURL_TYPE_MAP.get(syftPackageType.get().getValue());
125+
if (type == null) {
126+
return null;
127+
}
128+
129+
// Use all the data we have without overthinking about the type of PURL
130+
String namespace = PurlSanitizer.sanitizeNamespace(component.getGroup());
131+
String name = PurlSanitizer.sanitizeName(component.getName());
132+
String version = PurlSanitizer.sanitizeVersion(component.getVersion());
133+
134+
PackageURL purl = new PackageURL(type, namespace, name, version, null, null);
135+
return purl.canonicalize();
136+
}
137+
138+
}

core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/PurlSanitizer.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static String sanitizePurl(String purl) {
5050
return parsedPurl.canonicalize();
5151
} catch (MalformedPackageURLException e) {
5252
// If parsing fails, proceed to manual sanitization
53-
log.error("Malformed PURL detected, attempting to sanitize: {}", e.getMessage());
53+
log.error("Malformed PURL detected, attempting to sanitize: {}", purl, e.getMessage());
5454
}
5555

5656
// Manually parse and sanitize the PURL components
@@ -119,15 +119,15 @@ public static String sanitizePurl(String purl) {
119119
return sanitizedPurl.canonicalize();
120120

121121
} catch (Exception ex) {
122-
throw new IllegalArgumentException("Failed to sanitize PURL: " + ex.getMessage(), ex);
122+
throw new IllegalArgumentException("Failed to sanitize PURL: " + purl, ex);
123123
}
124124
}
125125

126-
private static String sanitizeType(String type) {
126+
public static String sanitizeType(String type) {
127127
return type.replaceAll(TYPE_INVALID_CHARS, "-").toLowerCase();
128128
}
129129

130-
private static String sanitizeNamespace(String namespace) {
130+
public static String sanitizeNamespace(String namespace) {
131131
if (namespace == null)
132132
return null;
133133
String[] segments = namespace.split("/");
@@ -137,17 +137,17 @@ private static String sanitizeNamespace(String namespace) {
137137
return String.join("/", segments);
138138
}
139139

140-
private static String sanitizeName(String name) {
140+
public static String sanitizeName(String name) {
141141
return name.replaceAll(NAME_VERSION_QKEY_QVALUE, "-");
142142
}
143143

144-
private static String sanitizeVersion(String version) {
144+
public static String sanitizeVersion(String version) {
145145
if (version == null)
146146
return null;
147147
return version.replaceAll(NAME_VERSION_QKEY_QVALUE, "-");
148148
}
149149

150-
private static String sanitizeSubpath(String subpath) {
150+
public static String sanitizeSubpath(String subpath) {
151151
if (subpath == null)
152152
return null;
153153
String[] segments = subpath.split("/");
@@ -157,7 +157,7 @@ private static String sanitizeSubpath(String subpath) {
157157
return String.join("/", segments);
158158
}
159159

160-
private static TreeMap<String, String> sanitizeQualifiers(TreeMap<String, String> qualifiers) {
160+
public static TreeMap<String, String> sanitizeQualifiers(TreeMap<String, String> qualifiers) {
161161
if (qualifiers == null)
162162
return null;
163163
TreeMap<String, String> sanitized = new TreeMap<>();

core/src/main/java/org/jboss/sbomer/core/features/sbom/utils/SbomUtils.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,4 +1006,64 @@ public static void addMissingSerialNumber(Bom bom) {
10061006
}
10071007
}
10081008
}
1009+
1010+
/**
1011+
*
1012+
* @param component the component whose purl needs to be analyzed
1013+
* @return true if the component has a valid purl, false if the purl is not valid even after a sanitization
1014+
*/
1015+
public static boolean hasValidOrSanitizablePurl(Component component) {
1016+
String purl = component.getPurl();
1017+
1018+
// Try to validate the PURL first
1019+
if (isValidPurl(purl)) {
1020+
return true;
1021+
}
1022+
1023+
// Try to sanitize the PURL if invalid
1024+
String sanitizedPurl = sanitizePurl(purl);
1025+
if (sanitizedPurl != null) {
1026+
component.setPurl(sanitizedPurl);
1027+
log.debug("Sanitized purl {} to {}", purl, sanitizedPurl);
1028+
return true;
1029+
}
1030+
1031+
// Attempt to rebuild the PURL if sanitization failed
1032+
String rebuiltPurl = rebuildPurl(component);
1033+
if (rebuiltPurl != null) {
1034+
component.setPurl(rebuiltPurl);
1035+
log.debug("Rebuilt purl {} to {}", purl, rebuiltPurl);
1036+
return true;
1037+
}
1038+
1039+
return false;
1040+
}
1041+
1042+
public static boolean isValidPurl(String purl) {
1043+
try {
1044+
new PackageURL(purl);
1045+
return true;
1046+
} catch (MalformedPackageURLException e) {
1047+
return false;
1048+
}
1049+
}
1050+
1051+
public static String sanitizePurl(String purl) {
1052+
try {
1053+
return PurlSanitizer.sanitizePurl(purl);
1054+
} catch (Exception e) {
1055+
log.debug("Failed to sanitize purl {}", purl, e);
1056+
return null;
1057+
}
1058+
}
1059+
1060+
private static String rebuildPurl(Component component) {
1061+
try {
1062+
log.debug("Purl was not valid and could not be sanitized, trying to rebuild it!");
1063+
return PurlRebuilder.rebuildPurlFromSyftComponent(component);
1064+
} catch (MalformedPackageURLException e) {
1065+
log.debug("Purl {} could not be rebuilt!", component.getPurl());
1066+
return null;
1067+
}
1068+
}
10091069
}

0 commit comments

Comments
 (0)