6464import static org .twdata .maven .mojoexecutor .MojoExecutor .*;
6565
6666/**
67- * Generates an enhanced Software Bill of Materials (SBOM) for Native Image consumption and refinement .
67+ * Generates a Software Bill of Materials (SBOM) that is augmented and refined by Native Image .
6868 * <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 AddedComponentFields}):
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.
69+ * Approach:
70+ * 1. The cyclonedx-maven-plugin creates a baseline SBOM.
71+ * 2. The components of the baseline SBOM are updated with additional metadata, most importantly being the set of package
72+ * names associated with the component (see {@link AddedComponentFields} for all additional metadata).
73+ * 3. The SBOM is stored at a known location.
74+ * 4. Native Image processes the SBOM and removes unreachable components and unnecessary dependencies.
8075 * <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.
76+ * Creating the package-name-to-component mapping in the context of Native Image, without the knowledge known at the
77+ * plugin build-time is difficult, which was the primary motivation for realizing this approach.
8378 * <p>
8479 * Benefits:
8580 * * 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.
81+ * * Enhanced Accuracy: Native Image augments and refines the SBOM, potentially significantly improving its accuracy.
8882 */
8983final public class SBOMGenerator {
9084 private final MavenProject mavenProject ;
@@ -94,12 +88,24 @@ final public class SBOMGenerator {
9488 private final String mainClass ;
9589 private final Logger logger ;
9690
91+ private static final String cycloneDXPluginName = "cyclonedx-maven-plugin" ;
9792 private static final String SBOM_NAME = "WIP_SBOM" ;
9893 private static final String FILE_FORMAT = "json" ;
9994
10095 private static final class AddedComponentFields {
96+ /**
97+ * The package names associated with this component.
98+ */
10199 static final String packageNames = "packageNames" ;
100+ /**
101+ * The path to the jar containing the class files. For a component embedded in a shaded jar, the path must
102+ * be pointing to the shaded jar.
103+ */
102104 static final String jarPath = "jarPath" ;
105+ /**
106+ * If set to false, then this component and all its transitive dependencies SHOULD NOT be pruned by Native Image.
107+ * This is set to false when the package names could not be derived accurately.
108+ */
103109 static final String prunable = "prunable" ;
104110 }
105111
@@ -124,15 +130,16 @@ public SBOMGenerator(
124130 * @throws MojoExecutionException if SBOM creation fails.
125131 */
126132 public void generate () throws MojoExecutionException {
133+ String outputDirectory = mavenProject .getBuild ().getDirectory ();
134+ Path sbomPath = Paths .get (outputDirectory , SBOM_NAME + "." + FILE_FORMAT );
127135 try {
128- String outputDirectory = mavenProject .getBuild ().getDirectory ();
129136 /* Suppress the output from the cyclonedx-maven-plugin. */
130137 int loggingLevel = logger .getThreshold ();
131138 logger .setThreshold (Logger .LEVEL_DISABLED );
132139 executeMojo (
133140 plugin (
134141 groupId ("org.cyclonedx" ),
135- artifactId ("cyclonedx-maven-plugin" ),
142+ artifactId (cycloneDXPluginName ),
136143 version ("2.8.1" )
137144 ),
138145 goal ("makeAggregateBom" ),
@@ -146,46 +153,77 @@ public void generate() throws MojoExecutionException {
146153 );
147154 logger .setThreshold (loggingLevel );
148155
149- Path sbomPath = Paths . get ( outputDirectory , SBOM_NAME + "." + FILE_FORMAT );
156+
150157 if (!Files .exists (sbomPath )) {
151158 return ;
152159 }
153160
161+ // TODO: debugging, remove before merge
162+ Path unmodifiedPath = Paths .get (outputDirectory , "SBOM_UNMODIFIED.json" );
163+ Files .deleteIfExists (unmodifiedPath );
164+ Files .copy (sbomPath , unmodifiedPath );
165+
154166 var resolver = new ArtifactToPackageNameResolver (mavenProject , repositorySystem , mavenSession .getRepositorySession (), mainClass );
155- Set <ArtifactAdapter > artifactsWithPackageNames = resolver .getArtifactPackageMappings ();
156- augmentSBOM (sbomPath , artifactsWithPackageNames );
167+ Set <ArtifactAdapter > artifacts = resolver .getArtifactAdapters ();
168+ augmentSBOM (sbomPath , artifacts );
169+
170+ // TODO: debugging, remove before merge
171+ Path testPath = Paths .get (outputDirectory , "SBOM_AUGMENTED.json" );
172+ Files .deleteIfExists (testPath );
173+ Files .copy (sbomPath , testPath );
174+
157175 } catch (Exception exception ) {
176+ deleteFileIfExists (sbomPath );
158177 String errorMsg = String .format ("Failed to create SBOM. Please try again and report this issue if it persists. " +
159178 "To bypass this failure, disable SBOM generation by setting %s to false." , NativeCompileNoForkMojo .enableSBOMParamName );
160179 throw new MojoExecutionException (errorMsg , exception );
161180 }
162181 }
163182
164- private void augmentSBOM (Path sbomPath , Set <ArtifactAdapter > artifactToPackageNames ) throws IOException {
183+ private static void deleteFileIfExists (Path sbomPath ) {
184+ try {
185+ Files .deleteIfExists (sbomPath );
186+ } catch (IOException e ) {
187+ /* Failed to delete file. */
188+ }
189+ }
190+
191+ /**
192+ * Augments the base SBOM with information from the derived {@param artifacts}.
193+ *
194+ * @param baseSBOMPath path to the base SBOM generated by the cyclonedx plugin.
195+ * @param artifacts artifacts that possibly have been extended with package name data.
196+ */
197+ private void augmentSBOM (Path baseSBOMPath , Set <ArtifactAdapter > artifacts ) throws IOException {
165198 ObjectMapper objectMapper = new ObjectMapper ();
166- ObjectNode sbomJson = (ObjectNode ) objectMapper .readTree (Files .newInputStream (sbomPath ));
199+ ObjectNode sbomJson = (ObjectNode ) objectMapper .readTree (Files .newInputStream (baseSBOMPath ));
167200
168201 ArrayNode componentsArray = (ArrayNode ) sbomJson .get ("components" );
169202 if (componentsArray == null ) {
170- return ;
203+ throw new RuntimeException ( String . format ( "SBOM generated by %s contained no components." , cycloneDXPluginName )) ;
171204 }
172205
173- /*
174- * Iterates over the components and finds the associated artifact by equality checks of the GAV coordinates.
175- * If a match is found, the component is augmented.
176- */
177- componentsArray .forEach (componentNode -> augmentComponentNode (componentNode , artifactToPackageNames , objectMapper ));
206+ /* Augment the "components" */
207+ componentsArray .forEach (componentNode -> augmentComponentNode (componentNode , artifacts , objectMapper ));
178208
179209 /* Augment the main component in "metadata/component" */
180210 JsonNode metadataNode = sbomJson .get ("metadata" );
181211 if (metadataNode != null && metadataNode .has ("component" )) {
182- augmentComponentNode (metadataNode .get ("component" ), artifactToPackageNames , objectMapper );
212+ augmentComponentNode (metadataNode .get ("component" ), artifacts , objectMapper );
183213 }
184214
185215 /* Save the augmented SBOM back to the file */
186- objectMapper .writerWithDefaultPrettyPrinter ().writeValue (Files .newOutputStream (sbomPath ), sbomJson );
216+ objectMapper .writerWithDefaultPrettyPrinter ().writeValue (Files .newOutputStream (baseSBOMPath ), sbomJson );
187217 }
188218
219+ /**
220+ * Updates the {@param componentNode} with {@link AddedComponentFields} from the artifact in {@param artifactsWithPackageNames}
221+ * with matching GAV coordinates.
222+ *
223+ * @param componentNode the node in the base SBOM that should be augmented.
224+ * @param artifactsWithPackageNames the artifact with information for {@link AddedComponentFields}.
225+ * @param objectMapper the objectMapper that is used to write the updates.
226+ */
189227 private void augmentComponentNode (JsonNode componentNode , Set <ArtifactAdapter > artifactsWithPackageNames , ObjectMapper objectMapper ) {
190228 String groupField = "group" ;
191229 String nameField = "name" ;
0 commit comments