Skip to content

Commit 37abca8

Browse files
committed
WIP: support for embedding dependency SBOMs in the built applications
1 parent 887ec52 commit 37abca8

File tree

3 files changed

+141
-37
lines changed

3 files changed

+141
-37
lines changed

extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CdxSbomBuildStep.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package io.quarkus.cyclonedx.deployment;
22

3+
import java.nio.charset.StandardCharsets;
4+
import java.util.List;
5+
36
import io.quarkus.cyclonedx.generator.CycloneDxSbomGenerator;
47
import io.quarkus.deployment.annotations.BuildProducer;
58
import io.quarkus.deployment.annotations.BuildStep;
69
import io.quarkus.deployment.builditem.AppModelProviderBuildItem;
10+
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
11+
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
712
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
813
import io.quarkus.deployment.sbom.ApplicationManifestsBuildItem;
914
import io.quarkus.deployment.sbom.SbomBuildItem;
15+
import io.quarkus.sbom.ApplicationManifest;
16+
import io.quarkus.sbom.ApplicationManifestConfig;
1017

1118
/**
1219
* Generates SBOMs for packaged applications if the corresponding config is enabled.
@@ -47,4 +54,45 @@ public void generate(ApplicationManifestsBuildItem applicationManifestsBuildItem
4754
}
4855
}
4956
}
57+
58+
@BuildStep
59+
public void embedDependencySbom(BuildProducer<GeneratedResourceBuildItem> generatedResourceBuildItem,
60+
CycloneDxConfig cdxConfig,
61+
CurateOutcomeBuildItem curateOutcomeBuildItem,
62+
AppModelProviderBuildItem appModelProviderBuildItem) {
63+
if (!cdxConfig.embeddedDependencySbom().enabled() || cdxConfig.skip()) {
64+
return;
65+
}
66+
67+
final CycloneDxConfig.EmbeddedDependencySbomConfig dependencySbomConfig = cdxConfig.embeddedDependencySbom();
68+
final String resourceName = dependencySbomConfig.resourceName();
69+
if (resourceName == null || resourceName.isEmpty()) {
70+
throw new IllegalArgumentException("resourceName is not configured for the embedded dependency SBOM");
71+
}
72+
73+
var depInfoProvider = appModelProviderBuildItem.getDependencyInfoProvider().get();
74+
List<String> result = CycloneDxSbomGenerator.newInstance()
75+
.setManifest(ApplicationManifest.fromConfig(ApplicationManifestConfig.builder()
76+
.setApplicationModel(curateOutcomeBuildItem.getApplicationModel())
77+
.build()))
78+
.setEffectiveModelResolver(depInfoProvider == null ? null : depInfoProvider.getMavenModelResolver())
79+
.setFormat(getFormat(resourceName))
80+
.setSchemaVersion(cdxConfig.schemaVersion().orElse(null))
81+
.setIncludeLicenseText(cdxConfig.includeLicenseText())
82+
.generateText();
83+
84+
if (result.size() != 1) {
85+
// this should never happen
86+
throw new RuntimeException(
87+
"Embedded dependency SBOM has more than 1 result for configured resource " + resourceName);
88+
}
89+
90+
generatedResourceBuildItem
91+
.produce(new GeneratedResourceBuildItem(resourceName, result.get(0).getBytes(StandardCharsets.UTF_8)));
92+
}
93+
94+
private static String getFormat(String resourceName) {
95+
int lastDot = resourceName.lastIndexOf('.');
96+
return lastDot == -1 ? "json" : resourceName.substring(lastDot + 1);
97+
}
5098
}

extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CycloneDxConfig.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Optional;
44

5+
import io.quarkus.runtime.annotations.ConfigDocSection;
56
import io.quarkus.runtime.annotations.ConfigRoot;
67
import io.smallrye.config.ConfigMapping;
78
import io.smallrye.config.WithDefault;
@@ -42,4 +43,32 @@ public interface CycloneDxConfig {
4243
*/
4344
@WithDefault("false")
4445
boolean includeLicenseText();
46+
47+
/**
48+
* Embedded dependency SBOM
49+
*/
50+
@ConfigDocSection
51+
EmbeddedDependencySbomConfig embeddedDependencySbom();
52+
53+
/**
54+
* Embedded dependency SBOM configuration
55+
*/
56+
interface EmbeddedDependencySbomConfig {
57+
58+
/**
59+
* Whether dependency SBOM should be embedded in the final application.
60+
*
61+
* @return true, if dependency SBOM should be embedded in the final application, false - otherwise
62+
*/
63+
@WithDefault("false")
64+
boolean enabled();
65+
66+
/**
67+
* Resource name for the embedded dependency SBOM
68+
*
69+
* @return resource name for the embedded dependency SBOM
70+
*/
71+
@WithDefault("META-INF/sbom/dependency-cdx.json")
72+
String resourceName();
73+
}
4574
}

extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.apache.commons.lang3.StringUtils;
1414
import org.apache.maven.model.MailingList;
1515
import org.apache.maven.model.Model;
16+
import org.cyclonedx.Format;
1617
import org.cyclonedx.Version;
1718
import org.cyclonedx.exception.GeneratorException;
1819
import org.cyclonedx.generators.BomGeneratorFactory;
@@ -73,10 +74,7 @@ public class CycloneDxSbomGenerator {
7374

7475
private static final String CLASSIFIER_CYCLONEDX = "cyclonedx";
7576
private static final String FORMAT_ALL = "all";
76-
private static final String FORMAT_JSON = "json";
77-
private static final String FORMAT_XML = "xml";
78-
private static final String DEFAULT_FORMAT = FORMAT_JSON;
79-
private static final List<String> SUPPORTED_FORMATS = List.of(FORMAT_JSON, FORMAT_XML);
77+
private static final String DEFAULT_FORMAT = "json";
8078

8179
public static CycloneDxSbomGenerator newInstance() {
8280
return new CycloneDxSbomGenerator();
@@ -138,37 +136,57 @@ public CycloneDxSbomGenerator setIncludeLicenseText(boolean includeLicenseText)
138136
return this;
139137
}
140138

139+
public List<String> generateText() {
140+
final Bom bom = createSbom();
141+
if (FORMAT_ALL.equals(format)) {
142+
Format[] formats = Format.values();
143+
final List<String> result = new ArrayList<>(formats.length);
144+
for (Format format : formats) {
145+
result.add(formatSbom(bom, format.getExtension()));
146+
}
147+
return result;
148+
}
149+
return List.of(formatSbom(bom, format));
150+
}
151+
141152
public List<SbomResult> generate() {
142-
ensureNotGenerated();
143-
Objects.requireNonNull(manifest, "Manifest is null");
144153
if (outputFile == null && outputDir == null) {
145154
throw new IllegalArgumentException("Either outputDir or outputFile must be provided");
146155
}
147-
generated = true;
156+
final Bom bom = createSbom();
148157

149-
var bom = new Bom();
150-
bom.setMetadata(new Metadata());
151-
addToolInfo(bom);
152-
153-
addApplicationComponent(bom, manifest.getMainComponent());
154-
for (var c : manifest.getComponents()) {
155-
addComponent(bom, c);
156-
}
157158
if (FORMAT_ALL.equalsIgnoreCase(format)) {
158159
if (outputFile != null) {
159160
throw new IllegalArgumentException("Can't use output file " + outputFile + " with format '"
160161
+ FORMAT_ALL + "', since it implies generating multiple files");
161162
}
162-
final List<SbomResult> result = new ArrayList<>(SUPPORTED_FORMATS.size());
163-
for (String format : SUPPORTED_FORMATS) {
164-
result.add(persistSbom(bom, getOutputFile(format), format));
163+
Format[] formats = Format.values();
164+
final List<SbomResult> result = new ArrayList<>(formats.length);
165+
for (Format format : formats) {
166+
result.add(persistSbom(bom, getOutputFile(format.getExtension()), format.getExtension()));
165167
}
166168
return result;
167169
}
168170
var outputFile = getOutputFile(format == null ? DEFAULT_FORMAT : format);
169171
return List.of(persistSbom(bom, outputFile, getFormat(outputFile)));
170172
}
171173

174+
private Bom createSbom() {
175+
ensureNotGenerated();
176+
Objects.requireNonNull(manifest, "Manifest is null");
177+
generated = true;
178+
179+
var bom = new Bom();
180+
bom.setMetadata(new Metadata());
181+
addToolInfo(bom);
182+
183+
addApplicationComponent(bom, manifest.getMainComponent());
184+
for (var c : manifest.getComponents()) {
185+
addComponent(bom, c);
186+
}
187+
return bom;
188+
}
189+
172190
private void addComponent(Bom bom, ApplicationComponent component) {
173191
final org.cyclonedx.model.Component c = getComponent(component);
174192
bom.addComponent(c);
@@ -421,25 +439,7 @@ private static List<ArtifactCoords> sortAlphabetically(Collection<ArtifactCoords
421439
}
422440

423441
private SbomResult persistSbom(Bom bom, Path sbomFile, String format) {
424-
425-
var specVersion = getSchemaVersion();
426-
final String sbomContent;
427-
if (format.equalsIgnoreCase("json")) {
428-
try {
429-
sbomContent = BomGeneratorFactory.createJson(specVersion, bom).toJsonString();
430-
} catch (Throwable e) {
431-
throw new RuntimeException("Failed to generate an SBOM in JSON format", e);
432-
}
433-
} else if (format.equalsIgnoreCase("xml")) {
434-
try {
435-
sbomContent = BomGeneratorFactory.createXml(specVersion, bom).toXmlString();
436-
} catch (GeneratorException e) {
437-
throw new RuntimeException("Failed to generate an SBOM in XML format", e);
438-
}
439-
} else {
440-
throw new RuntimeException(
441-
"Unsupported SBOM artifact type " + format + ", supported types are json and xml");
442-
}
442+
final String sbomContent = formatSbom(bom, format);
443443

444444
var outputDir = sbomFile.getParent();
445445
if (outputDir != null) {
@@ -462,6 +462,33 @@ private SbomResult persistSbom(Bom bom, Path sbomFile, String format) {
462462
manifest.getRunnerPath());
463463
}
464464

465+
private String formatSbom(Bom bom, String format) {
466+
var specVersion = getSchemaVersion();
467+
final String sbomContent;
468+
if (format.equalsIgnoreCase("json")) {
469+
try {
470+
sbomContent = BomGeneratorFactory.createJson(specVersion, bom).toJsonString();
471+
} catch (Throwable e) {
472+
throw new RuntimeException("Failed to generate an SBOM in JSON format", e);
473+
}
474+
} else if (format.equalsIgnoreCase("xml")) {
475+
try {
476+
sbomContent = BomGeneratorFactory.createXml(specVersion, bom).toXmlString();
477+
} catch (GeneratorException e) {
478+
throw new RuntimeException("Failed to generate an SBOM in XML format", e);
479+
}
480+
} else {
481+
var msg = new StringBuilder("Unsupported SBOM format ").append(format);
482+
var supportedFormats = Format.values();
483+
msg.append(". Supported formats are ").append(supportedFormats[0].getExtension());
484+
for (int i = 1; i < supportedFormats.length; i++) {
485+
msg.append(", ").append(supportedFormats[i].getExtension());
486+
}
487+
throw new IllegalArgumentException(msg.toString());
488+
}
489+
return sbomContent;
490+
}
491+
465492
private Path getOutputFile(String defaultFormat) {
466493
if (outputFile == null) {
467494
var fileName = toSbomFileName(manifest.getRunnerPath().getFileName().toString(), defaultFormat);

0 commit comments

Comments
 (0)