Skip to content

Commit

Permalink
Add support for custom external references (#421)
Browse files Browse the repository at this point in the history
Signed-off-by: Volkan Yazıcı <volkan@yazi.ci>
  • Loading branch information
vy committed Nov 10, 2023
1 parent 9557a6f commit c2de8f5
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 23 deletions.
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ updates:
directory: "/"
schedule:
interval: "daily"
ignore:
# JsonUnit `2.x` is the last Java 8 compatible major release
- dependency-name: "net.javacrumbs.json-unit:*"
update-types: ["version-update:semver-major"]

- package-ecosystem: "github-actions"
directory: "/"
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.javacrumbs.json-unit</groupId>
<artifactId>json-unit-assertj</artifactId>
<version>2.38.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
Expand Down
27 changes: 20 additions & 7 deletions src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginExecution;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
Expand All @@ -36,11 +37,7 @@
import org.cyclonedx.generators.json.BomJsonGenerator;
import org.cyclonedx.generators.xml.BomXmlGenerator;
import org.cyclonedx.maven.ProjectDependenciesConverter.BomDependencies;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.Dependency;
import org.cyclonedx.model.Metadata;
import org.cyclonedx.model.Property;
import org.cyclonedx.model.*;
import org.cyclonedx.parsers.JsonParser;
import org.cyclonedx.parsers.Parser;
import org.cyclonedx.parsers.XmlParser;
Expand Down Expand Up @@ -217,6 +214,22 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
@Parameter( defaultValue = "${project.build.outputTimestamp}" )
private String outputTimestamp;

/**
* External references to be added.
* <p>
* They will be injected in two locations:
* </p>
* <ol>
* <li><code>$.metadata.component.externalReferences[]</code></li>
* <li><code>$.components[].externalReferences[]</code> (only for <code>$.components[]</code> provided by the project)</li>
* </ol>
*/
@Parameter
private ExternalReference[] externalReferences;

@Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
private MojoExecution execution;

@org.apache.maven.plugins.annotations.Component
private MavenProjectHelper mavenProjectHelper;

Expand Down Expand Up @@ -257,7 +270,7 @@ protected String generatePackageUrl(final Artifact artifact) {
}

protected Component convert(Artifact artifact) {
return modelConverter.convert(artifact, schemaVersion(), includeLicenseText);
return modelConverter.convert(execution, artifact, schemaVersion(), includeLicenseText);
}

/**
Expand Down Expand Up @@ -292,7 +305,7 @@ public void execute() throws MojoExecutionException {

String analysis = extractComponentsAndDependencies(topLevelComponents, componentMap, dependencyMap);
if (analysis != null) {
final Metadata metadata = modelConverter.convert(project, projectType, schemaVersion(), includeLicenseText);
final Metadata metadata = modelConverter.convert(project, projectType, execution, schemaVersion(), includeLicenseText);

if (schemaVersion().getVersion() >= 1.3) {
metadata.addProperty(newProperty("maven.goal", analysis));
Expand Down
123 changes: 109 additions & 14 deletions src/main/java/org/cyclonedx/maven/DefaultModelConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,38 @@
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.handler.DefaultArtifactHandler;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.MailingList;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingResult;
import org.apache.maven.repository.RepositorySystem;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.cyclonedx.CycloneDxSchema;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.ExternalReference;
Expand Down Expand Up @@ -153,7 +165,9 @@ private String generatePackageUrl(String groupId, String artifactId, String vers
}

@Override
public Component convert(Artifact artifact, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText) {
public Component convert(MojoExecution execution, Artifact artifact, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText) {

// Populate basic fields from the `Artifact` instance
final Component component = new Component();
component.setGroup(artifact.getGroupId());
component.setName(artifact.getArtifactId());
Expand All @@ -172,21 +186,99 @@ public Component convert(Artifact artifact, CycloneDxSchema.Version schemaVersio
if (CycloneDxSchema.Version.VERSION_10 != schemaVersion) {
component.setBomRef(component.getPurl());
}
if (isDescribedArtifact(artifact)) {
try {
final MavenProject project = getEffectiveMavenProject(artifact);
if (project != null) {
extractComponentMetadata(project, component, schemaVersion, includeLicenseText);
}
} catch (ProjectBuildingException e) {
if (logger.isDebugEnabled()) {
logger.warn("Unable to create Maven project for " + artifact.getId() + " from repository.", e);
} else {
logger.warn("Unable to create Maven project for " + artifact.getId() + " from repository.");
}

// Read the project
MavenProject project = null;
try {
project = getEffectiveMavenProject(artifact);
} catch (ProjectBuildingException error) {
if (logger.isDebugEnabled()) {
logger.warn("Unable to create Maven project for `{}` from repository.", artifact.getId(), error);
} else {
logger.warn("Unable to create Maven project for `{}` from repository.", artifact.getId());
}
}

if (project != null) {

// Populate external references
List<ExternalReference> externalReferences = extractExternalReferences(project, execution);
component.setExternalReferences(externalReferences);

// Extract the rest of the metadata for JARs, i.e., *described* artifacts
if (isDescribedArtifact(artifact)) {
extractComponentMetadata(project, component, schemaVersion, includeLicenseText);
}

}

// Return the enriched component
return component;

}

private List<ExternalReference> extractExternalReferences(MavenProject project, MojoExecution activeExecution) {
Plugin activePlugin = activeExecution.getPlugin();
return project
.getBuild()
.getPlugins()
.stream()
.filter(plugin -> activePlugin.getGroupId().equals(plugin.getGroupId()) && activePlugin.getArtifactId().equals(plugin.getArtifactId()))
.findFirst()
.map(plugin -> extractExternalReferences(plugin, activeExecution))
.orElseGet(ArrayList::new);
}

private static List<ExternalReference> extractExternalReferences(Plugin plugin, MojoExecution activeExecution) {

// Collect external references from the execution configuration
List<ExternalReference> executionExternalReferences = plugin
.getExecutions()
.stream()
.filter(execution -> activeExecution.getExecutionId().equals(execution.getId()))
.flatMap(execution -> {
Xpp3Dom executionConfig = (Xpp3Dom) execution.getConfiguration();
return ExternalReferenceConfigDto.parseDom(executionConfig).stream();
})
.collect(Collectors.toList());

// Collect external references from the plugin configuration
Xpp3Dom pluginConfig = (Xpp3Dom) plugin.getConfiguration();
List<ExternalReference> pluginExternalReferences = ExternalReferenceConfigDto.parseDom(pluginConfig);

// Combine collected external references
return Stream
.concat(executionExternalReferences.stream(), pluginExternalReferences.stream())
.distinct()
.collect(Collectors.toList());

}

private static final class ExternalReferenceConfigDto {

private static final XmlMapper MAPPER = XmlMapper
.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.build();

private static List<ExternalReference> parseDom(@Nullable Xpp3Dom dom) {
if (dom == null) {
return new ArrayList<>();
}
String xml = dom.toString();
try {
ExternalReferenceConfigDto dto = MAPPER.readValue(xml, ExternalReferenceConfigDto.class);
@Nullable List<ExternalReference> externalReferences = dto.externalReferences;
return externalReferences != null ? externalReferences : new ArrayList<>();
} catch (JsonProcessingException error) {
throw new RuntimeException(error);
}
}

@JsonProperty
private List<ExternalReference> externalReferences;

}

private boolean isModified(Artifact artifact) {
Expand Down Expand Up @@ -341,7 +433,7 @@ else if (licenseChoiceToResolve.getExpression() != null && CycloneDxSchema.Versi
}

@Override
public Metadata convert(final MavenProject project, String projectType, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText) {
public Metadata convert(final MavenProject project, String projectType, MojoExecution execution, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText) {
final Tool tool = new Tool();
final Properties properties = readPluginProperties();
tool.setVendor(properties.getProperty("vendor"));
Expand All @@ -367,6 +459,9 @@ public Metadata convert(final MavenProject project, String projectType, CycloneD
component.setType(resolveProjectType(projectType));
component.setPurl(generatePackageUrl(project.getArtifact()));
component.setBomRef(component.getPurl());

List<ExternalReference> externalReferences = extractExternalReferences(project, execution);
component.setExternalReferences(externalReferences);
extractComponentMetadata(project, component, schemaVersion, includeLicenseText);

final Metadata metadata = new Metadata();
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/org/cyclonedx/maven/ModelConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.cyclonedx.maven;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject;
import org.cyclonedx.CycloneDxSchema;
import org.cyclonedx.model.Component;
Expand All @@ -43,21 +44,24 @@ public interface ModelConverter {
* Converts a Maven artifact (dependency or transitive dependency) into a
* CycloneDX component.
*
* @param execution the associated execution
* @param artifact the artifact to convert
* @param schemaVersion the target CycloneDX schema version
* @param includeLicenseText should license text be included in bom?
* @return a CycloneDX component
*/
Component convert(Artifact artifact, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText);
Component convert(MojoExecution execution, Artifact artifact, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText);

/**
* Converts a MavenProject into a Metadata object.
*
* @param project the MavenProject to convert
* @param projectType the target CycloneDX component type
* @param execution the associated execution
* @param schemaVersion the target CycloneDX schema version
* @param includeLicenseText should license text be included in bom?
* @return a CycloneDX Metadata object
*/
Metadata convert(MavenProject project, String projectType, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText);
Metadata convert(MavenProject project, String projectType, MojoExecution execution, CycloneDxSchema.Version schemaVersion, boolean includeLicenseText);

}
71 changes: 71 additions & 0 deletions src/test/java/org/cyclonedx/maven/ExternalReferenceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.cyclonedx.maven;

import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder;
import io.takari.maven.testing.executor.MavenVersions;
import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;

import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;

/**
* Verifies external references are populated as expected.
*/
@RunWith(MavenJUnitTestRunner.class)
@MavenVersions({"3.6.3"})
public class ExternalReferenceTest extends BaseMavenVerifier {

public ExternalReferenceTest(MavenRuntimeBuilder runtimeBuilder) throws Exception {
super(runtimeBuilder);
}

@Test
public void testAddedExternalReferences() throws Exception {

// Create the verifier
File projDir = resources.getBasedir("external-reference");
verifier
.forProject(projDir)
.withCliOption("-Dcyclonedx-maven-plugin.version=" + getCurrentVersion())
.withCliOption("-X")
.withCliOption("-B")
.execute("clean", "verify")
.assertErrorFreeLog();

// Verify parent metadata
assertExternalReferences(
new File(projDir, "target/bom.json"),
"$.metadata.component.externalReferences[?(@.type=='chat')].url",
Collections.singleton("https://acme.com/parent"));

// Verify parent components
assertExternalReferences(
new File(projDir, "target/bom.json"),
"$.components[?(@.name=='child')].externalReferences[?(@.type=='chat')].url",
Arrays.asList("https://acme.com/parent", "https://acme.com/child"));

// Verify child metadata
assertExternalReferences(
new File(projDir, "child/target/bom.json"),
"$.metadata.component.externalReferences[?(@.type=='chat')].url",
Arrays.asList("https://acme.com/parent", "https://acme.com/child"));

}

private static void assertExternalReferences(File bomFile, String jsonPath, Iterable<Object> expectedValues) throws IOException {
byte[] bomJsonBytes = Files.readAllBytes(bomFile.toPath());
String bomJson = new String(bomJsonBytes, StandardCharsets.UTF_8);
assertThatJson(bomJson)
.inPath(jsonPath)
.isArray()
.containsOnlyOnceElementsOf(expectedValues);
}

}
Loading

0 comments on commit c2de8f5

Please sign in to comment.