Skip to content

Commit

Permalink
Add support for alternate dependency trees for components
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Conner <kev.conner@gmail.com>
  • Loading branch information
knrc committed Mar 9, 2023
1 parent a2754bf commit d5966c6
Show file tree
Hide file tree
Showing 28 changed files with 1,131 additions and 322 deletions.
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()
113 changes: 99 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 @@ -35,6 +36,7 @@
import org.cyclonedx.model.Component;
import org.cyclonedx.model.Dependency;
import org.cyclonedx.model.Metadata;
import org.cyclonedx.model.Component.Scope;
import org.cyclonedx.parsers.JsonParser;
import org.cyclonedx.parsers.Parser;
import org.cyclonedx.parsers.XmlParser;
Expand All @@ -45,8 +47,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 +200,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 +233,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 purlToIdentity the map of 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 +249,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 +263,31 @@ 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);
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, components, dependencies);
generateBom(analysis, metadata, componentMap, dependencyMap);
}
}

private void generateBom(String analysis, Metadata metadata, Set<Component> components, Set<Dependency> dependencies) throws MojoExecutionException {
private void generateBom(String analysis, Metadata metadata, Map<String, Component> components, Map<String, 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(new ArrayList<>(components.values()));

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(new ArrayList<>(dependencies.values()));
}

/*if (schemaVersion().getVersion() >= 1.3) {
Expand Down Expand Up @@ -333,7 +344,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.Scopes include = new ProjectDependenciesConverter.Scopes(includeCompileScope, includeProvidedScope, includeRuntimeScope, includeTestScope, includeSystemScope);
return projectDependenciesConverter.extractBOMDependencies(mavenProject, include, excludeTypes);
}
Expand Down Expand Up @@ -378,4 +389,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 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 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 Scope mergeScopes(final Scope existing, final 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 Scope merged;
if (existing == null) {
merged = (project == Scope.REQUIRED ? Scope.REQUIRED : null);
} else {
switch (existing) {
case REQUIRED:
merged = Scope.REQUIRED;
break;
case OPTIONAL:
merged = (project == Scope.REQUIRED || project == null ? project : existing);
break;
case EXCLUDED:
merged = (project != Scope.EXCLUDED ? project : 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

0 comments on commit d5966c6

Please sign in to comment.