From c2de8f5727529ce1fcf31acf3baad0ea6a53eed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?= Date: Fri, 10 Nov 2023 16:18:03 +0100 Subject: [PATCH] Add support for custom external references (#421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Volkan Yazıcı --- .github/dependabot.yml | 4 + pom.xml | 6 + .../cyclonedx/maven/BaseCycloneDxMojo.java | 27 +++- .../maven/DefaultModelConverter.java | 123 ++++++++++++++++-- .../org/cyclonedx/maven/ModelConverter.java | 8 +- .../maven/ExternalReferenceTest.java | 71 ++++++++++ .../external-reference/child/pom.xml | 42 ++++++ src/test/resources/external-reference/pom.xml | 60 +++++++++ 8 files changed, 318 insertions(+), 23 deletions(-) create mode 100644 src/test/java/org/cyclonedx/maven/ExternalReferenceTest.java create mode 100644 src/test/resources/external-reference/child/pom.xml create mode 100644 src/test/resources/external-reference/pom.xml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8332ef39..9d763597 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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: "/" diff --git a/pom.xml b/pom.xml index 17799cf2..9c70985c 100644 --- a/pom.xml +++ b/pom.xml @@ -203,6 +203,12 @@ junit-vintage-engine test + + net.javacrumbs.json-unit + json-unit-assertj + 2.38.0 + test + 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: + *

+ *
    + *
  1. $.metadata.component.externalReferences[]
  2. + *
  3. $.components[].externalReferences[] (only for $.components[] provided by the project)
  4. + *
+ */ + @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 expectedValues) throws IOException { + byte[] bomJsonBytes = Files.readAllBytes(bomFile.toPath()); + String bomJson = new String(bomJsonBytes, StandardCharsets.UTF_8); + assertThatJson(bomJson) + .inPath(jsonPath) + .isArray() + .containsOnlyOnceElementsOf(expectedValues); + } + +} diff --git a/src/test/resources/external-reference/child/pom.xml b/src/test/resources/external-reference/child/pom.xml new file mode 100644 index 00000000..1455508e --- /dev/null +++ b/src/test/resources/external-reference/child/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + + com.acme + parent + ${revision} + + + child + + + + junit + junit + 4.1 + + + + + + + org.cyclonedx + cyclonedx-maven-plugin + ${cyclonedx-maven-plugin.version} + + + + CHAT + https://acme.com/child + + + + + + + + diff --git a/src/test/resources/external-reference/pom.xml b/src/test/resources/external-reference/pom.xml new file mode 100644 index 00000000..2e4c93d3 --- /dev/null +++ b/src/test/resources/external-reference/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + com.acme + parent + pom + ${revision} + + + + Apache-2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + 1.0.0 + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + child + + + + + + org.cyclonedx + cyclonedx-maven-plugin + ${cyclonedx-maven-plugin.version} + + + generate-sbom + + makeAggregateBom + + verify + + json + + + CHAT + https://acme.com/parent + + + + + + + + + +