Implement SBOM actuator endpoint

Closes gh-39799
This commit is contained in:
Moritz Halbritter
2024-01-15 09:56:58 +01:00
committed by Phillip Webb
parent 75012c5173
commit 4047c00aa5
30 changed files with 22016 additions and 1 deletions

View File

@@ -41,6 +41,9 @@ dependencies {
implementation("org.springframework:spring-core")
optional("org.graalvm.buildtools:native-gradle-plugin")
optional("org.cyclonedx:cyclonedx-gradle-plugin") {
exclude(group: "org.apache.maven", module: "maven-core")
}
optional("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") {
exclude(group: "commons-logging", module: "commons-logging")
}
@@ -55,6 +58,14 @@ dependencies {
testImplementation("org.testcontainers:testcontainers")
}
repositories {
gradlePluginPortal() {
content {
includeGroup("org.cyclonedx")
}
}
}
gradlePlugin {
plugins {
springBootPlugin {

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* 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
*
* https://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.springframework.boot.gradle.plugin;
import org.cyclonedx.gradle.CycloneDxPlugin;
import org.cyclonedx.gradle.CycloneDxTask;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.tasks.TaskProvider;
import org.springframework.boot.gradle.tasks.bundling.BootJar;
/**
* {@link Action} that is executed in response to the {@link CycloneDxPlugin} being
* applied.
*
* @author Moritz Halbritter
*/
final class CycloneDxPluginAction implements PluginApplicationAction {
@Override
public Class<? extends Plugin<? extends Project>> getPluginClass() {
return CycloneDxPlugin.class;
}
@Override
public void execute(Project project) {
TaskProvider<CycloneDxTask> cyclonedxBom = project.getTasks().named("cyclonedxBom", CycloneDxTask.class);
cyclonedxBom.configure((task) -> {
task.getProjectType().convention("application");
task.getOutputFormat().convention("json");
task.getOutputName().convention("application.cdx");
task.getIncludeLicenseText().convention(false);
});
project.getTasks().named(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class).configure((bootJar) -> {
CycloneDxTask cycloneDxTask = cyclonedxBom.get();
String sbomFileName = cycloneDxTask.getOutputName().get() + getSbomExtension(cycloneDxTask);
bootJar.from(cycloneDxTask, (spec) -> spec.include(sbomFileName).into("META-INF/sbom"));
bootJar.manifest((manifest) -> {
manifest.getAttributes().put("Sbom-Format", "CycloneDX");
manifest.getAttributes().put("Sbom-Location", "META-INF/sbom/" + sbomFileName);
});
});
}
private String getSbomExtension(CycloneDxTask task) {
String format = task.getOutputFormat().get();
if ("all".equals(format)) {
return ".json";
}
return "." + format;
}
}

View File

@@ -145,7 +145,8 @@ public class SpringBootPlugin implements Plugin<Project> {
project.getArtifacts());
List<PluginApplicationAction> actions = Arrays.asList(new JavaPluginAction(singlePublishedArtifact),
new WarPluginAction(singlePublishedArtifact), new DependencyManagementPluginAction(),
new ApplicationPluginAction(), new KotlinPluginAction(), new NativeImagePluginAction());
new ApplicationPluginAction(), new KotlinPluginAction(), new NativeImagePluginAction(),
new CycloneDxPluginAction());
for (PluginApplicationAction action : actions) {
withPluginClassOfAction(action,
(pluginClass) -> project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project)));

View File

@@ -72,6 +72,10 @@ public abstract class Packager {
private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index";
private static final String SBOM_LOCATION_ATTRIBUTE = "Sbom-Location";
private static final String SBOM_FORMAT_ATTRIBUTE = "Sbom-Format";
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
@@ -299,6 +303,7 @@ public abstract class Packager {
Manifest manifest = createInitialManifest(source);
addMainAndStartAttributes(source, manifest);
addBootAttributes(manifest.getMainAttributes());
addSbomAttributes(source, manifest.getMainAttributes());
return manifest;
}
@@ -408,6 +413,21 @@ public abstract class Packager {
}
}
private void addSbomAttributes(JarFile source, Attributes attributes) {
JarEntry sbomEntry = source.stream().filter(this::isCycloneDxBom).findAny().orElse(null);
if (sbomEntry != null) {
attributes.putValue(SBOM_LOCATION_ATTRIBUTE, sbomEntry.getName());
attributes.putValue(SBOM_FORMAT_ATTRIBUTE, "CycloneDX");
}
}
private boolean isCycloneDxBom(JarEntry entry) {
if (!entry.getName().startsWith("META-INF/sbom/")) {
return false;
}
return entry.getName().endsWith(".cdx.json") || entry.getName().endsWith("/bom.json");
}
private void putIfHasLength(Attributes attributes, String name, String value) {
if (StringUtils.hasLength(value)) {
attributes.putValue(name, value);

View File

@@ -656,6 +656,17 @@ abstract class AbstractPackagerTests<P extends Packager> {
.isEqualTo(String.join("\n", expected) + "\n");
}
@Test
void sbomManifestEntriesAreWritten() throws IOException {
this.testJarFile.addClass("com/example/Application.class", ClassWithMainMethod.class);
this.testJarFile.addFile("META-INF/sbom/application.cdx.json", new ByteArrayInputStream(new byte[0]));
P packager = createPackager(this.testJarFile.getFile());
execute(packager, NO_LIBRARIES);
assertThat(getPackagedManifest().getMainAttributes().getValue("Sbom-Format")).isEqualTo("CycloneDX");
assertThat(getPackagedManifest().getMainAttributes().getValue("Sbom-Location"))
.isEqualTo("META-INF/sbom/application.cdx.json");
}
private File createLibraryJar() throws IOException {
TestJarFile library = new TestJarFile(this.tempDir);
library.addClass("com/example/library/Library.class", ClassWithoutMainMethod.class);