1
1
/*
2
- * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
2
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3
3
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4
4
*
5
5
* The Universal Permissive License (UPL), Version 1.0
38
38
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39
39
* SOFTWARE.
40
40
*/
41
-
42
41
package org .graalvm .buildtools .maven .sbom ;
43
42
44
43
import com .fasterxml .jackson .databind .JsonNode ;
50
49
import org .apache .maven .plugin .MojoExecutionException ;
51
50
import org .apache .maven .project .MavenProject ;
52
51
import org .eclipse .aether .RepositorySystem ;
52
+ import org .graalvm .buildtools .maven .NativeCompileNoForkMojo ;
53
53
54
54
import java .io .IOException ;
55
55
import java .nio .file .Files ;
63
63
import static java .nio .file .StandardCopyOption .REPLACE_EXISTING ;
64
64
import static org .twdata .maven .mojoexecutor .MojoExecutor .*;
65
65
66
- // TODO: produced JSON is not formatted nicely
67
- // TODO: remove debug prints
68
- public class SBOMGenerator {
66
+ /**
67
+ * Generates an enhanced Software Bill of Materials (SBOM) for Native Image consumption and refinement.
68
+ * <p>
69
+ * Process overview:
70
+ * 1. Utilizes the cyclonedx-maven-plugin to create a baseline SBOM.
71
+ * 2. Augments the baseline SBOM components with additional metadata (see {@link AugmentedFields}):
72
+ * * "packageNames": A list of all package names associated with each component.
73
+ * * "jarPath": Path to the component jar.
74
+ * * "prunable": Boolean indicating if the component can be pruned. We currently set this to false for
75
+ * any dependencies to the main component that are shaded.
76
+ * 3. Stores the enhanced SBOM at a known location.
77
+ * 4. Native Image then processes this SBOM during its static analysis:
78
+ * * Unreachable components are removed.
79
+ * * Unnecessary dependency relationships are pruned.
80
+ * <p>
81
+ * Creating the package-name-to-component mapping in the context of Native Image, without any build-system
82
+ * knowledge is difficult, which was the primary motivation for realizing this approach.
83
+ * <p>
84
+ * Benefits:
85
+ * * Great Baseline: Produces an industry-standard SBOM at minimum.
86
+ * * Enhanced Accuracy: Native Image static analysis refines the SBOM,
87
+ * potentially significantly improving its accuracy.
88
+ */
89
+ final public class SBOMGenerator {
69
90
private final MavenProject mavenProject ;
70
91
private final MavenSession mavenSession ;
71
92
private final BuildPluginManager pluginManager ;
@@ -75,6 +96,12 @@ public class SBOMGenerator {
75
96
private static final String SBOM_NAME = "WIP_SBOM" ;
76
97
private static final String FILE_FORMAT = "json" ;
77
98
99
+ private static final class AugmentedFields {
100
+ static final String packageNames = "packageNames" ;
101
+ static final String jarPath = "jarPath" ;
102
+ static final String prunable = "prunable" ;
103
+ }
104
+
78
105
public SBOMGenerator (MavenProject mavenProject , MavenSession mavenSession , BuildPluginManager pluginManager , RepositorySystem repositorySystem , String mainClass ) {
79
106
this .mavenProject = mavenProject ;
80
107
this .mavenSession = mavenSession ;
@@ -83,9 +110,13 @@ public SBOMGenerator(MavenProject mavenProject, MavenSession mavenSession, Build
83
110
this .mainClass = mainClass ;
84
111
}
85
112
113
+ /**
114
+ * Generates an SBOM that will be further augmented by Native Image. The SBOM is stored in the build directory.
115
+ *
116
+ * @throws MojoExecutionException if SBOM creation fails.
117
+ */
86
118
public void generate () throws MojoExecutionException {
87
119
try {
88
- System .out .println ("💣 Generating SBOM..." );
89
120
String outputDirectory = mavenProject .getBuild ().getDirectory ();
90
121
executeMojo (
91
122
plugin (
@@ -104,49 +135,45 @@ public void generate() throws MojoExecutionException {
104
135
);
105
136
106
137
Path sbomPath = Paths .get (outputDirectory , SBOM_NAME + "." + FILE_FORMAT );
138
+ // TODO: remove
139
+ Files .copy (sbomPath , Paths .get (outputDirectory , "SBOM_UNMODIFIED.json" ), REPLACE_EXISTING );
107
140
if (!Files .exists (sbomPath )) {
108
- // TODO: warning instead?
109
- throw new MojoExecutionException ("SBOM file not found at " + sbomPath );
141
+ return ;
110
142
}
111
- // TODO: debugging only
112
- Files .copy (sbomPath , Paths .get (outputDirectory , "SBOM_UNMODIFIED" + "." + FILE_FORMAT ), REPLACE_EXISTING );
113
- System .out .println ("✅ CycloneDX SBOM generated successfully: " + sbomPath );
114
143
115
144
var resolver = new ArtifactToPackageNameResolver (mavenProject , repositorySystem , mavenSession .getRepositorySession (), mainClass );
116
145
Set <ArtifactAdapter > artifactsWithPackageNames = resolver .getArtifactPackageMappings ();
117
- augmentSBOMWithPackageNames (sbomPath , artifactsWithPackageNames );
118
- } catch (Exception e ) {
119
- throw new MojoExecutionException ("Failed to generate and augment SBOM" , e );
146
+ augmentSBOM (sbomPath , artifactsWithPackageNames );
147
+ } catch (Exception exception ) {
148
+ String errorMsg = String .format ("Failed to create SBOM. Please try again and report this issue if it persists. " +
149
+ "To bypass this failure, disable SBOM generation by setting %s to false." , NativeCompileNoForkMojo .enableSBOMParamName );
150
+ throw new MojoExecutionException (errorMsg , exception );
120
151
}
121
152
}
122
153
123
- private void augmentSBOMWithPackageNames (Path sbomPath , Set <ArtifactAdapter > artifactToPackageNames ) throws IOException {
154
+ private void augmentSBOM (Path sbomPath , Set <ArtifactAdapter > artifactToPackageNames ) throws IOException {
124
155
ObjectMapper objectMapper = new ObjectMapper ();
125
156
ObjectNode sbomJson = (ObjectNode ) objectMapper .readTree (Files .newInputStream (sbomPath ));
126
157
127
158
ArrayNode componentsArray = (ArrayNode ) sbomJson .get ("components" );
128
159
if (componentsArray == null ) {
129
- System .out .println ("⚠️ No components found in the SBOM." );
130
160
return ;
131
161
}
132
162
133
163
/*
134
164
* Iterates over the components and finds the associated artifact by equality checks of the GAV coordinates.
135
- * If a match is found, the component is augmented with "packageNames" and "jarPath" fields .
165
+ * If a match is found, the component is augmented.
136
166
*/
137
167
componentsArray .forEach (componentNode -> augmentComponentNode (componentNode , artifactToPackageNames , objectMapper ));
138
168
139
169
/* Augment the main component in "metadata/component" */
140
170
JsonNode metadataNode = sbomJson .get ("metadata" );
141
171
if (metadataNode != null && metadataNode .has ("component" )) {
142
172
augmentComponentNode (metadataNode .get ("component" ), artifactToPackageNames , objectMapper );
143
- } else {
144
- System .out .println ("⚠️ No main component found in metadata." );
145
173
}
146
174
147
175
/* Save the augmented SBOM back to the file */
148
176
objectMapper .writerWithDefaultPrettyPrinter ().writeValue (Files .newOutputStream (sbomPath ), sbomJson );
149
- System .out .println ("✅ SBOM updated successfully with package names." );
150
177
}
151
178
152
179
private void augmentComponentNode (JsonNode componentNode , Set <ArtifactAdapter > artifactsWithPackageNames , ObjectMapper objectMapper ) {
@@ -169,23 +196,15 @@ private void augmentComponentNode(JsonNode componentNode, Set<ArtifactAdapter> a
169
196
ArrayNode packageNamesArray = objectMapper .createArrayNode ();
170
197
List <String > sortedPackageNames = artifact .packageNames .stream ().sorted ().collect (Collectors .toList ());
171
198
sortedPackageNames .forEach (packageNamesArray ::add );
172
- ((ObjectNode ) componentNode ).set (" packageNames" , packageNamesArray );
199
+ ((ObjectNode ) componentNode ).set (AugmentedFields . packageNames , packageNamesArray );
173
200
174
201
String jarPath = "" ;
175
202
if (artifact .jarPath != null ) {
176
203
jarPath = artifact .jarPath .toString ();
177
- } else {
178
- System .out .printf ("⚠️ jarPath is null for %s.%s\n " , artifact .groupId , artifact .artifactId );
179
204
}
180
- ((ObjectNode ) componentNode ).put ("jarPath" , jarPath );
181
- ((ObjectNode ) componentNode ).put ("prunable" , artifact .prunable );
182
-
183
- System .out .printf ("Added package names and jarPath %s to component %s:%s:%s\n " , jarPath , groupId , artifactId , version );
184
- } else {
185
- System .out .printf ("⚠️ No matching artifact found for %s:%s:%s\n " , groupId , artifactId , version );
205
+ ((ObjectNode ) componentNode ).put (AugmentedFields .jarPath , jarPath );
206
+ ((ObjectNode ) componentNode ).put (AugmentedFields .prunable , artifact .prunable );
186
207
}
187
- } else {
188
- System .out .printf ("⚠️ component node missing group or name or version info: %s\n " , componentNode );
189
208
}
190
209
}
191
210
}
0 commit comments