diff --git a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
index 4b504804..3127b79c 100644
--- a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
+++ b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
@@ -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;
@@ -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;
@@ -217,6 +214,22 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
@Parameter( defaultValue = "${project.build.outputTimestamp}" )
private String outputTimestamp;
+ /**
+ * External references to be added.
+ *
+ * They will be injected in two locations:
+ *
+ *
+ * $.metadata.component.externalReferences[]
+ * $.components[].externalReferences[]
(only for $.components[]
provided by the project)
+ *
+ */
+ @Parameter
+ private ExternalReference[] externalReferences;
+
+ @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
+ private MojoExecution execution;
+
@org.apache.maven.plugins.annotations.Component
private MavenProjectHelper mavenProjectHelper;
@@ -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);
}
/**
@@ -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));
diff --git a/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java b/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java
index b8aaa200..62b9dc12 100644
--- a/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java
+++ b/src/main/java/org/cyclonedx/maven/DefaultModelConverter.java
@@ -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;
@@ -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());
@@ -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 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 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 extractExternalReferences(Plugin plugin, MojoExecution activeExecution) {
+
+ // Collect external references from the execution configuration
+ List 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 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 parseDom(@Nullable Xpp3Dom dom) {
+ if (dom == null) {
+ return new ArrayList<>();
+ }
+ String xml = dom.toString();
+ try {
+ ExternalReferenceConfigDto dto = MAPPER.readValue(xml, ExternalReferenceConfigDto.class);
+ @Nullable List externalReferences = dto.externalReferences;
+ return externalReferences != null ? externalReferences : new ArrayList<>();
+ } catch (JsonProcessingException error) {
+ throw new RuntimeException(error);
+ }
+ }
+
+ @JsonProperty
+ private List externalReferences;
+
}
private boolean isModified(Artifact artifact) {
@@ -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"));
@@ -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 externalReferences = extractExternalReferences(project, execution);
+ component.setExternalReferences(externalReferences);
extractComponentMetadata(project, component, schemaVersion, includeLicenseText);
final Metadata metadata = new Metadata();
diff --git a/src/main/java/org/cyclonedx/maven/ModelConverter.java b/src/main/java/org/cyclonedx/maven/ModelConverter.java
index 98721e21..d3ae77f8 100644
--- a/src/main/java/org/cyclonedx/maven/ModelConverter.java
+++ b/src/main/java/org/cyclonedx/maven/ModelConverter.java
@@ -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;
@@ -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);
+
}
diff --git a/src/test/java/org/cyclonedx/maven/ExternalReferenceTest.java b/src/test/java/org/cyclonedx/maven/ExternalReferenceTest.java
new file mode 100644
index 00000000..2d082b72
--- /dev/null
+++ b/src/test/java/org/cyclonedx/maven/ExternalReferenceTest.java
@@ -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