From 16bd7d433c155be94deae07e12c39b6c9b2cb94a Mon Sep 17 00:00:00 2001 From: Kevin Conner Date: Thu, 9 Mar 2023 11:38:48 -0800 Subject: [PATCH 1/2] Fixes #310, ensure that aggregate BOMs contain all dependency hierarchies Signed-off-by: Kevin Conner --- src/it/makeAggregateBom/verify.groovy | 8 +- .../cyclonedx/maven/BaseCycloneDxMojo.java | 113 ++++++++-- .../maven/CycloneDxAggregateMojo.java | 101 +++------ .../org/cyclonedx/maven/CycloneDxMojo.java | 67 ++---- .../cyclonedx/maven/CycloneDxPackageMojo.java | 30 +-- .../DefaultProjectDependenciesConverter.java | 123 +++++++++-- .../maven/ProjectDependenciesConverter.java | 14 +- .../cyclonedx/maven/BomDependenciesTest.java | 126 ++++++----- .../java/org/cyclonedx/maven/CyclicTest.java | 41 ++-- .../cyclonedx/maven/DependencyTreeTest.java | 199 ++++++++++++++++++ .../org/cyclonedx/maven/Issue116Test.java | 2 +- .../org/cyclonedx/maven/Issue284Test.java | 29 +-- .../java/org/cyclonedx/maven/RuntimeTest.java | 69 +++--- .../java/org/cyclonedx/maven/TestUtils.java | 136 ++++++++++-- .../exclusion/dependency_A/pom.xml | 26 +++ .../exclusion/dependency_B/pom.xml | 32 +++ .../exclusion/dependency_C/pom.xml | 26 +++ .../exclusion/dependency_D/pom.xml | 17 ++ .../exclusion/dependency_E/pom.xml | 17 ++ .../exclusion/dependency_F/pom.xml | 42 ++++ .../dependency_trees/exclusion/pom.xml | 28 +++ .../managed/dependency_A/pom.xml | 26 +++ .../managed/dependency_B/pom.xml | 26 +++ .../managed/dependency_C1/pom.xml | 26 +++ .../managed/dependency_C2/pom.xml | 27 +++ .../managed/dependency_D/pom.xml | 17 ++ .../managed/dependency_E/pom.xml | 36 ++++ .../dependency_trees/managed/pom.xml | 28 +++ src/test/resources/dependency_trees/pom.xml | 61 ++++++ 29 files changed, 1148 insertions(+), 345 deletions(-) create mode 100644 src/test/java/org/cyclonedx/maven/DependencyTreeTest.java create mode 100644 src/test/resources/dependency_trees/exclusion/dependency_A/pom.xml create mode 100644 src/test/resources/dependency_trees/exclusion/dependency_B/pom.xml create mode 100644 src/test/resources/dependency_trees/exclusion/dependency_C/pom.xml create mode 100644 src/test/resources/dependency_trees/exclusion/dependency_D/pom.xml create mode 100644 src/test/resources/dependency_trees/exclusion/dependency_E/pom.xml create mode 100644 src/test/resources/dependency_trees/exclusion/dependency_F/pom.xml create mode 100644 src/test/resources/dependency_trees/exclusion/pom.xml create mode 100644 src/test/resources/dependency_trees/managed/dependency_A/pom.xml create mode 100644 src/test/resources/dependency_trees/managed/dependency_B/pom.xml create mode 100644 src/test/resources/dependency_trees/managed/dependency_C1/pom.xml create mode 100644 src/test/resources/dependency_trees/managed/dependency_C2/pom.xml create mode 100644 src/test/resources/dependency_trees/managed/dependency_D/pom.xml create mode 100644 src/test/resources/dependency_trees/managed/dependency_E/pom.xml create mode 100644 src/test/resources/dependency_trees/managed/pom.xml create mode 100644 src/test/resources/dependency_trees/pom.xml diff --git a/src/it/makeAggregateBom/verify.groovy b/src/it/makeAggregateBom/verify.groovy index 5228fda4..d235184c 100644 --- a/src/it/makeAggregateBom/verify.groovy +++ b/src/it/makeAggregateBom/verify.groovy @@ -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(''), bom.indexOf('') + 13) -assert rootDependencies.contains('') -assert rootDependencies.contains('') -assert rootDependencies.contains('') +String rootDependencies = bom.substring(bom.indexOf(' components = new LinkedHashSet<>(); - final Set dependencies = new LinkedHashSet<>(); + final Map componentMap = new LinkedHashMap<>(); + final Map dependencyMap = new LinkedHashMap<>(); + final Map projectIdentities = new LinkedHashMap<>(); - String analysis = extractComponentsAndDependencies(components, dependencies); + String analysis = extractComponentsAndDependencies(componentMap, dependencyMap, projectIdentities); if (analysis != null) { List scopes = new ArrayList<>(); if (includeCompileScope) scopes.add("compile"); @@ -258,17 +263,23 @@ 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 components, Set dependencies) throws MojoExecutionException { + private void generateBom(String analysis, Metadata metadata, Map components, Map 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()); @@ -276,7 +287,7 @@ private void generateBom(String analysis, Metadata metadata, Set comp if (schemaVersion().getVersion() >= 1.2) { bom.setMetadata(metadata); - bom.setDependencies(new ArrayList<>(dependencies)); + bom.setDependencies(new ArrayList<>(dependencies.values())); } /*if (schemaVersion().getVersion() >= 1.3) { @@ -333,7 +344,7 @@ private void saveBomToFile(String bomString, String extension, Parser bomParser) } } - protected Set extractBOMDependencies(MavenProject mavenProject) throws MojoExecutionException { + protected Map extractBOMDependencies(MavenProject mavenProject) throws MojoExecutionException { ProjectDependenciesConverter.MavenDependencyScopes include = new ProjectDependenciesConverter.MavenDependencyScopes(includeCompileScope, includeProvidedScope, includeRuntimeScope, includeTestScope, includeSystemScope); return projectDependenciesConverter.extractBOMDependencies(mavenProject, include, excludeTypes); } @@ -378,4 +389,78 @@ protected void logParameters() { getLog().info("------------------------------------------------------------------------"); } } + + protected void populateComponents(final Map components, final Set artifacts, final Map 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 usedDeclaredArtifacts = projectDependencyAnalysis.getUsedDeclaredArtifacts(); + Set usedUndeclaredArtifacts = projectDependencyAnalysis.getUsedUndeclaredArtifacts(); + Set unusedDeclaredArtifacts = projectDependencyAnalysis.getUnusedDeclaredArtifacts(); + Set 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; + } } diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java index fb9123a0..cfc36d8a 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java @@ -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. @@ -106,11 +101,12 @@ protected void logAdditionalParameters() { getLog().info("outputReactorProjects : " + outputReactorProjects); } - protected String extractComponentsAndDependencies(final Set components, final Set dependencies) throws MojoExecutionException { + @Override + protected String extractComponentsAndDependencies(final Map components, final Map dependencies, final Map 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; @@ -118,15 +114,6 @@ protected String extractComponentsAndDependencies(final Set component // root project: analyze and aggregate all the modules getLog().info((reactorProjects.size() <= 1) ? MESSAGE_RESOLVING_DEPS : MESSAGE_RESOLVING_AGGREGATED_DEPS); - final Set componentRefs = new LinkedHashSet<>(); - - // Perform used/unused dependencies analysis for all projects upfront - final List 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)) { @@ -134,32 +121,24 @@ protected String extractComponentsAndDependencies(final Set component continue; } - // Add reference to BOM metadata component. - // Without this, direct dependencies of the Maven project cannot be determined. + final Map projectDependencies = extractBOMDependencies(mavenProject); + + final Map 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"; } @@ -170,53 +149,23 @@ protected String extractComponentsAndDependencies(final Set 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 reactorProjects, Set dependencies) { - Map dependenciesByRef = new HashMap<>(); - dependencies.forEach(d -> dependenciesByRef.put(d.getRef(), d)); - + private void addMavenProjectsAsParentDependencies(List reactorProjects, Map identities, Map 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 prepareMavenDependencyAnalysis() throws MojoExecutionException { - final List 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 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; - } } diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java index d061af66..316a35ee 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java @@ -18,7 +18,6 @@ */ 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; @@ -32,8 +31,9 @@ import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.cyclonedx.model.Component; import org.cyclonedx.model.Dependency; -import java.util.LinkedHashSet; -import java.util.Set; + +import java.util.HashMap; +import java.util.Map; /** * Creates a CycloneDX BOM for each Maven module with its dependencies. @@ -90,61 +90,24 @@ protected ProjectDependencyAnalysis doProjectDependencyAnalysis(MavenProject mav return null; } - protected String extractComponentsAndDependencies(final Set components, final Set dependencies) throws MojoExecutionException { - final Set componentRefs = new LinkedHashSet<>(); - + protected String extractComponentsAndDependencies(final Map components, final Map dependencies, final Map projectIdentities) throws MojoExecutionException { getLog().info(MESSAGE_RESOLVING_DEPS); - if (getProject() != null && getProject().getArtifacts() != null) { - ProjectDependencyAnalysis dependencyAnalysis = doProjectDependencyAnalysis(getProject()); - - // 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 Artifact artifact : getProject().getArtifacts()) { - final Component component = convert(artifact); - // ensure that only one component with the same bom-ref exists in the BOM - if (componentRefs.add(component.getBomRef())) { - component.setScope(inferComponentScope(artifact, dependencyAnalysis)); - components.add(component); - } - } - } - - dependencies.addAll(extractBOMDependencies(getProject())); - - return "makeBom"; - } + final Map projectDependencies = extractBOMDependencies(getProject()); - /** - * 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; - } + final Map projectPUrlToIdentity = new HashMap<>(); + projectDependenciesConverter.normalizeDependencies(schemaVersion(), projectDependencies, projectPUrlToIdentity); - Set usedDeclaredArtifacts = projectDependencyAnalysis.getUsedDeclaredArtifacts(); - Set usedUndeclaredArtifacts = projectDependencyAnalysis.getUsedUndeclaredArtifacts(); - Set unusedDeclaredArtifacts = projectDependencyAnalysis.getUnusedDeclaredArtifacts(); - Set testArtifactsWithNonTestScope = projectDependencyAnalysis.getTestArtifactsWithNonTestScope(); + final Component projectBomComponent = convert(getProject().getArtifact()); + final String identity = projectPUrlToIdentity.get(projectBomComponent.getPurl()); + projectBomComponent.setBomRef(identity); + components.put(identity, projectBomComponent); - // Is the artifact used? - if (usedDeclaredArtifacts.contains(artifact) || usedUndeclaredArtifacts.contains(artifact)) { - return Component.Scope.REQUIRED; - } + projectIdentities.put(projectBomComponent.getPurl(), projectBomComponent.getBomRef()); - // Is the artifact unused or test? - if (unusedDeclaredArtifacts.contains(artifact) || testArtifactsWithNonTestScope.contains(artifact)) { - return Component.Scope.OPTIONAL; - } + populateComponents(components, getProject().getArtifacts(), projectPUrlToIdentity, doProjectDependencyAnalysis(getProject())); + dependencies.putAll(projectDependencies); - return null; + return "makeBom"; } } diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java index 079f4386..5e3d1fca 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java @@ -18,7 +18,6 @@ */ 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; @@ -29,9 +28,9 @@ import org.cyclonedx.model.Dependency; import java.util.Arrays; -import java.util.LinkedHashSet; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Map; /** * Creates a CycloneDX BOM for each Maven module with {@code war} or {@code ear} packaging. @@ -56,8 +55,7 @@ protected boolean shouldInclude(MavenProject mavenProject) { return Arrays.asList(new String[]{"war", "ear"}).contains(mavenProject.getPackaging()); } - protected String extractComponentsAndDependencies(Set components, Set dependencies) throws MojoExecutionException { - final Set componentRefs = new LinkedHashSet<>(); + protected String extractComponentsAndDependencies(Map components, Map dependencies, final Map projectIdentities) throws MojoExecutionException { getLog().info(MESSAGE_RESOLVING_DEPS); for (final MavenProject mavenProject : reactorProjects) { @@ -65,15 +63,21 @@ protected String extractComponentsAndDependencies(Set components, Set continue; } getLog().info("Analyzing " + mavenProject.getArtifactId()); - for (final Artifact artifact : mavenProject.getArtifacts()) { - final Component component = convert(artifact); - // ensure that only one component with the same bom-ref exists in the BOM - if (componentRefs.add(component.getBomRef())) { - components.add(component); - } - } - dependencies.addAll(extractBOMDependencies(mavenProject)); + final Map projectDependencies = extractBOMDependencies(mavenProject); + + final Map projectPUrlToIdentity = new HashMap<>(); + projectDependenciesConverter.normalizeDependencies(schemaVersion(), projectDependencies, projectPUrlToIdentity); + + final Component projectBomComponent = convert(mavenProject.getArtifact()); + final String identity = projectPUrlToIdentity.get(projectBomComponent.getPurl()); + projectBomComponent.setBomRef(identity); + components.put(identity, projectBomComponent); + + projectIdentities.put(projectBomComponent.getPurl(), projectBomComponent.getBomRef()); + + populateComponents(components, mavenProject.getArtifacts(), projectPUrlToIdentity, null); + dependencies.putAll(projectDependencies); } return "makePackageBom"; diff --git a/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java b/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java index 763c85aa..440de503 100644 --- a/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java +++ b/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java @@ -18,6 +18,8 @@ */ package org.cyclonedx.maven; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecutionException; @@ -27,6 +29,7 @@ import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder; import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException; import org.apache.maven.shared.dependency.graph.internal.DefaultDependencyCollectorBuilder; +import org.cyclonedx.CycloneDxSchema; import org.cyclonedx.model.Component; import org.cyclonedx.model.Dependency; import org.cyclonedx.model.Metadata; @@ -38,10 +41,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -65,7 +76,7 @@ public class DefaultProjectDependenciesConverter implements ProjectDependenciesC private MavenDependencyScopes include; @Override - public Set extractBOMDependencies(MavenProject mavenProject, MavenDependencyScopes include, String[] excludeTypes) throws MojoExecutionException { + public Map extractBOMDependencies(MavenProject mavenProject, MavenDependencyScopes include, String[] excludeTypes) throws MojoExecutionException { this.include = include; excludeTypesSet = new HashSet<>(Arrays.asList(excludeTypes)); @@ -73,7 +84,7 @@ public Set extractBOMDependencies(MavenProject mavenProject, MavenDe final Map resolvedPUrls = generateResolvedPUrls(mavenProject); - final Map dependencies = new LinkedHashMap<>(); + final Map dependencies = new LinkedHashMap<>(); try { final DelegatingRepositorySystem delegateRepositorySystem = new DelegatingRepositorySystem(aetherRepositorySystem); final DependencyCollectorBuilder dependencyCollectorBuilder = new DefaultDependencyCollectorBuilder(delegateRepositorySystem); @@ -94,7 +105,7 @@ public Set extractBOMDependencies(MavenProject mavenProject, MavenDe // rather than throwing an exception https://github.com/CycloneDX/cyclonedx-maven-plugin/issues/55 logger.warn("An error occurred building dependency graph: " + e.getMessage()); } - return dependencies.keySet(); + return dependencies; } private boolean isFilteredNode(final DependencyNode node, final Set loggedFilteredArtifacts) { @@ -143,7 +154,7 @@ private boolean isExcludedNode(final DependencyNode node) { return ((type == null) || excludeTypesSet.contains(type)); } - private void buildDependencyGraphNode(final Map dependencies, DependencyNode node, + private void buildDependencyGraphNode(final Map dependencies, DependencyNode node, final Dependency parent, final String parentClassifierlessPUrl, final Map resolvedPUrls, final Set loggedReplacementPUrls, final Set loggedFilteredArtifacts) { String purl = modelConverter.generatePackageUrl(node.getArtifact()); @@ -172,7 +183,7 @@ private void buildDependencyGraphNode(final Map dependen } Dependency topDependency = new Dependency(purl); - final Dependency origDependency = dependencies.putIfAbsent(topDependency, topDependency); + final Dependency origDependency = dependencies.putIfAbsent(purl, topDependency); if (origDependency != null) { topDependency = origDependency; } @@ -214,42 +225,114 @@ private ProjectBuildingRequest getProjectBuildingRequest(final MavenProject mave } @Override - public void cleanupBomDependencies(Metadata metadata, Set components, Set dependencies) { - // map(component ref -> component) - final Map componentRefs = new HashMap<>(); - components.forEach(c -> componentRefs.put(c.getBomRef(), c)); - + public void cleanupBomDependencies(Metadata metadata, Map components, Map dependencies) { // set(dependencies refs) and set(dependencies of dependencies) - final Set dependencyRefs = new HashSet<>(); final Set dependsOns = new HashSet<>(); - dependencies.forEach(d -> { - dependencyRefs.add(d.getRef()); + dependencies.values().forEach(d -> { if (d.getDependencies() != null) { d.getDependencies().forEach(on -> dependsOns.add(on.getRef())); } }); // Check all BOM components have an associated BOM dependency - for (Map.Entry entry: componentRefs.entrySet()) { - if (!dependencyRefs.contains(entry.getKey())) { + + for (Iterator> it = components.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (!dependencies.containsKey(entry.getKey())) { if (logger.isDebugEnabled()) { logger.debug("Component reference not listed in dependencies, pruning from bom components: " + entry.getKey()); } - components.remove(entry.getValue()); + it.remove(); } else if (!dependsOns.contains(entry.getKey())) { logger.warn("BOM dependency listed but is not depended upon: " + entry.getKey()); } } - // add BOM main component + // include BOM main component Component main = metadata.getComponent(); - componentRefs.put(main.getBomRef(), main); + final String mainBomRef = main.getBomRef(); // Check all BOM dependencies have a BOM component - for (String dependencyRef: dependencyRefs) { - if (!componentRefs.containsKey(dependencyRef)) { + for (String dependencyRef: dependencies.keySet()) { + if (!mainBomRef.equals(dependencyRef) && !components.containsKey(dependencyRef)) { logger.warn("Dependency missing component entry: " + dependencyRef); } } } + + private String generateIdentity(final Map purlToIdentity, final Map dependencies, final String ref) { + final String identity = purlToIdentity.get(ref); + if (identity != null) { + return identity; + } else { + final Dependency dependency = dependencies.get(ref); + + final StringBuilder sb = new StringBuilder(ref); + + if (dependency.getDependencies() != null) { + for (Dependency child: dependency.getDependencies()) { + final String childIdentity = generateIdentity(purlToIdentity, dependencies, child.getRef()); + sb.append('+').append(childIdentity); + } + } + + final MessageDigest digest = DigestUtils.getSha512Digest(); + digest.update(sb.toString().getBytes(StandardCharsets.UTF_8)); + final String hash = Hex.encodeHexString(digest.digest()); + + final PackageURL purl; + try { + purl = new PackageURL(ref); + } catch(final MalformedPackageURLException mpurle) { + logger.warn("An unexpected issue occurred attempting to parse PackageURL " + ref, mpurle); + return ref; + } + + purl.getQualifiers().put("hash", hash); + + final String newIdentity = purl.canonicalize(); + + purlToIdentity.put(ref, newIdentity); + return newIdentity; + } + } + + private void generateIdentities(final Map purlToIdentity, final Map dependencies) { + for(String ref: dependencies.keySet()) { + generateIdentity(purlToIdentity, dependencies, ref); + } + } + + private Dependency normalizeDependency(final Dependency dependency, final Map purlToIdentity) { + final Dependency normalizedDependency = new Dependency(purlToIdentity.get(dependency.getRef())); + final List children = new ArrayList<>(); + if (dependency.getDependencies() != null) { + for(Dependency child: dependency.getDependencies()) { + children.add(new Dependency(purlToIdentity.get(child.getRef()))); + } + } + normalizedDependency.setDependencies(children); + return normalizedDependency; + } + + /** + * Normalize the dependencies, assigning distinct references based on their purl and dependencies. + * The map will be modified to reflect the distinct names, with references and the map keys + * being updated. + */ + @Override + public void normalizeDependencies(final CycloneDxSchema.Version schemaVersion, final Map dependencies, final Map purlToIdentity) { + // We only need to normalize dependencies if they are being included in the BOM, i.e. version 1.2 and above + if (schemaVersion.getVersion() >= 1.2) { + generateIdentities(purlToIdentity, dependencies); + + final Map normalizedDependencies = new LinkedHashMap<>(); + for (Dependency dependency: dependencies.values()) { + final Dependency normalizedDependency = normalizeDependency(dependency, purlToIdentity); + normalizedDependencies.put(normalizedDependency.getRef(), normalizedDependency); + } + dependencies.clear(); + dependencies.putAll(normalizedDependencies); + } + } } diff --git a/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java b/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java index 92f399d3..691b2d18 100644 --- a/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java +++ b/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java @@ -22,13 +22,14 @@ import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; +import org.cyclonedx.CycloneDxSchema; import org.cyclonedx.model.Component; import org.cyclonedx.model.Dependency; import org.cyclonedx.model.Metadata; import java.util.Collection; import java.util.HashSet; -import java.util.Set; +import java.util.Map; /** * Converts a Maven Project with its Maven dependencies resolution graph transformed into a SBOM dependencies list @@ -36,13 +37,20 @@ */ public interface ProjectDependenciesConverter { - Set extractBOMDependencies(MavenProject mavenProject, MavenDependencyScopes include, String[] excludeTypes) throws MojoExecutionException; + Map extractBOMDependencies(MavenProject mavenProject, MavenDependencyScopes include, String[] excludes) throws MojoExecutionException; + + /** + * Normalize the dependencies, assigning distinct references based on their purl and dependencies. + * The map will be modified to reflect the distinct names, with references and the map keys + * being updated. + */ + void normalizeDependencies(final CycloneDxSchema.Version schemaVersion, final Map dependencies, final Map purlToIdentity) ; /** * Check consistency between BOM components and BOM dependencies, and cleanup: drop components found while walking the * Maven dependency resolution graph but that are finally not kept in the effective dependencies list. */ - void cleanupBomDependencies(Metadata metadata, Set components, Set dependencies); + void cleanupBomDependencies(Metadata metadata, Map components, Map dependencies); public static class MavenDependencyScopes { public final boolean compile; diff --git a/src/test/java/org/cyclonedx/maven/BomDependenciesTest.java b/src/test/java/org/cyclonedx/maven/BomDependenciesTest.java index e6980757..a6ec5b7e 100644 --- a/src/test/java/org/cyclonedx/maven/BomDependenciesTest.java +++ b/src/test/java/org/cyclonedx/maven/BomDependenciesTest.java @@ -1,23 +1,28 @@ package org.cyclonedx.maven; +import static org.cyclonedx.maven.TestUtils.containsDependency; import static org.cyclonedx.maven.TestUtils.getComponentNode; +import static org.cyclonedx.maven.TestUtils.getComponentNodes; import static org.cyclonedx.maven.TestUtils.getComponentReferences; import static org.cyclonedx.maven.TestUtils.getDependencyNode; +import static org.cyclonedx.maven.TestUtils.getDependencyNodes; import static org.cyclonedx.maven.TestUtils.getDependencyReferences; +import static org.cyclonedx.maven.TestUtils.getPUrlToIdentities; import static org.cyclonedx.maven.TestUtils.readXML; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.File; +import java.util.Collection; +import java.util.Map; import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.w3c.dom.Document; -import org.w3c.dom.Node; +import org.w3c.dom.Element; import org.w3c.dom.NodeList; import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder; @@ -69,6 +74,7 @@ public void testBomDependencies() throws Exception { */ private void checkHiddenTestArtifacts(final File projDir) throws Exception { final Document bom = readXML(new File(projDir, "trustification/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); /* BOM should contain dependency elements for @@ -83,7 +89,7 @@ private void checkHiddenTestArtifacts(final File projDir) throws Exception { */ final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); /* @@ -91,19 +97,17 @@ private void checkHiddenTestArtifacts(final File projDir) throws Exception { */ - final Node sharedDependency1Node = getDependencyNode(dependencies, SHARED_DEPENDENCY1); - assertNotNull("Missing shared_dependency1 dependency", sharedDependency1Node); + final Element sharedDependency1Node = getDependencyNode(purlToIdentities, dependencies, SHARED_DEPENDENCY1); // Note: there are three dependencies for shared_dependency1, however one has runtime scope and should not be discovered final Set testSharedDependency1Dependencies = getDependencyReferences(sharedDependency1Node); assertEquals("Invalid dependency count for shared_dependency1", 2, testSharedDependency1Dependencies.size()); - assertTrue("Missing shared_dependency2 dependency for shared_dependency1", testSharedDependency1Dependencies.contains(SHARED_DEPENDENCY2)); - assertTrue("Missing test_nested_dependency2 dependency for shared_dependency1", testSharedDependency1Dependencies.contains(TEST_NESTED_DEPENDENCY2)); + containsDependency(purlToIdentities, testSharedDependency1Dependencies, SHARED_DEPENDENCY2); + containsDependency(purlToIdentities, testSharedDependency1Dependencies, TEST_NESTED_DEPENDENCY2); /* */ - final Node sharedDependency2Node = getDependencyNode(dependencies, SHARED_DEPENDENCY2); - assertNotNull("Missing shared_dependency2 dependency", sharedDependency2Node); + final Element sharedDependency2Node = getDependencyNode(purlToIdentities, dependencies, SHARED_DEPENDENCY2); final Set testSharedDependency2Dependencies = getDependencyReferences(sharedDependency2Node); assertEquals("Invalid dependency count for shared_dependency2", 0, testSharedDependency2Dependencies.size()); @@ -112,17 +116,15 @@ private void checkHiddenTestArtifacts(final File projDir) throws Exception { */ - final Node testNestedDependency2Node = getDependencyNode(dependencies, TEST_NESTED_DEPENDENCY2); - assertNotNull("Missing test_nested_dependency2 dependency", testNestedDependency2Node); + final Element testNestedDependency2Node = getDependencyNode(purlToIdentities, dependencies, TEST_NESTED_DEPENDENCY2); Set testNestedDependency2Dependencies = getDependencyReferences(testNestedDependency2Node); assertEquals("Invalid dependency count for test_nested_dependency2", 1, testNestedDependency2Dependencies.size()); - assertTrue("Missing test_nested_dependency3 dependency for test_nested_dependency2", testNestedDependency2Dependencies.contains(TEST_NESTED_DEPENDENCY3)); + containsDependency(purlToIdentities, testNestedDependency2Dependencies, TEST_NESTED_DEPENDENCY3); /* */ - final Node testNestedDependency3Node = getDependencyNode(dependencies, TEST_NESTED_DEPENDENCY3); - assertNotNull("Missing test_nested_dependency3 dependency", testNestedDependency3Node); + final Element testNestedDependency3Node = getDependencyNode(purlToIdentities, dependencies, TEST_NESTED_DEPENDENCY3); Set testNestedDependency3Dependencies = getDependencyReferences(testNestedDependency3Node); assertEquals("Invalid dependency count for test_nested_dependency3", 0, testNestedDependency3Dependencies.size()); } @@ -133,6 +135,7 @@ private void checkHiddenTestArtifacts(final File projDir) throws Exception { */ private void checkHiddenRuntimeArtifacts(final File projDir) throws Exception { final Document bom = readXML(new File(projDir, "trustification/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); /* BOM should contain dependency elements for @@ -142,24 +145,22 @@ private void checkHiddenRuntimeArtifacts(final File projDir) throws Exception { */ final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); /* */ - final Node sharedRuntimeDependency1Node = getDependencyNode(dependencies, SHARED_RUNTIME_DEPENDENCY1); - assertNotNull("Missing shared_runtime_dependency1 dependency", sharedRuntimeDependency1Node); + final Element sharedRuntimeDependency1Node = getDependencyNode(purlToIdentities, dependencies, SHARED_RUNTIME_DEPENDENCY1); final Set testSharedDependency1Dependencies = getDependencyReferences(sharedRuntimeDependency1Node); assertEquals("Invalid dependency count for shared_runtime_dependency1", 1, testSharedDependency1Dependencies.size()); - assertTrue("Missing shared_runtime_dependency2 dependency for shared_runtime_dependency1", testSharedDependency1Dependencies.contains(SHARED_RUNTIME_DEPENDENCY2)); + containsDependency(purlToIdentities, testSharedDependency1Dependencies, SHARED_RUNTIME_DEPENDENCY2); /* */ - final Node sharedRuntimeDependency2Node = getDependencyNode(dependencies, SHARED_RUNTIME_DEPENDENCY2); - assertNotNull("Missing shared_runtime_dependency2 dependency", sharedRuntimeDependency2Node); + final Element sharedRuntimeDependency2Node = getDependencyNode(purlToIdentities, dependencies, SHARED_RUNTIME_DEPENDENCY2); final Set testSharedDependency2Dependencies = getDependencyReferences(sharedRuntimeDependency2Node); assertEquals("Invalid dependency count for shared_runtime_dependency2", 0, testSharedDependency2Dependencies.size()); } @@ -173,17 +174,17 @@ private void checkExtraneousComponents(final File projDir) throws Exception { final NodeList metadataList = bom.getElementsByTagName("metadata"); assertEquals("Expected a single metadata element", 1, metadataList.getLength()); - final Node metadata = metadataList.item(0); + final Element metadata = (Element)metadataList.item(0); final Set metadataComponentReferences = getComponentReferences(metadata); final NodeList componentsList = bom.getElementsByTagName("components"); assertEquals("Expected a single components element", 1, componentsList.getLength()); - final Node components = componentsList.item(0); + final Element components = (Element)componentsList.item(0); final Set componentReferences = getComponentReferences(components); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); final Set dependencyReferences = getDependencyReferences(dependencies); // Each dependency reference should have a component @@ -194,7 +195,7 @@ private void checkExtraneousComponents(final File projDir) throws Exception { // Each component reference should have a top level dependency for (String componentRef: componentReferences) { - assertNotNull("Missing top level dependency for component reference " + componentRef, getDependencyNode(dependencies, componentRef)); + assertTrue("Missing dependency for component reference " + componentRef, dependencyReferences.contains(componentRef)); } } @@ -204,13 +205,13 @@ private void checkExtraneousComponents(final File projDir) throws Exception { */ private void checkTopLevelTestComponentsAsCompile(final File projDir) throws Exception { final Document bom = readXML(new File(projDir, "trustification/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); // BOM should contain a component element for pkg:maven/com.example/test_compile_dependency@1.0.0?type=jar final NodeList componentsList = bom.getElementsByTagName("components"); assertEquals("Expected a single components element", 1, componentsList.getLength()); - final Node components = componentsList.item(0); - final Node testCompileDependencyNode = getComponentNode(components, TEST_COMPILE_DEPENDENCY); - assertNotNull("Missing test_compile_dependency component", testCompileDependencyNode); + final Element components = (Element)componentsList.item(0); + getComponentNode(purlToIdentities, components, TEST_COMPILE_DEPENDENCY); } /** @@ -222,55 +223,52 @@ public void testTypeExcludes() throws Exception { final File projDir = cleanAndBuild("bom-dependencies", new String[]{"test-jar"}); final Document bom = readXML(new File(projDir, "trustification/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); final NodeList componentsList = bom.getElementsByTagName("components"); assertEquals("Expected a single components element", 1, componentsList.getLength()); - final Node components = componentsList.item(0); + final Element components = (Element)componentsList.item(0); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); // BOM should not contain pkg:maven/com.example/type_dependency@1.0.0?classifier=tests&type=test-jar // component nor top level dependency because of type test-jar - final Node testTypeDependencyComponentNode = getComponentNode(components, TYPE_DEPENDENCY); - assertNull("Unexpected type_dependency component discovered in BOM", testTypeDependencyComponentNode); - final Node testTypeDependencyNode = getDependencyNode(dependencies, TYPE_DEPENDENCY); - assertNull("Unexpected type_dependency dependency discovered in BOM", testTypeDependencyNode); + final Collection testTypeDependencyComponentNodes = getComponentNodes(purlToIdentities, components, TYPE_DEPENDENCY); + assertNull("Unexpected type_dependency component discovered in BOM", testTypeDependencyComponentNodes); + final Collection testTypeDependencyNodes = getDependencyNodes(purlToIdentities, dependencies, TYPE_DEPENDENCY); + assertNull("Unexpected type_dependency dependency discovered in BOM", testTypeDependencyNodes); // BOM should contain pkg:maven/com.example/shared_type_dependency1@1.0.0?type=jar and shared_test_dependency2 and // pkg:maven/com.example/shared_type_dependency2@1.0.0?type=jar components/dependencies as they are referenced by dependency2 - final Node sharedTypeDependency1ComponentNode = getComponentNode(components, SHARED_TYPE_DEPENDENCY1); - assertNotNull("Missing shared_type_dependency1 component", sharedTypeDependency1ComponentNode); - final Node sharedTypeDependency2ComponentNode = getComponentNode(components, SHARED_TYPE_DEPENDENCY2); - assertNotNull("Missing shared_type_dependency2 component", sharedTypeDependency2ComponentNode); + getComponentNode(purlToIdentities, components, SHARED_TYPE_DEPENDENCY1); + getComponentNode(purlToIdentities, components, SHARED_TYPE_DEPENDENCY2); /* */ - final Node sharedTypeDependency1Node = getDependencyNode(dependencies, SHARED_TYPE_DEPENDENCY1); - assertNotNull("Missing shared_type_dependency1 dependency", sharedTypeDependency1Node); + final Element sharedTypeDependency1Node = getDependencyNode(purlToIdentities, dependencies, SHARED_TYPE_DEPENDENCY1); Set sharedTypeDependency1Dependencies = getDependencyReferences(sharedTypeDependency1Node); assertEquals("Invalid dependency count for shared_type_dependency1", 1, sharedTypeDependency1Dependencies.size()); - assertTrue("Missing shared_type_dependency2 dependency for shared_type_dependency1", sharedTypeDependency1Dependencies.contains(SHARED_TYPE_DEPENDENCY2)); + containsDependency(purlToIdentities, sharedTypeDependency1Dependencies, SHARED_TYPE_DEPENDENCY2); - final Node sharedTypeDependency2Node = getDependencyNode(dependencies, SHARED_TYPE_DEPENDENCY2); - assertNotNull("Missing shared_type_dependency2 dependency", sharedTypeDependency2Node); + getDependencyNode(purlToIdentities, dependencies, SHARED_TYPE_DEPENDENCY2); // BOM should not contain pkg:maven/com.example/shared_type_dependency3@1.0.0?type=jar nor // pkg:maven/com.example/shared_type_dependency4@1.0.0?type=jar components/dependencies // as they are only referenced via type_dependency - final Node sharedTypeDependency3ComponentNode = getComponentNode(components, SHARED_TYPE_DEPENDENCY3); - assertNull("Unexpected shared_type_dependency3 component discovered in BOM", sharedTypeDependency3ComponentNode); - final Node sharedTypeDependency3Node = getDependencyNode(dependencies, SHARED_TYPE_DEPENDENCY3); - assertNull("Unexpected shared_type_dependency3 dependency discovered in BOM", sharedTypeDependency3Node); - - final Node sharedTypeDependency4ComponentNode = getComponentNode(components, SHARED_TYPE_DEPENDENCY4); - assertNull("Unexpected shared_type_dependency4 component discovered in BOM", sharedTypeDependency4ComponentNode); - final Node sharedTypeDependency4Node = getDependencyNode(dependencies, SHARED_TYPE_DEPENDENCY4); - assertNull("Unexpected shared_type_dependency4 dependency discovered in BOM", sharedTypeDependency4Node); + final Collection sharedTypeDependency3ComponentNodes = getComponentNodes(purlToIdentities, components, SHARED_TYPE_DEPENDENCY3); + assertNull("Unexpected shared_type_dependency3 component discovered in BOM", sharedTypeDependency3ComponentNodes); + final Collection sharedTypeDependency3Nodes = getDependencyNodes(purlToIdentities, dependencies, SHARED_TYPE_DEPENDENCY3); + assertNull("Unexpected shared_type_dependency3 dependency discovered in BOM", sharedTypeDependency3Nodes); + + final Collection sharedTypeDependency4ComponentNodes = getComponentNodes(purlToIdentities, components, SHARED_TYPE_DEPENDENCY4); + assertNull("Unexpected shared_type_dependency4 component discovered in BOM", sharedTypeDependency4ComponentNodes); + final Collection sharedTypeDependency4Nodes = getDependencyNodes(purlToIdentities, dependencies, SHARED_TYPE_DEPENDENCY4); + assertNull("Unexpected shared_type_dependency4 dependency discovered in BOM", sharedTypeDependency4Nodes); } /** @@ -280,45 +278,43 @@ public void testTypeExcludes() throws Exception { private void testHiddenVersionedTransitiveDependencies(final File projDir) throws Exception { // Note: checkExtraneousComponents will also catch missing versioned dependencies but doesn't check for transitive dependencies final Document bom = readXML(new File(projDir, "trustification/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); final NodeList componentsList = bom.getElementsByTagName("components"); assertEquals("Expected a single components element", 1, componentsList.getLength()); - final Node components = componentsList.item(0); + final Element components = (Element)componentsList.item(0); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); // BOM should not contain pkg:maven/com.example/versioned_dependency@1.0.0?type=jar - final Node testVersionedDependency1ComponentNode = getComponentNode(components, VERSIONED_DEPENDENCY1); - assertNull("Unexpected versioned_dependency:1.0.0 component discovered in BOM", testVersionedDependency1ComponentNode); - final Node testVersionedDependency1Node = getDependencyNode(dependencies, VERSIONED_DEPENDENCY1); - assertNull("Unexpected versioned_dependency:1.0.0 dependency discovered in BOM", testVersionedDependency1Node); + final Collection testVersionedDependency1ComponentNodes = getComponentNodes(purlToIdentities, components, VERSIONED_DEPENDENCY1); + assertNull("Unexpected versioned_dependency:1.0.0 component discovered in BOM", testVersionedDependency1ComponentNodes); + final Collection testVersionedDependency1Nodes = getDependencyNodes(purlToIdentities, dependencies, VERSIONED_DEPENDENCY1); + assertNull("Unexpected versioned_dependency:1.0.0 dependency discovered in BOM", testVersionedDependency1Nodes); // BOM should contain pkg:maven/com.example/versioned_dependency@2.0.0?type=jar - final Node testVersionedDependency2ComponentNode = getComponentNode(components, VERSIONED_DEPENDENCY2); - assertNotNull("Missing versioned_dependency:2.0.0 component component", testVersionedDependency2ComponentNode); + getComponentNode(purlToIdentities, components, VERSIONED_DEPENDENCY2); /* */ - final Node providedDependencyNode = getDependencyNode(dependencies, PROVIDED_DEPENDENCY); - assertNotNull("Missing provided_dependency dependency", providedDependencyNode); + final Element providedDependencyNode = getDependencyNode(purlToIdentities, dependencies, PROVIDED_DEPENDENCY); Set providedDependencyDependencies = getDependencyReferences(providedDependencyNode); assertEquals("Invalid dependency count for provided_dependency", 1, providedDependencyDependencies.size()); - assertTrue("Missing versioned_dependency:2.0.0 dependency for provided_dependency", providedDependencyDependencies.contains(VERSIONED_DEPENDENCY2)); + containsDependency(purlToIdentities, providedDependencyDependencies, VERSIONED_DEPENDENCY2); /* */ - final Node versionedDependencyNode = getDependencyNode(dependencies, VERSIONED_DEPENDENCY2); - assertNotNull("Missing versioned_dependency dependency", versionedDependencyNode); + final Element versionedDependencyNode = getDependencyNode(purlToIdentities, dependencies, VERSIONED_DEPENDENCY2); Set versionedDependencyDependencies = getDependencyReferences(versionedDependencyNode); assertEquals("Invalid dependency count for versioned_dependency", 1, versionedDependencyDependencies.size()); - assertTrue("Missing dependency1 dependency for versioned_dependency", versionedDependencyDependencies.contains(DEPENDENCY1)); + containsDependency(purlToIdentities, versionedDependencyDependencies, DEPENDENCY1); } } diff --git a/src/test/java/org/cyclonedx/maven/CyclicTest.java b/src/test/java/org/cyclonedx/maven/CyclicTest.java index 66427c40..a0a7d3e3 100644 --- a/src/test/java/org/cyclonedx/maven/CyclicTest.java +++ b/src/test/java/org/cyclonedx/maven/CyclicTest.java @@ -1,23 +1,24 @@ package org.cyclonedx.maven; import java.io.File; +import java.util.Collection; +import java.util.Map; import java.util.Set; +import static org.cyclonedx.maven.TestUtils.containsDependency; import static org.cyclonedx.maven.TestUtils.getComponentNode; import static org.cyclonedx.maven.TestUtils.getDependencyNode; import static org.cyclonedx.maven.TestUtils.getDependencyReferences; +import static org.cyclonedx.maven.TestUtils.getPUrlToIdentities; import static org.cyclonedx.maven.TestUtils.readXML; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import org.junit.Test; import org.junit.runner.RunWith; import org.w3c.dom.Document; -import org.w3c.dom.Node; +import org.w3c.dom.Element; import org.w3c.dom.NodeList; import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder; @@ -50,26 +51,24 @@ public void testCyclicDependency() throws Exception { } final Document bom = readXML(new File(projDir, "target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); final NodeList componentsList = bom.getElementsByTagName("components"); assertEquals("Expected a single components element", 1, componentsList.getLength()); - final Node components = componentsList.item(0); + final Element components = (Element)componentsList.item(0); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); // BOM should contain pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_1&type=jar - final Node cyclicAClassifier1ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_1); - assertNotNull("Missing cyclic_A:classifier_1:1.0.0 component", cyclicAClassifier1ComponentNode); + getComponentNode(purlToIdentities, components, CYCLIC_A_DEPENDENCY_CLASSIFIER_1); // BOM should contain pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_2&type=jar - final Node cyclicAClassifier2ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_2); - assertNotNull("Missing cyclic_A:classifier_2:1.0.0 component", cyclicAClassifier2ComponentNode); + getComponentNode(purlToIdentities, components, CYCLIC_A_DEPENDENCY_CLASSIFIER_2); // BOM should contain pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_3&type=jar - final Node cyclicAClassifier3ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_3); - assertNotNull("Missing cyclic_A:classifier_3:1.0.0 component", cyclicAClassifier3ComponentNode); + getComponentNode(purlToIdentities, components, CYCLIC_A_DEPENDENCY_CLASSIFIER_3); /* @@ -78,35 +77,31 @@ public void testCyclicDependency() throws Exception { */ - final Node cyclicADependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY); - assertNotNull("Missing cyclic_A:1.0.0 dependency", cyclicADependencyNode); + final Element cyclicADependencyNode = getDependencyNode(purlToIdentities, dependencies, CYCLIC_A_DEPENDENCY); Set cyclicADependencies = getDependencyReferences(cyclicADependencyNode); assertEquals("Invalid dependency count for cyclic_A:1.0.0", 3, cyclicADependencies.size()); - assertTrue("Missing cyclic_A:classifier_1:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_1)); - assertTrue("Missing cyclic_A:classifier_2:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_2)); - assertTrue("Missing cyclic_A:classifier_3:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_3)); + containsDependency(purlToIdentities, cyclicADependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_1); + containsDependency(purlToIdentities, cyclicADependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_2); + containsDependency(purlToIdentities, cyclicADependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_3); /* */ - final Node cyclicAClassifier1DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_1); - assertNotNull("Missing cyclic_A:classifier_1:1.0.0 dependency", cyclicAClassifier1DependencyNode); + final Element cyclicAClassifier1DependencyNode = getDependencyNode(purlToIdentities, dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_1); Set cyclicAClassifier1Dependencies = getDependencyReferences(cyclicAClassifier1DependencyNode); assertEquals("Invalid dependency count for cyclic_A:classifier_1:1.0.0", 0, cyclicAClassifier1Dependencies.size()); /* */ - final Node cyclicAClassifier2DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_2); - assertNotNull("Missing cyclic_A:classifier_2:1.0.0 dependency", cyclicAClassifier2DependencyNode); + final Element cyclicAClassifier2DependencyNode = getDependencyNode(purlToIdentities, dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_2); Set cyclicAClassifier2Dependencies = getDependencyReferences(cyclicAClassifier2DependencyNode); assertEquals("Invalid dependency count for cyclic_A:classifier_2:1.0.0", 0, cyclicAClassifier2Dependencies.size()); /* */ - final Node cyclicAClassifier3DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_3); - assertNotNull("Missing cyclic_A:classifier_3:1.0.0 dependency", cyclicAClassifier3DependencyNode); + final Element cyclicAClassifier3DependencyNode = getDependencyNode(purlToIdentities, dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_3); Set cyclicAClassifier3Dependencies = getDependencyReferences(cyclicAClassifier3DependencyNode); assertEquals("Invalid dependency count for cyclic_A:classifier_3:1.0.0", 0, cyclicAClassifier3Dependencies.size()); } diff --git a/src/test/java/org/cyclonedx/maven/DependencyTreeTest.java b/src/test/java/org/cyclonedx/maven/DependencyTreeTest.java new file mode 100644 index 00000000..535779e8 --- /dev/null +++ b/src/test/java/org/cyclonedx/maven/DependencyTreeTest.java @@ -0,0 +1,199 @@ +package org.cyclonedx.maven; + +import java.io.File; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import static org.cyclonedx.maven.TestUtils.containsDependency; +import static org.cyclonedx.maven.TestUtils.getComponentNode; +import static org.cyclonedx.maven.TestUtils.getComponentNodes; +import static org.cyclonedx.maven.TestUtils.getDependencyNode; +import static org.cyclonedx.maven.TestUtils.getDependencyNodeByIdentity; +import static org.cyclonedx.maven.TestUtils.getDependencyReferences; +import static org.cyclonedx.maven.TestUtils.getPUrlToIdentities; +import static org.cyclonedx.maven.TestUtils.readXML; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder; +import io.takari.maven.testing.executor.MavenVersions; +import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner; + +/** + * Fix BOM handling of conflicting dependency tree graphs + */ +@RunWith(MavenJUnitTestRunner.class) +@MavenVersions({"3.6.3"}) +public class DependencyTreeTest extends BaseMavenVerifier { + + private static final String EXCLUSION_DEPENDENCY_A = "pkg:maven/com.example.dependency_trees.exclusion/dependency_A@1.0.0?type=jar"; + private static final String EXCLUSION_DEPENDENCY_B = "pkg:maven/com.example.dependency_trees.exclusion/dependency_B@1.0.0?type=jar"; + private static final String EXCLUSION_DEPENDENCY_C = "pkg:maven/com.example.dependency_trees.exclusion/dependency_C@1.0.0?type=jar"; + private static final String EXCLUSION_DEPENDENCY_D = "pkg:maven/com.example.dependency_trees.exclusion/dependency_D@1.0.0?type=jar"; + private static final String EXCLUSION_DEPENDENCY_E = "pkg:maven/com.example.dependency_trees.exclusion/dependency_E@1.0.0?type=jar"; + private static final String EXCLUSION_DEPENDENCY_F = "pkg:maven/com.example.dependency_trees.exclusion/dependency_F@1.0.0?type=jar"; + + private static final String MANAGED_DEPENDENCY_A = "pkg:maven/com.example.dependency_trees.managed/dependency_A@1.0.0?type=jar"; + private static final String MANAGED_DEPENDENCY_B = "pkg:maven/com.example.dependency_trees.managed/dependency_B@1.0.0?type=jar"; + private static final String MANAGED_DEPENDENCY_C1 = "pkg:maven/com.example.dependency_trees.managed/dependency_C@1.0.0?type=jar"; + private static final String MANAGED_DEPENDENCY_C2 = "pkg:maven/com.example.dependency_trees.managed/dependency_C@2.0.0?type=jar"; + private static final String MANAGED_DEPENDENCY_D = "pkg:maven/com.example.dependency_trees.managed/dependency_D@1.0.0?type=jar"; + private static final String MANAGED_DEPENDENCY_E = "pkg:maven/com.example.dependency_trees.managed/dependency_E@1.0.0?type=jar"; + + public DependencyTreeTest(MavenRuntimeBuilder runtimeBuilder) throws Exception { + super(runtimeBuilder); + } + + @Test + public void testDependencyTrees() throws Exception { + final File projDir = cleanAndBuild("dependency_trees", null); + checkExclusion(projDir); + checkManaged(projDir); + } + + /** + * Test for alternative dependency trees generated through exclusions in the hierarchy + */ + public void checkExclusion(final File projDir) throws Exception { + final Document bom = readXML(new File(projDir, "target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); + + final NodeList componentsList = bom.getElementsByTagName("components"); + assertEquals("Expected a single components element", 1, componentsList.getLength()); + final Element components = (Element)componentsList.item(0); + + final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); + assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); + final Element dependencies = (Element)dependenciesList.item(0); + + /* + We create an aggregated BOM containing two dependency hierarchies which deviate at dependency_B. + + The first graph is rooted at dependency_A and includes dependency_E as below + + com.example.dependency_trees.exclusion:dependency_A:jar:1.0.0 + \- com.example.dependency_trees.exclusion:dependency_B:jar:1.0.0:compile + +- com.example.dependency_trees.exclusion:dependency_C:jar:1.0.0:compile + | \- com.example.dependency_trees.exclusion:dependency_D:jar:1.0.0:compile + \- com.example.dependency_trees.exclusion:dependency_E:jar:1.0.0:compile + + The second graph is rooted at dependency_F and excludes dependency_E as below + + com.example.dependency_trees.exclusion:dependency_F:jar:1.0.0 + \- com.example.dependency_trees.exclusion:dependency_B:jar:1.0.0:compile + \- com.example.dependency_trees.exclusion:dependency_C:jar:1.0.0:compile + \- com.example.dependency_trees.exclusion:dependency_D:jar:1.0.0:compile + */ + + // Ensure there are two components for Dependency_B + final Collection dependencyBNodes = getComponentNodes(purlToIdentities, components, EXCLUSION_DEPENDENCY_B); + assertNotNull("Could not find components for dependency_B", dependencyBNodes); + assertEquals("Incorrect component count for dependency_B", 2, dependencyBNodes.size()); + // Ensure there is a single component for Dependency_C + getComponentNode(purlToIdentities, components, EXCLUSION_DEPENDENCY_C); + // Ensure there is a single component for Dependency_D + getComponentNode(purlToIdentities, components, EXCLUSION_DEPENDENCY_D); + + // Check the first graph + + final Element firstDepA = getDependencyNode(purlToIdentities, dependencies, EXCLUSION_DEPENDENCY_A); + final Element firstDepADepB = getDependencyNode(purlToIdentities, firstDepA, EXCLUSION_DEPENDENCY_B); + + final String firstDepBPUrl = firstDepADepB.getAttribute("ref"); + final Element firstDepB = getDependencyNodeByIdentity(dependencies, firstDepBPUrl); + final Set firstDepBDependencies = getDependencyReferences(firstDepB); + assertEquals("Invalid dependency count for dependency_B", 2, firstDepBDependencies.size()); + containsDependency(purlToIdentities, firstDepBDependencies, EXCLUSION_DEPENDENCY_C); + containsDependency(purlToIdentities, firstDepBDependencies, EXCLUSION_DEPENDENCY_E); + + // Check the second graph + final Element secondDepF = getDependencyNode(purlToIdentities, dependencies, EXCLUSION_DEPENDENCY_F); + final Element secondDepFDepB = getDependencyNode(purlToIdentities, secondDepF, EXCLUSION_DEPENDENCY_B); + + final String secondDepBPUrl = secondDepFDepB.getAttribute("ref"); + final Element secondDepB = getDependencyNodeByIdentity(dependencies, secondDepBPUrl); + final Set secondDepBDependencies = getDependencyReferences(secondDepB); + assertEquals("Invalid dependency count for dependency_B", 1, secondDepBDependencies.size()); + containsDependency(purlToIdentities, secondDepBDependencies, EXCLUSION_DEPENDENCY_C); + + // Assert dependencies have different purls + assertNotEquals("Dependency B purls should be distinct", firstDepBPUrl, secondDepBPUrl); + } + + /** + * Test for alternative dependency trees generated through managed dependencies in the hierarchy + */ + public void checkManaged(final File projDir) throws Exception { + final Document bom = readXML(new File(projDir, "target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); + + final NodeList componentsList = bom.getElementsByTagName("components"); + assertEquals("Expected a single components element", 1, componentsList.getLength()); + final Element components = (Element)componentsList.item(0); + + final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); + assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); + final Element dependencies = (Element)dependenciesList.item(0); + + /* + We create an aggregated BOM containing two dependency hierarchies which deviate at dependency_B. + + The first graph is rooted at dependency_A and includes dependency_C with version 1.0.0 as below + + com.example.dependency_trees.managed:dependency_A:jar:1.0.0 + \- com.example.dependency_trees.managed:dependency_B:jar:1.0.0:compile + \- com.example.dependency_trees.managed:dependency_C:jar:1.0.0:compile + \- com.example.dependency_trees.managed:dependency_D:jar:1.0.0:compile + + The second graph is rooted at dependency_E and includes dependency_C with version 2.0.0 as below + + com.example.dependency_trees.managed:dependency_E:jar:1.0.0 + \- com.example.dependency_trees.managed:dependency_B:jar:1.0.0:compile + \- com.example.dependency_trees.managed:dependency_C:jar:2.0.0:compile + \- com.example.dependency_trees.managed:dependency_D:jar:1.0.0:compile + */ + + // Ensure there are two components for Dependency_B + final Collection dependencyBNodes = getComponentNodes(purlToIdentities, components, MANAGED_DEPENDENCY_B); + assertNotNull("Could not find components for dependency_B", dependencyBNodes); + assertEquals("Incorrect component count for dependency_B", 2, dependencyBNodes.size()); + // Ensure there are two components for Dependency_C + getComponentNode(purlToIdentities, components, MANAGED_DEPENDENCY_C1); + getComponentNode(purlToIdentities, components, MANAGED_DEPENDENCY_C2); + // Ensure there is a single component for Dependency_D + getComponentNode(purlToIdentities, components, MANAGED_DEPENDENCY_D); + + // Check the first graph + + final Element firstDepA = getDependencyNode(purlToIdentities, dependencies, MANAGED_DEPENDENCY_A); + final Element firstDepADepB = getDependencyNode(purlToIdentities, firstDepA, MANAGED_DEPENDENCY_B); + + final String firstDepBPUrl = firstDepADepB.getAttribute("ref"); + final Element firstDepB = getDependencyNodeByIdentity(dependencies, firstDepBPUrl); + final Set firstDepBDependencies = getDependencyReferences(firstDepB); + assertEquals("Invalid dependency count for dependency_B", 1, firstDepBDependencies.size()); + containsDependency(purlToIdentities, firstDepBDependencies, MANAGED_DEPENDENCY_C1); + + // Check the second graph + final Element secondDepE = getDependencyNode(purlToIdentities, dependencies, MANAGED_DEPENDENCY_E); + final Element secondDepEDepB = getDependencyNode(purlToIdentities, secondDepE, MANAGED_DEPENDENCY_B); + + final String secondDepBPUrl = secondDepEDepB.getAttribute("ref"); + final Element secondDepB = getDependencyNodeByIdentity(dependencies, secondDepBPUrl); + final Set secondDepBDependencies = getDependencyReferences(secondDepB); + assertEquals("Invalid dependency count for dependency_B", 1, secondDepBDependencies.size()); + containsDependency(purlToIdentities, secondDepBDependencies, MANAGED_DEPENDENCY_C2); + + // Assert dependencies have different purls + assertNotEquals("Dependency B purls should be distinct", firstDepBPUrl, secondDepBPUrl); + } +} diff --git a/src/test/java/org/cyclonedx/maven/Issue116Test.java b/src/test/java/org/cyclonedx/maven/Issue116Test.java index f71cf894..6bc969d1 100644 --- a/src/test/java/org/cyclonedx/maven/Issue116Test.java +++ b/src/test/java/org/cyclonedx/maven/Issue116Test.java @@ -38,7 +38,7 @@ public void testPluginWithActiviti() throws Exception { // assert commons-lang3 has appeared in the dependency graph multiple times String bomContents = fileRead(new File(projDir, "target/bom.xml"), true); - int matches = StringUtils.countMatches(bomContents, ""); + int matches = StringUtils.countMatches(bomContents, "> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); final NodeList componentsList = bom.getElementsByTagName("components"); assertEquals("Expected a single components element", 1, componentsList.getLength()); - final Node components = componentsList.item(0); + final Element components = (Element)componentsList.item(0); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); // BOM should not contain pkg:maven/com.example/issue284_provided_dependency@1.0.0?type=jar - final Node testIssue284ProvidedDependency1ComponentNode = getComponentNode(components, ISSUE284_PROVIDED_DEPENDENCY); - assertNull("Unexpected provided_dependency:1.0.0 component discovered in BOM", testIssue284ProvidedDependency1ComponentNode); + final Collection testIssue284ProvidedDependency1ComponentNodes = getComponentNodes(purlToIdentities, components, ISSUE284_PROVIDED_DEPENDENCY); + assertNull("Unexpected provided_dependency:1.0.0 component discovered in BOM", testIssue284ProvidedDependency1ComponentNodes); /* */ - final Node issue284Dependency2Node = getDependencyNode(dependencies, ISSUE284_DEPENDENCY2); - assertNotNull("Missing issue284_dependency2 dependency", issue284Dependency2Node); + final Element issue284Dependency2Node = getDependencyNode(purlToIdentities, dependencies, ISSUE284_DEPENDENCY2); Set issue284Dependency2Dependencies = getDependencyReferences(issue284Dependency2Node); assertEquals("Invalid dependency count for issue284_dependency2", 1, issue284Dependency2Dependencies.size()); - assertTrue("Missing issue284_shared_dependency1 dependency for issue284_dependency2", issue284Dependency2Dependencies.contains(ISSUE284_SHARED_DEPENDENCY1)); + containsDependency(purlToIdentities, issue284Dependency2Dependencies, ISSUE284_SHARED_DEPENDENCY1); /* */ - final Node issue284SharedDependency1Node = getDependencyNode(dependencies, ISSUE284_SHARED_DEPENDENCY1); - assertNotNull("Missing issue284_shared_dependency1 dependency", issue284SharedDependency1Node); + final Element issue284SharedDependency1Node = getDependencyNode(purlToIdentities, dependencies, ISSUE284_SHARED_DEPENDENCY1); Set issue284SharedDependency1Dependencies = getDependencyReferences(issue284SharedDependency1Node); assertEquals("Invalid dependency count for issue284_shared_dependency1", 1, issue284SharedDependency1Dependencies.size()); - assertTrue("Missing issue284_shared_dependency2 dependency for issue284_shared_dependency1", issue284SharedDependency1Dependencies.contains(ISSUE284_SHARED_DEPENDENCY2)); + containsDependency(purlToIdentities, issue284SharedDependency1Dependencies, ISSUE284_SHARED_DEPENDENCY2); } } diff --git a/src/test/java/org/cyclonedx/maven/RuntimeTest.java b/src/test/java/org/cyclonedx/maven/RuntimeTest.java index 1b7d627b..9b8da7a6 100644 --- a/src/test/java/org/cyclonedx/maven/RuntimeTest.java +++ b/src/test/java/org/cyclonedx/maven/RuntimeTest.java @@ -1,20 +1,22 @@ package org.cyclonedx.maven; import java.io.File; +import java.util.Collection; +import java.util.Map; import java.util.Set; +import static org.cyclonedx.maven.TestUtils.containsDependency; import static org.cyclonedx.maven.TestUtils.getDependencyNode; import static org.cyclonedx.maven.TestUtils.getDependencyReferences; +import static org.cyclonedx.maven.TestUtils.getPUrlToIdentities; import static org.cyclonedx.maven.TestUtils.readXML; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; import org.junit.Test; import org.junit.runner.RunWith; import org.w3c.dom.Document; -import org.w3c.dom.Node; +import org.w3c.dom.Element; import org.w3c.dom.NodeList; import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder; @@ -35,7 +37,7 @@ public class RuntimeTest extends BaseMavenVerifier { private static final String RUNTIME_PROVIDED = "pkg:maven/com.example/runtime_provided@1.0.0?type=jar"; private static final String RUNTIME_TEST = "pkg:maven/com.example/runtime_test@1.0.0?type=jar"; private static final String RUNTIME_SHARED_DEPENDENCY1 = "pkg:maven/com.example/runtime_shared_dependency1@1.0.0?type=jar"; - private static final Object RUNTIME_SHARED_DEPENDENCY2 = "pkg:maven/com.example/runtime_shared_dependency2@1.0.0?type=jar"; + private static final String RUNTIME_SHARED_DEPENDENCY2 = "pkg:maven/com.example/runtime_shared_dependency2@1.0.0?type=jar"; public RuntimeTest(MavenRuntimeBuilder runtimeBuilder) throws Exception { super(runtimeBuilder); @@ -51,10 +53,11 @@ public void testRuntime() throws Exception { public void checkRuntimeCompile(final File projDir) throws Exception { final Document bom = readXML(new File(projDir, "runtime_compile/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); /* @@ -62,18 +65,16 @@ public void checkRuntimeCompile(final File projDir) throws Exception { */ - final Node runtimeCompileNode = getDependencyNode(dependencies, RUNTIME_COMPILE); - assertNotNull("Missing runtime_compile dependency", runtimeCompileNode); + final Element runtimeCompileNode = getDependencyNode(purlToIdentities, dependencies, RUNTIME_COMPILE); Set runtimeCompileDependencies = getDependencyReferences(runtimeCompileNode); assertEquals("Invalid dependency count for runtime_compile", 2, runtimeCompileDependencies.size()); - assertTrue("Missing runtime_runtime_dependency dependency for runtime_compile", runtimeCompileDependencies.contains(RUNTIME_RUNTIME_DEPENDENCY)); - assertTrue("Missing runtime_dependency dependency for runtime_compile", runtimeCompileDependencies.contains(RUNTIME_DEPENDENCY)); + containsDependency(purlToIdentities, runtimeCompileDependencies, RUNTIME_RUNTIME_DEPENDENCY); + containsDependency(purlToIdentities, runtimeCompileDependencies, RUNTIME_DEPENDENCY); /* */ - final Node runtimeRuntimeDependencyNode = getDependencyNode(dependencies, RUNTIME_RUNTIME_DEPENDENCY); - assertNotNull("Missing runtime_runtime_dependency dependency", runtimeRuntimeDependencyNode); + final Element runtimeRuntimeDependencyNode = getDependencyNode(purlToIdentities, dependencies, RUNTIME_RUNTIME_DEPENDENCY); Set runtimeRuntimeDependencyDependencies = getDependencyReferences(runtimeRuntimeDependencyNode); assertEquals("Invalid dependency count for runtime_runtime_dependency", 0, runtimeRuntimeDependencyDependencies.size()); @@ -82,30 +83,29 @@ public void checkRuntimeCompile(final File projDir) throws Exception { */ - final Node runtimeDependencyNode = getDependencyNode(dependencies, RUNTIME_DEPENDENCY); - assertNotNull("Missing runtime_dependency dependency", runtimeDependencyNode); + final Element runtimeDependencyNode = getDependencyNode(purlToIdentities, dependencies, RUNTIME_DEPENDENCY); Set runtimeDependencyDependencies = getDependencyReferences(runtimeDependencyNode); assertEquals("Invalid dependency count for runtime_dependency", 1, runtimeDependencyDependencies.size()); - assertTrue("Missing runtime_shared_dependency1 dependency for runtime_dependency", runtimeDependencyDependencies.contains(RUNTIME_SHARED_DEPENDENCY1)); + containsDependency(purlToIdentities, runtimeDependencyDependencies, RUNTIME_SHARED_DEPENDENCY1); /* */ - final Node runtimeSharedDependency1Node = getDependencyNode(dependencies, RUNTIME_SHARED_DEPENDENCY1); - assertNotNull("Missing runtime_shared_dependency1 dependency", runtimeSharedDependency1Node); + final Element runtimeSharedDependency1Node = getDependencyNode(purlToIdentities, dependencies, RUNTIME_SHARED_DEPENDENCY1); Set runtimeSharedDependency1Dependencies = getDependencyReferences(runtimeSharedDependency1Node); assertEquals("Invalid dependency count for runtime_shared_dependency1", 1, runtimeSharedDependency1Dependencies.size()); - assertTrue("Missing runtime_shared_dependency2 dependency for runtime_shared_dependency1", runtimeSharedDependency1Dependencies.contains(RUNTIME_SHARED_DEPENDENCY2)); + containsDependency(purlToIdentities, runtimeSharedDependency1Dependencies, RUNTIME_SHARED_DEPENDENCY2); } public void checkRuntimeProvided(final File projDir) throws Exception { final Document bom = readXML(new File(projDir, "runtime_provided/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); /* @@ -113,39 +113,37 @@ public void checkRuntimeProvided(final File projDir) throws Exception { */ - final Node runtimeProvidedNode = getDependencyNode(dependencies, RUNTIME_PROVIDED); - assertNotNull("Missing runtime_provided dependency", runtimeProvidedNode); + final Element runtimeProvidedNode = getDependencyNode(purlToIdentities, dependencies, RUNTIME_PROVIDED); Set runtimeProvidedDependencies = getDependencyReferences(runtimeProvidedNode); assertEquals("Invalid dependency count for runtime_provided", 2, runtimeProvidedDependencies.size()); - assertTrue("Missing runtime_shared_dependency1 dependency for runtime_provided", runtimeProvidedDependencies.contains(RUNTIME_SHARED_DEPENDENCY1)); - assertTrue("Missing runtime_runtime_dependency dependency for runtime_provided", runtimeProvidedDependencies.contains(RUNTIME_RUNTIME_DEPENDENCY)); + containsDependency(purlToIdentities, runtimeProvidedDependencies, RUNTIME_SHARED_DEPENDENCY1); + containsDependency(purlToIdentities, runtimeProvidedDependencies, RUNTIME_RUNTIME_DEPENDENCY); /* */ - final Node runtimeSharedDependency1Node = getDependencyNode(dependencies, RUNTIME_SHARED_DEPENDENCY1); - assertNotNull("Missing runtime_shared_dependency1 dependency", runtimeSharedDependency1Node); + final Element runtimeSharedDependency1Node = getDependencyNode(purlToIdentities, dependencies, RUNTIME_SHARED_DEPENDENCY1); Set runtimeSharedDependency1Dependencies = getDependencyReferences(runtimeSharedDependency1Node); assertEquals("Invalid dependency count for runtime_shared_dependency1", 1, runtimeSharedDependency1Dependencies.size()); - assertTrue("Missing runtime_shared_dependency2 dependency for runtime_shared_dependency1", runtimeSharedDependency1Dependencies.contains(RUNTIME_SHARED_DEPENDENCY2)); + containsDependency(purlToIdentities, runtimeSharedDependency1Dependencies, RUNTIME_SHARED_DEPENDENCY2); /* */ - final Node runtimeRuntimeDependencyNode = getDependencyNode(dependencies, RUNTIME_RUNTIME_DEPENDENCY); - assertNotNull("Missing runtime_runtime_dependency dependency", runtimeRuntimeDependencyNode); + final Element runtimeRuntimeDependencyNode = getDependencyNode(purlToIdentities, dependencies, RUNTIME_RUNTIME_DEPENDENCY); Set runtimeRuntimeDependencyDependencies = getDependencyReferences(runtimeRuntimeDependencyNode); assertEquals("Invalid dependency count for runtime_runtime_dependency", 0, runtimeRuntimeDependencyDependencies.size()); } public void checkRuntimeTest(final File projDir) throws Exception { final Document bom = readXML(new File(projDir, "runtime_test/target/bom.xml")); + final Map> purlToIdentities = getPUrlToIdentities(bom.getDocumentElement()); final NodeList dependenciesList = bom.getElementsByTagName("dependencies"); assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength()); - final Node dependencies = dependenciesList.item(0); + final Element dependencies = (Element)dependenciesList.item(0); /* @@ -153,29 +151,26 @@ public void checkRuntimeTest(final File projDir) throws Exception { */ - final Node runtimeTestNode = getDependencyNode(dependencies, RUNTIME_TEST); - assertNotNull("Missing runtime_test dependency", runtimeTestNode); + final Element runtimeTestNode = getDependencyNode(purlToIdentities, dependencies, RUNTIME_TEST); Set runtimeTestDependencies = getDependencyReferences(runtimeTestNode); assertEquals("Invalid dependency count for runtime_test", 2, runtimeTestDependencies.size()); - assertTrue("Missing runtime_shared_dependency1 dependency for runtime_test", runtimeTestDependencies.contains(RUNTIME_SHARED_DEPENDENCY1)); - assertTrue("Missing runtime_runtime_dependency dependency for runtime_test", runtimeTestDependencies.contains(RUNTIME_RUNTIME_DEPENDENCY)); + containsDependency(purlToIdentities, runtimeTestDependencies, RUNTIME_SHARED_DEPENDENCY1); + containsDependency(purlToIdentities, runtimeTestDependencies, RUNTIME_RUNTIME_DEPENDENCY); /* */ - final Node runtimeSharedDependency1Node = getDependencyNode(dependencies, RUNTIME_SHARED_DEPENDENCY1); - assertNotNull("Missing runtime_shared_dependency1 dependency", runtimeSharedDependency1Node); + final Element runtimeSharedDependency1Node = getDependencyNode(purlToIdentities, dependencies, RUNTIME_SHARED_DEPENDENCY1); Set runtimeSharedDependency1Dependencies = getDependencyReferences(runtimeSharedDependency1Node); assertEquals("Invalid dependency count for runtime_shared_dependency1", 1, runtimeSharedDependency1Dependencies.size()); - assertTrue("Missing runtime_shared_dependency2 dependency for runtime_shared_dependency1", runtimeSharedDependency1Dependencies.contains(RUNTIME_SHARED_DEPENDENCY2)); + containsDependency(purlToIdentities, runtimeSharedDependency1Dependencies, RUNTIME_SHARED_DEPENDENCY2); /* */ - final Node runtimeRuntimeDependencyNode = getDependencyNode(dependencies, RUNTIME_RUNTIME_DEPENDENCY); - assertNotNull("Missing runtime_runtime_dependency dependency", runtimeRuntimeDependencyNode); + final Element runtimeRuntimeDependencyNode = getDependencyNode(purlToIdentities, dependencies, RUNTIME_RUNTIME_DEPENDENCY); Set runtimeRuntimeDependencyDependencies = getDependencyReferences(runtimeRuntimeDependencyNode); assertEquals("Invalid dependency count for runtime_runtime_dependency", 0, runtimeRuntimeDependencyDependencies.size()); } diff --git a/src/test/java/org/cyclonedx/maven/TestUtils.java b/src/test/java/org/cyclonedx/maven/TestUtils.java index bb9731b4..51ec4a2b 100644 --- a/src/test/java/org/cyclonedx/maven/TestUtils.java +++ b/src/test/java/org/cyclonedx/maven/TestUtils.java @@ -2,7 +2,11 @@ import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilder; @@ -10,58 +14,113 @@ import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; class TestUtils { - static Node getDependencyNode(final Node dependencies, final String ref) { - return getChildElement(dependencies, ref, "dependency", "ref"); + static Element getDependencyNode(final Map> purlToIdentities, final Element dependencies, final String purl) throws Exception { + int numElements = 0; + Collection elements = getDependencyNodes(purlToIdentities, dependencies, purl); + if (elements != null) { + numElements = elements.size(); + if (numElements == 1) { + return elements.iterator().next(); + } + } + throw new Exception("Expected a single dependency for purl " + purl + ", found " + numElements + " dependencies"); } - static Node getComponentNode(final Node components, final String ref) { - return getChildElement(components, ref, "component", "bom-ref"); + static Element getComponentNode(final Map> purlToIdentities, final Element components, final String purl) throws Exception { + int numElements = 0; + Collection elements = getComponentNodes(purlToIdentities, components, purl); + if (elements != null) { + numElements = elements.size(); + if (numElements == 1) { + return elements.iterator().next(); + } + } + throw new Exception("Expected a single component for purl " + purl + ", found " + numElements + " components"); } - private static Node getChildElement(final Node parent, final String ref, final String elementName, final String attrName) { + static Collection getDependencyNodes(final Map> purlToIdentities, final Element dependencies, final String purl) { + return getChildElements(purlToIdentities, dependencies, purl, "dependency", "ref"); + } + + static Collection getComponentNodes(final Map> purlToIdentities, final Element components, final String purl) { + return getChildElements(purlToIdentities, components, purl, "component", "bom-ref"); + } + + static Element getDependencyNodeByIdentity(final Element dependencies, final String identity) throws Exception { + int numElements = 0; + Collection elements = getChildElementsByIdentity(dependencies, Arrays.asList(identity), "dependency", "ref"); + if (elements != null) { + numElements = elements.size(); + if (numElements == 1) { + return elements.iterator().next(); + } + } + throw new Exception("Expected a single dependency for identity " + identity + ", found " + numElements + " dependencies"); + } + + static Element getComponentNodeByIdentity(final Element components, final String identity) throws Exception { + int numElements = 0; + Collection elements = getChildElementsByIdentity(components, Arrays.asList(identity), "component", "bom-ref"); + if (elements != null) { + numElements = elements.size(); + if (numElements == 1) { + return elements.iterator().next(); + } + } + throw new Exception("Expected a single compoennt for identity " + identity + ", found " + numElements + " components"); + } + + private static Collection getChildElements(final Map> purlToIdentities, final Element parent, final String purl, final String elementName, final String attrName) { + final Collection identities = purlToIdentities.get(purl); + return getChildElementsByIdentity(parent, identities, elementName, attrName); + } + + private static Collection getChildElementsByIdentity(final Element parent, final Collection identities, final String elementName, final String attrName) { + if (identities == null) { + return null; + } + final Collection childElements = new HashSet<>(); + final NodeList children = parent.getChildNodes(); final int numChildNodes = children.getLength(); for (int index = 0 ; index < numChildNodes ; index++) { final Node child = children.item(index); if ((child.getNodeType() == Node.ELEMENT_NODE) && elementName.equals(child.getNodeName())) { final Node refNode = child.getAttributes().getNamedItem(attrName); - if (ref.equals(refNode.getNodeValue())) { - return child; + if (identities.contains(refNode.getNodeValue())) { + childElements.add((Element)child); } } } - return null; + return childElements; } - static Set getComponentReferences(final Node parent) { + static Set getComponentReferences(final Element parent) { return getReferences(null, parent, "component", "bom-ref"); } - static Set getDependencyReferences(final Node parent) { + static Set getDependencyReferences(final Element parent) { return getReferences(null, parent, "dependency", "ref"); } - private static Set getReferences(Set references, final Node rootNode, final String elementName, final String attrName) { + private static Set getReferences(Set references, final Element root, final String elementName, final String attrName) { if (references == null) { references = new HashSet<>(); } - final NodeList children = rootNode.getChildNodes(); - final int numChildNodes = children.getLength(); - for (int index = 0 ; index < numChildNodes ; index++) { - final Node child = children.item(index); - if (child.getNodeType() == Node.ELEMENT_NODE) { - if (elementName.equals(child.getNodeName())) { - final Node refNode = child.getAttributes().getNamedItem(attrName); - if (refNode != null) { - references.add(refNode.getNodeValue()); - } - } - getReferences(references, child, elementName, attrName); + + final NodeList components = root.getElementsByTagName(elementName); + final int numComponents = components.getLength(); + for (int index = 0 ; index < numComponents ; index++) { + final Element component = (Element) components.item(index); + final String value = component.getAttribute(attrName); + if (value != null) { + references.add(value); } } return references; @@ -77,4 +136,35 @@ static Document readXML(File file) throws IOException, SAXException, ParserConfi final DocumentBuilder builder = factory.newDocumentBuilder(); return builder.parse(file); } + + static Map> getPUrlToIdentities(final Element root) { + final Map> purlToIdentities = new HashMap<>(); + final NodeList components = root.getElementsByTagName("component"); + for (int index = 0 ; index < components.getLength() ; index++) { + final Element component = (Element)components.item(index); + final String bomRef = component.getAttribute("bom-ref"); + final String purl = component.getElementsByTagName("purl").item(0).getTextContent(); + final Collection identities = purlToIdentities.get(purl); + if (identities != null) { + identities.add(bomRef); + } else { + final Collection newIdentities = new HashSet<>(); + newIdentities.add(bomRef); + purlToIdentities.put(purl, newIdentities); + } + } + return purlToIdentities; + } + + static boolean containsDependency(final Map> purlToIdentities, final Set dependencies, final String purl) throws Exception { + final Collection identities = purlToIdentities.get(purl); + int numIdentities = 0; + if (identities != null) { + numIdentities = identities.size(); + if (numIdentities == 1) { + return dependencies.contains(identities.iterator().next()); + } + } + throw new Exception("Expected a single identity for purl " + purl + ", found " + numIdentities + " identities"); + } } diff --git a/src/test/resources/dependency_trees/exclusion/dependency_A/pom.xml b/src/test/resources/dependency_trees/exclusion/dependency_A/pom.xml new file mode 100644 index 00000000..a4a2d7f4 --- /dev/null +++ b/src/test/resources/dependency_trees/exclusion/dependency_A/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + com.example.dependency_trees.exclusion + exclusion_parent + 1.0.0 + + + dependency_A + + Dependency A + + + + com.example.dependency_trees.exclusion + dependency_B + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/exclusion/dependency_B/pom.xml b/src/test/resources/dependency_trees/exclusion/dependency_B/pom.xml new file mode 100644 index 00000000..9c8d9429 --- /dev/null +++ b/src/test/resources/dependency_trees/exclusion/dependency_B/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + + com.example.dependency_trees.exclusion + exclusion_parent + 1.0.0 + + + dependency_B + + Dependency B + + + + com.example.dependency_trees.exclusion + dependency_C + 1.0.0 + compile + + + com.example.dependency_trees.exclusion + dependency_E + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/exclusion/dependency_C/pom.xml b/src/test/resources/dependency_trees/exclusion/dependency_C/pom.xml new file mode 100644 index 00000000..fcf8d10e --- /dev/null +++ b/src/test/resources/dependency_trees/exclusion/dependency_C/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + com.example.dependency_trees.exclusion + exclusion_parent + 1.0.0 + + + dependency_C + + Dependency C + + + + com.example.dependency_trees.exclusion + dependency_D + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/exclusion/dependency_D/pom.xml b/src/test/resources/dependency_trees/exclusion/dependency_D/pom.xml new file mode 100644 index 00000000..36fde135 --- /dev/null +++ b/src/test/resources/dependency_trees/exclusion/dependency_D/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + com.example.dependency_trees.exclusion + exclusion_parent + 1.0.0 + + + dependency_D + + Dependency D + diff --git a/src/test/resources/dependency_trees/exclusion/dependency_E/pom.xml b/src/test/resources/dependency_trees/exclusion/dependency_E/pom.xml new file mode 100644 index 00000000..197610be --- /dev/null +++ b/src/test/resources/dependency_trees/exclusion/dependency_E/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + com.example.dependency_trees.exclusion + exclusion_parent + 1.0.0 + + + dependency_E + + Dependency E + diff --git a/src/test/resources/dependency_trees/exclusion/dependency_F/pom.xml b/src/test/resources/dependency_trees/exclusion/dependency_F/pom.xml new file mode 100644 index 00000000..bda260c3 --- /dev/null +++ b/src/test/resources/dependency_trees/exclusion/dependency_F/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + + com.example.dependency_trees.exclusion + exclusion_parent + 1.0.0 + + + dependency_F + + Dependency F + + + + + com.example.dependency_trees.exclusion + dependency_B + 1.0.0 + + + com.example.dependency_trees.exclusion + dependency_E + + + + + + + + + com.example.dependency_trees.exclusion + dependency_B + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/exclusion/pom.xml b/src/test/resources/dependency_trees/exclusion/pom.xml new file mode 100644 index 00000000..fdef4e30 --- /dev/null +++ b/src/test/resources/dependency_trees/exclusion/pom.xml @@ -0,0 +1,28 @@ + + + + 4.0.0 + + com.example.dependency_trees.exclusion + exclusion_parent + pom + 1.0.0 + + Exclusion Tests Parent + + + dependency_A + dependency_B + dependency_C + dependency_D + dependency_E + dependency_F + + + + UTF-8 + + + diff --git a/src/test/resources/dependency_trees/managed/dependency_A/pom.xml b/src/test/resources/dependency_trees/managed/dependency_A/pom.xml new file mode 100644 index 00000000..f2d3c3b5 --- /dev/null +++ b/src/test/resources/dependency_trees/managed/dependency_A/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + com.example.dependency_trees.managed + managed_parent + 1.0.0 + + + dependency_A + + Dependency A + + + + com.example.dependency_trees.managed + dependency_B + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/managed/dependency_B/pom.xml b/src/test/resources/dependency_trees/managed/dependency_B/pom.xml new file mode 100644 index 00000000..827daa8d --- /dev/null +++ b/src/test/resources/dependency_trees/managed/dependency_B/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + com.example.dependency_trees.managed + managed_parent + 1.0.0 + + + dependency_B + + Dependency B + + + + com.example.dependency_trees.managed + dependency_C + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/managed/dependency_C1/pom.xml b/src/test/resources/dependency_trees/managed/dependency_C1/pom.xml new file mode 100644 index 00000000..799d8025 --- /dev/null +++ b/src/test/resources/dependency_trees/managed/dependency_C1/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + com.example.dependency_trees.managed + managed_parent + 1.0.0 + + + dependency_C + + Dependency C + + + + com.example.dependency_trees.managed + dependency_D + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/managed/dependency_C2/pom.xml b/src/test/resources/dependency_trees/managed/dependency_C2/pom.xml new file mode 100644 index 00000000..49eb8d11 --- /dev/null +++ b/src/test/resources/dependency_trees/managed/dependency_C2/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + + com.example.dependency_trees.managed + managed_parent + 1.0.0 + + + dependency_C + 2.0.0 + + Dependency C + + + + com.example.dependency_trees.managed + dependency_D + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/managed/dependency_D/pom.xml b/src/test/resources/dependency_trees/managed/dependency_D/pom.xml new file mode 100644 index 00000000..aad385f0 --- /dev/null +++ b/src/test/resources/dependency_trees/managed/dependency_D/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + com.example.dependency_trees.managed + managed_parent + 1.0.0 + + + dependency_D + + Dependency D + diff --git a/src/test/resources/dependency_trees/managed/dependency_E/pom.xml b/src/test/resources/dependency_trees/managed/dependency_E/pom.xml new file mode 100644 index 00000000..d9eabf0c --- /dev/null +++ b/src/test/resources/dependency_trees/managed/dependency_E/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + + com.example.dependency_trees.managed + managed_parent + 1.0.0 + + + dependency_E + + Dependency E + + + + + com.example.dependency_trees.managed + dependency_C + 2.0.0 + + + + + + + com.example.dependency_trees.managed + dependency_B + 1.0.0 + compile + + + diff --git a/src/test/resources/dependency_trees/managed/pom.xml b/src/test/resources/dependency_trees/managed/pom.xml new file mode 100644 index 00000000..622300f9 --- /dev/null +++ b/src/test/resources/dependency_trees/managed/pom.xml @@ -0,0 +1,28 @@ + + + + 4.0.0 + + com.example.dependency_trees.managed + managed_parent + pom + 1.0.0 + + Managed Dependency Tests Parent + + + dependency_A + dependency_B + dependency_C1 + dependency_C2 + dependency_D + dependency_E + + + + UTF-8 + + + diff --git a/src/test/resources/dependency_trees/pom.xml b/src/test/resources/dependency_trees/pom.xml new file mode 100644 index 00000000..103ec4bd --- /dev/null +++ b/src/test/resources/dependency_trees/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + com.example.dependency_trees + dependency_trees_parent + pom + 1.0.0 + + Dependency Trees Tests Parent + + + exclusion + managed + + + + + + org.cyclonedx + cyclonedx-maven-plugin + ${current.version} + + + package + + makeAggregateBom + + + + + library + 1.3 + true + true + true + false + false + false + false + xml + + + + + + + maven-jar-plugin + 3.3.0 + + + + + + + UTF-8 + + From 019db60a6eb63b311736bbf63e4cc3e34c9964d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Boutemy?= Date: Wed, 15 Mar 2023 08:21:53 +0100 Subject: [PATCH 2/2] fix code structure consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hervé Boutemy --- .../cyclonedx/maven/BaseCycloneDxMojo.java | 29 ++++++++++--------- .../org/cyclonedx/maven/CycloneDxMojo.java | 1 + .../cyclonedx/maven/CycloneDxPackageMojo.java | 1 + .../maven/ProjectDependenciesConverter.java | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java index e2118f02..8fb405ed 100644 --- a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java +++ b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java @@ -36,7 +36,6 @@ 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; @@ -263,23 +262,25 @@ public void execute() throws MojoExecutionException { if (includeTestScope) scopes.add("test"); final Metadata metadata = modelConverter.convert(project, analysis + " " + String.join("+", scopes), projectType, schemaVersion(), includeLicenseText); + 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, componentMap, dependencyMap); + generateBom(analysis, metadata, new ArrayList<>(componentMap.values()), new ArrayList<>(dependencyMap.values())); } } - private void generateBom(String analysis, Metadata metadata, Map components, Map dependencies) throws MojoExecutionException { + private void generateBom(String analysis, Metadata metadata, List components, List dependencies) throws MojoExecutionException { try { getLog().info(String.format(MESSAGE_CREATING_BOM, schemaVersion, components.size())); final Bom bom = new Bom(); - bom.setComponents(new ArrayList<>(components.values())); + bom.setComponents(components); if (schemaVersion().getVersion() >= 1.1 && includeBomSerialNumber) { bom.setSerialNumber("urn:uuid:" + UUID.randomUUID()); @@ -287,7 +288,7 @@ private void generateBom(String analysis, Metadata metadata, Map= 1.2) { bom.setMetadata(metadata); - bom.setDependencies(new ArrayList<>(dependencies.values())); + bom.setDependencies(dependencies); } /*if (schemaVersion().getVersion() >= 1.3) { @@ -395,7 +396,7 @@ protected void populateComponents(final Map components, final 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.Scope artifactScope = (dependencyAnalysis != null ? inferComponentScope(artifact, dependencyAnalysis) : null); final Component component = components.get(identity); if (component == null) { final Component newComponent = convert(artifact); @@ -410,12 +411,12 @@ protected void populateComponents(final Map components, final } /** - * Infer BOM component scope based on Maven project dependency analysis. + * 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 + * @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) { @@ -440,22 +441,22 @@ protected Component.Scope inferComponentScope(Artifact artifact, ProjectDependen return null; } - private Scope mergeScopes(final Scope existing, final Scope project) { + 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 Scope merged; + final Component.Scope merged; if (existing == null) { - merged = (project == Scope.REQUIRED ? Scope.REQUIRED : null); + merged = (project == Component.Scope.REQUIRED ? Component.Scope.REQUIRED : null); } else { switch (existing) { case REQUIRED: - merged = Scope.REQUIRED; + merged = Component.Scope.REQUIRED; break; case OPTIONAL: - merged = (project == Scope.REQUIRED || project == null ? project : existing); + merged = (project == Component.Scope.REQUIRED || project == null ? project : existing); break; case EXCLUDED: - merged = (project != Scope.EXCLUDED ? project : Scope.EXCLUDED); + merged = (project != Component.Scope.EXCLUDED ? project : Component.Scope.EXCLUDED); break; default: merged = project; diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java index 316a35ee..0a38fc31 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java @@ -106,6 +106,7 @@ protected String extractComponentsAndDependencies(final Map c projectIdentities.put(projectBomComponent.getPurl(), projectBomComponent.getBomRef()); populateComponents(components, getProject().getArtifacts(), projectPUrlToIdentity, doProjectDependencyAnalysis(getProject())); + dependencies.putAll(projectDependencies); return "makeBom"; diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java index 5e3d1fca..2adc4cce 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java @@ -77,6 +77,7 @@ protected String extractComponentsAndDependencies(Map compone projectIdentities.put(projectBomComponent.getPurl(), projectBomComponent.getBomRef()); populateComponents(components, mavenProject.getArtifacts(), projectPUrlToIdentity, null); + dependencies.putAll(projectDependencies); } diff --git a/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java b/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java index 691b2d18..383a9a51 100644 --- a/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java +++ b/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java @@ -44,7 +44,7 @@ public interface ProjectDependenciesConverter { * The map will be modified to reflect the distinct names, with references and the map keys * being updated. */ - void normalizeDependencies(final CycloneDxSchema.Version schemaVersion, final Map dependencies, final Map purlToIdentity) ; + void normalizeDependencies(CycloneDxSchema.Version schemaVersion, Map dependencies, Map purlToIdentity) ; /** * Check consistency between BOM components and BOM dependencies, and cleanup: drop components found while walking the