From 1a43f980092ac4d487187b290fa1d076ba8f4f04 Mon Sep 17 00:00:00 2001 From: XenoAmess Date: Sun, 8 May 2022 07:22:21 +0800 Subject: [PATCH] add CycloneDxAggregateEnforceMojo Signed-off-by: XenoAmess --- pom.xml | 6 + .../cyclonedx/maven/BaseCycloneDxMojo.java | 61 +++++- .../maven/CycloneDxAggregateEnforceMojo.java | 176 ++++++++++++++++++ .../maven/CycloneDxAggregateMojo.java | 5 +- 4 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/cyclonedx/maven/CycloneDxAggregateEnforceMojo.java diff --git a/pom.xml b/pom.xml index 6254f978..d364222a 100644 --- a/pom.xml +++ b/pom.xml @@ -203,6 +203,12 @@ junit-vintage-engine test + + org.jetbrains + annotations + provided + 23.0.0 + 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..5e95eff7 --- /dev/null +++ b/src/main/java/org/cyclonedx/maven/CycloneDxAggregateEnforceMojo.java @@ -0,0 +1,176 @@ +/* + * 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.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.model.Bom; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.License; +import org.cyclonedx.model.LicenseChoice; +import org.jetbrains.annotations.NotNull; + +@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())) { + getLog().error("[ERROR]Cannot handle spdx license expression for " + group + ":" + name + " , use license id instead"); + } + 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