diff --git a/pom.xml b/pom.xml
index 6254f978..e1b3cd88 100644
--- a/pom.xml
+++ b/pom.xml
@@ -203,6 +203,17 @@
junit-vintage-engine
test
+
+ org.spdx
+ java-spdx-library
+ 1.0.10
+
+
+ org.jetbrains
+ annotations
+ provided
+ 23.0.0
+
@@ -214,6 +225,18 @@
pom
import
+
+ com.google.code.gson
+ gson
+ 2.9.0
+
+
+ org.apache.logging.log4j
+ log4j-bom
+ 2.17.2
+ pom
+ import
+
diff --git a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
index 16e588fa..fd3e2006 100644
--- a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
+++ b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
@@ -49,6 +49,10 @@
import java.util.List;
import java.util.Set;
import java.util.UUID;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import org.apache.commons.io.input.BOMInputStream;
+import org.jetbrains.annotations.NotNull;
public abstract class BaseCycloneDxMojo extends AbstractMojo {
@@ -61,7 +65,7 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
/**
* The component type associated to the SBOM metadata. See
* CycloneDX reference for supported
- * values.
+ * values.
*/
@Parameter(property = "projectType", defaultValue = "library", required = false)
private String projectType;
@@ -199,6 +203,21 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
@org.apache.maven.plugins.annotations.Component
private ProjectDependenciesConverter projectDependenciesConverter;
+ @Parameter(property = "enforceExcludeArtifactId", required = false)
+ protected String[] enforceExcludeArtifactId;
+
+ @Parameter(property = "enforceComponentsSameVersion", defaultValue = "true", required = false)
+ protected boolean enforceComponentsSameVersion = true;
+
+ @Parameter(property = "enforceLicensesBlackList", required = false)
+ protected String[] enforceLicensesBlackList;
+
+ @Parameter(property = "enforceLicensesWhiteList", required = false)
+ protected String[] enforceLicensesWhiteList;
+
+ @Parameter(property = "mergeBomFile", required = false)
+ protected File mergeBomFile;
+
/**
* Various messages sent to console.
*/
@@ -267,7 +286,7 @@ public void execute() throws MojoExecutionException {
private void generateBom(String analysis, Metadata metadata, Set components, Set dependencies) throws MojoExecutionException {
try {
getLog().info(String.format(MESSAGE_CREATING_BOM, schemaVersion, components.size()));
- final Bom bom = new Bom();
+ Bom bom = new Bom();
bom.setComponents(new ArrayList<>(components));
if (schemaVersion().getVersion() >= 1.1 && includeBomSerialNumber) {
@@ -291,6 +310,7 @@ private void generateBom(String analysis, Metadata metadata, Set comp
if ("all".equalsIgnoreCase(outputFormat)
|| "xml".equalsIgnoreCase(outputFormat)
|| "json".equalsIgnoreCase(outputFormat)) {
+ bom = postProcessingBom(bom);
saveBom(bom);
} else {
getLog().error("Unsupported output format. Valid options are XML and JSON");
@@ -300,6 +320,43 @@ private void generateBom(String analysis, Metadata metadata, Set comp
}
}
+ @NotNull
+ protected Bom postProcessingBom(@NotNull Bom bom) throws MojoExecutionException {
+ if (mergeBomFile != null) {
+ Bom mergeBom;
+ try {
+ try {
+ mergeBom = new JsonParser().parse(mergeBomFile);
+ } catch (Exception e) {
+ mergeBom = new XmlParser().parse(mergeBomFile);
+ }
+ } catch (Exception e) {
+ throw new MojoExecutionException("parse failed", e);
+ }
+ {
+ LinkedHashSet components = new LinkedHashSet<>();
+ if (mergeBom.getComponents() != null) {
+ components.addAll(mergeBom.getComponents());
+ }
+ if (bom.getComponents() != null) {
+ components.addAll(bom.getComponents());
+ }
+ bom.setComponents(new ArrayList<>(components));
+ }
+ {
+ LinkedHashSet dependencies = new LinkedHashSet<>();
+ if (mergeBom.getDependencies() != null) {
+ dependencies.addAll(mergeBom.getDependencies());
+ }
+ if (bom.getDependencies() != null) {
+ dependencies.addAll(bom.getDependencies());
+ }
+ bom.setDependencies(new ArrayList<>(dependencies));
+ }
+ }
+ return bom;
+ }
+
private void saveBom(Bom bom) throws ParserConfigurationException, IOException, GeneratorException,
MojoExecutionException {
if ("all".equalsIgnoreCase(outputFormat) || "xml".equalsIgnoreCase(outputFormat)) {
diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxAggregateEnforceMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateEnforceMojo.java
new file mode 100644
index 00000000..7665434e
--- /dev/null
+++ b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateEnforceMojo.java
@@ -0,0 +1,208 @@
+/*
+ * This file is part of CycloneDX Maven Plugin.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) OWASP Foundation. All Rights Reserved.
+ */
+package org.cyclonedx.maven;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+import org.cyclonedx.maven.utils.SpdxLicenseUtil;
+import org.cyclonedx.model.Bom;
+import org.cyclonedx.model.Component;
+import org.cyclonedx.model.License;
+import org.cyclonedx.model.LicenseChoice;
+import org.jetbrains.annotations.NotNull;
+import org.spdx.library.InvalidSPDXAnalysisException;
+import org.spdx.library.model.license.AnyLicenseInfo;
+import org.spdx.library.model.license.LicenseInfoFactory;
+
+@Mojo(
+ name = "enforceAggregateBom",
+ defaultPhase = LifecyclePhase.PACKAGE,
+ aggregator = true,
+ requiresOnline = true,
+ requiresDependencyCollection = ResolutionScope.TEST,
+ requiresDependencyResolution = ResolutionScope.TEST
+)
+public class CycloneDxAggregateEnforceMojo extends CycloneDxAggregateMojo {
+
+ @Override
+ protected boolean shouldExclude(@NotNull MavenProject mavenProject) {
+ if (super.shouldExclude(mavenProject)) {
+ return true;
+ }
+ if (enforceExcludeArtifactId != null && enforceExcludeArtifactId.length > 0) {
+ if (Arrays.asList(enforceExcludeArtifactId).contains(mavenProject.getArtifactId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ @NotNull
+ protected Bom postProcessingBom(@NotNull Bom bom) throws MojoExecutionException {
+ bom = super.postProcessingBom(bom);
+ doEnforceComponentsSameVersion(bom);
+ doEnforceLicensesBlackListAndWhiteList(bom);
+ return bom;
+ }
+
+ private void doEnforceComponentsSameVersion(@NotNull Bom bom) throws MojoExecutionException {
+ if (this.enforceComponentsSameVersion) {
+ List components = bom.getComponents();
+ if (components != null) {
+ Map, Set> componentMap =
+ new HashMap<>((int) Math.ceil(components.size() / 0.75));
+ for (Component component : components) {
+ if (component == null) {
+ continue;
+ }
+ String group = component.getGroup();
+ String name = component.getName();
+ String version = component.getVersion();
+ Pair key = Pair.of(group, name);
+ Set versions = componentMap.computeIfAbsent(
+ key,
+ stringStringPair -> new HashSet<>()
+ );
+ versions.add(version);
+ }
+ StringBuilder stringBuilder = new StringBuilder();
+ for (Map.Entry, Set> entry : componentMap.entrySet()) {
+ Pair key = entry.getKey();
+ Set versions = entry.getValue();
+ if (versions.size() > 1) {
+ stringBuilder
+ .append("[ERROR]Duplicated versions for ")
+ .append(key.getLeft())
+ .append(":")
+ .append(key.getRight())
+ .append(" , versions : ")
+ .append(StringUtils.join(versions.iterator(), ","))
+ .append("\n");
+ }
+ }
+ if (stringBuilder.length() > 0) {
+ throw new MojoExecutionException(stringBuilder.toString());
+ }
+ }
+ }
+ }
+
+ private void doEnforceLicensesBlackListAndWhiteList(@NotNull Bom bom) throws MojoExecutionException {
+ List components = bom.getComponents();
+ if (components != null) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (Component component : components) {
+ if (component == null) {
+ continue;
+ }
+ String group = component.getGroup();
+ String name = component.getName();
+ LicenseChoice licenseChoice = component.getLicenseChoice();
+ if (licenseChoice == null) {
+ continue;
+ }
+ if (StringUtils.isNotBlank(licenseChoice.getExpression())) {
+ try {
+ AnyLicenseInfo anyLicenseInfo = LicenseInfoFactory.parseSPDXLicenseString(licenseChoice.getExpression());
+ if (!ArrayUtils.isEmpty(this.enforceLicensesBlackList)) {
+ if (!SpdxLicenseUtil.isLicensePassBlackList(anyLicenseInfo, this.enforceLicensesBlackList)) {
+ stringBuilder
+ .append("[ERROR]License in blackList for ")
+ .append(group)
+ .append(":")
+ .append(name)
+ .append(" , license : ")
+ .append(licenseChoice.getExpression())
+ .append("\n");
+ }
+ }
+ if (!ArrayUtils.isEmpty(this.enforceLicensesWhiteList)) {
+ if (!SpdxLicenseUtil.isLicensePassWhiteList(anyLicenseInfo, this.enforceLicensesWhiteList)) {
+ stringBuilder
+ .append("[ERROR]License not in whiteList for ")
+ .append(group)
+ .append(":")
+ .append(name)
+ .append(" , license : ")
+ .append(licenseChoice.getExpression())
+ .append("\n");
+ }
+ }
+ } catch (InvalidSPDXAnalysisException e) {
+ getLog().warn(e);
+ }
+ } else if (licenseChoice.getLicenses() != null) {
+ for (License license : licenseChoice.getLicenses()) {
+ if (!ArrayUtils.isEmpty(this.enforceLicensesBlackList)) {
+ if (
+ ArrayUtils.contains(this.enforceLicensesBlackList, license.getId())
+ || ArrayUtils.contains(this.enforceLicensesBlackList, license.getName())
+ ) {
+ stringBuilder
+ .append("[ERROR]License in blackList for ")
+ .append(group)
+ .append(":")
+ .append(name)
+ .append(" , license : ")
+ .append(license.getId())
+ .append("\n");
+ }
+ }
+ if (!ArrayUtils.isEmpty(this.enforceLicensesWhiteList)) {
+ if (
+ !(
+ ArrayUtils.contains(this.enforceLicensesBlackList, license.getId())
+ || ArrayUtils.contains(this.enforceLicensesBlackList, license.getName())
+ )
+ ) {
+ stringBuilder
+ .append("[ERROR]License not in whiteList for ")
+ .append(group)
+ .append(":")
+ .append(name)
+ .append(" , license : ")
+ .append(license.getId())
+ .append("\n");
+ }
+ }
+ }
+ }
+ }
+ if (stringBuilder.length() > 0) {
+ throw new MojoExecutionException(stringBuilder.toString());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java
index fb9123a0..12930255 100644
--- a/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java
+++ b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java
@@ -28,6 +28,7 @@
import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.Dependency;
+import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
@@ -87,7 +88,7 @@ public class CycloneDxAggregateMojo extends CycloneDxMojo {
@Parameter(property = "excludeTestProject", defaultValue = "false", required = false)
protected Boolean excludeTestProject;
- protected boolean shouldExclude(MavenProject mavenProject) {
+ protected boolean shouldExclude(@NotNull MavenProject mavenProject) {
boolean shouldExclude = false;
if (excludeArtifactId != null && excludeArtifactId.length > 0) {
shouldExclude = Arrays.asList(excludeArtifactId).contains(mavenProject.getArtifactId());
@@ -98,7 +99,7 @@ protected boolean shouldExclude(MavenProject mavenProject) {
if (excludeTestProject && mavenProject.getArtifactId().contains("test")) {
shouldExclude = true;
}
- return shouldExclude;
+ return excludeTestProject && mavenProject.getArtifactId().contains("test");
}
@Override
diff --git a/src/main/java/org/cyclonedx/maven/utils/SpdxLicenseUtil.java b/src/main/java/org/cyclonedx/maven/utils/SpdxLicenseUtil.java
new file mode 100644
index 00000000..9487e70e
--- /dev/null
+++ b/src/main/java/org/cyclonedx/maven/utils/SpdxLicenseUtil.java
@@ -0,0 +1,83 @@
+package org.cyclonedx.maven.utils;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.spdx.library.InvalidSPDXAnalysisException;
+import org.spdx.library.model.license.AnyLicenseInfo;
+import org.spdx.library.model.license.ConjunctiveLicenseSet;
+import org.spdx.library.model.license.DisjunctiveLicenseSet;
+
+public class SpdxLicenseUtil {
+
+ /**
+ * Detect if a license pass black lists
+ * @param license license
+ * @param blackList license black list
+ * @return if the license pass black lists
+ * @throws InvalidSPDXAnalysisException actually shall never
+ */
+ public static boolean isLicensePassBlackList(
+ AnyLicenseInfo license,
+ String... blackList
+ ) throws InvalidSPDXAnalysisException {
+ if (license == null) {
+ return true;
+ }
+ if (blackList == null || blackList.length == 0) {
+ return true;
+ }
+ if (license instanceof ConjunctiveLicenseSet) {
+ for (AnyLicenseInfo member : ((ConjunctiveLicenseSet) license).getMembers()) {
+ if (!isLicensePassBlackList(member, blackList)) {
+ return false;
+ }
+ }
+ return true;
+ } else if (license instanceof DisjunctiveLicenseSet) {
+ for (AnyLicenseInfo member : ((DisjunctiveLicenseSet) license).getMembers()) {
+ if (isLicensePassBlackList(member, blackList)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return !ArrayUtils.contains(blackList, license.toString());
+ }
+ }
+
+ /**
+ * Detect if a license pass white lists
+ * @param license license
+ * @param whiteList license white list
+ * @return if the license pass white lists
+ * @throws InvalidSPDXAnalysisException actually shall never
+ */
+ public static boolean isLicensePassWhiteList(
+ AnyLicenseInfo license,
+ String... whiteList
+ ) throws InvalidSPDXAnalysisException {
+ if (license == null) {
+ return false;
+ }
+ if (whiteList == null || whiteList.length == 0) {
+ return false;
+ }
+ if (license instanceof ConjunctiveLicenseSet) {
+ for (AnyLicenseInfo member : ((ConjunctiveLicenseSet) license).getMembers()) {
+ if (!isLicensePassWhiteList(member, whiteList)) {
+ return false;
+ }
+ }
+ return true;
+ } else if (license instanceof DisjunctiveLicenseSet) {
+ for (AnyLicenseInfo member : ((DisjunctiveLicenseSet) license).getMembers()) {
+ if (isLicensePassWhiteList(member, whiteList)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return ArrayUtils.contains(whiteList, license.toString());
+ }
+ }
+
+}