Skip to content

Commit 28d20b4

Browse files
committed
WIP: support for embedding dependency SBOMs in the built applications
1 parent a3efab0 commit 28d20b4

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;
@@ -74,10 +75,7 @@ public class CycloneDxSbomGenerator {
7475

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

8280
public static CycloneDxSbomGenerator newInstance() {
8381
return new CycloneDxSbomGenerator();
@@ -139,37 +137,57 @@ public CycloneDxSbomGenerator setIncludeLicenseText(boolean includeLicenseText)
139137
return this;
140138
}
141139

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

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

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

443461
private SbomResult persistSbom(Bom bom, Path sbomFile, String format) {
444-
445-
var specVersion = getSchemaVersion();
446-
final String sbomContent;
447-
if (format.equalsIgnoreCase("json")) {
448-
try {
449-
sbomContent = BomGeneratorFactory.createJson(specVersion, bom).toJsonString();
450-
} catch (Throwable e) {
451-
throw new RuntimeException("Failed to generate an SBOM in JSON format", e);
452-
}
453-
} else if (format.equalsIgnoreCase("xml")) {
454-
try {
455-
sbomContent = BomGeneratorFactory.createXml(specVersion, bom).toXmlString();
456-
} catch (GeneratorException e) {
457-
throw new RuntimeException("Failed to generate an SBOM in XML format", e);
458-
}
459-
} else {
460-
throw new RuntimeException(
461-
"Unsupported SBOM artifact type " + format + ", supported types are json and xml");
462-
}
462+
final String sbomContent = formatSbom(bom, format);
463463

464464
var outputDir = sbomFile.getParent();
465465
if (outputDir != null) {
@@ -482,6 +482,33 @@ private SbomResult persistSbom(Bom bom, Path sbomFile, String format) {
482482
manifest.getRunnerPath());
483483
}
484484

485+
private String formatSbom(Bom bom, String format) {
486+
var specVersion = getSchemaVersion();
487+
final String sbomContent;
488+
if (format.equalsIgnoreCase("json")) {
489+
try {
490+
sbomContent = BomGeneratorFactory.createJson(specVersion, bom).toJsonString();
491+
} catch (Throwable e) {
492+
throw new RuntimeException("Failed to generate an SBOM in JSON format", e);
493+
}
494+
} else if (format.equalsIgnoreCase("xml")) {
495+
try {
496+
sbomContent = BomGeneratorFactory.createXml(specVersion, bom).toXmlString();
497+
} catch (GeneratorException e) {
498+
throw new RuntimeException("Failed to generate an SBOM in XML format", e);
499+
}
500+
} else {
501+
var msg = new StringBuilder("Unsupported SBOM format ").append(format);
502+
var supportedFormats = Format.values();
503+
msg.append(". Supported formats are ").append(supportedFormats[0].getExtension());
504+
for (int i = 1; i < supportedFormats.length; i++) {
505+
msg.append(", ").append(supportedFormats[i].getExtension());
506+
}
507+
throw new IllegalArgumentException(msg.toString());
508+
}
509+
return sbomContent;
510+
}
511+
485512
private Path getOutputFile(String defaultFormat) {
486513
if (outputFile == null) {
487514
var fileName = toSbomFileName(manifest.getRunnerPath().getFileName().toString(), defaultFormat);

0 commit comments

Comments
 (0)