From 670b059a79e702b45d3b5c6879e11ac912ef4a2e Mon Sep 17 00:00:00 2001 From: Joel Rudsberg Date: Thu, 10 Oct 2024 10:30:50 +0200 Subject: [PATCH] Add SBOM integration tests --- .../maven/SBOMFunctionalTest.groovy | 184 ++++++++++++++++++ .../maven/AbstractNativeImageMojo.java | 32 +-- .../maven/NativeCompileNoForkMojo.java | 9 +- .../buildtools/maven/sbom/SBOMGenerator.java | 3 +- .../utils/NativeImageConfigurationUtils.java | 7 +- .../AbstractGraalVMMavenFunctionalTest.groovy | 6 + samples/java-application/pom.xml | 36 ++++ 7 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/SBOMFunctionalTest.groovy diff --git a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/SBOMFunctionalTest.groovy b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/SBOMFunctionalTest.groovy new file mode 100644 index 000000000..8494cc8a3 --- /dev/null +++ b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/SBOMFunctionalTest.groovy @@ -0,0 +1,184 @@ +/* + * 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 + +import com.fasterxml.jackson.databind.node.ObjectNode +import org.graalvm.buildtools.maven.sbom.SBOMGenerator +import org.graalvm.buildtools.utils.NativeImageUtils +import spock.lang.Requires +import com.fasterxml.jackson.databind.ObjectMapper + +class SBOMFunctionalTest extends AbstractGraalVMMavenFunctionalTest { + private static boolean EE() { + NativeCompileNoForkMojo.isOracleGraalVM(null) + } + + private static boolean CE() { + !EE() + } + + private static boolean jdkVersionSupportsAugmentedSBOM() { + NativeImageUtils.getMajorJDKVersion(NativeCompileNoForkMojo.getVersionInformation(null)) >= SBOMGenerator.requiredNativeImageVersion + } + + private static boolean unsupportedJDKVersion() { + !jdkVersionSupportsAugmentedSBOM() + } + + private static boolean supportedAugmentedSBOMVersion() { + EE() && jdkVersionSupportsAugmentedSBOM() + } + + @Requires({ EE() }) + def "sbom is created when buildArg '--enable-sbom=export,embed' is used"() { + withSample 'java-application' + + when: + /* The 'native-sbom' profile sets the '--enable-sbom' argument. */ + mvn '-Pnative-sbom', '-DquickBuild', '-DskipTests', 'package', 'exec:exec@native' + + def sbom = file("target/example-app.sbom.json") + + then: + buildSucceeded + outputContainsPattern".*CycloneDX SBOM with \\d+ component\\(s\\) is embedded in binary \\(.*?\\) and exported as JSON \\(see build artifacts\\)\\." + outputDoesNotContain "Use '--enable-sbom' to assemble a Software Bill of Materials (SBOM)" + validateSbom sbom + !file(String.format("target/%s", SBOMGenerator.SBOM_FILENAME)).exists() + outputContains "Hello, native!" + } + + /** + * If user sets {@link NativeCompileNoForkMojo#AUGMENTED_SBOM_PARAM_NAME} to true then an SBOM should be generated + * with default SBOM arguments even if user did not explicitly specify '--enable-sbom' as a buildArg. + */ + @Requires({ supportedAugmentedSBOMVersion() }) + def "sbom is created when only the augmented sbom parameter is used (but not the '--enable-sbom' buildArg)"() { + withSample 'java-application' + + when: + mvn '-Pnative-augmentedSBOM-only', '-DquickBuild', '-DskipTests', 'package', 'exec:exec@native' + + def sbom = file("target/example-app.sbom.json") + + then: + buildSucceeded + outputContainsPattern".*CycloneDX SBOM with \\d+ component\\(s\\) is embedded in binary \\(.*?\\)." + outputDoesNotContain "Use '--enable-sbom' to assemble a Software Bill of Materials (SBOM)" + validateSbom sbom + !file(String.format("target/%s", SBOMGenerator.SBOM_FILENAME)).exists() + outputContains "Hello, native!" + } + + @Requires({ CE() }) + def "error is thrown when augmented sbom parameter is used with CE"() { + withSample 'java-application' + + when: + mvn '-Pnative-augmentedSBOM-only', '-DquickBuild', '-DskipTests', 'package' + + then: + buildFailed + } + + @Requires({ EE() && unsupportedJDKVersion() }) + def "error is thrown when augmented sbom parameter is used with EE but not with an unsupported JDK version"() { + withSample 'java-application' + + when: + mvn '-Pnative-augmentedSBOM-only', '-DquickBuild', '-DskipTests', 'package' + + then: + buildFailed + } + + /** + * Validates the SBOM produced from 'java-application'. + * @param sbom path to the SBOM. + * @return true if validation succeeded. + */ + private static boolean validateSbom(File sbom) { + try { + if (!sbom.exists()) { + println "SBOM not found: ${sbom}" + return false + } + + def mapper = new ObjectMapper() + def rootNode = mapper.readTree(sbom) + + // Check root fields + assert rootNode.has('bomFormat') + assert rootNode.get('bomFormat').asText() == 'CycloneDX' + assert rootNode.has('specVersion') + assert rootNode.has('serialNumber') + assert rootNode.has('version') + assert rootNode.has('metadata') + assert rootNode.has('components') + assert rootNode.has('dependencies') + + // Check metadata/component + def metadataComponent = rootNode.path('metadata').path('component') + assert metadataComponent.has('group') + assert metadataComponent.get('group').asText() == 'org.graalvm.buildtools.examples' + assert metadataComponent.has('name') + assert metadataComponent.get('name').asText() == 'maven' + + // Check that components and dependencies are non-empty + assert !rootNode.get('components').isEmpty() + assert !rootNode.get('dependencies').isEmpty() + + // Check that the main component has no dependencies + def mainComponentId = metadataComponent.get('bom-ref').asText() + def mainComponentDependency = rootNode.get('dependencies').find { it.get('ref').asText() == mainComponentId } as ObjectNode + assert mainComponentDependency.get('dependsOn').isEmpty() + + // Check that the main component is not found in "components" + assert !rootNode.get('components').any { it.get('bom-ref').asText() == mainComponentId } + + return true + } catch (AssertionError | Exception e) { + println "SBOM validation failed: ${e.message}" + return false + } + } +} diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java index 2640c3d17..c33775435 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java @@ -49,33 +49,19 @@ import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.toolchain.ToolchainManager; +import org.codehaus.plexus.logging.Logger; import org.graalvm.buildtools.maven.config.ExcludeConfigConfiguration; import org.graalvm.buildtools.utils.NativeImageConfigurationUtils; import org.graalvm.buildtools.utils.NativeImageUtils; import org.graalvm.buildtools.utils.SharedConstants; import javax.inject.Inject; -import java.io.File; -import java.io.InputStream; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.IOException; +import java.io.*; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.FileSystemAlreadyExistsException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.nio.file.*; +import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -89,7 +75,7 @@ public abstract class AbstractNativeImageMojo extends AbstractNativeMojo { protected static final String NATIVE_IMAGE_META_INF = "META-INF/native-image"; protected static final String NATIVE_IMAGE_PROPERTIES_FILENAME = "native-image.properties"; protected static final String NATIVE_IMAGE_DRY_RUN = "nativeDryRun"; - private String nativeImageVersionInformation = null; + private static String nativeImageVersionInformation = null; @Parameter(defaultValue = "${plugin}", readonly = true) // Maven 3 only protected PluginDescriptor plugin; @@ -450,11 +436,11 @@ protected void checkRequiredVersionIfNeeded() throws MojoExecutionException { if (requiredVersion == null) { return; } - NativeImageUtils.checkVersion(requiredVersion, getVersionInformation()); + NativeImageUtils.checkVersion(requiredVersion, getVersionInformation(logger)); } - protected boolean isOracleGraalVM() throws MojoExecutionException { - return getVersionInformation().contains(ORACLE_GRAALVM_IDENTIFIER); + static protected boolean isOracleGraalVM(Logger logger) throws MojoExecutionException { + return getVersionInformation(logger).contains(ORACLE_GRAALVM_IDENTIFIER); } /** @@ -462,7 +448,7 @@ protected boolean isOracleGraalVM() throws MojoExecutionException { * @return the output as a string joined by "\n". * @throws MojoExecutionException when any errors occurred. */ - protected String getVersionInformation() throws MojoExecutionException { + static protected String getVersionInformation(Logger logger) throws MojoExecutionException { if (nativeImageVersionInformation != null) { return nativeImageVersionInformation; } diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeCompileNoForkMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeCompileNoForkMojo.java index e988084f6..d91117345 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeCompileNoForkMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeCompileNoForkMojo.java @@ -85,9 +85,6 @@ public class NativeCompileNoForkMojo extends AbstractNativeImageMojo { private Boolean augmentedSBOM; public static final String AUGMENTED_SBOM_PARAM_NAME = "augmentedSBOM"; - @Parameter - private Boolean myBooleanOption; - private PluginParameterExpressionEvaluator evaluator; @Override @@ -141,7 +138,7 @@ private void generateAugmentedSBOMIfNeeded() throws IllegalArgumentException, Mo boolean optionWasSet = augmentedSBOM != null; augmentedSBOM = optionWasSet ? augmentedSBOM : true; - int detectedJDKVersion = NativeImageUtils.getMajorJDKVersion(getVersionInformation()); + int detectedJDKVersion = NativeImageUtils.getMajorJDKVersion(getVersionInformation(logger)); String sbomNativeImageFlag = "--enable-sbom"; boolean sbomEnabledForNativeImage = getBuildArgs().stream().anyMatch(v -> v.contains(sbomNativeImageFlag)); if (optionWasSet) { @@ -150,7 +147,7 @@ private void generateAugmentedSBOMIfNeeded() throws IllegalArgumentException, Mo return; } - if (!isOracleGraalVM()) { + if (!isOracleGraalVM(logger)) { throw new IllegalArgumentException( String.format("Configuration option %s is only supported in %s.", AUGMENTED_SBOM_PARAM_NAME, ORACLE_GRAALVM_IDENTIFIER)); } @@ -165,7 +162,7 @@ private void generateAugmentedSBOMIfNeeded() throws IllegalArgumentException, Mo /* Continue to generate augmented SBOM because parameter option explicitly set and all conditions are met. */ } else { - if (!isOracleGraalVM() || !sbomEnabledForNativeImage) { + if (!isOracleGraalVM(logger) || !sbomEnabledForNativeImage) { return; } diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/sbom/SBOMGenerator.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/sbom/SBOMGenerator.java index 0e1178875..28ad8cb81 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/sbom/SBOMGenerator.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/sbom/SBOMGenerator.java @@ -95,9 +95,10 @@ final public class SBOMGenerator { private static final String SBOM_FILE_FORMAT = "json"; private static final String SBOM_FILENAME_WITHOUT_EXTENSION = "base_sbom"; - private static final String SBOM_FILENAME = SBOM_FILENAME_WITHOUT_EXTENSION + "." + SBOM_FILE_FORMAT; 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. diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/utils/NativeImageConfigurationUtils.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/utils/NativeImageConfigurationUtils.java index 364e051c6..69b0c13bb 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/utils/NativeImageConfigurationUtils.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/utils/NativeImageConfigurationUtils.java @@ -93,8 +93,9 @@ public static Path getJavaHomeNativeImage(String javaHomeVariable, Boolean failF return null; } } - - logger.info("Found GraalVM installation from " + javaHomeVariable + " variable."); + if (logger != null) { + logger.info("Found GraalVM installation from " + javaHomeVariable + " variable."); + } return nativeImageExe; } @@ -119,7 +120,7 @@ public static Path getNativeImage(Logger logger) throws MojoExecutionException { if (nativeImage == null) { nativeImage = getNativeImageFromPath(); - if (nativeImage != null) { + if (nativeImage != null && logger != null) { logger.info("Found GraalVM installation from PATH variable."); } } diff --git a/native-maven-plugin/src/testFixtures/groovy/org/graalvm/buildtools/maven/AbstractGraalVMMavenFunctionalTest.groovy b/native-maven-plugin/src/testFixtures/groovy/org/graalvm/buildtools/maven/AbstractGraalVMMavenFunctionalTest.groovy index a02de1d93..9184bb000 100644 --- a/native-maven-plugin/src/testFixtures/groovy/org/graalvm/buildtools/maven/AbstractGraalVMMavenFunctionalTest.groovy +++ b/native-maven-plugin/src/testFixtures/groovy/org/graalvm/buildtools/maven/AbstractGraalVMMavenFunctionalTest.groovy @@ -213,6 +213,12 @@ abstract class AbstractGraalVMMavenFunctionalTest extends Specification { normalizeString(result.stdOut).contains(normalizeString(text)) } + boolean outputContainsPattern(String pattern) { + def normalizedOutput = normalizeString(result.stdOut) + def lines = normalizedOutput.split('\n') + return lines.any { line -> line.trim().matches(pattern) } + } + String after(String text) { def out = normalizeString(result.stdOut) out.substring(out.indexOf(normalizeString(text))) diff --git a/samples/java-application/pom.xml b/samples/java-application/pom.xml index 70a6d1d8f..243c1279d 100644 --- a/samples/java-application/pom.xml +++ b/samples/java-application/pom.xml @@ -55,6 +55,8 @@ 0.10.4-SNAPSHOT example-app org.graalvm.demo.Application + + --color=auto @@ -68,6 +70,15 @@ native + + + native-sbom + + --enable-sbom=embed,export + + + + native-augmentedSBOM-only @@ -88,6 +99,7 @@ false ${imageName} false + true @@ -150,6 +162,30 @@ ${project.artifactId} + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + true + + + build-native + + compile-no-fork + + package + + + + false + ${imageName} + false + + ${native.build.arg} + + + + org.apache.maven.plugins maven-surefire-plugin