Skip to content

Commit

Permalink
feat: address PR comments
Browse files Browse the repository at this point in the history
Signed-off-by: Gordon <[email protected]>
  • Loading branch information
gordonrousselle committed Nov 7, 2024
1 parent e2a2290 commit f235111
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 54 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ dependencies {
testImplementation("org.spockframework:spock-core:2.2-M1-groovy-3.0") {
exclude(module = "groovy-all")
}
testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.3")
}

tasks.withType<Test> {
Expand Down
24 changes: 23 additions & 1 deletion src/main/java/org/cyclonedx/gradle/CycloneDxTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,13 @@ public void setDestination(final File destination) {
@TaskAction
public void createBom() {

logParameters();

final SbomBuilder builder = new SbomBuilder(getLogger(), this);
final SbomGraph components = getComponents().get();
final Bom bom = builder.buildBom(components.getGraph(), components.getRootComponent());

getLogger().info(MESSAGE_WRITING_BOM_OUTPUT);

CycloneDxUtils.writeBom(
bom,
getDestination().get(),
Expand Down Expand Up @@ -333,4 +334,25 @@ public void setLicenseChoice(final Consumer<LicenseChoice> customizer) {
// Definition of gradle Input via Hashmap because Hashmap is serializable (LicenseChoice isn't serializable)
getInputs().property("LicenseChoice", licenseChoice);
}

private void logParameters() {
if (getLogger().isInfoEnabled()) {
getLogger().info("CycloneDX: Parameters");
getLogger().info("------------------------------------------------------------------------");
getLogger().info("schemaVersion : " + schemaVersion.get());
getLogger().info("includeLicenseText : " + includeLicenseText.get());
getLogger().info("includeBomSerialNumber : " + includeBomSerialNumber.get());
getLogger().info("includeConfigs : " + includeConfigs.get());
getLogger().info("skipConfigs : " + skipConfigs.get());
getLogger().info("skipProjects : " + skipProjects.get());
getLogger().info("includeMetadataResolution : " + includeMetadataResolution.get());
getLogger().info("destination : " + destination.get());
getLogger().info("outputName : " + outputName.get());
getLogger().info("componentName : " + componentName.get());
getLogger().info("componentVersion : " + componentVersion.get());
getLogger().info("outputFormat : " + outputFormat.get());
getLogger().info("projectType : " + projectType.get());
getLogger().info("------------------------------------------------------------------------");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.maven.model.License;
import org.apache.maven.project.MavenProject;
import org.cyclonedx.gradle.model.ConfigurationScope;
Expand Down Expand Up @@ -125,7 +126,7 @@ private SbomComponent toSbomComponent(final GraphNode node, final Set<GraphNode>
final File artifactFile = getArtifactFile(node);
final SbomComponentId id = DependencyUtils.toComponentId(node.getResult(), artifactFile);

List<License> licenses = null;
List<License> licenses = new ArrayList<>();
SbomMetaData metaData = null;
if (includeMetaData && node.id instanceof ModuleComponentIdentifier) {
logger.debug("CycloneDX: Including meta data for node {}", node.id);
Expand All @@ -146,13 +147,13 @@ private SbomComponent toSbomComponent(final GraphNode node, final Set<GraphNode>
}

private void extractMetaDataFromArtifactPom(
final File artifactFile, final Component component, final ResolvedComponentResult result) {
@Nullable final File artifactFile, final Component component, final ResolvedComponentResult result) {

if (artifactFile == null || result.getModuleVersion() == null) {
return;
}

final MavenProject mavenProject = mavenHelper.extractPom(artifactFile, result.getModuleVersion());
@Nullable final MavenProject mavenProject = mavenHelper.extractPom(artifactFile, result.getModuleVersion());
if (mavenProject != null) {
logger.debug("CycloneDX: parse artifact pom file of component {}", result.getId());
mavenHelper.getClosestMetadata(artifactFile, mavenProject, component, result.getModuleVersion());
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/cyclonedx/gradle/MavenProjectLookup.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class MavenProjectLookup {
*
* @return a MavenProject instance for this component
*/
@Nullable MavenProject getResolvedMavenProject(final ResolvedComponentResult result) {
@Nullable MavenProject getResolvedMavenProject(@Nullable final ResolvedComponentResult result) {

if (result == null) {
return null;
Expand Down
57 changes: 42 additions & 15 deletions src/main/java/org/cyclonedx/gradle/SbomBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
import com.networknt.schema.utils.StringUtils;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -33,6 +33,7 @@
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.cyclonedx.Version;
import org.cyclonedx.gradle.model.ComponentComparator;
import org.cyclonedx.gradle.model.DependencyComparator;
Expand Down Expand Up @@ -134,7 +135,10 @@ private void addDependency(final Set<Dependency> dependencies, final SbomCompone
try {
dependency.addDependency(toDependency(dependencyComponent));
} catch (MalformedPackageURLException e) {
logger.warn("Error constructing packageUrl for component dependency. Skipping...", e);
logger.warn(
"Error constructing packageUrl for component dependency {}. Skipping...",
dependencyComponent.getName(),
e);
}
});
dependencies.add(dependency);
Expand All @@ -149,11 +153,14 @@ private Dependency toDependency(final SbomComponentId componentId) throws Malfor
private void addComponent(
final Set<Component> components, final SbomComponent component, final SbomComponent parentComponent) {
if (!component.equals(parentComponent)) {
final File artifactFile = component.getArtifactFile().orElse(null);
@Nullable final File artifactFile = component.getArtifactFile().orElse(null);
try {
components.add(toComponent(component, artifactFile, Component.Type.LIBRARY));
} catch (MalformedPackageURLException e) {
logger.warn("Error constructing packageUrl for component. Skipping...", e);
logger.warn(
"Error constructing packageUrl for component {}. Skipping...",
component.getId().getName(),
e);
}
}
}
Expand Down Expand Up @@ -185,10 +192,10 @@ private Component toComponent(final SbomComponent component, final File artifact
});
});

component.getLicenses().ifPresent(licenses -> {
LicenseChoice licenseChoice = mavenHelper.resolveMavenLicenses(licenses);
if (!component.getLicenses().isEmpty()) {
LicenseChoice licenseChoice = mavenHelper.resolveMavenLicenses(component.getLicenses());
resultComponent.setLicenses(licenseChoice);
});
}

logger.debug(MESSAGE_CALCULATING_HASHES);
if (artifactFile != null) {
Expand All @@ -199,17 +206,37 @@ private Component toComponent(final SbomComponent component, final File artifact
}

private List<Property> buildProperties(final SbomComponent component) {
final List<Property> inScopeProperties = buildScopeProperties(component);
final Property isTestProperty = buildIsTestProperty(component);

final List<Property> resultProperties = new ArrayList<>();
resultProperties.addAll(inScopeProperties);
resultProperties.add(isTestProperty);

return resultProperties;
}

private List<Property> buildScopeProperties(final SbomComponent component) {
return component.getInScopeConfigurations().stream()
.map(v -> {
Property property = new Property();
property.setName("cdx:maven:package:projectsAndScopes");
property.setValue(String.format("%s:%s", v.getProjectName(), v.getConfigName()));
return property;
})
.sorted(Comparator.comparing(Property::getValue))
.collect(Collectors.toList());
}

final String value = component.getInScopeConfigurations().stream()
.map(v -> String.format(
"%s:%s", URLEncoder.encode(v.getProjectName()), URLEncoder.encode(v.getConfigName())))
.collect(Collectors.joining(","));
private Property buildIsTestProperty(final SbomComponent component) {

final Property property = new Property();
property.setName("inScopeConfiguration");
property.setValue(value);
boolean isTestComponent = component.getInScopeConfigurations().stream()
.allMatch(v -> v.getConfigName().startsWith("test"));

return Collections.singletonList(property);
Property property = new Property();
property.setName("cdx:maven:package:test");
property.setValue(Boolean.toString(isTestComponent));
return property;
}

private List<Hash> calculateHashes(final File artifactFile) {
Expand Down
40 changes: 14 additions & 26 deletions src/main/java/org/cyclonedx/gradle/SbomGraphProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.cyclonedx.gradle;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
Expand All @@ -45,16 +46,18 @@ public class SbomGraphProvider implements Callable<SbomGraph> {

private final Project project;
private final CycloneDxTask task;
private final MavenProjectLookup mavenLookup;

public SbomGraphProvider(final Project project, final CycloneDxTask task) {
this.project = project;
this.task = task;
this.mavenLookup = new MavenProjectLookup(project);
}

/**
* Calculates the aggregated dependency graph across all the configurations of both the parent project and
* Calculates the aggregated dependency graph across all the configurations of both the current project and
* child projects. The steps are as follows:
* 1) generate dependency graphs for the parent project, one for each configuration
* 1) generate dependency graphs for the current project, one for each configuration
* 2) if child projects exist, generate dependency graphs across all the child projects
* 3) merge all generated graphs from the step 1) and 2)
*
Expand All @@ -71,11 +74,10 @@ public SbomGraph call() throws Exception {

project.getLogger().info(MESSAGE_RESOLVING_DEPS);

final DependencyGraphTraverser traverser = new DependencyGraphTraverser(
project.getLogger(), getArtifacts(), new MavenProjectLookup(project), task);

final Map<SbomComponentId, SbomComponent> graph = Stream.concat(
traverseCurrentProject(traverser), traverseChildProjects(traverser))
Stream.of(project), project.getSubprojects().stream())
.filter(project -> !shouldSkipProject(project))
.flatMap(this::traverseProject)
.reduce(new HashMap<>(), DependencyUtils::mergeGraphs);

return buildSbomGraph(graph);
Expand All @@ -99,18 +101,18 @@ private SbomGraph buildSbomGraph(final Map<SbomComponentId, SbomComponent> graph
.withId(rootProjectId)
.withDependencyComponents(new HashSet<>())
.withInScopeConfigurations(new HashSet<>())
.withLicenses(new ArrayList<>())
.build();

return new SbomGraph(graph, sbomComponent);
}
}

private Stream<Map<SbomComponentId, SbomComponent>> traverseCurrentProject(
final DependencyGraphTraverser traverser) {
private Stream<Map<SbomComponentId, SbomComponent>> traverseProject(final Project project) {

final DependencyGraphTraverser traverser =
new DependencyGraphTraverser(project.getLogger(), getArtifacts(), mavenLookup, task);

if (shouldSkipProject(project)) {
return Stream.empty();
}
return project.getConfigurations().stream()
.filter(configuration -> shouldIncludeConfiguration(configuration)
&& !shouldSkipConfiguration(configuration)
Expand All @@ -119,22 +121,8 @@ private Stream<Map<SbomComponentId, SbomComponent>> traverseCurrentProject(
config.getIncoming().getResolutionResult().getRoot(), project.getName(), config.getName()));
}

private Stream<Map<SbomComponentId, SbomComponent>> traverseChildProjects(
final DependencyGraphTraverser traverser) {
return project.getChildProjects().entrySet().stream()
.filter(project -> !shouldSkipProject(project.getValue()))
.flatMap(project -> project.getValue().getConfigurations().stream()
.filter(configuration -> shouldIncludeConfiguration(configuration)
&& !shouldSkipConfiguration(configuration)
&& configuration.isCanBeResolved())
.map(config -> traverser.traverseGraph(
config.getIncoming().getResolutionResult().getRoot(),
project.getKey(),
config.getName())));
}

private Map<ComponentIdentifier, File> getArtifacts() {
return project.getAllprojects().stream()
return Stream.concat(Stream.of(project), project.getSubprojects().stream())
.filter(project -> !shouldSkipProject(project))
.flatMap(project -> project.getConfigurations().stream())
.filter(configuration -> shouldIncludeConfiguration(configuration)
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/org/cyclonedx/gradle/model/SbomComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ public final class SbomComponent implements Serializable {

@Nullable private final SbomMetaData metaData;

@Nullable private final List<License> licenses;
private final List<License> licenses;

public SbomComponent(
private SbomComponent(
final SbomComponentId id,
final Set<ConfigurationScope> inScopeConfigurations,
final Set<SbomComponentId> dependencyComponents,
@Nullable final File artifactFile,
@Nullable final SbomMetaData metaData,
@Nullable final List<License> licenses) {
final List<License> licenses) {
this.id = id;
this.inScopeConfigurations = inScopeConfigurations;
this.dependencyComponents = dependencyComponents;
Expand Down Expand Up @@ -73,8 +73,8 @@ public Optional<SbomMetaData> getSbomMetaData() {
return Optional.ofNullable(metaData);
}

public Optional<List<License>> getLicenses() {
return Optional.ofNullable(licenses);
public List<License> getLicenses() {
return licenses;
}

public static class Builder {
Expand Down Expand Up @@ -113,7 +113,7 @@ public Builder withMetaData(@Nullable final SbomMetaData metaData) {
return this;
}

public Builder withLicenses(@Nullable final List<License> licenses) {
public Builder withLicenses(final List<License> licenses) {
this.licenses = licenses;
return this;
}
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/cyclonedx/gradle/utils/CycloneDxUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.cyclonedx.Version;
import org.cyclonedx.exception.ParseException;
import org.cyclonedx.generators.BomGeneratorFactory;
import org.cyclonedx.generators.json.BomJsonGenerator;
import org.cyclonedx.generators.xml.BomXmlGenerator;
import org.cyclonedx.model.Bom;
import org.cyclonedx.parsers.JsonParser;
import org.cyclonedx.parsers.Parser;
import org.cyclonedx.parsers.XmlParser;
import org.gradle.api.GradleException;

public class CycloneDxUtils {
Expand Down Expand Up @@ -84,6 +89,8 @@ private static void writeJSONBom(final Version schemaVersion, final Bom bom, fin
} catch (Exception e) {
throw new GradleException("Error writing json bom file", e);
}

validateBom(new JsonParser(), schemaVersion, destination);
}

private static void writeXmlBom(final Version schemaVersion, final Bom bom, final File destination) {
Expand All @@ -95,5 +102,18 @@ private static void writeXmlBom(final Version schemaVersion, final Bom bom, fina
} catch (Exception e) {
throw new GradleException("Error writing xml bom file", e);
}

validateBom(new XmlParser(), schemaVersion, destination);
}

private static void validateBom(final Parser bomParser, final Version schemaVersion, final File destination) {
try {
final List<ParseException> exceptions = bomParser.validate(destination, schemaVersion);
if (!exceptions.isEmpty()) {
throw exceptions.get(0);
}
} catch (Exception e) {
throw new GradleException("Error whilst validating XML BOM", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public static SbomComponentId toComponentId(final ResolvedComponentResult node,
node.getModuleVersion().getVersion(),
type);
} else {
return new SbomComponentId("N/A", node.getId().getDisplayName(), "N/A", type);
return new SbomComponentId("undefined", node.getId().getDisplayName(), "undefined", type);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class PluginConfigurationSpec extends Specification {
dependencies {
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version:'2.8.11'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version:'1.5.18.RELEASE'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version:'1.5.18.RELEASE'
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-native-prebuilt', version: '2.0.20'
}""", "rootProject.name = 'hello-world'")

Expand Down
Loading

0 comments on commit f235111

Please sign in to comment.