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()); + } + } + +}