diff --git a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java index 78e6dd83..28c2afca 100644 --- a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java +++ b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java @@ -46,10 +46,15 @@ import org.cyclonedx.parsers.JsonParser; import org.cyclonedx.parsers.Parser; import org.cyclonedx.parsers.XmlParser; +import org.eclipse.aether.repository.ArtifactRepository; +import org.eclipse.aether.repository.RemoteRepository; import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; @@ -511,17 +516,37 @@ protected void logParameters() { } } - protected void populateComponents(final Set topLevelComponents, final Map components, final Map artifacts, final ProjectDependencyAnalysis dependencyAnalysis) { + protected void populateComponents(final Set topLevelComponents, final Map components, final Map artifacts, final Map artifactRemoteRepositories, final ProjectDependencyAnalysis dependencyAnalysis) { for (Map.Entry entry: artifacts.entrySet()) { final String purl = entry.getKey(); final Artifact artifact = entry.getValue(); final Component.Scope artifactScope = getComponentScope(artifact, dependencyAnalysis); final Component component = components.get(purl); + final ArtifactRepository repository = artifactRemoteRepositories.get(purl); + String repository_url = ""; + if (repository instanceof RemoteRepository) { + try { + String url = ((RemoteRepository) repository).getUrl(); + // As per purl spec, only repo.maven.apache.org/maven2 is considered the default + if (url != null && !url.startsWith("https://repo.maven.apache.org/maven2")) { + repository_url = "&repository_url=" + URLEncoder.encode(url, "UTF-8"); + } + } catch (UnsupportedEncodingException e) { + // ignore + } + } if (component == null) { final Component newComponent = convertMavenDependency(artifact); newComponent.setScope(artifactScope); + if (!repository_url.isEmpty()) { + newComponent.setPurl(newComponent.getPurl() + repository_url); + } components.put(purl, newComponent); } else if (!topLevelComponents.contains(purl)) { + String currentPurl = component.getPurl(); + if (!repository_url.isEmpty() && !currentPurl.contains("repository_url")) { + component.setPurl(currentPurl + repository_url); + } component.setScope(mergeScopes(component.getScope(), artifactScope)); } } diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java index 416596ad..08da1314 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java @@ -130,7 +130,7 @@ protected String extractComponentsAndDependencies(final Set topLevelComp components.put(projectBomComponent.getPurl(), projectBomComponent); topLevelComponents.add(projectBomComponent.getPurl()); - populateComponents(topLevelComponents, components, bomDependencies.getArtifacts(), doProjectDependencyAnalysis(mavenProject, bomDependencies)); + populateComponents(topLevelComponents, components, bomDependencies.getArtifacts(), bomDependencies.getArtifactRemoteRepositories(), doProjectDependencyAnalysis(mavenProject, bomDependencies)); projectDependencies.forEach(dependencies::putIfAbsent); } diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java index 2ca6e15a..b22a8526 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxMojo.java @@ -127,7 +127,7 @@ protected String extractComponentsAndDependencies(final Set topLevelComp components.put(projectBomComponent.getPurl(), projectBomComponent); topLevelComponents.add(projectBomComponent.getPurl()); - populateComponents(topLevelComponents, components, bomDependencies.getArtifacts(), doProjectDependencyAnalysis(getProject(), bomDependencies)); + populateComponents(topLevelComponents, components, bomDependencies.getArtifacts(), bomDependencies.getArtifactRemoteRepositories(), doProjectDependencyAnalysis(getProject(), bomDependencies)); projectDependencies.forEach(dependencies::putIfAbsent); diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java index 828d1037..0424b2ab 100644 --- a/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java +++ b/src/main/java/org/cyclonedx/maven/CycloneDxPackageMojo.java @@ -70,7 +70,7 @@ protected String extractComponentsAndDependencies(Set topLevelComponents components.put(projectBomComponent.getPurl(), projectBomComponent); topLevelComponents.add(projectBomComponent.getPurl()); - populateComponents(topLevelComponents, components, bomDependencies.getArtifacts(), null); + populateComponents(topLevelComponents, components, bomDependencies.getArtifacts(), bomDependencies.getArtifactRemoteRepositories(), null); projectDependencies.forEach(dependencies::putIfAbsent); } diff --git a/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java b/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java index 2d28c5fb..6c6bd663 100644 --- a/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java +++ b/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java @@ -34,11 +34,15 @@ import org.apache.maven.repository.RepositorySystem; import org.cyclonedx.CycloneDxSchema; import org.cyclonedx.model.Component; +import org.cyclonedx.model.Evidence; import org.cyclonedx.model.ExternalReference; +import org.cyclonedx.model.Hash; import org.cyclonedx.model.License; import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.Metadata; import org.cyclonedx.model.Tool; +import org.cyclonedx.model.component.evidence.Identity; +import org.cyclonedx.model.component.evidence.Method; import org.cyclonedx.util.BomUtils; import org.cyclonedx.util.LicenseResolver; import org.eclipse.aether.artifact.ArtifactProperties; @@ -52,15 +56,26 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.TreeMap; import java.util.stream.Collectors; + @Singleton @Named public class DefaultModelConverter implements ModelConverter { + // Confidence value for filename based identity detection + public static final double FILENAME_CONFIDENCE = 0.1; + + // Confidence value for hash comparison based identity detection + public static final double HASH_COMPARISON_CONFIDENCE = 0.8; private final Logger logger = LoggerFactory.getLogger(DefaultModelConverter.class); @Inject @@ -122,7 +137,7 @@ public String generateClassifierlessPackageUrl(final org.eclipse.aether.artifact } private boolean isEmpty(final String value) { - return (value == null) || (value.length() == 0); + return (value == null) || (value.isEmpty()); } private String generatePackageUrl(final org.eclipse.aether.artifact.Artifact artifact, final boolean includeVersion, final boolean includeClassifier) { @@ -184,10 +199,87 @@ public Component convertMavenDependency(Artifact artifact, CycloneDxSchema.Versi logger.warn("Unable to create Maven project for " + artifact.getId() + " from repository."); } } + // Set component evidence for cyclonedx 1.5 + if (CycloneDxSchema.Version.VERSION_15 == schemaVersion) { + addComponentEvidence(component, artifact); + } return component; } + /** + * Adds component identity evidence for the component using the filename, hash and gpg key files + * + * @param component Component for which the evidence needs to be attached + * @param artifact Maven artifact + */ + private static void addComponentEvidence(final Component component, final Artifact artifact) { + if (artifact.getFile() != null) { + final List sha1OrMd5Hashes = component.getHashes().stream().filter(hash -> hash.getAlgorithm().equals("SHA-1") || hash.getAlgorithm().equals("MD5")).collect(Collectors.toList()); + final Evidence evidence = new Evidence(); + final Identity identity = new Identity(); + final List methods = new ArrayList<>(); + final Method jarFileMethod = new Method(); + final Method ascFileMethod = new Method(); + double overallConfidence = 0.0; + final String jarPath = artifact.getFile().getAbsolutePath(); + final String sha1Path = jarPath.replace(".jar", ".jar.sha1"); + final String ascPath = jarPath.replace(".jar", ".jar.asc"); + final String md5Path = jarPath.replace(".jar", ".jar.md5"); + jarFileMethod.setValue(".m2/" + jarPath.replaceFirst("^(.*).m2/", "")); + jarFileMethod.setConfidence(FILENAME_CONFIDENCE); + jarFileMethod.setTechnique(Method.Technique.FILENAME); + methods.add(jarFileMethod); + overallConfidence += FILENAME_CONFIDENCE; + // For gpg signed artifacts, we can bump up the confidence a bit more + // In the future, there could be a setting to explicitly verify the gpg keys + if (Files.exists(Paths.get(ascPath))) { + ascFileMethod.setValue(".m2/" + ascPath.replaceFirst("^(.*).m2/", "")); + ascFileMethod.setConfidence(FILENAME_CONFIDENCE); + ascFileMethod.setTechnique(Method.Technique.FILENAME); + methods.add(ascFileMethod); + overallConfidence += FILENAME_CONFIDENCE; + } + final Path sha1FilePath = Paths.get(sha1Path); + final Path md5FilePath = Paths.get(md5Path); + if (!sha1OrMd5Hashes.isEmpty()) { + Path hashFileToUse = null; + // Prefer the .sha1 file followed by .md5 + if (Files.exists(sha1FilePath)) { + hashFileToUse = sha1FilePath; + } else if (Files.exists(md5FilePath)) { + hashFileToUse = md5FilePath; + } + if (hashFileToUse != null) { + try { + final Optional hashFileContent = Files.readAllLines(hashFileToUse).stream().findFirst(); + if (hashFileContent.isPresent()) { + String storedHashValue = hashFileContent.get().split(" ")[0].trim(); + for (Hash sha1OrMd5Hash : sha1OrMd5Hashes) { + if (storedHashValue.equals(sha1OrMd5Hash.getValue())) { + final Method hashMethod = new Method(); + hashMethod.setConfidence(HASH_COMPARISON_CONFIDENCE); + overallConfidence += HASH_COMPARISON_CONFIDENCE; + hashMethod.setTechnique(Method.Technique.HASH_COMPARISON); + hashMethod.setValue(storedHashValue); + methods.add(hashMethod); + break; + } + } + } + } catch (IOException e) { + // Invalid hash + } + } + } + identity.setField(Identity.Field.PURL); + identity.setConfidence(overallConfidence); + identity.setMethods(methods); + evidence.setIdentity(identity); + component.setEvidence(evidence); + } + } + private static void setExternalReferences(Component component, ExternalReference[] externalReferences) { if (externalReferences == null || externalReferences.length == 0) { return; @@ -240,7 +332,7 @@ private void extractComponentMetadata(MavenProject project, Component component, if (project.getIssueManagement() != null) { addExternalReference(ExternalReference.Type.ISSUE_TRACKER, project.getIssueManagement().getUrl(), component); } - if (project.getMailingLists() != null && project.getMailingLists().size() > 0) { + if (project.getMailingLists() != null && !project.getMailingLists().isEmpty()) { for (MailingList list : project.getMailingLists()) { String url = list.getArchive(); if (url == null) { @@ -401,6 +493,6 @@ private Component.Type resolveProjectType(String projectType) { } private static boolean isURLBlank(String url) { - return url == null || url.isEmpty() || url.trim().length() == 0; + return url == null || url.isEmpty() || url.trim().isEmpty(); } } diff --git a/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java b/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java index 3b00edaf..c3988bda 100644 --- a/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java +++ b/src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java @@ -35,6 +35,7 @@ import org.eclipse.aether.artifact.ArtifactProperties; import org.eclipse.aether.collection.CollectResult; import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.util.graph.transformer.ConflictResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,6 +67,8 @@ public class DefaultProjectDependenciesConverter implements ProjectDependenciesC private Set excludeTypesSet; private MavenDependencyScopes include; + final Map artifactRemoteRepositories = new LinkedHashMap<>(); + @Override public BomDependencies extractBOMDependencies(MavenProject mavenProject, MavenDependencyScopes include, String[] excludeTypes) throws MojoExecutionException { this.include = include; @@ -77,9 +80,8 @@ public BomDependencies extractBOMDependencies(MavenProject mavenProject, MavenDe final Map mavenArtifacts = new LinkedHashMap<>(); final Map mavenDependencyArtifacts = new LinkedHashMap<>(); try { - final DelegatingRepositorySystem delegateRepositorySystem = new DelegatingRepositorySystem(aetherRepositorySystem); + final DelegatingRepositorySystem delegateRepositorySystem = new DelegatingRepositorySystem(aetherRepositorySystem, modelConverter, artifactRemoteRepositories); final DependencyCollectorBuilder dependencyCollectorBuilder = new DefaultDependencyCollectorBuilder(delegateRepositorySystem); - final org.apache.maven.shared.dependency.graph.DependencyNode mavenRoot = dependencyCollectorBuilder.collectDependencyGraph(buildingRequest, null); populateArtifactMap(mavenArtifacts, mavenDependencyArtifacts, mavenRoot, 0); @@ -98,7 +100,7 @@ public BomDependencies 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 new BomDependencies(dependencies, mavenArtifacts, mavenDependencyArtifacts); + return new BomDependencies(dependencies, mavenArtifacts, mavenDependencyArtifacts, artifactRemoteRepositories); } private void populateArtifactMap(final Map artifactMap, final Map dependencyArtifactMap, final org.apache.maven.shared.dependency.graph.DependencyNode node, final int level) { diff --git a/src/main/java/org/cyclonedx/maven/DelegatingRepositorySystem.java b/src/main/java/org/cyclonedx/maven/DelegatingRepositorySystem.java index 6f99f104..f3ec351d 100644 --- a/src/main/java/org/cyclonedx/maven/DelegatingRepositorySystem.java +++ b/src/main/java/org/cyclonedx/maven/DelegatingRepositorySystem.java @@ -2,10 +2,12 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.SyncContext; +import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.CollectRequest; import org.eclipse.aether.collection.CollectResult; import org.eclipse.aether.collection.DependencyCollectionException; @@ -17,6 +19,7 @@ import org.eclipse.aether.installation.InstallRequest; import org.eclipse.aether.installation.InstallResult; import org.eclipse.aether.installation.InstallationException; +import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; import org.eclipse.aether.repository.RemoteRepository; @@ -49,8 +52,18 @@ class DelegatingRepositorySystem implements RepositorySystem { private final RepositorySystem delegate; private CollectResult collectResult; - public DelegatingRepositorySystem(final RepositorySystem repositorySystem) { + private final ModelConverter modelConverter; + + private final Map artifactRemoteRepositories; + + public Map getArtifactRemoteRepositories() { + return artifactRemoteRepositories; + } + + public DelegatingRepositorySystem(final RepositorySystem repositorySystem, final ModelConverter modelConverter, final Map artifactRemoteRepositories) { this.delegate = repositorySystem; + this.modelConverter = modelConverter; + this.artifactRemoteRepositories = artifactRemoteRepositories; } public CollectResult getCollectResult() { @@ -69,7 +82,9 @@ public boolean visitEnter(final DependencyNode node) if (root != node) { try { final ArtifactResult resolveArtifact = resolveArtifact(session, new ArtifactRequest(node)); - node.setArtifact(resolveArtifact.getArtifact()); + final Artifact artifact = resolveArtifact.getArtifact(); + node.setArtifact(artifact); + artifactRemoteRepositories.put(modelConverter.generatePackageUrl(artifact), resolveArtifact.getRepository()); } catch (ArtifactResolutionException e) {} // ignored } return true; diff --git a/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java b/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java index 00d9d0ef..54f6142c 100644 --- a/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java +++ b/src/main/java/org/cyclonedx/maven/ProjectDependenciesConverter.java @@ -24,6 +24,7 @@ import org.cyclonedx.model.Component; import org.cyclonedx.model.Dependency; import org.cyclonedx.model.Metadata; +import org.eclipse.aether.repository.ArtifactRepository; import java.util.Map; @@ -66,10 +67,13 @@ public static class BomDependencies { private final Map artifacts; private final Map dependencyArtifacts; - public BomDependencies(final Map dependencies, final Map artifacts, final Map dependencyArtifacts) { + private final Map artifactRemoteRepositories; + + public BomDependencies(final Map dependencies, final Map artifacts, final Map dependencyArtifacts, final Map artifactRemoteRepositories) { this.dependencies = dependencies; this.artifacts = artifacts; this.dependencyArtifacts = dependencyArtifacts; + this.artifactRemoteRepositories = artifactRemoteRepositories; } public final Map getDependencies() { @@ -83,5 +87,7 @@ public final Map getDependencyArtifacts() { public final Map getArtifacts() { return artifacts; } + + public final Map getArtifactRemoteRepositories() { return artifactRemoteRepositories; } } }