Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure aggregate BOMs contain exact dependency hierarchies from each Maven module #306

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/it/makeAggregateBom/verify.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ assertBomEqualsNonAggregate("impls/impl-B/target/bom")

// dependencies for root component in makeAggregateBom is the list of modules
String bom = new File(basedir, 'target/bom.xml').text
String rootDependencies = bom.substring(bom.indexOf('<dependency ref="pkg:maven/org.cyclonedx.its/makeAggregateBom@1.0-SNAPSHOT?type=pom">'), bom.indexOf('</dependency>') + 13)
assert rootDependencies.contains('<dependency ref="pkg:maven/org.cyclonedx.its/api@1.0-SNAPSHOT?type=jar"/>')
assert rootDependencies.contains('<dependency ref="pkg:maven/org.cyclonedx.its/impls@1.0-SNAPSHOT?type=pom"/>')
assert rootDependencies.contains('<dependency ref="pkg:maven/org.cyclonedx.its/util@1.0-SNAPSHOT?type=jar"/>')
String rootDependencies = bom.substring(bom.indexOf('<dependency ref="pkg:maven/org.cyclonedx.its/makeAggregateBom@1.0-SNAPSHOT?'), bom.indexOf('</dependency>') + 13)
assert rootDependencies.contains('<dependency ref="pkg:maven/org.cyclonedx.its/api@1.0-SNAPSHOT?')
assert rootDependencies.contains('<dependency ref="pkg:maven/org.cyclonedx.its/impls@1.0-SNAPSHOT?')
assert rootDependencies.contains('<dependency ref="pkg:maven/org.cyclonedx.its/util@1.0-SNAPSHOT?')
assert 4 == (rootDependencies =~ /<dependency ref="pkg:maven/).size()
114 changes: 100 additions & 14 deletions src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
import org.cyclonedx.BomGeneratorFactory;
import org.cyclonedx.CycloneDxSchema;
import org.cyclonedx.exception.GeneratorException;
Expand All @@ -45,8 +46,9 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

Expand Down Expand Up @@ -197,7 +199,7 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
private ModelConverter modelConverter;

@org.apache.maven.plugins.annotations.Component
private ProjectDependenciesConverter projectDependenciesConverter;
protected ProjectDependenciesConverter projectDependenciesConverter;

/**
* Various messages sent to console.
Expand Down Expand Up @@ -230,12 +232,13 @@ protected Component convert(Artifact artifact) {
/**
* Analyze the current Maven project to extract the BOM components list and their dependencies.
*
* @param components the components set to fill
* @param dependencies the dependencies set to fill
* @param components the components map to fill
* @param dependencies the dependencies map to fill
* @param projectIdentities the map of project purls to identities for this build
* @return the name of the analysis done to store as a BOM, or {@code null} to not save result.
* @throws MojoExecutionException something weird happened...
*/
protected abstract String extractComponentsAndDependencies(Set<Component> components, Set<Dependency> dependencies) throws MojoExecutionException;
protected abstract String extractComponentsAndDependencies(Map<String, Component> components, Map<String, Dependency> dependencies, Map<String, String> projectIdentities) throws MojoExecutionException;

public void execute() throws MojoExecutionException {
final boolean shouldSkip = Boolean.parseBoolean(System.getProperty("cyclonedx.skip", Boolean.toString(skip)));
Expand All @@ -245,10 +248,11 @@ public void execute() throws MojoExecutionException {
}
logParameters();

final Set<Component> components = new LinkedHashSet<>();
final Set<Dependency> dependencies = new LinkedHashSet<>();
final Map<String, Component> componentMap = new LinkedHashMap<>();
final Map<String, Dependency> dependencyMap = new LinkedHashMap<>();
final Map<String, String> projectIdentities = new LinkedHashMap<>();

String analysis = extractComponentsAndDependencies(components, dependencies);
String analysis = extractComponentsAndDependencies(componentMap, dependencyMap, projectIdentities);
if (analysis != null) {
List<String> scopes = new ArrayList<>();
if (includeCompileScope) scopes.add("compile");
Expand All @@ -258,25 +262,33 @@ public void execute() throws MojoExecutionException {
if (includeTestScope) scopes.add("test");

final Metadata metadata = modelConverter.convert(project, analysis + " " + String.join("+", scopes), projectType, schemaVersion(), includeLicenseText);
projectDependenciesConverter.cleanupBomDependencies(metadata, components, dependencies);

generateBom(analysis, metadata, components, dependencies);
final Component rootComponent = metadata.getComponent();
final String rootBomRef = projectIdentities.get(rootComponent.getPurl());
if (rootBomRef != null) {
componentMap.remove(rootBomRef);
metadata.getComponent().setBomRef(rootBomRef);
}

projectDependenciesConverter.cleanupBomDependencies(metadata, componentMap, dependencyMap);

generateBom(analysis, metadata, new ArrayList<>(componentMap.values()), new ArrayList<>(dependencyMap.values()));
}
}

private void generateBom(String analysis, Metadata metadata, Set<Component> components, Set<Dependency> dependencies) throws MojoExecutionException {
private void generateBom(String analysis, Metadata metadata, List<Component> components, List<Dependency> dependencies) throws MojoExecutionException {
try {
getLog().info(String.format(MESSAGE_CREATING_BOM, schemaVersion, components.size()));
final Bom bom = new Bom();
bom.setComponents(new ArrayList<>(components));
bom.setComponents(components);

if (schemaVersion().getVersion() >= 1.1 && includeBomSerialNumber) {
bom.setSerialNumber("urn:uuid:" + UUID.randomUUID());
}

if (schemaVersion().getVersion() >= 1.2) {
bom.setMetadata(metadata);
bom.setDependencies(new ArrayList<>(dependencies));
bom.setDependencies(dependencies);
}

/*if (schemaVersion().getVersion() >= 1.3) {
Expand Down Expand Up @@ -333,7 +345,7 @@ private void saveBomToFile(String bomString, String extension, Parser bomParser)
}
}

protected Set<Dependency> extractBOMDependencies(MavenProject mavenProject) throws MojoExecutionException {
protected Map<String, Dependency> extractBOMDependencies(MavenProject mavenProject) throws MojoExecutionException {
ProjectDependenciesConverter.MavenDependencyScopes include = new ProjectDependenciesConverter.MavenDependencyScopes(includeCompileScope, includeProvidedScope, includeRuntimeScope, includeTestScope, includeSystemScope);
return projectDependenciesConverter.extractBOMDependencies(mavenProject, include, excludeTypes);
}
Expand Down Expand Up @@ -378,4 +390,78 @@ protected void logParameters() {
getLog().info("------------------------------------------------------------------------");
}
}

protected void populateComponents(final Map<String, Component> components, final Set<Artifact> artifacts, final Map<String, String> purlToIdentity, final ProjectDependencyAnalysis dependencyAnalysis) {
for (Artifact artifact: artifacts) {
final String purl = generatePackageUrl(artifact);
final String identity = purlToIdentity.get(purl);
if (identity != null) {
final Component.Scope artifactScope = (dependencyAnalysis != null ? inferComponentScope(artifact, dependencyAnalysis) : null);
final Component component = components.get(identity);
if (component == null) {
final Component newComponent = convert(artifact);
newComponent.setBomRef(identity);
newComponent.setScope(artifactScope);
components.put(identity, newComponent);
} else {
component.setScope(mergeScopes(component.getScope(), artifactScope));
}
}
}
}

/**
* Infer BOM component scope (required/optional/excluded) based on Maven project dependency analysis.
*
* @param artifact Artifact from maven project
* @param projectDependencyAnalysis Maven Project Dependency Analysis data
*
* @return Component.Scope - REQUIRED: If the component is used (as detected by project dependency analysis). OPTIONAL: If it is unused
*/
protected Component.Scope inferComponentScope(Artifact artifact, ProjectDependencyAnalysis projectDependencyAnalysis) {
if (projectDependencyAnalysis == null) {
return null;
}

Set<Artifact> usedDeclaredArtifacts = projectDependencyAnalysis.getUsedDeclaredArtifacts();
Set<Artifact> usedUndeclaredArtifacts = projectDependencyAnalysis.getUsedUndeclaredArtifacts();
Set<Artifact> unusedDeclaredArtifacts = projectDependencyAnalysis.getUnusedDeclaredArtifacts();
Set<Artifact> testArtifactsWithNonTestScope = projectDependencyAnalysis.getTestArtifactsWithNonTestScope();

// Is the artifact used?
if (usedDeclaredArtifacts.contains(artifact) || usedUndeclaredArtifacts.contains(artifact)) {
return Component.Scope.REQUIRED;
}

// Is the artifact unused or test?
if (unusedDeclaredArtifacts.contains(artifact) || testArtifactsWithNonTestScope.contains(artifact)) {
return Component.Scope.OPTIONAL;
}

return null;
}

private Component.Scope mergeScopes(final Component.Scope existing, final Component.Scope project) {
// If scope is null we don't know anything about the artifact, so we assume it's not optional.
// This is likely a result of the dependency analysis part being unable to run.
final Component.Scope merged;
if (existing == null) {
merged = (project == Component.Scope.REQUIRED ? Component.Scope.REQUIRED : null);
} else {
switch (existing) {
case REQUIRED:
merged = Component.Scope.REQUIRED;
break;
case OPTIONAL:
merged = (project == Component.Scope.REQUIRED || project == null ? project : existing);
break;
case EXCLUDED:
merged = (project != Component.Scope.EXCLUDED ? project : Component.Scope.EXCLUDED);
break;
default:
merged = project;
}
}
return merged;
}
}
101 changes: 25 additions & 76 deletions src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,19 @@
*/
package org.cyclonedx.maven;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
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.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.Dependency;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Creates a CycloneDX aggregate BOM at build root (with dependencies from the whole multi-modules build), and eventually a BOM for each module.
Expand Down Expand Up @@ -106,60 +101,44 @@ protected void logAdditionalParameters() {
getLog().info("outputReactorProjects : " + outputReactorProjects);
}

protected String extractComponentsAndDependencies(final Set<Component> components, final Set<Dependency> dependencies) throws MojoExecutionException {
@Override
protected String extractComponentsAndDependencies(final Map<String, Component> components, final Map<String, Dependency> dependencies, final Map<String, String> projectIdentities) throws MojoExecutionException {
if (! getProject().isExecutionRoot()) {
// non-root project: let parent class create a module-only BOM?
if (outputReactorProjects) {
return super.extractComponentsAndDependencies(components, dependencies);
return super.extractComponentsAndDependencies(components, dependencies, projectIdentities);
}
getLog().info("Skipping CycloneDX on non-execution root");
return null;
}

// root project: analyze and aggregate all the modules
getLog().info((reactorProjects.size() <= 1) ? MESSAGE_RESOLVING_DEPS : MESSAGE_RESOLVING_AGGREGATED_DEPS);
final Set<String> componentRefs = new LinkedHashSet<>();

// Perform used/unused dependencies analysis for all projects upfront
final List<ProjectDependencyAnalysis> projectsDependencyAnalysis = prepareMavenDependencyAnalysis();

// Add reference to BOM metadata component.
// Without this, direct dependencies of the Maven project cannot be determined.
final Component bomComponent = convert(getProject().getArtifact());
componentRefs.add(bomComponent.getBomRef());

for (final MavenProject mavenProject : reactorProjects) {
if (shouldExclude(mavenProject)) {
getLog().info("Excluding " + mavenProject.getArtifactId());
continue;
}

// Add reference to BOM metadata component.
// Without this, direct dependencies of the Maven project cannot be determined.
final Map<String, Dependency> projectDependencies = extractBOMDependencies(mavenProject);

final Map<String, String> projectPUrlToIdentity = new HashMap<>();
projectDependenciesConverter.normalizeDependencies(schemaVersion(), projectDependencies, projectPUrlToIdentity);

final Component projectBomComponent = convert(mavenProject.getArtifact());
if (! mavenProject.isExecutionRoot()) {
// DO NOT include root project as it's already been included as a bom metadata component
// Also, ensure that only one project component with the same bom-ref exists in the BOM
if (!componentRefs.contains(projectBomComponent.getBomRef())) {
components.add(projectBomComponent);
}
}
componentRefs.add(projectBomComponent.getBomRef());
final String identity = projectPUrlToIdentity.get(projectBomComponent.getPurl());
projectBomComponent.setBomRef(identity);
components.put(identity, projectBomComponent);

for (final Artifact artifact : mavenProject.getArtifacts()) {
final Component component = convert(artifact);
projectIdentities.put(projectBomComponent.getPurl(), projectBomComponent.getBomRef());

// ensure that only one component with the same bom-ref exists in the BOM
if (componentRefs.add(component.getBomRef())) {
component.setScope(inferComponentScope(artifact, projectsDependencyAnalysis));
components.add(component);
}
}
populateComponents(components, mavenProject.getArtifacts(), projectPUrlToIdentity, doProjectDependencyAnalysis(mavenProject));

dependencies.addAll(extractBOMDependencies(mavenProject));
dependencies.putAll(projectDependencies);
}

addMavenProjectsAsParentDependencies(reactorProjects, dependencies);
addMavenProjectsAsParentDependencies(reactorProjects, projectIdentities, dependencies);

return "makeAggregateBom";
}
Expand All @@ -170,53 +149,23 @@ protected String extractComponentsAndDependencies(final Set<Component> component
* code dependency, but only the build reactor.
*
* @param reactorProjects the Maven projects from the reactor
* @param identities reactor project identities
* @param dependencies all BOM dependencies found in reactor
*/
private void addMavenProjectsAsParentDependencies(List<MavenProject> reactorProjects, Set<Dependency> dependencies) {
Map<String, Dependency> dependenciesByRef = new HashMap<>();
dependencies.forEach(d -> dependenciesByRef.put(d.getRef(), d));

private void addMavenProjectsAsParentDependencies(List<MavenProject> reactorProjects, Map<String, String> identities, Map<String, Dependency> dependencies) {
for (final MavenProject project: reactorProjects) {
if (project.hasParent()) {
final String parentRef = generatePackageUrl(project.getParentArtifact());
Dependency parentDependency = dependenciesByRef.get(parentRef);
if (parentDependency != null) {
final Dependency child = new Dependency(generatePackageUrl(project.getArtifact()));
parentDependency.addDependency(child);
final String parentIdentity = identities.get(parentRef);
if (parentIdentity != null) {
Dependency parentDependency = dependencies.get(parentIdentity);
if (parentDependency != null) {
final String projectRef = generatePackageUrl(project.getArtifact());
final String projectIdentity = identities.get(projectRef);
parentDependency.addDependency(new Dependency(projectIdentity));
}
}
}
}
}

private List<ProjectDependencyAnalysis> prepareMavenDependencyAnalysis() throws MojoExecutionException {
final List<ProjectDependencyAnalysis> dependencyAnalysisMap = new ArrayList<>();
for (final MavenProject mavenProject : reactorProjects) {
if (shouldExclude(mavenProject)) {
continue;
}
ProjectDependencyAnalysis dependencyAnalysis = doProjectDependencyAnalysis(mavenProject);
if (dependencyAnalysis != null) {
dependencyAnalysisMap.add(dependencyAnalysis);
}
}
return dependencyAnalysisMap;
}

private Component.Scope inferComponentScope(Artifact artifact, List<ProjectDependencyAnalysis> projectsDependencyAnalysis) {
Component.Scope componentScope = null;
for (ProjectDependencyAnalysis dependencyAnalysis : projectsDependencyAnalysis) {
Component.Scope currentProjectScope = inferComponentScope(artifact, dependencyAnalysis);

// Set scope to required if the component is used in any project
if (Component.Scope.REQUIRED.equals(currentProjectScope)) {
return Component.Scope.REQUIRED;
}

if (componentScope == null && currentProjectScope != null) {
// Set optional or excluded scope
componentScope = currentProjectScope;
}
}
return componentScope;
}
}
Loading