Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Track repository_url #495

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -511,17 +516,37 @@ protected void logParameters() {
}
}

protected void populateComponents(final Set<String> topLevelComponents, final Map<String, Component> components, final Map<String, Artifact> artifacts, final ProjectDependencyAnalysis dependencyAnalysis) {
protected void populateComponents(final Set<String> topLevelComponents, final Map<String, Component> components, final Map<String, Artifact> artifacts, final Map<String, ArtifactRepository> artifactRemoteRepositories, final ProjectDependencyAnalysis dependencyAnalysis) {
for (Map.Entry<String, Artifact> 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));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ protected String extractComponentsAndDependencies(final Set<String> 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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/cyclonedx/maven/CycloneDxMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ protected String extractComponentsAndDependencies(final Set<String> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ protected String extractComponentsAndDependencies(Set<String> 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);
}
Expand Down
98 changes: 95 additions & 3 deletions src/main/java/org/cyclonedx/maven/DefaultModelConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Hash> 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<Method> 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<String> 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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +67,8 @@ public class DefaultProjectDependenciesConverter implements ProjectDependenciesC
private Set<String> excludeTypesSet;
private MavenDependencyScopes include;

final Map<String, ArtifactRepository> artifactRemoteRepositories = new LinkedHashMap<>();

@Override
public BomDependencies extractBOMDependencies(MavenProject mavenProject, MavenDependencyScopes include, String[] excludeTypes) throws MojoExecutionException {
this.include = include;
Expand All @@ -77,9 +80,8 @@ public BomDependencies extractBOMDependencies(MavenProject mavenProject, MavenDe
final Map<String, Artifact> mavenArtifacts = new LinkedHashMap<>();
final Map<String, Artifact> 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);

Expand All @@ -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<String, Artifact> artifactMap, final Map<String, Artifact> dependencyArtifactMap, final org.apache.maven.shared.dependency.graph.DependencyNode node, final int level) {
Expand Down
19 changes: 17 additions & 2 deletions src/main/java/org/cyclonedx/maven/DelegatingRepositorySystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, ArtifactRepository> artifactRemoteRepositories;

public Map<String, ArtifactRepository> getArtifactRemoteRepositories() {
return artifactRemoteRepositories;
}

public DelegatingRepositorySystem(final RepositorySystem repositorySystem, final ModelConverter modelConverter, final Map<String, ArtifactRepository> artifactRemoteRepositories) {
this.delegate = repositorySystem;
this.modelConverter = modelConverter;
this.artifactRemoteRepositories = artifactRemoteRepositories;
}

public CollectResult getCollectResult() {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -66,10 +67,13 @@ public static class BomDependencies {
private final Map<String, Artifact> artifacts;
private final Map<String, Artifact> dependencyArtifacts;

public BomDependencies(final Map<String, Dependency> dependencies, final Map<String, Artifact> artifacts, final Map<String, Artifact> dependencyArtifacts) {
private final Map<String, ArtifactRepository> artifactRemoteRepositories;

public BomDependencies(final Map<String, Dependency> dependencies, final Map<String, Artifact> artifacts, final Map<String, Artifact> dependencyArtifacts, final Map<String, ArtifactRepository> artifactRemoteRepositories) {
this.dependencies = dependencies;
this.artifacts = artifacts;
this.dependencyArtifacts = dependencyArtifacts;
this.artifactRemoteRepositories = artifactRemoteRepositories;
}

public final Map<String, Dependency> getDependencies() {
Expand All @@ -83,5 +87,7 @@ public final Map<String, Artifact> getDependencyArtifacts() {
public final Map<String, Artifact> getArtifacts() {
return artifacts;
}

public final Map<String, ArtifactRepository> getArtifactRemoteRepositories() { return artifactRemoteRepositories; }
}
}