Skip to content

Commit d0581ed

Browse files
committed
Improve SBOM documentation and code quality
1 parent 69ddf1d commit d0581ed

File tree

3 files changed

+175
-90
lines changed

3 files changed

+175
-90
lines changed

native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/sbom/ArtifactToPackageNameResolver.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,40 @@ final class ArtifactToPackageNameResolver {
7171
this.shadedPackageNameResolver = new ShadedPackageNameResolver(mavenProject, mainClass);
7272
}
7373

74-
Set<ArtifactAdapter> getArtifactPackageMappings() throws Exception {
74+
/**
75+
* Maps the artifacts of the maven project to {@link ArtifactAdapter}s. {@link ArtifactAdapter#packageNames} will
76+
* be non-empty if package names could accurately be derived for an artifact. If not, it will be non-empty and
77+
* {@link ArtifactAdapter#prunable} will be set to false. {@link ArtifactAdapter#prunable} will also be set to
78+
* false if an artifact is not the main artifact and its part of a shaded jar.
79+
*
80+
* @return the artifacts of this project as {@link ArtifactAdapter}s.
81+
* @throws Exception if an error was encountered when deriving the artifacts.
82+
*/
83+
Set<ArtifactAdapter> getArtifactAdapters() throws Exception {
7584
Set<ArtifactAdapter> artifactsWithPackageNameMappings = new HashSet<>();
7685
List<Artifact> artifacts = new ArrayList<>(mavenProject.getArtifacts());
7786
/* Purposefully add the project artifact last. This is important for the resolution of shaded jars. */
7887
artifacts.add(mavenProject.getArtifact());
7988
for (Artifact artifact : artifacts) {
8089
Optional<ArtifactAdapter> optionalArtifact = resolvePackageNamesFromArtifact(artifact);
81-
optionalArtifact.ifPresent(artifactsWithPackageNameMappings::add);
90+
if (optionalArtifact.isPresent()) {
91+
artifactsWithPackageNameMappings.add(optionalArtifact.get());
92+
} else {
93+
/* If resolve failed, then there are no package name mappings, so we mark it as not prunable. */
94+
var artifactAdapter = ArtifactAdapter.fromMavenArtifact(artifact);
95+
artifactAdapter.prunable = false;
96+
artifactsWithPackageNameMappings.add(artifactAdapter);
97+
}
8298
}
8399

100+
/*
101+
* Currently we cannot ensure that package name are derived accurately for shaded dependencies.
102+
* Thus, we mark such artifacts as non-prunable.
103+
*/
84104
Set<ArtifactAdapter> dependencies = artifactsWithPackageNameMappings.stream()
85105
.filter(v -> !v.equals(mavenProject.getArtifact()))
86106
.collect(Collectors.toSet());
87-
ShadedPackageNameResolver.markShadedDependencies(dependencies);
107+
ShadedPackageNameResolver.markShadedArtifactsAsNonPrunable(dependencies);
88108
return artifactsWithPackageNameMappings;
89109
}
90110

native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/sbom/SBOMGenerator.java

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -64,27 +64,21 @@
6464
import 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
*/
8983
final 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

Comments
 (0)