Skip to content

Commit

Permalink
Support ingestion of CycloneDX v1.6 BOMs
Browse files Browse the repository at this point in the history
* Updates `cyclonedx-core-java` to version `9.0.3`
* ~Bumps Jackson to version `2.17.1` to resolve compatibility issues with `cyclonedx-core-java`~
    * Jackson was already updated
* Resolve various compilation errors due to refactoring in `cyclonedx-core-java`
* Add validator tests for all CycloneDX versions

Note that BOM exports will continue to use v1.5 for the time being. This avoids breaking users' workflows in case their tooling doesn't yet support v1.6.

Ports DependencyTrack/dependency-track#3863 from Dependency-Track v4.11.4.

Signed-off-by: nscuro <nscuro@protonmail.com>
  • Loading branch information
nscuro committed Jun 24, 2024
1 parent d6a5ec9 commit 6cd2d4c
Show file tree
Hide file tree
Showing 33 changed files with 2,113 additions and 150 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
<lib.cpe-parser.version>2.1.0</lib.cpe-parser.version>
<lib.cvss-calculator.version>1.4.3</lib.cvss-calculator.version>
<lib.owasp-rr-calculator.version>1.0.1</lib.owasp-rr-calculator.version>
<lib.cyclonedx-java.version>8.0.3</lib.cyclonedx-java.version>
<lib.cyclonedx-java.version>9.0.3</lib.cyclonedx-java.version>
<lib.jackson.version>2.17.1</lib.jackson.version>
<lib.jackson-databind.version>2.17.1</lib.jackson-databind.version>
<lib.jaxb.runtime.version>2.3.6</lib.jaxb.runtime.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
*/
package org.dependencytrack.parser.cyclonedx;

import org.cyclonedx.BomGeneratorFactory;
import org.cyclonedx.CycloneDxSchema;
import org.cyclonedx.Version;
import org.cyclonedx.exception.GeneratorException;
import org.cyclonedx.generators.BomGeneratorFactory;
import org.cyclonedx.model.Bom;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Finding;
Expand Down Expand Up @@ -95,10 +95,12 @@ private Bom create(List<Component>components, final List<ServiceComponent> servi
}

public String export(final Bom bom, final Format format) throws GeneratorException {
// TODO: The output version should be user-controllable.

if (Format.JSON == format) {
return BomGeneratorFactory.createJson(CycloneDxSchema.VERSION_LATEST, bom).toJsonString();
return BomGeneratorFactory.createJson(Version.VERSION_15, bom).toJsonString();
} else {
return BomGeneratorFactory.createXml(CycloneDxSchema.VERSION_LATEST, bom).toXmlString();
return BomGeneratorFactory.createXml(Version.VERSION_15, bom).toXmlString();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.codehaus.stax2.XMLInputFactory2;
import org.cyclonedx.CycloneDxSchema;
import org.cyclonedx.Version;
import org.cyclonedx.exception.ParseException;
import org.cyclonedx.parsers.JsonParser;
import org.cyclonedx.parsers.Parser;
Expand All @@ -45,12 +45,14 @@
import static org.cyclonedx.CycloneDxSchema.NS_BOM_13;
import static org.cyclonedx.CycloneDxSchema.NS_BOM_14;
import static org.cyclonedx.CycloneDxSchema.NS_BOM_15;
import static org.cyclonedx.CycloneDxSchema.Version.VERSION_10;
import static org.cyclonedx.CycloneDxSchema.Version.VERSION_11;
import static org.cyclonedx.CycloneDxSchema.Version.VERSION_12;
import static org.cyclonedx.CycloneDxSchema.Version.VERSION_13;
import static org.cyclonedx.CycloneDxSchema.Version.VERSION_14;
import static org.cyclonedx.CycloneDxSchema.Version.VERSION_15;
import static org.cyclonedx.CycloneDxSchema.NS_BOM_16;
import static org.cyclonedx.Version.VERSION_10;
import static org.cyclonedx.Version.VERSION_11;
import static org.cyclonedx.Version.VERSION_12;
import static org.cyclonedx.Version.VERSION_13;
import static org.cyclonedx.Version.VERSION_14;
import static org.cyclonedx.Version.VERSION_15;
import static org.cyclonedx.Version.VERSION_16;

/**
* @since 4.11.0
Expand Down Expand Up @@ -93,7 +95,7 @@ public void validate(final byte[] bomBytes) {

private FormatAndVersion detectFormatAndSchemaVersion(final byte[] bomBytes) {
try {
final CycloneDxSchema.Version version = detectSchemaVersionFromJson(bomBytes);
final Version version = detectSchemaVersionFromJson(bomBytes);
return new FormatAndVersion(Format.JSON, version);
} catch (JsonParseException e) {
if (LOGGER.isDebugEnabled()) {
Expand All @@ -104,7 +106,7 @@ private FormatAndVersion detectFormatAndSchemaVersion(final byte[] bomBytes) {
}

try {
final CycloneDxSchema.Version version = detectSchemaVersionFromXml(bomBytes);
final Version version = detectSchemaVersionFromXml(bomBytes);
return new FormatAndVersion(Format.XML, version);
} catch (XMLStreamException e) {
if (LOGGER.isDebugEnabled()) {
Expand All @@ -115,7 +117,7 @@ private FormatAndVersion detectFormatAndSchemaVersion(final byte[] bomBytes) {
throw new InvalidBomException("BOM is neither valid JSON nor XML");
}

private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomBytes) throws IOException {
private Version detectSchemaVersionFromJson(final byte[] bomBytes) throws IOException {
try (final com.fasterxml.jackson.core.JsonParser jsonParser = jsonMapper.createParser(bomBytes)) {
JsonToken currentToken = jsonParser.nextToken();
if (currentToken != JsonToken.START_OBJECT) {
Expand All @@ -125,7 +127,7 @@ private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomByte
.formatted(JsonToken.START_OBJECT.asString(), currentTokenAsString));
}

CycloneDxSchema.Version schemaVersion = null;
Version schemaVersion = null;
while (jsonParser.nextToken() != null) {
final String fieldName = jsonParser.getCurrentName();
if ("specVersion".equals(fieldName)) {
Expand All @@ -138,6 +140,7 @@ private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomByte
case "1.3" -> VERSION_13;
case "1.4" -> VERSION_14;
case "1.5" -> VERSION_15;
case "1.6" -> VERSION_16;
default ->
throw new InvalidBomException("Unrecognized specVersion %s".formatted(specVersion));
};
Expand All @@ -153,12 +156,12 @@ private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomByte
}
}

private CycloneDxSchema.Version detectSchemaVersionFromXml(final byte[] bomBytes) throws XMLStreamException {
private Version detectSchemaVersionFromXml(final byte[] bomBytes) throws XMLStreamException {
final XMLInputFactory xmlInputFactory = XMLInputFactory2.newFactory();
final var bomBytesStream = new ByteArrayInputStream(bomBytes);
final XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(bomBytesStream);

CycloneDxSchema.Version schemaVersion = null;
Version schemaVersion = null;
while (xmlStreamReader.hasNext()) {
if (xmlStreamReader.next() == XMLEvent.START_ELEMENT) {
if (!"bom".equalsIgnoreCase(xmlStreamReader.getLocalName())) {
Expand All @@ -177,6 +180,7 @@ private CycloneDxSchema.Version detectSchemaVersionFromXml(final byte[] bomBytes
case NS_BOM_13 -> VERSION_13;
case NS_BOM_14 -> VERSION_14;
case NS_BOM_15 -> VERSION_15;
case NS_BOM_16 -> VERSION_16;
default -> null;
};
}
Expand All @@ -202,7 +206,7 @@ private enum Format {
XML
}

private record FormatAndVersion(Format format, CycloneDxSchema.Version version) {
private record FormatAndVersion(Format format, Version version) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.cyclonedx.model.Metadata;
import org.cyclonedx.model.Swid;
import org.cyclonedx.model.Tool;
import org.cyclonedx.model.license.Expression;
import org.dependencytrack.model.Analysis;
import org.dependencytrack.model.AnalysisJustification;
import org.dependencytrack.model.AnalysisResponse;
Expand Down Expand Up @@ -226,9 +227,9 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx
}

final var licenseCandidates = new ArrayList<org.cyclonedx.model.License>();
if (cdxComponent.getLicenseChoice() != null) {
if (cdxComponent.getLicenseChoice().getLicenses() != null) {
cdxComponent.getLicenseChoice().getLicenses().stream()
if (cdxComponent.getLicenses() != null) {
if (cdxComponent.getLicenses().getLicenses() != null) {
cdxComponent.getLicenses().getLicenses().stream()
.filter(license -> isNotBlank(license.getId()) || isNotBlank(license.getName()))
.peek(license -> {
// License text can be large, but we don't need it for further processing. Drop it.
Expand All @@ -237,12 +238,13 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx
.forEach(licenseCandidates::add);
}

if (isNotBlank(cdxComponent.getLicenseChoice().getExpression())) {
final Expression licenseExpression = cdxComponent.getLicenses().getExpression();
if (licenseExpression != null && isNotBlank(licenseExpression.getValue())) {
// If the expression consists of just one license ID, add it as another option.
final var expressionParser = new SpdxExpressionParser();
final SpdxExpression expression = expressionParser.parse(cdxComponent.getLicenseChoice().getExpression());
final SpdxExpression expression = expressionParser.parse(licenseExpression.getValue());
if (!SpdxExpression.INVALID.equals(expression)) {
component.setLicenseExpression(trim(cdxComponent.getLicenseChoice().getExpression()));
component.setLicenseExpression(trim(licenseExpression.getValue()));

if (expression.getSpdxLicenseId() != null) {
final var expressionLicense = new org.cyclonedx.model.License();
Expand All @@ -254,7 +256,7 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx
LOGGER.warn("""
Encountered invalid license expression "%s" for \
Component{group=%s, name=%s, version=%s, bomRef=%s}; Skipping\
""".formatted(cdxComponent.getLicenseChoice().getExpression(), component.getGroup(),
""".formatted(cdxComponent.getLicenses().getExpression(), component.getGroup(),
component.getName(), component.getVersion(), component.getBomRef()));
}
}
Expand Down Expand Up @@ -626,28 +628,30 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final
cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA3_512, component.getSha3_512()));
}

final LicenseChoice licenseChoice = new LicenseChoice();
final LicenseChoice licenses = new LicenseChoice();
if (component.getResolvedLicense() != null) {
final org.cyclonedx.model.License license = new org.cyclonedx.model.License();
license.setId(component.getResolvedLicense().getLicenseId());
license.setUrl(component.getLicenseUrl());
licenseChoice.addLicense(license);
cycloneComponent.setLicenseChoice(licenseChoice);
licenses.addLicense(license);
cycloneComponent.setLicenses(licenses);
} else if (component.getLicense() != null) {
final org.cyclonedx.model.License license = new org.cyclonedx.model.License();
license.setName(component.getLicense());
license.setUrl(component.getLicenseUrl());
licenseChoice.addLicense(license);
cycloneComponent.setLicenseChoice(licenseChoice);
licenses.addLicense(license);
cycloneComponent.setLicenses(licenses);
} else if (StringUtils.isNotEmpty(component.getLicenseUrl())) {
final org.cyclonedx.model.License license = new org.cyclonedx.model.License();
license.setUrl(component.getLicenseUrl());
licenseChoice.addLicense(license);
cycloneComponent.setLicenseChoice(licenseChoice);
licenses.addLicense(license);
cycloneComponent.setLicenses(licenses);
}
if (component.getLicenseExpression() != null) {
licenseChoice.setExpression(component.getLicenseExpression());
cycloneComponent.setLicenseChoice(licenseChoice);
final var licenseExpression = new Expression();
licenseExpression.setValue(component.getLicenseExpression());
licenses.setExpression(licenseExpression);
cycloneComponent.setLicenses(licenses);
}

if (component.getExternalReferences() != null && !component.getExternalReferences().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.cyclonedx.BomParserFactory;
import org.cyclonedx.exception.ParseException;
import org.cyclonedx.parsers.BomParserFactory;
import org.cyclonedx.parsers.Parser;
import org.datanucleus.flush.FlushMode;
import org.datanucleus.store.query.QueryNotUniqueException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import alpine.event.framework.Subscriber;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
import org.cyclonedx.BomParserFactory;
import org.cyclonedx.parsers.BomParserFactory;
import org.cyclonedx.parsers.Parser;
import org.dependencytrack.event.VexUploadEvent;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public class VulnerabilityPolicyUtil {
static {
try {
schema = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012))
.objectMapper(MAPPER).build()
.jsonMapper(MAPPER).build()
.getSchema(resourceToString("/schema/vulnerability-policy-v1.schema.json", StandardCharsets.UTF_8));
} catch (IOException e) {
LOGGER.error("Exception occurred while creating schema for YAML validation", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
package org.dependencytrack.parser.cyclonedx;

import org.assertj.core.api.Assertions;
import org.cyclonedx.BomParserFactory;
import org.cyclonedx.parsers.BomParserFactory;
import org.dependencytrack.PersistenceCapableTest;
import org.dependencytrack.model.Analysis;
import org.dependencytrack.model.AnalysisJustification;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,26 @@
*/
package org.dependencytrack.parser.cyclonedx;

import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;

@RunWith(JUnitParamsRunner.class)
public class CycloneDxValidatorTest {

private CycloneDxValidator validator;
Expand Down Expand Up @@ -176,4 +190,32 @@ public void testValidateJsonWithSpecVersionAtTheBottom() {
""".getBytes()));
}

@SuppressWarnings("unused")
private Object[] testValidateWithValidBomParameters() throws Exception {
final PathMatcher pathMatcherJson = FileSystems.getDefault().getPathMatcher("glob:**/valid-bom-*.json");
final PathMatcher pathMatcherXml = FileSystems.getDefault().getPathMatcher("glob:**/valid-bom-*.xml");
final var bomFilePaths = new ArrayList<Path>();

Files.walkFileTree(Paths.get("./src/test/resources/unit/cyclonedx"), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) {
if (pathMatcherJson.matches(file) || pathMatcherXml.matches(file)) {
bomFilePaths.add(file);
}

return FileVisitResult.CONTINUE;
}
});

return bomFilePaths.stream().sorted().toArray();
}

@Test
@Parameters(method = "testValidateWithValidBomParameters")
public void testValidateWithValidBom(final Path bomFilePath) throws Exception {
final byte[] bomBytes = Files.readAllBytes(bomFilePath);

assertThatNoException().isThrownBy(() -> validator.validate(bomBytes));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void testValidPolicyYamlWithSchema() throws IOException {
ObjectMapper objMapper = new ObjectMapper(new YAMLFactory());
final String jsonSchemaContent = resourceToString("/schema/vulnerability-policy-v1.schema.json", StandardCharsets.UTF_8);
final String policyContent = resourceToString("/unit/policy/vulnerability-policy-v1-valid.yaml", StandardCharsets.UTF_8);
JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)).objectMapper(objMapper).build();
JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)).jsonMapper(objMapper).build();
JsonSchema schema = factory.getSchema(jsonSchemaContent);
JsonNode jsonNode = objMapper.readTree(policyContent);
Set<ValidationMessage> validateMsg = schema.validate(jsonNode);
Expand All @@ -54,7 +54,7 @@ public void testInvalidPolicyYamlWithSchema() throws IOException {
ObjectMapper objMapper = new ObjectMapper(new YAMLFactory());
final String jsonSchemaContent = resourceToString("/schema/vulnerability-policy-v1.schema.json", StandardCharsets.UTF_8);
final String policyContent = resourceToString("/unit/policy/vulnerability-policy-v1-invalid.yaml", StandardCharsets.UTF_8);
JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)).objectMapper(objMapper).build();
JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)).jsonMapper(objMapper).build();
JsonSchema schema = factory.getSchema(jsonSchemaContent);
JsonNode jsonNode = objMapper.readTree(policyContent);
Set<ValidationMessage> validateMsg = schema.validate(jsonNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1029,8 +1029,8 @@ public void uploadBomInvalidParentTest() throws Exception {

@SuppressWarnings("unused")
private Object[] uploadBomSchemaValidationTestParameters() throws Exception {
final PathMatcher pathMatcherJson = FileSystems.getDefault().getPathMatcher("glob:**/bom-schema*.json");
final PathMatcher pathMatcherXml = FileSystems.getDefault().getPathMatcher("glob:**/bom-schema*.xml");
final PathMatcher pathMatcherJson = FileSystems.getDefault().getPathMatcher("glob:**/valid-bom-*.json");
final PathMatcher pathMatcherXml = FileSystems.getDefault().getPathMatcher("glob:**/valid-bom-*.xml");
final var bomFilePaths = new ArrayList<Path>();

Files.walkFileTree(Paths.get("./src/test/resources"), new SimpleFileVisitor<>() {
Expand Down
10 changes: 0 additions & 10 deletions src/test/resources/unit/bom-schema1.0.xml

This file was deleted.

9 changes: 0 additions & 9 deletions src/test/resources/unit/bom-schema1.1.xml

This file was deleted.

13 changes: 0 additions & 13 deletions src/test/resources/unit/bom-schema1.2.json

This file was deleted.

Loading

0 comments on commit 6cd2d4c

Please sign in to comment.