org.graalvm.buildtools.maven.sbom.SBOMGenerator Maven / Gradle / Ivy
Show all versions of native-maven-plugin Show documentation
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
*
* Subject to the condition set forth below, permission is hereby granted to any
* person obtaining a copy of this software, associated documentation and/or
* data (collectively the "Software"), free of charge and under any and all
* copyright rights in the Software, and any and all patent rights owned or
* freely licensable by each licensor hereunder covering either (i) the
* unmodified Software as contributed to or provided by such licensor, or (ii)
* the Larger Works (as defined below), to deal in both
*
* (a) the Software, and
*
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
* one is included with the Software each a "Larger Work" to which the Software
* is contributed by such licensors),
*
* without restriction, including without limitation the rights to copy, create
* derivative works of, display, perform, and distribute the Software and make,
* use, sell, offer for sale, import, export, have made, and have sold the
* Software and the Larger Work(s), and to sublicense the foregoing rights on
* either these or other terms.
*
* This license is subject to the following condition:
*
* The above copyright notice and either this complete permission notice or at a
* minimum a reference to the UPL must be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.graalvm.buildtools.maven.sbom;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.BuildPluginManager;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.logging.Logger;
import org.eclipse.aether.RepositorySystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.graalvm.buildtools.maven.NativeCompileNoForkMojo.AUGMENTED_SBOM_PARAM_NAME;
import static org.graalvm.buildtools.utils.NativeImageUtils.ORACLE_GRAALVM_IDENTIFIER;
import static org.twdata.maven.mojoexecutor.MojoExecutor.*;
/**
* Generates a Software Bill of Materials (SBOM) that is augmented and refined by Native Image. This feature is only
* supported in Oracle GraalVM for JDK {@link SBOMGenerator#requiredNativeImageVersion} or later.
*
* Approach:
* 1. The cyclonedx-maven-plugin creates a baseline SBOM.
* 2. The components of the baseline SBOM (referred to as the "base" SBOM) are updated with additional metadata,
* most importantly being the set of package names associated with the component (see {@link AddedComponentFields}
* for all additional metadata).
* 3. The SBOM is stored at a known location.
* 4. Native Image processes the SBOM and removes unreachable components and unnecessary dependencies.
*
* Creating the package-name-to-component mapping in the context of Native Image, without the knowledge known at the
* plugin build-time is difficult, which was the primary motivation for realizing this approach.
*
* Benefits:
* * Great Baseline: Produces an industry-standard SBOM at minimum.
* * Enhanced Accuracy: Native Image augments and refines the SBOM, potentially significantly improving its accuracy.
*/
final public class SBOMGenerator {
public static final int requiredNativeImageVersion = 24;
private final MavenProject mavenProject;
private final MavenSession mavenSession;
private final BuildPluginManager pluginManager;
private final RepositorySystem repositorySystem;
private final String mainClass;
private final Logger logger;
private static final String SBOM_FILE_FORMAT = "json";
private static final String SBOM_FILENAME_WITHOUT_EXTENSION = "base_sbom";
private final String outputDirectory;
public static final String SBOM_FILENAME = SBOM_FILENAME_WITHOUT_EXTENSION + "." + SBOM_FILE_FORMAT;
private static final class AddedComponentFields {
/**
* The package names associated with this component.
*/
static final String packageNames = "packageNames";
/**
* The path to the jar containing the class files. For a component embedded in a shaded jar, the path must
* be pointing to the shaded jar.
*/
static final String jarPath = "jarPath";
/**
* If set to false, then this component and all its transitive dependencies SHOULD NOT be pruned by Native Image.
* This is set to false when the package names could not be derived accurately.
*/
static final String prunable = "prunable";
}
/**
* The external plugin used to generate the baseline SBOM.
*/
private static final class Plugin {
static final String artifactId = "cyclonedx-maven-plugin";
static final String groupId = "org.cyclonedx";
static final String version = "2.8.1";
static final String goal = "makeAggregateBom";
private static final class Configuration {
static final String outputFormat = SBOM_FILE_FORMAT;
static final String outputName = SBOM_FILENAME_WITHOUT_EXTENSION;
static final String skipNotDeployed = "false";
static final String schemaVersion = "1.5";
}
}
public SBOMGenerator(
MavenProject mavenProject,
MavenSession mavenSession,
BuildPluginManager pluginManager,
RepositorySystem repositorySystem,
String mainClass,
Logger logger) {
this.mavenProject = mavenProject;
this.mavenSession = mavenSession;
this.pluginManager = pluginManager;
this.repositorySystem = repositorySystem;
this.mainClass = mainClass;
this.logger = logger;
this.outputDirectory = mavenProject.getBuild().getDirectory();
}
/**
* Checks if the JDK version supports augmented SBOMs.
*
* @param detectedJdkVersion the JDK version used.
* @param throwErrorIfNotSupported if true, then an error is thrown if the check failed.
* @return true if the JDK version supports the flag, otherwise false (if throwErrorIfNotSupported is false).
* @throws IllegalArgumentException when throwErrorIfNotSupported is true and the version check failed.
*/
public static boolean checkAugmentedSBOMSupportedByJDKVersion(int detectedJdkVersion, boolean throwErrorIfNotSupported) throws IllegalArgumentException {
if (detectedJdkVersion < SBOMGenerator.requiredNativeImageVersion) {
if (throwErrorIfNotSupported) {
throw new IllegalArgumentException(
String.format("%s version %s is required to use configuration option %s but major JDK version %s has been detected.",
ORACLE_GRAALVM_IDENTIFIER, SBOMGenerator.requiredNativeImageVersion, AUGMENTED_SBOM_PARAM_NAME, detectedJdkVersion));
}
return false;
}
return true;
}
/**
* Generates an SBOM that will be further augmented by Native Image. The SBOM is stored in the build directory.
*
* @throws MojoExecutionException if SBOM creation fails.
*/
public void generate() throws MojoExecutionException {
Path sbomPath = Paths.get(outputDirectory, SBOM_FILENAME);
try {
/* Suppress the output from the plugin. */
int loggingLevel = logger.getThreshold();
logger.setThreshold(Logger.LEVEL_DISABLED);
executeMojo(
plugin(
groupId(Plugin.groupId),
artifactId(Plugin.artifactId),
version(Plugin.version)
),
goal(Plugin.goal),
configuration(
element(name("outputFormat"), Plugin.Configuration.outputFormat),
element(name("outputName"), Plugin.Configuration.outputName),
element(name("outputDirectory"), outputDirectory),
element(name("skipNotDeployed"), Plugin.Configuration.skipNotDeployed),
element(name("schemaVersion"), Plugin.Configuration.schemaVersion)
),
executionEnvironment(mavenProject, mavenSession, pluginManager)
);
logger.setThreshold(loggingLevel);
if (!Files.exists(sbomPath)) {
return;
}
var resolver = new ArtifactToPackageNameResolver(mavenProject, repositorySystem, mavenSession.getRepositorySession(), mainClass);
Set artifacts = resolver.getArtifactAdapters();
augmentSBOM(sbomPath, artifacts);
} catch (Exception exception) {
deleteFileIfExists(sbomPath);
String errorMsg = String.format("Failed to create SBOM. Please try again and report this issue if it persists. " +
"To bypass this failure, disable SBOM generation by setting configuration option %s to false.", AUGMENTED_SBOM_PARAM_NAME);
throw new MojoExecutionException(errorMsg, exception);
}
}
private static void deleteFileIfExists(Path sbomPath) {
try {
Files.deleteIfExists(sbomPath);
} catch (IOException e) {
/* Failed to delete file. */
}
}
/**
* Augments the base SBOM with information from the derived {@param artifacts}.
*
* @param baseSBOMPath path to the base SBOM generated by the cyclonedx plugin.
* @param artifacts artifacts that possibly have been extended with package name data.
*/
private void augmentSBOM(Path baseSBOMPath, Set artifacts) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode sbomJson = (ObjectNode) objectMapper.readTree(Files.newInputStream(baseSBOMPath));
ArrayNode componentsArray = (ArrayNode) sbomJson.get("components");
if (componentsArray == null) {
throw new RuntimeException(String.format("SBOM generated by %s:%s contained no components.", Plugin.groupId, Plugin.artifactId));
}
/* Augment the "components" */
componentsArray.forEach(componentNode -> augmentComponentNode(componentNode, artifacts, objectMapper));
/* Augment the main component in "metadata/component" */
JsonNode metadataNode = sbomJson.get("metadata");
if (metadataNode != null && metadataNode.has("component")) {
augmentComponentNode(metadataNode.get("component"), artifacts, objectMapper);
}
/* Save the augmented SBOM back to the file */
objectMapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(baseSBOMPath), sbomJson);
}
/**
* Updates the {@param componentNode} with {@link AddedComponentFields} from the artifact in {@param artifactsWithPackageNames}
* with matching GAV coordinates.
*
* @param componentNode the node in the base SBOM that should be augmented.
* @param artifactsWithPackageNames the artifact with information for {@link AddedComponentFields}.
* @param objectMapper the objectMapper that is used to write the updates.
*/
private void augmentComponentNode(JsonNode componentNode, Set artifactsWithPackageNames, ObjectMapper objectMapper) {
String groupField = "group";
String nameField = "name";
String versionField = "version";
if (componentNode.has(groupField) && componentNode.has(nameField) && componentNode.has(versionField)) {
String groupId = componentNode.get(groupField).asText();
String artifactId = componentNode.get(nameField).asText();
String version = componentNode.get(versionField).asText();
Optional optionalArtifact = artifactsWithPackageNames.stream()
.filter(artifact -> artifact.groupId.equals(groupId)
&& artifact.artifactId.equals(artifactId)
&& artifact.version.equals(version))
.findFirst();
if (optionalArtifact.isPresent()) {
ArtifactAdapter artifact = optionalArtifact.get();
ArrayNode packageNamesArray = objectMapper.createArrayNode();
List sortedPackageNames = artifact.packageNames.stream().sorted().collect(Collectors.toList());
sortedPackageNames.forEach(packageNamesArray::add);
((ObjectNode) componentNode).set(AddedComponentFields.packageNames, packageNamesArray);
String jarPath = "";
if (artifact.jarPath != null) {
jarPath = artifact.jarPath.toString();
}
((ObjectNode) componentNode).put(AddedComponentFields.jarPath, jarPath);
((ObjectNode) componentNode).put(AddedComponentFields.prunable, artifact.prunable);
}
}
}
}