Skip to content

Commit 9647fa1

Browse files
committed
add CycloneDxAggregateEnforceMojo
Signed-off-by: XenoAmess <[email protected]>
1 parent a2754bf commit 9647fa1

File tree

5 files changed

+376
-4
lines changed

5 files changed

+376
-4
lines changed

pom.xml

+23
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@
203203
<artifactId>junit-vintage-engine</artifactId>
204204
<scope>test</scope>
205205
</dependency>
206+
<dependency>
207+
<groupId>org.spdx</groupId>
208+
<artifactId>java-spdx-library</artifactId>
209+
<version>1.0.10</version>
210+
</dependency>
211+
<dependency>
212+
<groupId>org.jetbrains</groupId>
213+
<artifactId>annotations</artifactId>
214+
<scope>provided</scope>
215+
<version>23.0.0</version>
216+
</dependency>
206217
</dependencies>
207218

208219
<dependencyManagement>
@@ -214,6 +225,18 @@
214225
<type>pom</type>
215226
<scope>import</scope>
216227
</dependency>
228+
<dependency>
229+
<groupId>com.google.code.gson</groupId>
230+
<artifactId>gson</artifactId>
231+
<version>2.9.0</version>
232+
</dependency>
233+
<dependency>
234+
<groupId>org.apache.logging.log4j</groupId>
235+
<artifactId>log4j-bom</artifactId>
236+
<version>2.17.2</version>
237+
<type>pom</type>
238+
<scope>import</scope>
239+
</dependency>
217240
</dependencies>
218241
</dependencyManagement>
219242

src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java

+59-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
import java.util.List;
5050
import java.util.Set;
5151
import java.util.UUID;
52+
import java.util.jar.JarEntry;
53+
import java.util.jar.JarFile;
54+
import org.apache.commons.io.input.BOMInputStream;
55+
import org.jetbrains.annotations.NotNull;
5256

5357
public abstract class BaseCycloneDxMojo extends AbstractMojo {
5458

@@ -61,7 +65,7 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
6165
/**
6266
* The component type associated to the SBOM metadata. See
6367
* <a href="https://cyclonedx.org/docs/1.4/json/#metadata_component_type">CycloneDX reference</a> for supported
64-
* values.
68+
* values.
6569
*/
6670
@Parameter(property = "projectType", defaultValue = "library", required = false)
6771
private String projectType;
@@ -199,6 +203,21 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
199203
@org.apache.maven.plugins.annotations.Component
200204
private ProjectDependenciesConverter projectDependenciesConverter;
201205

206+
@Parameter(property = "enforceExcludeArtifactId", required = false)
207+
protected String[] enforceExcludeArtifactId;
208+
209+
@Parameter(property = "enforceComponentsSameVersion", defaultValue = "true", required = false)
210+
protected boolean enforceComponentsSameVersion = true;
211+
212+
@Parameter(property = "enforceLicensesBlackList", required = false)
213+
protected String[] enforceLicensesBlackList;
214+
215+
@Parameter(property = "enforceLicensesWhiteList", required = false)
216+
protected String[] enforceLicensesWhiteList;
217+
218+
@Parameter(property = "mergeBomFile", required = false)
219+
protected File mergeBomFile;
220+
202221
/**
203222
* Various messages sent to console.
204223
*/
@@ -267,7 +286,7 @@ public void execute() throws MojoExecutionException {
267286
private void generateBom(String analysis, Metadata metadata, Set<Component> components, Set<Dependency> dependencies) throws MojoExecutionException {
268287
try {
269288
getLog().info(String.format(MESSAGE_CREATING_BOM, schemaVersion, components.size()));
270-
final Bom bom = new Bom();
289+
Bom bom = new Bom();
271290
bom.setComponents(new ArrayList<>(components));
272291

273292
if (schemaVersion().getVersion() >= 1.1 && includeBomSerialNumber) {
@@ -291,6 +310,7 @@ private void generateBom(String analysis, Metadata metadata, Set<Component> comp
291310
if ("all".equalsIgnoreCase(outputFormat)
292311
|| "xml".equalsIgnoreCase(outputFormat)
293312
|| "json".equalsIgnoreCase(outputFormat)) {
313+
bom = postProcessingBom(bom);
294314
saveBom(bom);
295315
} else {
296316
getLog().error("Unsupported output format. Valid options are XML and JSON");
@@ -300,6 +320,43 @@ private void generateBom(String analysis, Metadata metadata, Set<Component> comp
300320
}
301321
}
302322

323+
@NotNull
324+
protected Bom postProcessingBom(@NotNull Bom bom) throws MojoExecutionException {
325+
if (mergeBomFile != null) {
326+
Bom mergeBom;
327+
try {
328+
try {
329+
mergeBom = new JsonParser().parse(mergeBomFile);
330+
} catch (Exception e) {
331+
mergeBom = new XmlParser().parse(mergeBomFile);
332+
}
333+
} catch (Exception e) {
334+
throw new MojoExecutionException("parse failed", e);
335+
}
336+
{
337+
LinkedHashSet<Component> components = new LinkedHashSet<>();
338+
if (mergeBom.getComponents() != null) {
339+
components.addAll(mergeBom.getComponents());
340+
}
341+
if (bom.getComponents() != null) {
342+
components.addAll(bom.getComponents());
343+
}
344+
bom.setComponents(new ArrayList<>(components));
345+
}
346+
{
347+
LinkedHashSet<Dependency> dependencies = new LinkedHashSet<>();
348+
if (mergeBom.getDependencies() != null) {
349+
dependencies.addAll(mergeBom.getDependencies());
350+
}
351+
if (bom.getDependencies() != null) {
352+
dependencies.addAll(bom.getDependencies());
353+
}
354+
bom.setDependencies(new ArrayList<>(dependencies));
355+
}
356+
}
357+
return bom;
358+
}
359+
303360
private void saveBom(Bom bom) throws ParserConfigurationException, IOException, GeneratorException,
304361
MojoExecutionException {
305362
if ("all".equalsIgnoreCase(outputFormat) || "xml".equalsIgnoreCase(outputFormat)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* This file is part of CycloneDX Maven Plugin.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
package org.cyclonedx.maven;
20+
21+
import java.util.Arrays;
22+
import java.util.HashMap;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
28+
import org.apache.commons.lang3.ArrayUtils;
29+
import org.apache.commons.lang3.StringUtils;
30+
import org.apache.commons.lang3.tuple.Pair;
31+
import org.apache.maven.plugin.MojoExecutionException;
32+
import org.apache.maven.plugin.MojoFailureException;
33+
import org.apache.maven.plugins.annotations.LifecyclePhase;
34+
import org.apache.maven.plugins.annotations.Mojo;
35+
import org.apache.maven.plugins.annotations.ResolutionScope;
36+
import org.apache.maven.project.MavenProject;
37+
import org.cyclonedx.maven.utils.SpdxLicenseUtil;
38+
import org.cyclonedx.model.Bom;
39+
import org.cyclonedx.model.Component;
40+
import org.cyclonedx.model.License;
41+
import org.cyclonedx.model.LicenseChoice;
42+
import org.jetbrains.annotations.NotNull;
43+
import org.spdx.library.InvalidSPDXAnalysisException;
44+
import org.spdx.library.model.license.AnyLicenseInfo;
45+
import org.spdx.library.model.license.LicenseInfoFactory;
46+
47+
@Mojo(
48+
name = "enforceAggregateBom",
49+
defaultPhase = LifecyclePhase.PACKAGE,
50+
aggregator = true,
51+
requiresOnline = true,
52+
requiresDependencyCollection = ResolutionScope.TEST,
53+
requiresDependencyResolution = ResolutionScope.TEST
54+
)
55+
public class CycloneDxAggregateEnforceMojo extends CycloneDxAggregateMojo {
56+
57+
@Override
58+
protected boolean shouldExclude(@NotNull MavenProject mavenProject) {
59+
if (super.shouldExclude(mavenProject)) {
60+
return true;
61+
}
62+
if (enforceExcludeArtifactId != null && enforceExcludeArtifactId.length > 0) {
63+
if (Arrays.asList(enforceExcludeArtifactId).contains(mavenProject.getArtifactId())) {
64+
return true;
65+
}
66+
}
67+
return false;
68+
}
69+
70+
@Override
71+
@NotNull
72+
protected Bom postProcessingBom(@NotNull Bom bom) throws MojoExecutionException {
73+
bom = super.postProcessingBom(bom);
74+
doEnforceComponentsSameVersion(bom);
75+
doEnforceLicensesBlackListAndWhiteList(bom);
76+
return bom;
77+
}
78+
79+
private void doEnforceComponentsSameVersion(@NotNull Bom bom) throws MojoExecutionException {
80+
if (this.enforceComponentsSameVersion) {
81+
List<Component> components = bom.getComponents();
82+
if (components != null) {
83+
Map<Pair<String, String>, Set<String>> componentMap =
84+
new HashMap<>((int) Math.ceil(components.size() / 0.75));
85+
for (Component component : components) {
86+
if (component == null) {
87+
continue;
88+
}
89+
String group = component.getGroup();
90+
String name = component.getName();
91+
String version = component.getVersion();
92+
Pair<String, String> key = Pair.of(group, name);
93+
Set<String> versions = componentMap.computeIfAbsent(
94+
key,
95+
stringStringPair -> new HashSet<>()
96+
);
97+
versions.add(version);
98+
}
99+
StringBuilder stringBuilder = new StringBuilder();
100+
for (Map.Entry<Pair<String, String>, Set<String>> entry : componentMap.entrySet()) {
101+
Pair<String, String> key = entry.getKey();
102+
Set<String> versions = entry.getValue();
103+
if (versions.size() > 1) {
104+
stringBuilder
105+
.append("[ERROR]Duplicated versions for ")
106+
.append(key.getLeft())
107+
.append(":")
108+
.append(key.getRight())
109+
.append(" , versions : ")
110+
.append(StringUtils.join(versions.iterator(), ","))
111+
.append("\n");
112+
}
113+
}
114+
if (stringBuilder.length() > 0) {
115+
throw new MojoExecutionException(stringBuilder.toString());
116+
}
117+
}
118+
}
119+
}
120+
121+
private void doEnforceLicensesBlackListAndWhiteList(@NotNull Bom bom) throws MojoExecutionException {
122+
List<Component> components = bom.getComponents();
123+
if (components != null) {
124+
StringBuilder stringBuilder = new StringBuilder();
125+
for (Component component : components) {
126+
if (component == null) {
127+
continue;
128+
}
129+
String group = component.getGroup();
130+
String name = component.getName();
131+
LicenseChoice licenseChoice = component.getLicenseChoice();
132+
if (licenseChoice == null) {
133+
continue;
134+
}
135+
if (StringUtils.isNotBlank(licenseChoice.getExpression())) {
136+
try {
137+
AnyLicenseInfo anyLicenseInfo = LicenseInfoFactory.parseSPDXLicenseString(licenseChoice.getExpression());
138+
if (!ArrayUtils.isEmpty(this.enforceLicensesBlackList)) {
139+
if (!SpdxLicenseUtil.isLicensePassBlackList(anyLicenseInfo, this.enforceLicensesBlackList)) {
140+
stringBuilder
141+
.append("[ERROR]License in blackList for ")
142+
.append(group)
143+
.append(":")
144+
.append(name)
145+
.append(" , license : ")
146+
.append(licenseChoice.getExpression())
147+
.append("\n");
148+
}
149+
}
150+
if (!ArrayUtils.isEmpty(this.enforceLicensesWhiteList)) {
151+
if (!SpdxLicenseUtil.isLicensePassWhiteList(anyLicenseInfo, this.enforceLicensesWhiteList)) {
152+
stringBuilder
153+
.append("[ERROR]License not in whiteList for ")
154+
.append(group)
155+
.append(":")
156+
.append(name)
157+
.append(" , license : ")
158+
.append(licenseChoice.getExpression())
159+
.append("\n");
160+
}
161+
}
162+
} catch (InvalidSPDXAnalysisException e) {
163+
getLog().warn(e);
164+
}
165+
} else if (licenseChoice.getLicenses() != null) {
166+
for (License license : licenseChoice.getLicenses()) {
167+
if (!ArrayUtils.isEmpty(this.enforceLicensesBlackList)) {
168+
if (
169+
ArrayUtils.contains(this.enforceLicensesBlackList, license.getId())
170+
|| ArrayUtils.contains(this.enforceLicensesBlackList, license.getName())
171+
) {
172+
stringBuilder
173+
.append("[ERROR]License in blackList for ")
174+
.append(group)
175+
.append(":")
176+
.append(name)
177+
.append(" , license : ")
178+
.append(license.getId())
179+
.append("\n");
180+
}
181+
}
182+
if (!ArrayUtils.isEmpty(this.enforceLicensesWhiteList)) {
183+
if (
184+
!(
185+
ArrayUtils.contains(this.enforceLicensesBlackList, license.getId())
186+
|| ArrayUtils.contains(this.enforceLicensesBlackList, license.getName())
187+
)
188+
) {
189+
stringBuilder
190+
.append("[ERROR]License not in whiteList for ")
191+
.append(group)
192+
.append(":")
193+
.append(name)
194+
.append(" , license : ")
195+
.append(license.getId())
196+
.append("\n");
197+
}
198+
}
199+
}
200+
}
201+
}
202+
if (stringBuilder.length() > 0) {
203+
throw new MojoExecutionException(stringBuilder.toString());
204+
}
205+
}
206+
}
207+
208+
}

src/main/java/org/cyclonedx/maven/CycloneDxAggregateMojo.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
2929
import org.cyclonedx.model.Component;
3030
import org.cyclonedx.model.Dependency;
31+
import org.jetbrains.annotations.NotNull;
3132

3233
import java.util.ArrayList;
3334
import java.util.Arrays;
@@ -87,7 +88,7 @@ public class CycloneDxAggregateMojo extends CycloneDxMojo {
8788
@Parameter(property = "excludeTestProject", defaultValue = "false", required = false)
8889
protected Boolean excludeTestProject;
8990

90-
protected boolean shouldExclude(MavenProject mavenProject) {
91+
protected boolean shouldExclude(@NotNull MavenProject mavenProject) {
9192
boolean shouldExclude = false;
9293
if (excludeArtifactId != null && excludeArtifactId.length > 0) {
9394
shouldExclude = Arrays.asList(excludeArtifactId).contains(mavenProject.getArtifactId());
@@ -98,7 +99,7 @@ protected boolean shouldExclude(MavenProject mavenProject) {
9899
if (excludeTestProject && mavenProject.getArtifactId().contains("test")) {
99100
shouldExclude = true;
100101
}
101-
return shouldExclude;
102+
return excludeTestProject && mavenProject.getArtifactId().contains("test");
102103
}
103104

104105
@Override

0 commit comments

Comments
 (0)