diff --git a/bom/application/pom.xml b/bom/application/pom.xml index ca9e364917f2a..5b6af78e7d60e 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -18,6 +18,7 @@ 1.78.1 1.0.2.5 1.0.19 + 9.0.5 5.0.0 3.0.2 3.2.2 @@ -708,6 +709,21 @@ quarkus-config-yaml-deployment ${project.version} + + io.quarkus + quarkus-cyclonedx + ${project.version} + + + io.quarkus + quarkus-cyclonedx-deployment + ${project.version} + + + io.quarkus + quarkus-cyclonedx-generator + ${project.version} + io.quarkus quarkus-datasource-common @@ -5017,6 +5033,12 @@ ${wildfly-common.version} + + org.cyclonedx + cyclonedx-core-java + ${cyclonedx.version} + + org.wildfly.openssl wildfly-openssl-java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java index 028d481c2d91b..ef13a5eeb0ea8 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java @@ -11,9 +11,11 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Supplier; import org.jboss.logging.Logger; +import io.quarkus.bootstrap.app.DependencyInfoProvider; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.builder.BuildChain; @@ -55,6 +57,7 @@ public class QuarkusAugmentor { private final Properties buildSystemProperties; private final Path targetDir; private final ApplicationModel effectiveModel; + private final Supplier depInfoProvider; private final String baseName; private final String originalBaseName; private final boolean rebuild; @@ -82,6 +85,7 @@ public class QuarkusAugmentor { this.auxiliaryApplication = builder.auxiliaryApplication; this.auxiliaryDevModeType = Optional.ofNullable(builder.auxiliaryDevModeType); this.test = builder.test; + this.depInfoProvider = builder.depInfoProvider; } public BuildResult run() throws Exception { @@ -152,7 +156,7 @@ public BuildResult run() throws Exception { auxiliaryDevModeType, test)) .produce(new BuildSystemTargetBuildItem(targetDir, baseName, originalBaseName, rebuild, buildSystemProperties == null ? new Properties() : buildSystemProperties)) - .produce(new AppModelProviderBuildItem(effectiveModel)); + .produce(new AppModelProviderBuildItem(effectiveModel, depInfoProvider)); for (PathCollection i : additionalApplicationArchives) { execBuilder.produce(new AdditionalApplicationArchiveBuildItem(i)); } @@ -214,6 +218,7 @@ public static final class Builder { DevModeType devModeType; boolean test; boolean auxiliaryApplication; + private Supplier depInfoProvider; public Builder addBuildChainCustomizer(Consumer customizer) { this.buildChainCustomizers.add(customizer); @@ -353,5 +358,10 @@ public Builder setDeploymentClassLoader(ClassLoader deploymentClassLoader) { this.deploymentClassLoader = deploymentClassLoader; return this; } + + public Builder setDependencyInfoProvider(Supplier depInfoProvider) { + this.depInfoProvider = depInfoProvider; + return this; + } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java index 19e8d697a8ef4..aff617365d12e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java @@ -1,7 +1,11 @@ package io.quarkus.deployment.builditem; +import java.util.Objects; +import java.util.function.Supplier; + import org.jboss.logging.Logger; +import io.quarkus.bootstrap.app.DependencyInfoProvider; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.PlatformImports; import io.quarkus.builder.item.SimpleBuildItem; @@ -12,9 +16,15 @@ public final class AppModelProviderBuildItem extends SimpleBuildItem { private static final Logger log = Logger.getLogger(AppModelProviderBuildItem.class); private final ApplicationModel appModel; + private final Supplier depInfoProvider; public AppModelProviderBuildItem(ApplicationModel appModel) { - this.appModel = appModel; + this(appModel, null); + } + + public AppModelProviderBuildItem(ApplicationModel appModel, Supplier depInfoProvider) { + this.appModel = Objects.requireNonNull(appModel); + this.depInfoProvider = depInfoProvider; } public ApplicationModel validateAndGet(BootstrapConfig config) { @@ -34,4 +44,8 @@ public ApplicationModel validateAndGet(BootstrapConfig config) { } return appModel; } + + public Supplier getDependencyInfoProvider() { + return depInfoProvider; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java index a96468ab8b83a..1c1b60fafc88b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java @@ -4,6 +4,7 @@ import java.util.Map; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.sbom.ApplicationManifestConfig; /** * Represents a runnable artifact, such as an uberjar or thin jar. @@ -17,11 +18,18 @@ public final class ArtifactResultBuildItem extends MultiBuildItem { private final Path path; private final String type; private final Map metadata; + private final ApplicationManifestConfig manifestConfig; public ArtifactResultBuildItem(Path path, String type, Map metadata) { + this(path, type, metadata, null); + } + + public ArtifactResultBuildItem(Path path, String type, Map metadata, + ApplicationManifestConfig manifestConfig) { this.path = path; this.type = type; this.metadata = metadata; + this.manifestConfig = manifestConfig; } public Path getPath() { @@ -32,6 +40,10 @@ public String getType() { return type; } + public ApplicationManifestConfig getManifestConfig() { + return manifestConfig; + } + public Map getMetadata() { return metadata; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java index 8eb4a30d84d43..ba5a0e5601bb3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java @@ -3,10 +3,13 @@ import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.*; import java.nio.file.Path; +import java.util.Collection; import io.quarkus.bootstrap.app.JarResult; +import io.quarkus.bootstrap.app.SbomResult; import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.sbom.ApplicationManifestConfig; public final class JarBuildItem extends SimpleBuildItem { @@ -15,14 +18,21 @@ public final class JarBuildItem extends SimpleBuildItem { private final Path libraryDir; private final PackageConfig.JarConfig.JarType type; private final String classifier; + private final ApplicationManifestConfig manifestConfig; public JarBuildItem(Path path, Path originalArtifact, Path libraryDir, PackageConfig.JarConfig.JarType type, String classifier) { + this(path, originalArtifact, libraryDir, type, classifier, null); + } + + public JarBuildItem(Path path, Path originalArtifact, Path libraryDir, PackageConfig.JarConfig.JarType type, + String classifier, ApplicationManifestConfig manifestConfig) { this.path = path; this.originalArtifact = originalArtifact; this.libraryDir = libraryDir; this.type = type; this.classifier = classifier; + this.manifestConfig = manifestConfig; } public boolean isUberJar() { @@ -49,8 +59,16 @@ public String getClassifier() { return classifier; } + public ApplicationManifestConfig getManifestConfig() { + return manifestConfig; + } + public JarResult toJarResult() { + return toJarResult(null); + } + + public JarResult toJarResult(Collection sboms) { return new JarResult(path, originalArtifact, libraryDir, type == MUTABLE_JAR, - classifier); + classifier, sboms); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index 7791e149c73b8..3323c0824516a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -1,7 +1,9 @@ package io.quarkus.deployment.pkg.steps; import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; -import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.*; +import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.LEGACY_JAR; +import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.MUTABLE_JAR; +import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.UBER_JAR; import java.io.BufferedInputStream; import java.io.BufferedWriter; @@ -86,8 +88,11 @@ import io.quarkus.maven.dependency.DependencyFlags; import io.quarkus.maven.dependency.GACT; import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; import io.quarkus.paths.PathVisit; import io.quarkus.paths.PathVisitor; +import io.quarkus.sbom.ApplicationComponent; +import io.quarkus.sbom.ApplicationManifestConfig; import io.quarkus.utilities.JavaBinFinder; /** @@ -175,12 +180,10 @@ OutputTargetBuildItem outputTarget(BuildSystemTargetBuildItem bst, PackageConfig @BuildStep(onlyIf = JarRequired.class) ArtifactResultBuildItem jarOutput(JarBuildItem jarBuildItem) { - if (jarBuildItem.getLibraryDir() != null) { - return new ArtifactResultBuildItem(jarBuildItem.getPath(), "jar", - Collections.singletonMap("library-dir", jarBuildItem.getLibraryDir().toString())); - } else { - return new ArtifactResultBuildItem(jarBuildItem.getPath(), "jar", Collections.emptyMap()); - } + return new ArtifactResultBuildItem(jarBuildItem.getPath(), "jar", + jarBuildItem.getLibraryDir() == null ? Map.of() + : Map.of("library-dir", jarBuildItem.getLibraryDir().toString()), + jarBuildItem.getManifestConfig()); } @SuppressWarnings("deprecation") // JarType#LEGACY_JAR @@ -310,8 +313,31 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, .resolve(outputTargetBuildItem.getOriginalBaseName() + DOT_JAR); final Path originalJar = Files.exists(standardJar) ? standardJar : null; + ResolvedDependency appArtifact = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); + final String classifier = suffixToClassifier(packageConfig.computedRunnerSuffix()); + if (classifier != null && !classifier.isEmpty()) { + appArtifact = ResolvedDependencyBuilder.newInstance() + .setGroupId(appArtifact.getGroupId()) + .setArtifactId(appArtifact.getArtifactId()) + .setClassifier(classifier) + .setType(appArtifact.getType()) + .setVersion(appArtifact.getVersion()) + .setResolvedPaths(appArtifact.getResolvedPaths()) + .addDependencies(appArtifact.getDependencies()) + .setWorkspaceModule(appArtifact.getWorkspaceModule()) + .setFlags(appArtifact.getFlags()) + .build(); + } + final ApplicationManifestConfig manifestConfig = ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setMainComponent(ApplicationComponent.builder() + .setPath(runnerJar) + .setResolvedDependency(appArtifact) + .build()) + .setRunnerPath(runnerJar) + .build(); return new JarBuildItem(runnerJar, originalJar, null, UBER_JAR, - suffixToClassifier(packageConfig.computedRunnerSuffix())); + suffixToClassifier(packageConfig.computedRunnerSuffix()), manifestConfig); } private String suffixToClassifier(String suffix) { @@ -356,16 +382,14 @@ public boolean test(String path) { } }; - final Collection appDeps = curateOutcomeBuildItem.getApplicationModel() - .getRuntimeDependencies(); - ResolvedDependency appArtifact = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); + // the manifest needs to be the first entry in the jar, otherwise JarInputStream does not work properly // see https://bugs.openjdk.java.net/browse/JDK-8031748 generateManifest(runnerZipFs, "", packageConfig, appArtifact, mainClassBuildItem.getClassName(), applicationInfo); - for (ResolvedDependency appDep : appDeps) { + for (ResolvedDependency appDep : curateOutcomeBuildItem.getApplicationModel().getRuntimeDependencies()) { // Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852) // and are not part of the optional dependencies to include @@ -570,6 +594,9 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, buildDir = outputTargetBuildItem.getOutputDirectory().resolve(DEFAULT_FAST_JAR_DIRECTORY_NAME); } + final ApplicationManifestConfig.Builder manifestConfig = ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setDistributionDirectory(buildDir); //unmodified 3rd party dependencies Path libDir = buildDir.resolve(LIB); Path mainLib = libDir.resolve(MAIN); @@ -680,7 +707,6 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, if (!rebuild) { Predicate ignoredEntriesPredicate = getThinJarIgnoredEntriesPredicate(packageConfig); - try (FileSystem runnerZipFs = createNewZip(runnerJar, packageConfig)) { copyFiles(applicationArchivesBuildItem.getRootArchive(), runnerZipFs, null, ignoredEntriesPredicate); } @@ -693,7 +719,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, if (!rebuild) { copyDependency(parentFirstKeys, outputTargetBuildItem, copiedArtifacts, mainLib, baseLib, fastJarJarsBuilder::addDep, true, - classPath, appDep, transformedClasses, removed, packageConfig); + classPath, appDep, transformedClasses, removed, packageConfig, manifestConfig); } else if (includeAppDep(appDep, outputTargetBuildItem.getIncludedOptionalDependencies(), removed)) { appDep.getResolvedPaths().forEach(fastJarJarsBuilder::addDep); } @@ -750,6 +776,8 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, runnerJar.toFile().setReadable(true, false); Path initJar = buildDir.resolve(QUARKUS_RUN_JAR); + manifestConfig.setMainComponent(ApplicationComponent.builder().setPath(initJar)) + .setRunnerPath(initJar); boolean mutableJar = packageConfig.jar().type() == MUTABLE_JAR; if (mutableJar) { //we output the properties in a reproducible manner, so we remove the date comment @@ -761,6 +789,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, List lines = Arrays.stream(out.toString(StandardCharsets.UTF_8).split("\n")) .filter(s -> !s.startsWith("#")).sorted().collect(Collectors.toList()); Path buildSystemProps = quarkus.resolve(BUILD_SYSTEM_PROPERTIES); + manifestConfig.addComponent(ApplicationComponent.builder().setPath(buildSystemProps).setDevelopmentScope()); try (OutputStream fileOutput = Files.newOutputStream(buildSystemProps)) { fileOutput.write(String.join("\n", lines).getBytes(StandardCharsets.UTF_8)); } @@ -778,10 +807,9 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, Path deploymentLib = libDir.resolve(DEPLOYMENT_LIB); Files.createDirectories(deploymentLib); for (ResolvedDependency appDep : curateOutcomeBuildItem.getApplicationModel().getDependencies()) { - copyDependency(parentFirstKeys, outputTargetBuildItem, copiedArtifacts, deploymentLib, baseLib, (p) -> { - }, - false, classPath, - appDep, new TransformedClassesBuildItem(Map.of()), removed, packageConfig); //we don't care about transformation here, so just pass in an empty item + copyDependency(parentFirstKeys, outputTargetBuildItem, copiedArtifacts, deploymentLib, baseLib, p -> { + }, false, classPath, appDep, new TransformedClassesBuildItem(Map.of()), removed, packageConfig, + manifestConfig); //we don't care about transformation here, so just pass in an empty item } Map> relativePaths = new HashMap<>(); for (Map.Entry> e : copiedArtifacts.entrySet()) { @@ -797,6 +825,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, curateOutcomeBuildItem.getApplicationModel(), packageConfig.jar().userProvidersDirectory().orElse(null), buildDir.relativize(runnerJar).toString()); Path appmodelDat = deploymentLib.resolve(APPMODEL_DAT); + manifestConfig.addComponent(ApplicationComponent.builder().setPath(appmodelDat).setDevelopmentScope()); try (OutputStream out = Files.newOutputStream(appmodelDat)) { ObjectOutputStream obj = new ObjectOutputStream(out); obj.writeObject(model); @@ -807,6 +836,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, //as we don't really have a resolved bootstrap CP //once we have the app model it will all be done in QuarkusClassLoader anyway Path deploymentCp = deploymentLib.resolve(DEPLOYMENT_CLASS_PATH_DAT); + manifestConfig.addComponent(ApplicationComponent.builder().setPath(deploymentCp).setDevelopmentScope()); try (OutputStream out = Files.newOutputStream(deploymentCp)) { ObjectOutputStream obj = new ObjectOutputStream(out); List paths = new ArrayList<>(); @@ -842,7 +872,7 @@ public void accept(Path path) { } }); } - return new JarBuildItem(initJar, null, libDir, packageConfig.jar().type(), null); + return new JarBuildItem(initJar, null, libDir, packageConfig.jar().type(), null, manifestConfig.build()); } /** @@ -883,7 +913,7 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB Map> runtimeArtifacts, Path libDir, Path baseLib, Consumer targetPathConsumer, boolean allowParentFirst, StringBuilder classPath, ResolvedDependency appDep, TransformedClassesBuildItem transformedClasses, Set removedDeps, - PackageConfig packageConfig) + PackageConfig packageConfig, ApplicationManifestConfig.Builder manifestConfig) throws IOException { // Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852) @@ -923,6 +953,9 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB } } } + var appComponent = ApplicationComponent.builder() + .setPath(targetPath) + .setResolvedDependency(appDep); if (removedFromThisArchive.isEmpty()) { Files.copy(resolvedDep, targetPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); @@ -930,7 +963,16 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB // we copy jars for which we remove entries to the same directory // which seems a bit odd to me filterJarFile(resolvedDep, targetPath, removedFromThisArchive); + + var list = new ArrayList<>(removedFromThisArchive); + Collections.sort(list); + var sb = new StringBuilder("Removed ").append(list.get(0)); + for (int i = 1; i < list.size(); ++i) { + sb.append(",").append(list.get(i)); + } + appComponent.setPedigree(sb.toString()); } + manifestConfig.addComponent(appComponent); } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 64ad00259e468..cb844cd4e5887 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -51,6 +51,8 @@ import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.runtime.LocalesBuildTimeConfig; import io.quarkus.runtime.graal.DisableLoggingFeature; +import io.quarkus.sbom.ApplicationComponent; +import io.quarkus.sbom.ApplicationManifestConfig; public class NativeImageBuildStep { @@ -86,10 +88,16 @@ void nativeImageFeatures(BuildProducer features) { } @BuildStep(onlyIf = NativeBuild.class) - ArtifactResultBuildItem result(NativeImageBuildItem image) { + ArtifactResultBuildItem result(NativeImageBuildItem image, + CurateOutcomeBuildItem curateOutcomeBuildItem) { NativeImageBuildItem.GraalVMVersion graalVMVersion = image.getGraalVMInfo(); return new ArtifactResultBuildItem(image.getPath(), "native", - graalVMVersion.toMap()); + graalVMVersion.toMap(), + ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setMainComponent(ApplicationComponent.builder().setPath(image.getPath())) + .setRunnerPath(image.getPath()) + .build()); } @BuildStep(onlyIf = NativeSourcesBuild.class) @@ -106,7 +114,8 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, List jpmsExportBuildItems, List nativeImageSecurityProviders, List nativeImageFeatures, - NativeImageRunnerBuildItem nativeImageRunner) { + NativeImageRunnerBuildItem nativeImageRunner, + CurateOutcomeBuildItem curateOutcomeBuildItem) { Path outputDir; try { @@ -159,7 +168,14 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, return new ArtifactResultBuildItem(nativeImageSourceJarBuildItem.getPath(), "native-sources", - Collections.emptyMap()); + Collections.emptyMap(), + ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setMainComponent(ApplicationComponent.builder() + .setPath(nativeImageSourceJarBuildItem.getPath()) + .setResolvedDependency(curateOutcomeBuildItem.getApplicationModel().getAppArtifact())) + .setRunnerPath(nativeImageSourceJarBuildItem.getPath()) + .build()); } @BuildStep diff --git a/core/deployment/src/main/java/io/quarkus/deployment/sbom/SbomBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/sbom/SbomBuildItem.java new file mode 100644 index 0000000000000..03a6136ab5ecf --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/sbom/SbomBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.deployment.sbom; + +import java.util.Objects; + +import io.quarkus.bootstrap.app.SbomResult; +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Aggregates SBOMs generated for packaged applications. + * The API around this is still in development and will likely change in the near future. + */ +public final class SbomBuildItem extends MultiBuildItem { + + private final SbomResult result; + + public SbomBuildItem(SbomResult result) { + this.result = Objects.requireNonNull(result); + } + + public SbomResult getResult() { + return result; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index 9512528a725cf..9303b87ebcddd 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -7,8 +7,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; @@ -29,6 +31,7 @@ import io.quarkus.bootstrap.app.ClassChangeInformation; import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.app.SbomResult; import io.quarkus.bootstrap.classloading.ClassLoaderEventListener; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.util.PropertyUtils; @@ -48,6 +51,7 @@ import io.quarkus.deployment.pkg.builditem.DeploymentResultBuildItem; import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; +import io.quarkus.deployment.sbom.SbomBuildItem; import io.quarkus.dev.spi.DevModeType; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.QuarkusConfigFactory; @@ -172,7 +176,7 @@ public AugmentResult createProductionApplication() { } try (QuarkusClassLoader classLoader = curatedApplication.createDeploymentClassLoader()) { BuildResult result = runAugment(true, Collections.emptySet(), null, classLoader, ArtifactResultBuildItem.class, - DeploymentResultBuildItem.class); + DeploymentResultBuildItem.class, SbomBuildItem.class); writeDebugSourceFile(result); @@ -180,6 +184,7 @@ public AugmentResult createProductionApplication() { NativeImageBuildItem nativeImageBuildItem = result.consumeOptional(NativeImageBuildItem.class); List artifactResultBuildItems = result.consumeMulti(ArtifactResultBuildItem.class); BuildSystemTargetBuildItem buildSystemTargetBuildItem = result.consume(BuildSystemTargetBuildItem.class); + Map> sboms = getSboms(result.consumeMulti(SbomBuildItem.class)); // this depends on the fact that the order in which we can obtain MultiBuildItems is the same as they are produced // we want to write result of the final artifact created @@ -192,12 +197,25 @@ public AugmentResult createProductionApplication() { return new AugmentResult(artifactResultBuildItems.stream() .map(a -> new ArtifactResult(a.getPath(), a.getType(), a.getMetadata())) .collect(Collectors.toList()), - jarBuildItem != null ? jarBuildItem.toJarResult() : null, + jarBuildItem != null ? jarBuildItem.toJarResult(sboms.getOrDefault(jarBuildItem.getPath(), List.of())) + : null, nativeImageBuildItem != null ? nativeImageBuildItem.getPath() : null, - nativeImageBuildItem != null ? nativeImageBuildItem.getGraalVMInfo().toMap() : Collections.emptyMap()); + nativeImageBuildItem != null ? nativeImageBuildItem.getGraalVMInfo().toMap() : Map.of()); } } + private Map> getSboms(List sbomBuildItems) { + if (sbomBuildItems.isEmpty()) { + return Map.of(); + } + final Map> result = new HashMap<>(); + for (var sbomBuildItem : sbomBuildItems) { + result.computeIfAbsent(sbomBuildItem.getResult().getApplicationRunner(), p -> new ArrayList<>()) + .add(sbomBuildItem.getResult()); + } + return result; + } + private void writeDebugSourceFile(BuildResult result) { String debugSourcesDir = BootstrapDebug.DEBUG_SOURCES_DIR; if (debugSourcesDir != null) { @@ -287,7 +305,8 @@ private BuildResult runAugment(boolean firstRun, Set changedResources, .setTargetDir(quarkusBootstrap.getTargetDirectory()) .setDeploymentClassLoader(deploymentClassLoader) .setBuildSystemProperties(quarkusBootstrap.getBuildSystemProperties()) - .setEffectiveModel(curatedApplication.getApplicationModel()); + .setEffectiveModel(curatedApplication.getApplicationModel()) + .setDependencyInfoProvider(quarkusBootstrap.getDependencyInfoProvider()); if (quarkusBootstrap.getBaseName() != null) { builder.setBaseName(quarkusBootstrap.getBaseName()); } diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 30b24d7e5c1dc..cf312d073a5ad 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -447,6 +447,19 @@ + + io.quarkus + quarkus-cyclonedx + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-datasource diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/worker/QuarkusWorker.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/worker/QuarkusWorker.java index f366d7977f9cf..099de675493e8 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/worker/QuarkusWorker.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/worker/QuarkusWorker.java @@ -32,6 +32,7 @@ CuratedApplication createAppCreationContext() throws BootstrapException { .setAppArtifact(appModel.getAppArtifact()) .setLocalProjectDiscovery(false) .setIsolateDeployment(true) + .setDependencyInfoProvider(() -> null) .build().bootstrap(); } } diff --git a/devtools/maven/pom.xml b/devtools/maven/pom.xml index b9a94d09390ff..8c6fee44610b4 100644 --- a/devtools/maven/pom.xml +++ b/devtools/maven/pom.xml @@ -73,6 +73,10 @@ io.quarkus quarkus-analytics-common + + io.quarkus + quarkus-cyclonedx-generator + org.apache.maven maven-plugin-api diff --git a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java index 4018d44696c62..257cd1a0fd559 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java @@ -73,6 +73,12 @@ public class BuildMojo extends QuarkusBootstrapMojo { @Parameter(property = "attachRunnerAsMainArtifact", required = false) boolean attachRunnerAsMainArtifact; + /** + * Whether to attach SBOMs generated for Uber JARs as project artifacts + */ + @Parameter(property = "attachSboms") + boolean attachSboms = true; + @Parameter(defaultValue = "${project.build.directory}", readonly = true) File buildDirectory; @@ -166,6 +172,12 @@ protected void doExecute() throws MojoExecutionException { result.getJar().getClassifier()); } } + if (attachSboms && result.getJar().isUberJar() && !result.getJar().getSboms().isEmpty()) { + for (var sbom : result.getJar().getSboms()) { + projectHelper.attachArtifact(mavenProject(), sbom.getFormat(), sbom.getClassifier(), + sbom.getSbomFile().toFile()); + } + } } } finally { // Clear all the system properties set by the plugin diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DependencySbomMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DependencySbomMojo.java new file mode 100644 index 0000000000000..008d372d81ce5 --- /dev/null +++ b/devtools/maven/src/main/java/io/quarkus/maven/DependencySbomMojo.java @@ -0,0 +1,170 @@ +package io.quarkus.maven; + +import java.io.File; +import java.nio.file.*; +import java.util.List; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.eclipse.aether.repository.RemoteRepository; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; +import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; +import io.quarkus.cyclonedx.generator.CycloneDxSbomGenerator; +import io.quarkus.maven.components.QuarkusWorkspaceProvider; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.sbom.ApplicationManifest; +import io.quarkus.sbom.ApplicationManifestConfig; + +/** + * Quarkus application SBOM generator + */ +@Mojo(name = "dependency-sbom", defaultPhase = LifecyclePhase.NONE, requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true) +public class DependencySbomMojo extends AbstractMojo { + + @Component + QuarkusWorkspaceProvider workspaceProvider; + + @Parameter(defaultValue = "${project}", readonly = true, required = true) + MavenProject project; + + @Parameter(defaultValue = "${session}", readonly = true) + MavenSession session; + + @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true) + List repos; + + /** + * Whether to skip the execution of the goal + */ + @Parameter(defaultValue = "false", property = "quarkus.dependency.sbom.skip") + boolean skip = false; + + /** + * Target launch mode corresponding to {@link io.quarkus.runtime.LaunchMode} for which the SBOM should be built. + * {@code io.quarkus.runtime.LaunchMode.NORMAL} is the default. + */ + @Parameter(property = "quarkus.dependency.sbom.mode", defaultValue = "prod") + String mode; + + /** + * CycloneDX BOM format. Allowed values are {@code json} and {@code xml}. The default is {@code json}. + */ + @Parameter(property = "quarkus.dependency.sbom.format", defaultValue = "json") + String format; + + /** + * File to store the SBOM in. If not configured, the SBOM will be stored in the ${project.build.directory} directory. + */ + @Parameter(property = "quarkus.dependency.sbom.output-file") + File outputFile; + + /** + * Whether to include license text in the generated SBOM. The default is {@code false} + */ + @Parameter(property = "quarkus.dependency.sbom.include-license-text", defaultValue = "false") + boolean includeLicenseText; + + /** + * CycloneDX BOM schema version + */ + @Parameter(property = "quarkus.dependency.sbom.schema-version") + String schemaVersion; + + protected MavenArtifactResolver resolver; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + getLog().info("Skipping config dump"); + return; + } + final Path outputFile = getSbomFile().toPath(); + CycloneDxSbomGenerator.newInstance() + .setManifest(ApplicationManifest.fromConfig( + ApplicationManifestConfig.builder() + .setApplicationModel(resolveApplicationModel()) + .build())) + .setOutputFile(outputFile) + .setEffectiveModelResolver(EffectiveModelResolver.of(getResolver())) + .setSchemaVersion(schemaVersion) + .setIncludeLicenseText(includeLicenseText) + .generate(); + getLog().info("The SBOM has been saved in " + outputFile); + } + + private ApplicationModel resolveApplicationModel() + throws MojoExecutionException { + final ArtifactCoords appArtifact = ArtifactCoords.pom(project.getGroupId(), project.getArtifactId(), + project.getVersion()); + final BootstrapAppModelResolver modelResolver; + try { + modelResolver = new BootstrapAppModelResolver(getResolver()); + if (mode != null) { + if (mode.equalsIgnoreCase("test")) { + modelResolver.setTest(true); + } else if (mode.equalsIgnoreCase("dev") || mode.equalsIgnoreCase("development")) { + modelResolver.setDevMode(true); + } else if (mode.equalsIgnoreCase("prod") || mode.isEmpty()) { + // ignore, that's the default + } else { + throw new MojoExecutionException( + "Parameter 'mode' was set to '" + mode + "' while expected one of 'dev', 'test' or 'prod'"); + } + } + // enable the incubating model resolver impl by default for this mojo + modelResolver.setIncubatingModelResolver( + !IncubatingApplicationModelResolver.isIncubatingModelResolverProperty(project.getProperties(), "false")); + return modelResolver.resolveModel(appArtifact); + } catch (Exception e) { + throw new MojoExecutionException("Failed to resolve application model " + appArtifact + " dependencies", e); + } + } + + private String getSbomFilename() { + var a = project.getArtifact(); + var sb = new StringBuilder().append(a.getArtifactId()).append("-").append(a.getVersion()).append("-"); + if (!"prod".equalsIgnoreCase(mode)) { + sb.append(mode).append("-"); + } + return sb.append("dependency-cyclonedx").append(".").append(format).toString(); + } + + private File getSbomFile() { + var f = outputFile; + if (f == null) { + f = new File(project.getBuild().getDirectory(), getSbomFilename()); + } + if (getLog().isDebugEnabled()) { + getLog().debug("SBOM will be stored in " + f); + } + return f; + } + + protected MavenArtifactResolver getResolver() { + return resolver == null + ? resolver = workspaceProvider.createArtifactResolver(BootstrapMavenContext.config() + .setUserSettings(session.getRequest().getUserSettingsFile()) + // The system needs to be initialized with the bootstrap model builder to properly interpolate system properties set on the command line + // e.g. -Dquarkus.platform.version=xxx + //.setRepositorySystem(repoSystem) + // The session should be initialized with the loaded workspace + //.setRepositorySystemSession(repoSession) + .setRemoteRepositories(repos) + // To support multi-module projects that haven't been installed + .setPreferPomsFromWorkspace(true)) + : resolver; + } +} diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java index 261d7bcee4288..461e619c65fd4 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java @@ -32,7 +32,7 @@ /** * Displays Quarkus application build dependency tree including the deployment ones. */ -@Mojo(name = "dependency-tree", defaultPhase = LifecyclePhase.NONE, requiresDependencyResolution = ResolutionScope.NONE) +@Mojo(name = "dependency-tree", defaultPhase = LifecyclePhase.NONE, requiresDependencyResolution = ResolutionScope.NONE, threadSafe = true) public class DependencyTreeMojo extends AbstractMojo { @Component diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index 56eff8bfba092..8b3a2d7a4bb44 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -34,12 +34,14 @@ import io.quarkus.bootstrap.BootstrapConstants; import io.quarkus.bootstrap.BootstrapException; import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.DependencyInfoProvider; import io.quarkus.bootstrap.app.QuarkusBootstrap; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.maven.components.ManifestSection; @@ -131,8 +133,7 @@ public CuratedApplication bootstrapApplication(QuarkusBootstrapMojo mojo, Launch } public CuratedApplication bootstrapApplication(QuarkusBootstrapMojo mojo, LaunchMode mode, - Consumer builderCustomizer) - throws MojoExecutionException { + Consumer builderCustomizer) throws MojoExecutionException { return bootstrapper(mojo).bootstrapApplication(mojo, mode, builderCustomizer); } @@ -182,8 +183,7 @@ public class QuarkusMavenAppBootstrap implements Closeable { private CuratedApplication devApp; private CuratedApplication testApp; - private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, LaunchMode mode) - throws MojoExecutionException { + private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, LaunchMode mode) { try { if (mode == LaunchMode.DEVELOPMENT || mode == LaunchMode.TEST || isWorkspaceDiscovery(mojo)) { return workspaceProvider.createArtifactResolver( @@ -206,13 +206,12 @@ private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, Launch .setRemoteRepositoryManager(remoteRepoManager) .build(); } catch (BootstrapMavenException e) { - throw new MojoExecutionException("Failed to initialize Quarkus bootstrap Maven artifact resolver", e); + throw new RuntimeException("Failed to initialize Quarkus bootstrap Maven artifact resolver", e); } } private CuratedApplication doBootstrap(QuarkusBootstrapMojo mojo, LaunchMode mode, - Consumer builderCustomizer) - throws MojoExecutionException { + Consumer builderCustomizer) throws MojoExecutionException { final BootstrapAppModelResolver modelResolver = new BootstrapAppModelResolver(artifactResolver(mojo, mode)) .setIncubatingModelResolver( @@ -261,7 +260,10 @@ private CuratedApplication doBootstrap(QuarkusBootstrapMojo mojo, LaunchMode mod .setBaseName(mojo.finalName()) .setOriginalBaseName(mojo.mavenProject().getBuild().getFinalName()) .setTargetDirectory(mojo.buildDir().toPath()) - .setForcedDependencies(forcedDependencies); + .setForcedDependencies(forcedDependencies) + .setDependencyInfoProvider(() -> DependencyInfoProvider.builder() + .setMavenModelResolver(EffectiveModelResolver.of(artifactResolver(mojo, mode))) + .build()); try { if (builderCustomizer != null) { @@ -361,8 +363,7 @@ private String toManifestSectionAttributeKey(String section, String key) throws } protected CuratedApplication bootstrapApplication(QuarkusBootstrapMojo mojo, LaunchMode mode, - Consumer builderCustomizer) - throws MojoExecutionException { + Consumer builderCustomizer) throws MojoExecutionException { if (mode == LaunchMode.DEVELOPMENT) { return devApp == null ? devApp = doBootstrap(mojo, mode, builderCustomizer) : devApp; } diff --git a/docs/pom.xml b/docs/pom.xml index 1d95a0c9c2478..fe7fb6bad3350 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -458,6 +458,19 @@ + + io.quarkus + quarkus-cyclonedx-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-datasource-deployment diff --git a/docs/src/main/asciidoc/cyclonedx.adoc b/docs/src/main/asciidoc/cyclonedx.adoc new file mode 100644 index 0000000000000..b75802b64e0ee --- /dev/null +++ b/docs/src/main/asciidoc/cyclonedx.adoc @@ -0,0 +1,198 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// +[id="cyclonedx"] += Generating CycloneDX BOMs +include::_attributes.adoc[] +:categories: tooling +:summary: This guide explains how to generate SBOMs for Quarkus applications in the CycloneDX format. +:topics: sbom +:extensions: io.quarkus:quarkus-cyclonedx + +An SBOM (Software Bill of Material) is a manifest that describes what a given software distribution consists of in terms of components. In addition to that, it may include a lot more information such as relationships between those components, licenses, provenance, etc. +SBOMs would typically be used by software security and software supply chain risk management tools to perform vulnerability and compliance related analysis. + +This guide describes Quarkus SBOM generation capabilities following https://cyclonedx.org/[CycloneDX] specification. + +== Why Quarkus-specific tooling? + +While Quarkus integrates with build tools such as https://maven.apache.org/[Maven] and https://gradle.org/[Gradle], it could itself be categorized as a build tool with its own component and dependency model, build steps, and build outcomes. One of the essential component types of a Quarkus application is a Quarkus extension, which consists of a runtime and a build time artifacts, and their dependencies. + +To properly resolve Quarkus extension and other application dependencies Quarkus uses its own dependency resolver, which is implemented on top of the dependency resolver provided by the underlying build tool: Maven or Gradle. + +As a consequence, in case of Maven, for example, the results of `dependency:tree` will not include all the dependencies Quarkus will use to build an application. A similar issue will affect other dependency analysis tools that assume a project adheres to the standard Maven dependency model: they will not be able to capture the effective Quarkus application dependency graph. Unfortunately, that includes the implementation of the https://github.com/CycloneDX/cyclonedx-maven-plugin[CycloneDX Maven plugin]. + +Besides the dependencies, that are an input to a build process, there is also an outcome of the build that is the final distribution of an application. Users of an application may request an SBOM manifesting not only the dependencies (the input to a build) but also the final distribution (the outcome of the build) before they agree to deploy the application. Quarkus allows application developers to choose various packaging types for their applications, some of which are Quarkus-specific. Providing certain Quarkus-specific details about components included in a distribution may help better evaluate the impact of potential security-related issues. + +== Dependency SBOMs + +This chapter describes how to generate SBOMs manifesting only the dependencies of an application before it is built. In other words, these SBOMs will manifest the input into a build. These SBOMs could be used to perform vulnerability and compliance related analysis before building applications. + +=== Maven Dependency SBOMs + +For Quarkus Maven projects dependency SBOMs can be generated with the `quarkus:dependency-sbom` goal. The outcome of the goal will be saved in a `target/--dependency-cyclonedx.json` file (which can be changed by setting the `outputFile` goal parameter or the `quarkus.dependency.sbom.output-file` property). The complete Quarkus build and runtime dependency graphs will be recorded in the https://cyclonedx.org/[CycloneDX] `JSON` format. + +`XML` format can be requested by setting `format` goal parameter (or `quarkus.dependency.sbom.format` property) to `xml`. + +Each component in the generated SBOM will include the `quarkus:component:scope` property that will indicate whether this component is used at runtime or only development/build time. +[source,json] +---- + { + "name" : "quarkus:component:scope", + "value" : "runtime" + } +---- + +By default, `quarkus:dependency-sbom` captures the dependencies of a production build. Quarkus supports three application bootstrap modes: normal (production), test, and dev. In all three modes, an application may have different dependency graphs. The `mode` parameter can be used to indicate which dependency graph should be recorded. If the `mode` is set to `test` or `dev`, the target file name will become `target/---dependency-cyclonedx.json`. + +The complete set of parameters and their description can be obtained by executing `mvn help:describe -Dcmd=quarkus:dependency-sbom -Ddetail`. + +=== Gradle Dependency SBOMs + +Unlike Maven, the https://github.com/CycloneDX/cyclonedx-gradle-plugin[Gradle CycloneDX plugin implementation] can be used in Quarkus projects to generate dependency SBOMs, since the implementation manifests dependency configurations registered by configured plugins. + +Please, refer to the https://github.com/CycloneDX/cyclonedx-gradle-plugin[Gradle CycloneDX plugin] documentation for its configuration options. Here is a list of Quarkus dependency configurations that would be relevant for manifesting: + +* `quarkusProdRuntimeClasspathConfiguration` - Quarkus application production runtime dependencies; +* `quarkusProdRuntimeClasspathConfigurationDeployment` - Quarkus application production runtime and build time dependencies; +* `quarkusTestRuntimeClasspathConfiguration` - Quarkus application test runtime dependencies; +* `quarkusTestRuntimeClasspathConfigurationDeployment` - Quarkus application test runtime and build time dependencies; +* `quarkusDevRuntimeClasspathConfiguration` - Quarkus application dev mode runtime dependencies; +* `quarkusDevRuntimeClasspathConfigurationDeployment` - Quarkus application dev mode runtime and build time dependencies. + +Given that the plugin is not aware of how Quarkus uses these dependencies, it will not be able to set the `quarkus:component:scope` property for components. On the other hand, the requested configuration name can be used indicate which scope to target. + +== Distribution SBOMs + +This chapter describes SBOMs that manifest outcomes of Quarkus builds that are final application distributions. + +During an application build and package assembly process, Quarkus captures certain details about the produced distribution and then allows an SBOM generator to consume and record that information in an SBOM format. + +At this point, the only SBOM generator available for Quarkus users that can manifest application distributions is `io.quarkus:quarkus-cyclonedx`. Once it's added as a project dependency it will generate SBOMs every time an application is built. SBOMs will be saved in the project's build output directory under `-cyclonedx.` name, where + +* `` is the base file name (without the extension) of the executable that launches an application; +* `` is either `json` (the default) or `xml`, which can be configured using `quarkus.cyclonedx.format` property. If both formats are desired `quarkus.cyclonedx.format` can be set to `all`. + +=== Fast JAR + +Fast JAR packaging uses a Quarkus-specific filesystem directory layout that contains files generated by Quarkus and Maven artifacts that are runtime dependencies of an application. + +SBOMs for Fast JAR packaging type will use the executable JAR file as their main component and record both runtime and build time Quarkus application dependencies. + +==== Runtime Components + +Every file in the resulting Fast JAR distribution will appear in the SBOM with the `quarkus:component:scope` property set to `runtime` and `evidence.occurrences.location` field pointing to the location of the component in the application distribution directory, for example + +[source,json] +---- + "purl" : "pkg:maven/org.jboss.slf4j/slf4j-jboss-logmanager@2.0.0.Final?type=jar", + "properties" : [ + { + "name" : "quarkus:component:scope", + "value" : "runtime" + } + ], + "evidence" : { + "occurrences" : [ + { + "location" : "lib/main/org.jboss.slf4j.slf4j-jboss-logmanager-2.0.0.Final.jar" + } + ] + } +---- + +NOTE: `evidence.occurrences.location` was introduced in CycloneDX schema version 1.5, for older versions the location will be indicated using the `quarkus:component:location` property. + +==== Pedigree + +Pedigree is a way to provide information that certain patches, or changes in general, have been applied to a certain component. + +In certain cases, Quarkus may copy modified versions of dependency artifacts to an application distribution. Manipulating the original content of a component will change its hash sums which may get highlighted as suspicious by the tools comparing original component hash sums to those found in the distribution. + +When Quarkus applies modifications to artifacts resolved from Maven repositories, it can manifest these changes as pedigree notes in the generated SBOM. +For example, if an application developer decided to remove certain classpath resources from a dependency, such as + +[source,properties] +---- +quarkus.class-loading.removed-resources."jakarta.transaction\:jakarta.transaction-api"=META-INF/NOTICE.md,jakarta/transaction/package.html +---- + +The resulting SBOM will include +[source,json] +---- + "purl" : "pkg:maven/jakarta.transaction/jakarta.transaction-api@2.0.1?type=jar", + "pedigree" : { + "notes" : "Removed META-INF/NOTICE.md,jakarta/transaction/package.html" + }, +---- + +==== Build time dependencies + +Build time dependencies will be recorded with the `quarkus:component:scope` property set to `development`: + +[source,json] +---- + "purl" : "pkg:maven/org.apache.httpcomponents/httpclient@4.5.14?type=jar", + "properties" : [ + { + "name" : "quarkus:component:scope", + "value" : "development" + } + ] +---- + +They will not include `evidence.occurrences.location` since they will not be found in the distribution. + +=== Uber JAR + +SBOMs for Uber JARs will use the Uber JAR Maven artifact as their main component. + +Since an Uber JAR is published as a Maven artifact itself, SBOMs generated for Uber JARs will also be automatically published as Maven artifacts. This, however, can be disabled by setting the `attachSboms` parameter of the `quarkus:build` goal to `false`. + +Gradle users will have to explicitly configure a publishing plugin to deploy SBOMs as Maven artifacts. + +Runtime components in an SBOM generated for an Uber JAR will not include `evidence.occurrences.location` since their content is merged in a single JAR file. + +=== Native image + +SBOMs for native images will use the native executable file as their main component. + +Since native executables are not currently attached to projects as Maven artifacts, their SBOMs will not be attached as Maven artifacts either. + +As in the case of an Uber JAR, runtime components in an SBOM generated for an native executable will not include `evidence.occurrences.location` since their corresponding code and resources are included in a single native executable file. + +=== Mutable JAR + +Mutable JAR distribution is similar to the Fast JAR one except it also includes build time dependencies to support re-augmentation (re-building) of an application. + +SBOMs generated for Mutable JAR distributions will also record locations of components that will be used during re-augmentation process using `evidence.occurrences.location` but keeping their `quarkus:component:scope` property set to `development`. For example: + +[source,json] +---- + "purl" : "pkg:maven/org.apache.httpcomponents/httpcore@4.4.16?type=jar", + "properties" : [ + { + "name" : "quarkus:component:scope", + "value" : "development" + } + ], + "evidence" : { + "occurrences" : [ + { + "location" : "lib/deployment/org.apache.httpcomponents.httpcore-4.4.16.jar" + } + ] + } +---- + +== Quarkus Property Taxonomy + +[cols="1,1,1"] +|=== +|Name |Value range |Description + +|`quarkus:component:scope` |`runtime` or `development` |Indicates whether a component is a runtime or a build/development time dependency of an application. +|`quarkus:component:location` |String representing a file system path using `/` as a path element |Used in SBOMs with schema versions 1.4 or older. Starting from schema 1.5, `evidence.occurrences.location` is used instead. This property is used only if a component is found in the distribution. The value is a relative path to a file pointing to the location of a component in a distribution using `/` as a path element separator. +|=== \ No newline at end of file diff --git a/extensions/cyclonedx/deployment/pom.xml b/extensions/cyclonedx/deployment/pom.xml new file mode 100644 index 0000000000000..272132a949776 --- /dev/null +++ b/extensions/cyclonedx/deployment/pom.xml @@ -0,0 +1,58 @@ + + + + quarkus-cyclonedx-parent + io.quarkus + 3.14.999-SNAPSHOT + + + 4.0.0 + + quarkus-cyclonedx-deployment + Quarkus - CycloneDX - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-bootstrap-maven-resolver + provided + + + io.quarkus + quarkus-cyclonedx + + + io.quarkus + quarkus-cyclonedx-generator + + + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + + + \ No newline at end of file diff --git a/extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CdxSbomBuildStep.java b/extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CdxSbomBuildStep.java new file mode 100644 index 0000000000000..6bf606bf798bc --- /dev/null +++ b/extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CdxSbomBuildStep.java @@ -0,0 +1,48 @@ +package io.quarkus.cyclonedx.deployment; + +import java.util.List; + +import io.quarkus.cyclonedx.generator.*; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.AppModelProviderBuildItem; +import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.sbom.SbomBuildItem; +import io.quarkus.sbom.ApplicationManifest; + +/** + * Generates SBOMs for packaged applications if the corresponding config is enabled. + * The API around this is still in development and will likely change in the near future. + */ +public class CdxSbomBuildStep { + + @BuildStep + public void generate(List artifactResultBuildItems, + OutputTargetBuildItem outputTargetBuildItem, + AppModelProviderBuildItem appModelProviderBuildItem, + CycloneDxConfig cdxSbomConfig, + BuildProducer sbomProducer) { + if (cdxSbomConfig.skip()) { + // until there is a proper way to request the desired build items as build outcome + return; + } + var depInfoProvider = appModelProviderBuildItem.getDependencyInfoProvider().get(); + for (var artifactResult : artifactResultBuildItems) { + var manifestConfig = artifactResult.getManifestConfig(); + if (manifestConfig != null) { + var manifest = ApplicationManifest.fromConfig(manifestConfig); + for (var sbom : CycloneDxSbomGenerator.newInstance() + .setManifest(manifest) + .setOutputDirectory(outputTargetBuildItem.getOutputDirectory()) + .setEffectiveModelResolver(depInfoProvider == null ? null : depInfoProvider.getMavenModelResolver()) + .setFormat(cdxSbomConfig.format()) + .setSchemaVersion(cdxSbomConfig.schemaVersion().orElse(null)) + .setIncludeLicenseText(cdxSbomConfig.includeLicenseText()) + .generate()) { + sbomProducer.produce(new SbomBuildItem(sbom)); + } + } + } + } +} diff --git a/extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CycloneDxConfig.java b/extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CycloneDxConfig.java new file mode 100644 index 0000000000000..bf0728b4bd2d6 --- /dev/null +++ b/extensions/cyclonedx/deployment/src/main/java/io/quarkus/cyclonedx/deployment/CycloneDxConfig.java @@ -0,0 +1,45 @@ +package io.quarkus.cyclonedx.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +/** + * CycloneDX SBOM generator configuration + */ +@ConfigMapping(prefix = "quarkus.cyclonedx") +@ConfigRoot +public interface CycloneDxConfig { + /** + * Whether to skip SBOM generation + */ + @WithDefault("false") + boolean skip(); + + /** + * SBOM file format. Supported formats are {code json} and {code xml}. + * The default format is JSON. + * If both are desired then {@code all} could be used as the value of this option. + * + * @return SBOM file format + */ + @WithDefault("json") + String format(); + + /** + * CycloneDX specification version. The default value be the latest supported by the integrated CycloneDX library. + * + * @return CycloneDX specification version + */ + Optional schemaVersion(); + + /** + * Whether to include the license text into generated SBOMs. + * + * @return whether to include the license text into generated SBOMs + */ + @WithDefault("false") + boolean includeLicenseText(); +} diff --git a/extensions/cyclonedx/generator/pom.xml b/extensions/cyclonedx/generator/pom.xml new file mode 100644 index 0000000000000..54c865bf62fd8 --- /dev/null +++ b/extensions/cyclonedx/generator/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + io.quarkus + quarkus-cyclonedx-parent + 3.14.999-SNAPSHOT + + + quarkus-cyclonedx-generator + Quarkus - CycloneDX - Generator + Quarkus CycloneDX generator library + + + org.cyclonedx + cyclonedx-core-java + + + commons-io + commons-io + + + + + io.quarkus + quarkus-bootstrap-maven-resolver + + + io.quarkus + quarkus-bootstrap-core + + + \ No newline at end of file diff --git a/extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java b/extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java new file mode 100644 index 0000000000000..2489dbfaf8c44 --- /dev/null +++ b/extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java @@ -0,0 +1,653 @@ +package io.quarkus.cyclonedx.generator; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.model.MailingList; +import org.apache.maven.model.Model; +import org.cyclonedx.Version; +import org.cyclonedx.exception.GeneratorException; +import org.cyclonedx.generators.BomGeneratorFactory; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.Dependency; +import org.cyclonedx.model.Evidence; +import org.cyclonedx.model.ExternalReference; +import org.cyclonedx.model.Hash; +import org.cyclonedx.model.License; +import org.cyclonedx.model.LicenseChoice; +import org.cyclonedx.model.Metadata; +import org.cyclonedx.model.Pedigree; +import org.cyclonedx.model.Property; +import org.cyclonedx.model.Tool; +import org.cyclonedx.model.component.evidence.Occurrence; +import org.cyclonedx.model.metadata.ToolInformation; +import org.cyclonedx.util.BomUtils; +import org.cyclonedx.util.LicenseResolver; +import org.jboss.logging.Logger; + +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; + +import io.quarkus.bootstrap.app.SbomResult; +import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.paths.PathTree; +import io.quarkus.sbom.ApplicationComponent; +import io.quarkus.sbom.ApplicationManifest; + +public class CycloneDxSbomGenerator { + + private static final Logger log = Logger.getLogger(CycloneDxSbomGenerator.class); + + private static final String QUARKUS_COMPONENT_SCOPE = "quarkus:component:scope"; + private static final String QUARKUS_COMPONENT_LOCATION = "quarkus:component:location"; + + private static final Comparator ARTIFACT_COORDS_COMPARATOR = (c1, c2) -> { + var i = c1.getGroupId().compareTo(c2.getGroupId()); + if (i != 0) { + return i; + } + i = c1.getArtifactId().compareTo(c2.getArtifactId()); + if (i != 0) { + return i; + } + i = c1.getVersion().compareTo(c2.getVersion()); + if (i != 0) { + return i; + } + i = c1.getClassifier().compareTo(c2.getClassifier()); + if (i != 0) { + return i; + } + return c1.getType().compareTo(c2.getType()); + }; + + private static final String CLASSIFIER_CYCLONEDX = "cyclonedx"; + private static final String FORMAT_ALL = "all"; + private static final String FORMAT_JSON = "json"; + private static final String FORMAT_XML = "xml"; + private static final String DEFAULT_FORMAT = FORMAT_JSON; + private static final List SUPPORTED_FORMATS = List.of(FORMAT_JSON, FORMAT_XML); + private static final String SCOPE_RUNTIME = "runtime"; + private static final String SCOPE_DEVELOPMENT = "development"; + + public static CycloneDxSbomGenerator newInstance() { + return new CycloneDxSbomGenerator(); + } + + private boolean generated; + private ApplicationManifest manifest; + private Path outputDir; + private Path outputFile; + private String schemaVersion; + private String format; + private EffectiveModelResolver modelResolver; + private boolean includeLicenseText; + + private Version effectiveSchemaVersion; + + private CycloneDxSbomGenerator() { + } + + public CycloneDxSbomGenerator setManifest(ApplicationManifest manifest) { + ensureNotGenerated(); + this.manifest = manifest; + return this; + } + + public CycloneDxSbomGenerator setOutputDirectory(Path outputDir) { + ensureNotGenerated(); + this.outputDir = outputDir; + return this; + } + + public CycloneDxSbomGenerator setOutputFile(Path outputFile) { + ensureNotGenerated(); + this.outputFile = outputFile; + return this; + } + + public CycloneDxSbomGenerator setFormat(String format) { + ensureNotGenerated(); + this.format = format; + return this; + } + + public CycloneDxSbomGenerator setSchemaVersion(String schemaVersion) { + ensureNotGenerated(); + this.schemaVersion = schemaVersion; + return this; + } + + public CycloneDxSbomGenerator setEffectiveModelResolver(EffectiveModelResolver modelResolver) { + ensureNotGenerated(); + this.modelResolver = modelResolver; + return this; + } + + public CycloneDxSbomGenerator setIncludeLicenseText(boolean includeLicenseText) { + ensureNotGenerated(); + this.includeLicenseText = includeLicenseText; + return this; + } + + public List generate() { + ensureNotGenerated(); + Objects.requireNonNull(manifest, "Manifest is null"); + if (outputFile == null && outputDir == null) { + throw new IllegalArgumentException("Either outputDir or outputFile must be provided"); + } + generated = true; + + var bom = new Bom(); + bom.setMetadata(new Metadata()); + addToolInfo(bom); + + addApplicationComponent(bom, manifest.getMainComponent()); + for (var c : manifest.getComponents()) { + addComponent(bom, c); + } + if (FORMAT_ALL.equalsIgnoreCase(format)) { + if (outputFile != null) { + throw new IllegalArgumentException("Can't use output file " + outputFile + " with format '" + + FORMAT_ALL + "', since it implies generating multiple files"); + } + final List result = new ArrayList<>(SUPPORTED_FORMATS.size()); + for (String format : SUPPORTED_FORMATS) { + result.add(persistSbom(bom, getOutputFile(format), format)); + } + return result; + } + var outputFile = getOutputFile(format == null ? DEFAULT_FORMAT : format); + return List.of(persistSbom(bom, outputFile, getFormat(outputFile))); + } + + private void addComponent(Bom bom, ApplicationComponent component) { + final org.cyclonedx.model.Component c = getComponent(component); + bom.addComponent(c); + if (component.getResolvedDependency() != null) { + var deps = component.getResolvedDependency().getDependencies(); + if (!deps.isEmpty()) { + final Dependency d = new Dependency(c.getBomRef()); + for (var depCoords : sortAlphabetically(deps)) { + d.addDependency(new Dependency(getPackageURL(depCoords).toString())); + } + bom.addDependency(d); + } + } + } + + private void addApplicationComponent(Bom bom, ApplicationComponent component) { + var c = getComponent(component); + c.setType(org.cyclonedx.model.Component.Type.APPLICATION); + bom.getMetadata().setComponent(c); + bom.addComponent(c); + } + + private org.cyclonedx.model.Component getComponent(ApplicationComponent component) { + final org.cyclonedx.model.Component c = new org.cyclonedx.model.Component(); + var dep = component.getResolvedDependency(); + if (dep != null) { + initMavenComponent(dep, c); + } else if (component.getDistributionPath() != null) { + c.setBomRef(component.getDistributionPath()); + c.setType(org.cyclonedx.model.Component.Type.FILE); + c.setName(component.getPath().getFileName().toString()); + } else if (component.getPath() != null) { + final String fileName = component.getPath().getFileName().toString(); + c.setName(fileName); + c.setBomRef(fileName); + c.setType(org.cyclonedx.model.Component.Type.FILE); + } else { + throw new RuntimeException("Component is not associated with any file system path"); + } + + final List props = new ArrayList<>(2); + String quarkusScope = component.getScope(); + if (quarkusScope == null) { + quarkusScope = dep == null || dep.isRuntimeCp() ? SCOPE_RUNTIME : SCOPE_DEVELOPMENT; + } + addProperty(props, QUARKUS_COMPONENT_SCOPE, quarkusScope); + if (component.getDistributionPath() != null) { + if (getSchemaVersion().getVersion() >= 1.5) { + var occurence = new Occurrence(); + occurence.setLocation(component.getDistributionPath()); + var evidence = new Evidence(); + evidence.setOccurrences(List.of(occurence)); + c.setEvidence(evidence); + } else { + addProperty(props, QUARKUS_COMPONENT_LOCATION, component.getDistributionPath()); + } + } + c.setProperties(props); + + if (component.getPedigree() != null) { + var pedigree = new Pedigree(); + pedigree.setNotes(component.getPedigree()); + c.setPedigree(pedigree); + } + + if (component.getPath() != null) { + try { + c.setHashes(BomUtils.calculateHashes(component.getPath().toFile(), getSchemaVersion())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return c; + } + + private void initMavenComponent(ArtifactCoords coords, Component c) { + addPomMetadata(coords, c); + c.setGroup(coords.getGroupId()); + c.setName(coords.getArtifactId()); + c.setVersion(coords.getVersion()); + final PackageURL purl = getPackageURL(coords); + c.setPurl(purl); + c.setBomRef(purl.toString()); + c.setType(Component.Type.LIBRARY); + } + + private void addPomMetadata(ArtifactCoords dep, org.cyclonedx.model.Component component) { + var model = modelResolver == null ? null : modelResolver.resolveEffectiveModel(dep); + if (model != null) { + extractComponentMetadata(model, component); + } + } + + private void extractComponentMetadata(Model model, org.cyclonedx.model.Component component) { + if (component.getPublisher() == null) { + // If we don't already have publisher information, retrieve it. + if (model.getOrganization() != null) { + component.setPublisher(model.getOrganization().getName()); + } + } + if (component.getDescription() == null) { + // If we don't already have description information, retrieve it. + component.setDescription(model.getDescription()); + } + var schemaVersion = getSchemaVersion(); + if (component.getLicenseChoice() == null || component.getLicenseChoice().getLicenses() == null + || component.getLicenseChoice().getLicenses().isEmpty()) { + // If we don't already have license information, retrieve it. + if (model.getLicenses() != null) { + component.setLicenseChoice(resolveMavenLicenses(model.getLicenses(), schemaVersion, includeLicenseText)); + } + } + if (Version.VERSION_10 != schemaVersion) { + addExternalReference(ExternalReference.Type.WEBSITE, model.getUrl(), component); + if (model.getCiManagement() != null) { + addExternalReference(ExternalReference.Type.BUILD_SYSTEM, model.getCiManagement().getUrl(), component); + } + if (model.getDistributionManagement() != null) { + addExternalReference(ExternalReference.Type.DISTRIBUTION, model.getDistributionManagement().getDownloadUrl(), + component); + if (model.getDistributionManagement().getRepository() != null) { + ExternalReference.Type type = (schemaVersion.getVersion() < 1.5) ? ExternalReference.Type.DISTRIBUTION + : ExternalReference.Type.DISTRIBUTION_INTAKE; + addExternalReference(type, model.getDistributionManagement().getRepository().getUrl(), component); + } + } + if (model.getIssueManagement() != null) { + addExternalReference(ExternalReference.Type.ISSUE_TRACKER, model.getIssueManagement().getUrl(), component); + } + if (model.getMailingLists() != null && !model.getMailingLists().isEmpty()) { + for (MailingList list : model.getMailingLists()) { + String url = list.getArchive(); + if (url == null) { + url = list.getSubscribe(); + } + addExternalReference(ExternalReference.Type.MAILING_LIST, url, component); + } + } + if (model.getScm() != null) { + addExternalReference(ExternalReference.Type.VCS, model.getScm().getUrl(), component); + } + } + } + + private LicenseChoice resolveMavenLicenses(final List projectLicenses, + final Version schemaVersion, boolean includeLicenseText) { + final LicenseChoice licenseChoice = new LicenseChoice(); + for (org.apache.maven.model.License artifactLicense : projectLicenses) { + boolean resolved = false; + if (artifactLicense.getName() != null) { + final LicenseChoice resolvedByName = LicenseResolver.resolve(artifactLicense.getName(), includeLicenseText); + resolved = resolveLicenseInfo(licenseChoice, resolvedByName, schemaVersion); + } + if (artifactLicense.getUrl() != null && !resolved) { + final LicenseChoice resolvedByUrl = LicenseResolver.resolve(artifactLicense.getUrl(), includeLicenseText); + resolved = resolveLicenseInfo(licenseChoice, resolvedByUrl, schemaVersion); + } + if (artifactLicense.getName() != null && !resolved) { + final License license = new License(); + license.setName(artifactLicense.getName().trim()); + if (StringUtils.isNotBlank(artifactLicense.getUrl())) { + try { + final URI uri = new URI(artifactLicense.getUrl().trim()); + license.setUrl(uri.toString()); + } catch (URISyntaxException e) { + // throw it away + } + } + licenseChoice.addLicense(license); + } + } + return licenseChoice; + } + + private boolean resolveLicenseInfo(final LicenseChoice licenseChoice, final LicenseChoice licenseChoiceToResolve, + final Version schemaVersion) { + if (licenseChoiceToResolve != null) { + if (licenseChoiceToResolve.getLicenses() != null && !licenseChoiceToResolve.getLicenses().isEmpty()) { + licenseChoice.addLicense(licenseChoiceToResolve.getLicenses().get(0)); + return true; + } else if (licenseChoiceToResolve.getExpression() != null && Version.VERSION_10 != schemaVersion) { + licenseChoice.setExpression(licenseChoiceToResolve.getExpression()); + return true; + } + } + return false; + } + + private static boolean doesComponentHaveExternalReference(final org.cyclonedx.model.Component component, + final ExternalReference.Type type) { + if (component.getExternalReferences() != null && !component.getExternalReferences().isEmpty()) { + for (final ExternalReference ref : component.getExternalReferences()) { + if (type == ref.getType()) { + return true; + } + } + } + return false; + } + + private static void addExternalReference(final ExternalReference.Type referenceType, final String url, + final org.cyclonedx.model.Component component) { + if (url == null) { + return; + } + try { + final URI uri = new URI(url.trim()); + final ExternalReference ref = new ExternalReference(); + ref.setType(referenceType); + ref.setUrl(uri.toString()); + component.addExternalReference(ref); + } catch (URISyntaxException e) { + // throw it away + } + } + + private static PackageURL getPackageURL(ArtifactCoords dep) { + final TreeMap qualifiers = new TreeMap<>(); + qualifiers.put("type", dep.getType()); + if (!dep.getClassifier().isEmpty()) { + qualifiers.put("classifier", dep.getClassifier()); + } + final PackageURL purl; + try { + purl = new PackageURL(PackageURL.StandardTypes.MAVEN, + dep.getGroupId(), + dep.getArtifactId(), + dep.getVersion(), + qualifiers, null); + } catch (MalformedPackageURLException e) { + throw new RuntimeException("Failed to generate Purl for " + dep.toCompactCoords(), e); + } + return purl; + } + + static void addProperty(List props, String name, String value) { + var prop = new Property(); + prop.setName(name); + prop.setValue(value); + props.add(prop); + } + + private static List sortAlphabetically(Collection col) { + var list = new ArrayList<>(col); + list.sort(ARTIFACT_COORDS_COMPARATOR); + return list; + } + + private SbomResult persistSbom(Bom bom, Path sbomFile, String format) { + + var specVersion = getSchemaVersion(); + final String sbomContent; + if (format.equalsIgnoreCase("json")) { + try { + sbomContent = BomGeneratorFactory.createJson(specVersion, bom).toJsonString(); + } catch (Throwable e) { + throw new RuntimeException("Failed to generate an SBOM in JSON format", e); + } + } else if (format.equalsIgnoreCase("xml")) { + try { + sbomContent = BomGeneratorFactory.createXml(specVersion, bom).toXmlString(); + } catch (GeneratorException e) { + throw new RuntimeException("Failed to generate an SBOM in XML format", e); + } + } else { + throw new RuntimeException( + "Unsupported SBOM artifact type " + format + ", supported types are json and xml"); + } + + var outputDir = sbomFile.getParent(); + if (outputDir != null) { + try { + Files.createDirectories(outputDir); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + if (log.isDebugEnabled()) { + log.debug("SBOM Content:" + System.lineSeparator() + sbomContent); + } + try (BufferedWriter writer = Files.newBufferedWriter(sbomFile)) { + writer.write(sbomContent); + } catch (IOException e) { + throw new UncheckedIOException("Failed to write to " + sbomFile, e); + } + + return new SbomResult(sbomFile, "CycloneDX", bom.getSpecVersion(), format, CLASSIFIER_CYCLONEDX, + manifest.getRunnerPath()); + } + + private Path getOutputFile(String defaultFormat) { + if (outputFile == null) { + var fileName = toSbomFileName(manifest.getRunnerPath().getFileName().toString(), defaultFormat); + return outputDir == null ? Path.of(fileName) : outputDir.resolve(fileName); + } + return outputFile; + } + + private String toSbomFileName(String deliverableName, String defaultFormat) { + return stripExtension(deliverableName) + "-" + CLASSIFIER_CYCLONEDX + "." + defaultFormat; + } + + private static String stripExtension(String fileName) { + var lastDot = fileName.lastIndexOf('.'); + if (lastDot <= 0) { + return fileName; + } + var lastDash = fileName.lastIndexOf('-'); + return lastDot < lastDash ? fileName : fileName.substring(0, lastDot); + } + + private String getFormat(Path outputFile) { + if (format == null || "all".equalsIgnoreCase(format)) { + var name = outputFile.getFileName().toString(); + var lastDot = name.lastIndexOf('.'); + if (lastDot < 0 || lastDot == name.length() - 1) { + throw new IllegalArgumentException("Failed to determine file extension of " + outputFile); + } + return name.substring(lastDot + 1); + } + return format; + } + + private Version getSchemaVersion() { + if (effectiveSchemaVersion == null) { + if (schemaVersion == null) { + effectiveSchemaVersion = Collections.max(List.of(Version.values())); + } else { + for (var v : Version.values()) { + if (schemaVersion.equals(v.getVersionString())) { + effectiveSchemaVersion = v; + break; + } + } + if (effectiveSchemaVersion == null) { + var versions = Version.values(); + var sb = new StringBuilder(); + sb.append("Requested CycloneDX schema version ").append(schemaVersion) + .append(" does not appear in the list of supported versions: ") + .append(versions[0].getVersionString()); + for (int i = 1; i < versions.length; ++i) { + sb.append(", ").append(versions[i].getVersionString()); + } + throw new IllegalArgumentException(sb.toString()); + } + } + } + return effectiveSchemaVersion; + } + + private void addToolInfo(Bom bom) { + + var toolLocation = getToolLocation(); + if (toolLocation == null) { + return; + } + List hashes = null; + if (!Files.isDirectory(toolLocation)) { + try { + hashes = BomUtils.calculateHashes(toolLocation.toFile(), getSchemaVersion()); + } catch (IOException e) { + throw new RuntimeException("Failed to calculate hashes for the tool at " + toolLocation, e); + } + } else { + log.warn("skipping tool hashing because " + toolLocation + " appears to be a directory"); + } + + if (getSchemaVersion().getVersion() >= 1.5) { + final ToolInformation toolInfo = new ToolInformation(); + final Component toolComponent = new Component(); + toolComponent.setType(Component.Type.LIBRARY); + final ApplicationComponent appComponent = findApplicationComponent(toolLocation); + if (appComponent != null && appComponent.getResolvedDependency() != null) { + initMavenComponent(appComponent.getResolvedDependency(), toolComponent); + } else { + var coords = getMavenArtifact(toolLocation); + if (coords != null) { + initMavenComponent(coords, toolComponent); + } else { + toolComponent.setName(toolLocation.getFileName().toString()); + } + } + if (hashes != null) { + toolComponent.setHashes(hashes); + } + toolInfo.setComponents(List.of(toolComponent)); + bom.getMetadata().setToolChoice(toolInfo); + } else { + var tool = new Tool(); + final ApplicationComponent appComponent = findApplicationComponent(toolLocation); + if (appComponent != null && appComponent.getResolvedDependency() != null) { + tool.setVendor(appComponent.getResolvedDependency().getGroupId()); + tool.setName(appComponent.getResolvedDependency().getArtifactId()); + tool.setVersion(appComponent.getResolvedDependency().getVersion()); + } else { + var coords = getMavenArtifact(toolLocation); + if (coords != null) { + tool.setVendor(coords.getGroupId()); + tool.setName(coords.getArtifactId()); + tool.setVersion(coords.getVersion()); + } else { + tool.setName(toolLocation.getFileName().toString()); + } + } + if (hashes != null) { + tool.setHashes(hashes); + } + bom.getMetadata().setTools(List.of(tool)); + } + } + + private ApplicationComponent findApplicationComponent(Path path) { + for (var c : manifest.getComponents()) { + if (c.getResolvedDependency().getResolvedPaths().contains(path)) { + return c; + } + } + return null; + } + + private static ArtifactCoords getMavenArtifact(Path toolLocation) { + final List toolArtifact = new ArrayList<>(1); + PathTree.ofDirectoryOrArchive(toolLocation).walkIfContains("META-INF/maven", visit -> { + if (!Files.isDirectory(visit.getPath()) && visit.getPath().getFileName().toString().equals("pom.properties")) { + try (BufferedReader reader = Files.newBufferedReader(visit.getPath())) { + var props = new Properties(); + props.load(reader); + final String groupId = props.getProperty("groupId"); + if (isBlanc(groupId)) { + return; + } + final String artifactId = props.getProperty("artifactId"); + if (isBlanc(artifactId)) { + return; + } + final String version = props.getProperty("version"); + if (isBlanc(version)) { + return; + } + toolArtifact.add(ArtifactCoords.jar(groupId, artifactId, version)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }); + if (toolArtifact.size() != 1) { + return null; + } + return toolArtifact.get(0); + } + + private static boolean isBlanc(String s) { + return s == null || s.trim().isEmpty(); + } + + private Path getToolLocation() { + var cs = getClass().getProtectionDomain().getCodeSource(); + if (cs == null) { + log.warn("Failed to determine code source of the tool"); + return null; + } + var url = cs.getLocation(); + if (url == null) { + log.warn("Failed to determine code source URL of the tool"); + return null; + } + try { + return Path.of(url.toURI()); + } catch (URISyntaxException e) { + log.warn("Failed to translate " + url + " to a file system path", e); + return null; + } + } + + private void ensureNotGenerated() { + if (generated) { + throw new RuntimeException("This instance has already been used to generate an SBOM"); + } + } +} diff --git a/extensions/cyclonedx/pom.xml b/extensions/cyclonedx/pom.xml new file mode 100644 index 0000000000000..56904028095ef --- /dev/null +++ b/extensions/cyclonedx/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-extensions-parent + io.quarkus + 3.14.999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-cyclonedx-parent + Quarkus - CycloneDX + pom + + generator + deployment + runtime + + + \ No newline at end of file diff --git a/extensions/cyclonedx/runtime/pom.xml b/extensions/cyclonedx/runtime/pom.xml new file mode 100644 index 0000000000000..9d33a1da2d2e6 --- /dev/null +++ b/extensions/cyclonedx/runtime/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + io.quarkus + quarkus-cyclonedx-parent + 3.14.999-SNAPSHOT + + + quarkus-cyclonedx + Quarkus - CycloneDX - Runtime + Generate application SBOM following CycloneDX specification + + + io.quarkus + quarkus-core + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + + \ No newline at end of file diff --git a/extensions/cyclonedx/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/cyclonedx/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..6941a4af6f5fc --- /dev/null +++ b/extensions/cyclonedx/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,8 @@ +name: "CycloneDX" +artifact: ${project.groupId}:${project.artifactId}:${project.version} +metadata: + keywords: + - "cyclonedx" + - "cdx" + status: "preview" + guide: "https://quarkus.io/guides/cyclonedx" \ No newline at end of file diff --git a/extensions/pom.xml b/extensions/pom.xml index 72910a5ac9507..bee86889032bf 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -152,6 +152,9 @@ keycloak-admin-resteasy-client credentials + + cyclonedx + infinispan-client infinispan-cache diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationComponent.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationComponent.java new file mode 100644 index 0000000000000..8548f2117def6 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationComponent.java @@ -0,0 +1,102 @@ +package io.quarkus.sbom; + +import java.nio.file.Path; + +import io.quarkus.maven.dependency.ResolvedDependency; + +public class ApplicationComponent { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends ApplicationComponent { + + private Builder() { + super(); + } + + public Builder(ApplicationComponent component) { + super(component); + } + + public Builder setPath(Path componentPath) { + path = componentPath; + return this; + } + + public Builder setDistributionPath(String distributionPath) { + this.distributionPath = distributionPath; + return this; + } + + public Builder setResolvedDependency(ResolvedDependency dep) { + this.dep = dep; + return this; + } + + public Builder setPedigree(String pedigree) { + this.pedigree = pedigree; + return this; + } + + public Builder setDevelopmentScope() { + return setScope("development"); + } + + public Builder setScope(String scope) { + this.scope = scope; + return this; + } + + public ApplicationComponent build() { + return ensureImmutable(); + } + + @Override + protected ApplicationComponent ensureImmutable() { + return new ApplicationComponent(this); + } + } + + protected Path path; + protected String distributionPath; + protected ResolvedDependency dep; + protected String pedigree; + protected String scope; + + private ApplicationComponent() { + } + + private ApplicationComponent(ApplicationComponent builder) { + this.path = builder.path; + this.distributionPath = builder.distributionPath; + this.dep = builder.dep; + this.pedigree = builder.pedigree; + this.scope = builder.scope; + } + + public Path getPath() { + return path; + } + + public String getDistributionPath() { + return distributionPath; + } + + public ResolvedDependency getResolvedDependency() { + return dep; + } + + public String getPedigree() { + return pedigree; + } + + public String getScope() { + return scope; + } + + protected ApplicationComponent ensureImmutable() { + return this; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifest.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifest.java new file mode 100644 index 0000000000000..2cdaead983b59 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifest.java @@ -0,0 +1,127 @@ +package io.quarkus.sbom; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ApplicationManifest { + + public static ApplicationManifest fromConfig(ApplicationManifestConfig config) { + if (config.getDistributionDirectory() != null) { + var builder = ApplicationManifestConfig.builder() + .setDistributionDirectory(config.getDistributionDirectory()) + .setMainComponent(config.getMainComponent()) + .setRunnerPath(config.getRunnerPath()); + for (var c : config.getComponents()) { + builder.addComponent(c); + } + addRemainingContent(config, builder); + config = builder.build(); + } + var builder = ApplicationManifest.builder(); + builder.setMainComponent(config.getMainComponent()) + .setRunnerPath(config.getRunnerPath()); + for (var c : config.getComponents()) { + builder.addComponent(c); + } + return builder.build(); + } + + private static void addRemainingContent(ApplicationManifestConfig config, ApplicationManifestConfig.Builder builder) { + try { + Files.walkFileTree(config.getDistributionDirectory(), new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + builder.addComponent(ApplicationComponent.builder().setPath(file)); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends ApplicationManifest { + + private Builder() { + super(); + } + + private Builder(ApplicationManifest manifest) { + super(manifest); + } + + public Builder setMainComponent(ApplicationComponent main) { + this.mainComponent = main; + return this; + } + + public Builder addComponent(ApplicationComponent component) { + if (component == null) { + throw new IllegalArgumentException("component is null"); + } + if (components == null) { + components = new ArrayList<>(); + } + components.add(component); + return this; + } + + public Builder setRunnerPath(Path runnerPath) { + this.runnerPath = runnerPath; + return this; + } + + public ApplicationManifest build() { + return new ApplicationManifest(this); + } + } + + protected ApplicationComponent mainComponent; + protected Collection components; + protected Path runnerPath; + + private ApplicationManifest() { + } + + private ApplicationManifest(ApplicationManifest builder) { + if (builder.mainComponent == null) { + throw new IllegalArgumentException("Main component is null"); + } + this.mainComponent = builder.mainComponent.ensureImmutable(); + if (builder.components == null || builder.components.isEmpty()) { + this.components = List.of(); + } else { + var tmp = new ApplicationComponent[builder.components.size()]; + int i = 0; + for (var c : builder.components) { + tmp[i++] = c.ensureImmutable(); + } + this.components = List.of(tmp); + } + this.runnerPath = builder.runnerPath; + } + + public ApplicationComponent getMainComponent() { + return mainComponent; + } + + public Collection getComponents() { + return components == null ? List.of() : components; + } + + public Path getRunnerPath() { + return runnerPath; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifestConfig.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifestConfig.java new file mode 100644 index 0000000000000..f350cd7e7c6b0 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifestConfig.java @@ -0,0 +1,194 @@ +package io.quarkus.sbom; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.maven.dependency.ArtifactKey; + +public class ApplicationManifestConfig { + + public static Builder builder() { + return new ApplicationManifestConfig().new Builder(); + } + + public class Builder { + + private class ComponentHolder { + private Path path; + private ApplicationComponent component; + + private ComponentHolder(ApplicationComponent component) { + this.component = component; + this.path = component.getPath(); + } + } + + private boolean built; + private ComponentHolder main; + private Map compArtifacts = new HashMap<>(); + private Map compPaths = new HashMap<>(); + private List compList = new ArrayList<>(); + + private Builder() { + } + + public Builder setApplicationModel(ApplicationModel model) { + setMainComponent(ApplicationComponent.builder().setResolvedDependency(model.getAppArtifact()).build()); + for (var d : model.getDependencies()) { + addComponent(ApplicationComponent.builder() + .setResolvedDependency(d) + .setPath(d.getResolvedPaths().iterator().next()) + .build()); + } + return this; + } + + public Builder setMainComponent(ApplicationComponent applicationRunner) { + ensureNotBuilt(); + main = applicationRunner == null ? null : new ComponentHolder(applicationRunner); + return this; + } + + public Builder setDistributionDirectory(Path distributionDirectory) { + ensureNotBuilt(); + distDir = distributionDirectory; + return this; + } + + public Builder setRunnerPath(Path runnerPath) { + ensureNotBuilt(); + ApplicationManifestConfig.this.runnerPath = runnerPath; + return this; + } + + public Builder addComponent(ApplicationComponent component) { + ComponentHolder holder = null; + if (component.getPath() != null) { + holder = compPaths.get(component.getPath()); + } + if (holder == null && component.getResolvedDependency() != null) { + holder = compArtifacts.get(component.getResolvedDependency().getKey()); + } + if (holder == null) { + holder = new ComponentHolder(component); + if (holder.path != null) { + compPaths.put(holder.path, holder); + } + if (holder.component.getResolvedDependency() != null) { + compArtifacts.put(holder.component.getResolvedDependency().getKey(), holder); + } + compList.add(holder); + } else { + if (component.getPath() != null) { + if (holder.path != null) { + compPaths.remove(holder.path); + } + holder.path = component.getPath(); + compPaths.put(holder.path, holder); + } + if (holder.component.getResolvedDependency() == null && component.getResolvedDependency() != null) { + holder.component = component; + compArtifacts.put(holder.component.getResolvedDependency().getKey(), holder); + } + if (component.getPedigree() != null) { + if (holder.component.getPedigree() == null) { + holder.component.pedigree = component.getPedigree(); + } else if (!holder.component.getPedigree().contains(component.getPedigree())) { + holder.component.pedigree += System.lineSeparator() + component.getPedigree(); + } + } + if (component.getScope() != null) { + holder.component.scope = component.scope; + } + } + return this; + } + + public ApplicationManifestConfig build() { + ensureNotBuilt(); + Objects.requireNonNull(main, "mainComponent is null"); + built = true; + if (!compList.isEmpty()) { + ApplicationManifestConfig.this.components = new ArrayList<>(compList.size()); + for (var holder : compList) { + if (holder.path != null && !holder.path.equals(holder.component.getPath())) { + holder.component = ApplicationComponent.builder() + .setPath(holder.path) + .setResolvedDependency(holder.component.getResolvedDependency()) + .setPedigree(holder.component.getPedigree()); + } + if (distDir != null) { + setDistributionPath(holder, false); + } + ApplicationManifestConfig.this.components.add(holder.component.ensureImmutable()); + } + } + if (distDir != null) { + setDistributionPath(main, true); + } + ApplicationManifestConfig.this.main = main.component.ensureImmutable(); + return ApplicationManifestConfig.this; + } + + private void setDistributionPath(ComponentHolder holder, boolean main) { + ApplicationComponent c = holder.component; + if (c.getDistributionPath() == null + && c.getPath() != null && c.getPath().startsWith(distDir)) { + final ApplicationComponent.Builder builder; + if (c instanceof ApplicationComponent.Builder) { + builder = (ApplicationComponent.Builder) c; + } else { + builder = new ApplicationComponent.Builder(c); + if (!main) { + holder.component = builder; + } + } + var relativePath = distDir.relativize(c.getPath()); + var sb = new StringBuilder(); + for (var i = 0; i < relativePath.getNameCount(); ++i) { + if (i > 0) { + sb.append("/"); + } + sb.append(relativePath.getName(i)); + } + builder.setDistributionPath(sb.toString()); + } + } + + private void ensureNotBuilt() { + if (built) { + throw new RuntimeException("This builder instance has already been built"); + } + } + } + + private ApplicationManifestConfig() { + } + + private Path distDir; + private Path runnerPath; + private ApplicationComponent main; + private List components = List.of(); + + public Path getDistributionDirectory() { + return distDir; + } + + public Path getRunnerPath() { + return runnerPath; + } + + public ApplicationComponent getMainComponent() { + return main; + } + + public Collection getComponents() { + return components; + } +} diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/DependencyInfoProvider.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/DependencyInfoProvider.java new file mode 100644 index 0000000000000..467176c6b88d6 --- /dev/null +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/DependencyInfoProvider.java @@ -0,0 +1,16 @@ +package io.quarkus.bootstrap.app; + +import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; + +public interface DependencyInfoProvider { + + static DependencyInfoProviderBuilder builder() { + return new DependencyInfoProviderBuilder(); + } + + default String getId() { + return "default"; + } + + EffectiveModelResolver getMavenModelResolver(); +} diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/DependencyInfoProviderBuilder.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/DependencyInfoProviderBuilder.java new file mode 100644 index 0000000000000..cd53a65e074bc --- /dev/null +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/DependencyInfoProviderBuilder.java @@ -0,0 +1,23 @@ +package io.quarkus.bootstrap.app; + +import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; + +public class DependencyInfoProviderBuilder { + + private EffectiveModelResolver mavenModelResolver; + + public DependencyInfoProviderBuilder setMavenModelResolver(EffectiveModelResolver mavenModelResolver) { + this.mavenModelResolver = mavenModelResolver; + return this; + } + + public DependencyInfoProvider build() { + var mmr = mavenModelResolver; + return new DependencyInfoProvider() { + @Override + public EffectiveModelResolver getMavenModelResolver() { + return mmr; + } + }; + } +} diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/JarResult.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/JarResult.java index d57671f72828d..d763872be763f 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/JarResult.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/JarResult.java @@ -1,6 +1,7 @@ package io.quarkus.bootstrap.app; import java.nio.file.Path; +import java.util.Collection; public final class JarResult { @@ -9,13 +10,20 @@ public final class JarResult { private final Path libraryDir; private final boolean mutable; private final String classifier; + private final Collection sboms; public JarResult(Path path, Path originalArtifact, Path libraryDir, boolean mutable, String classifier) { + this(path, originalArtifact, libraryDir, mutable, classifier, null); + } + + public JarResult(Path path, Path originalArtifact, Path libraryDir, boolean mutable, String classifier, + Collection sboms) { this.path = path; this.originalArtifact = originalArtifact; this.libraryDir = libraryDir; this.mutable = mutable; this.classifier = classifier; + this.sboms = sboms; } public boolean isUberJar() { @@ -41,4 +49,8 @@ public boolean mutable() { public String getClassifier() { return classifier; } + + public Collection getSboms() { + return sboms; + } } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java index 70ba824e22dce..966629bcc767e 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java @@ -10,6 +10,7 @@ import java.util.Objects; import java.util.Properties; import java.util.Set; +import java.util.function.Supplier; import io.quarkus.bootstrap.BootstrapAppModelFactory; import io.quarkus.bootstrap.BootstrapException; @@ -91,6 +92,7 @@ public class QuarkusBootstrap implements Serializable { private final boolean assertionsEnabled; private final boolean defaultFlatTestClassPath; private final Collection parentFirstArtifacts; + private final Supplier depInfoProvider; private QuarkusBootstrap(Builder builder) { this.applicationRoot = builder.applicationRoot; @@ -123,6 +125,7 @@ private QuarkusBootstrap(Builder builder) { this.hostApplicationIsTestOnly = builder.hostApplicationIsTestOnly; this.defaultFlatTestClassPath = builder.flatClassPath; this.parentFirstArtifacts = builder.parentFirstArtifacts; + this.depInfoProvider = builder.depInfoProvider; } public CuratedApplication bootstrap() throws BootstrapException { @@ -295,6 +298,10 @@ public boolean isTest() { return test; } + public Supplier getDependencyInfoProvider() { + return depInfoProvider; + } + public static class Builder { public List classLoadListeners = new ArrayList<>(); public boolean hostApplicationIsTestOnly; @@ -326,6 +333,7 @@ public static class Builder { final Set localArtifacts = new HashSet<>(); boolean auxiliaryApplication; List parentFirstArtifacts = new ArrayList<>(); + Supplier depInfoProvider; public Builder() { } @@ -537,6 +545,11 @@ private boolean inheritedAssertionsEnabled() { return result; } + public Builder setDependencyInfoProvider(Supplier depInfoProvider) { + this.depInfoProvider = depInfoProvider; + return this; + } + public QuarkusBootstrap build() { Objects.requireNonNull(applicationRoot, "Application root must not be null"); if (appArtifact != null) { diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/SbomResult.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/SbomResult.java new file mode 100644 index 0000000000000..ff3c740d89b6b --- /dev/null +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/SbomResult.java @@ -0,0 +1,47 @@ +package io.quarkus.bootstrap.app; + +import java.nio.file.Path; + +public class SbomResult { + + private final Path sbomFile; + private final String sbomSpec; + private final String sbomSpecVersion; + private final String format; + private final String classifier; + private final Path appRunner; + + public SbomResult(Path sbomFile, String sbomSpec, String sbomSpecVersion, String format, String classifier, + Path appRunner) { + this.sbomFile = sbomFile; + this.sbomSpec = sbomSpec; + this.sbomSpecVersion = sbomSpecVersion; + this.format = format; + this.classifier = classifier; + this.appRunner = appRunner; + } + + public Path getSbomFile() { + return sbomFile; + } + + public String getSbomSpec() { + return sbomSpec; + } + + public String getSbomSpecVersion() { + return sbomSpecVersion; + } + + public String getFormat() { + return format; + } + + public String getClassifier() { + return classifier; + } + + public Path getApplicationRunner() { + return appRunner; + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java index 050846b40ae72..6893d1602abe5 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java @@ -288,7 +288,7 @@ private ApplicationModel doResolveModel(ArtifactCoords coords, } List aggregatedRepos = mvn.aggregateRepositories(managedRepos, mvn.getRepositories()); - final ResolvedDependencyBuilder appArtifact = resolve(coords, mvnArtifact, aggregatedRepos); + final ResolvedDependencyBuilder appArtifact = resolve(coords, mvnArtifact, aggregatedRepos).setRuntimeCp(); mvnArtifact = toAetherArtifact(appArtifact); final ArtifactDescriptorResult appArtifactDescr = resolveDescriptor(mvnArtifact, aggregatedRepos); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DeffaultEffectiveModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DeffaultEffectiveModelResolver.java new file mode 100644 index 0000000000000..1e7eee0c08ae6 --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DeffaultEffectiveModelResolver.java @@ -0,0 +1,218 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.model.Model; +import org.apache.maven.model.Parent; +import org.apache.maven.model.Repository; +import org.apache.maven.model.RepositoryPolicy; +import org.apache.maven.model.building.*; +import org.apache.maven.model.resolution.ModelResolver; +import org.eclipse.aether.DefaultRepositoryCache; +import org.eclipse.aether.RepositoryCache; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactResult; + +import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; +import io.quarkus.bootstrap.resolver.maven.workspace.LocalWorkspace; +import io.quarkus.bootstrap.resolver.maven.workspace.ModelUtils; +import io.quarkus.maven.dependency.ArtifactCoords; + +class DefaultEffectiveModelResolver implements EffectiveModelResolver { + + private final MavenArtifactResolver resolver; + private final ModelBuilder modelBuilder; + private final ModelCache modelCache; + private final Map effectiveModels = new HashMap<>(); + + DefaultEffectiveModelResolver(MavenArtifactResolver resolver) { + this.resolver = resolver; + try { + modelCache = new BootstrapModelCache(resolver.getMavenContext().getRepositorySystemSession()); + } catch (BootstrapMavenException e) { + throw new RuntimeException("Failed to initialize Maven model resolver", e); + } + modelBuilder = BootstrapModelBuilderFactory.getDefaultModelBuilder(); + } + + public Model resolveEffectiveModel(ArtifactCoords coords) { + return resolveEffectiveModel(coords, List.of()); + } + + public Model resolveEffectiveModel(ArtifactCoords coords, List repos) { + + if (!ArtifactCoords.TYPE_POM.equals(coords.getType())) { + coords = ArtifactCoords.pom(coords.getGroupId(), coords.getArtifactId(), coords.getVersion()); + } + + var cached = effectiveModels.get(coords); + if (cached != null) { + return cached; + } + + final LocalWorkspace ws = resolver.getMavenContext().getWorkspace(); + if (ws != null) { + final LocalProject project = ws.getProject(coords.getGroupId(), coords.getArtifactId()); + if (project != null && coords.getVersion().equals(project.getVersion()) + && project.getModelBuildingResult() != null) { + return project.getModelBuildingResult().getEffectiveModel(); + } + } + + final File pomFile; + final ArtifactResult pomResult; + try { + pomResult = resolver.resolve(new DefaultArtifact(coords.getGroupId(), coords.getArtifactId(), + coords.getClassifier(), coords.getType(), coords.getVersion()), repos); + pomFile = pomResult.getArtifact().getFile(); + } catch (BootstrapMavenException e) { + throw new RuntimeException("Failed to resolve " + coords.toCompactCoords(), e); + } + + final Model rawModel; + try { + rawModel = ModelUtils.readModel(pomFile.toPath()); + } catch (IOException e1) { + throw new RuntimeException("Failed to read " + pomFile, e1); + } + + final ModelResolver modelResolver; + try { + modelResolver = BootstrapModelResolver.newInstance(resolver.getMavenContext(), null); + } catch (BootstrapMavenException e) { + throw new RuntimeException("Failed to initialize model resolver", e); + } + + // override the relative path to the parent in case it's in the local Maven repo + Parent parent = rawModel.getParent(); + if (parent != null) { + final Artifact parentPom = new DefaultArtifact(parent.getGroupId(), parent.getArtifactId(), + ArtifactCoords.TYPE_POM, parent.getVersion()); + final ArtifactResult parentResult; + final Path parentPomPath; + try { + parentResult = resolver.resolve(parentPom, repos); + parentPomPath = parentResult.getArtifact().getFile().toPath(); + } catch (BootstrapMavenException e) { + throw new RuntimeException("Failed to resolve " + parentPom, e); + } + rawModel.getParent().setRelativePath(pomFile.toPath().getParent().relativize(parentPomPath).toString()); + + String repoUrl = null; + for (RemoteRepository r : repos) { + if (r.getId().equals(parentResult.getRepository().getId())) { + repoUrl = r.getUrl(); + break; + } + } + if (repoUrl != null) { + Repository modelRepo = null; + for (Repository r : rawModel.getRepositories()) { + if (r.getId().equals(parentResult.getRepository().getId())) { + modelRepo = r; + break; + } + } + if (modelRepo == null) { + modelRepo = new Repository(); + modelRepo.setId(parentResult.getRepository().getId()); + modelRepo.setLayout("default"); + modelRepo.setReleases(new RepositoryPolicy()); + } + modelRepo.setUrl(repoUrl); + + try { + modelResolver.addRepository(modelRepo, false); + } catch (Exception e) { + throw new RuntimeException("Failed to add repository " + modelRepo, e); + } + } + } + + final ModelBuildingRequest req = new DefaultModelBuildingRequest(); + req.setPomFile(pomFile); + req.setRawModel(rawModel); + req.setModelResolver(modelResolver); + req.setSystemProperties(System.getProperties()); + req.setUserProperties(System.getProperties()); + req.setModelCache(modelCache); + + try { + return modelBuilder.build(req).getEffectiveModel(); + } catch (ModelBuildingException e) { + throw new RuntimeException("Failed to resolve the effective model of " + coords.toCompactCoords(), e); + } + } + + static class BootstrapModelCache implements ModelCache { + + private final RepositorySystemSession session; + + private final RepositoryCache cache; + + BootstrapModelCache(RepositorySystemSession session) { + this.session = session; + this.cache = session.getCache() == null ? new DefaultRepositoryCache() : session.getCache(); + } + + @Override + public Object get(String groupId, String artifactId, String version, String tag) { + return cache.get(session, new Key(groupId, artifactId, version, tag)); + } + + @Override + public void put(String groupId, String artifactId, String version, String tag, Object data) { + cache.put(session, new Key(groupId, artifactId, version, tag), data); + } + + static class Key { + + private final String groupId; + private final String artifactId; + private final String version; + private final String tag; + private final int hash; + + public Key(String groupId, String artifactId, String version, String tag) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.tag = tag; + + int h = 17; + h = h * 31 + this.groupId.hashCode(); + h = h * 31 + this.artifactId.hashCode(); + h = h * 31 + this.version.hashCode(); + h = h * 31 + this.tag.hashCode(); + hash = h; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (null == obj || !getClass().equals(obj.getClass())) { + return false; + } + + Key that = (Key) obj; + return artifactId.equals(that.artifactId) && groupId.equals(that.groupId) + && version.equals(that.version) && tag.equals(that.tag); + } + + @Override + public int hashCode() { + return hash; + } + } + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/EffectiveModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/EffectiveModelResolver.java new file mode 100644 index 0000000000000..8cd3ecc41e77a --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/EffectiveModelResolver.java @@ -0,0 +1,21 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.util.List; + +import org.apache.maven.model.Model; +import org.eclipse.aether.repository.RemoteRepository; + +import io.quarkus.maven.dependency.ArtifactCoords; + +public interface EffectiveModelResolver { + + static EffectiveModelResolver of(MavenArtifactResolver resolver) { + return new DefaultEffectiveModelResolver(resolver); + } + + default Model resolveEffectiveModel(ArtifactCoords coords) { + return resolveEffectiveModel(coords, List.of()); + } + + Model resolveEffectiveModel(ArtifactCoords coords, List repos); +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalPomResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalPomResolver.java new file mode 100644 index 0000000000000..21daf83627f20 --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalPomResolver.java @@ -0,0 +1,8 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.io.File; + +interface LocalPomResolver { + + File resolvePom(String groupId, String artifactId, String version); +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalRepoModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalRepoModelResolver.java new file mode 100644 index 0000000000000..999c410937aff --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalRepoModelResolver.java @@ -0,0 +1,93 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.List; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Parent; +import org.apache.maven.model.Repository; +import org.apache.maven.model.building.ModelSource; +import org.apache.maven.model.building.ModelSource2; +import org.apache.maven.model.resolution.InvalidRepositoryException; +import org.apache.maven.model.resolution.ModelResolver; +import org.apache.maven.model.resolution.UnresolvableModelException; + +class LocalRepoModelResolver implements ModelResolver, LocalPomResolver { + + static LocalRepoModelResolver of(LocalPomResolver... pomResolvers) { + return new LocalRepoModelResolver(List.of(pomResolvers)); + } + + private final List pomResolvers; + + LocalRepoModelResolver(List pomResolvers) { + this.pomResolvers = pomResolvers; + } + + @Override + public File resolvePom(String groupId, String artifactId, String version) { + for (LocalPomResolver pomResolver : pomResolvers) { + var pom = pomResolver.resolvePom(groupId, artifactId, version); + if (pom != null) { + return pom; + } + } + return null; + } + + @Override + public ModelSource resolveModel(String groupId, String artifactId, String version) throws UnresolvableModelException { + var pomXml = resolvePom(groupId, artifactId, version); + if (pomXml == null) { + throw new UnresolvableModelException("Has not been previously resolved", groupId, artifactId, version); + } + return new ModelSource2() { + @Override + public InputStream getInputStream() throws IOException { + return new FileInputStream(pomXml); + } + + @Override + public String getLocation() { + return pomXml.getAbsolutePath(); + } + + @Override + public ModelSource2 getRelatedSource(String relPath) { + return null; + } + + @Override + public URI getLocationURI() { + return null; + } + }; + } + + @Override + public ModelSource resolveModel(Parent parent) throws UnresolvableModelException { + return resolveModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion()); + } + + @Override + public ModelSource resolveModel(Dependency dependency) throws UnresolvableModelException { + return resolveModel(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()); + } + + @Override + public void addRepository(Repository repository) throws InvalidRepositoryException { + } + + @Override + public void addRepository(Repository repository, boolean replace) throws InvalidRepositoryException { + } + + @Override + public ModelResolver newCopy() { + return this; + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalRepositoryEffectiveModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalRepositoryEffectiveModelResolver.java new file mode 100644 index 0000000000000..bbfaae30fc98e --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/LocalRepositoryEffectiveModelResolver.java @@ -0,0 +1,51 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.io.File; +import java.util.List; + +import org.apache.maven.model.Model; +import org.apache.maven.model.building.DefaultModelBuilder; +import org.apache.maven.model.building.DefaultModelBuilderFactory; +import org.apache.maven.model.building.DefaultModelBuildingRequest; +import org.apache.maven.model.building.ModelBuildingException; +import org.apache.maven.model.building.ModelBuildingRequest; +import org.eclipse.aether.repository.RemoteRepository; + +import io.quarkus.maven.dependency.ArtifactCoords; + +class LocalRepositoryEffectiveModelResolver implements EffectiveModelResolver { + + private final LocalRepoModelResolver modelResolver; + + LocalRepositoryEffectiveModelResolver(File localRepoDir) { + modelResolver = LocalRepoModelResolver.of(new MavenLocalPomResolver(localRepoDir)); + } + + @Override + public Model resolveEffectiveModel(ArtifactCoords coords) { + return resolveEffectiveModel(coords, List.of()); + } + + @Override + public Model resolveEffectiveModel(ArtifactCoords coords, List repos) { + File pom = modelResolver.resolvePom(coords.getGroupId(), coords.getArtifactId(), coords.getVersion()); + if (pom == null) { + return null; + } + ModelBuildingRequest req = new DefaultModelBuildingRequest(); + req.setModelResolver(modelResolver); + req.setPomFile(pom); + req.getSystemProperties().putAll(System.getProperties()); + req.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + + // execute the model building request + DefaultModelBuilderFactory factory = new DefaultModelBuilderFactory(); + DefaultModelBuilder builder = factory.newInstance(); + try { + return builder.build(req).getEffectiveModel(); + } catch (ModelBuildingException e) { + throw new RuntimeException("An error occurred attempting to resolve effective POM", e); + } + } + +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenLocalPomResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenLocalPomResolver.java new file mode 100644 index 0000000000000..3875a934023f1 --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenLocalPomResolver.java @@ -0,0 +1,27 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.io.File; +import java.util.Objects; + +public class MavenLocalPomResolver implements LocalPomResolver { + + private static String getRelativePomPath(String groupId, String artifactId, String version) { + return groupId.replace('.', File.separatorChar) + File.separator + + artifactId + File.separator + + version + File.separator + + artifactId + '-' + + version + ".pom"; + } + + private final File repoDir; + + public MavenLocalPomResolver(File repoDir) { + this.repoDir = Objects.requireNonNull(repoDir); + } + + @Override + public File resolvePom(String groupId, String artifactId, String version) { + var pom = new File(repoDir, getRelativePomPath(groupId, artifactId, version)); + return pom.exists() ? pom : null; + } +}