Skip to content

Commit

Permalink
Merge pull request #3710 from nscuro/issue-3584
Browse files Browse the repository at this point in the history
Support ingestion of CycloneDX v1.6 BOMs
  • Loading branch information
nscuro committed May 15, 2024
2 parents 63da913 + c97bb54 commit b221329
Show file tree
Hide file tree
Showing 23 changed files with 2,104 additions and 32 deletions.
4 changes: 3 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@
<lib.commons-compress.version>1.26.1</lib.commons-compress.version>
<lib.cvss-calculator.version>1.4.2</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.0</lib.cyclonedx-java.version>
<lib.greenmail.version>1.6.15</lib.greenmail.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.9</lib.jaxb.runtime.version>
<lib.json-java.version>20240303</lib.json-java.version>
<lib.json-unit.version>3.2.7</lib.json-unit.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> serv
}

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 @@ -33,6 +33,7 @@
import org.cyclonedx.model.Hash;
import org.cyclonedx.model.LicenseChoice;
import org.cyclonedx.model.Swid;
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 @@ -223,12 +224,13 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx
.forEach(licenseCandidates::add);
}

if (isNotBlank(cdxComponent.getLicenseChoice().getExpression())) {
final Expression licenseExpression = cdxComponent.getLicenseChoice().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 Down Expand Up @@ -529,13 +531,13 @@ public static Component convert(final QueryManager qm, final org.cyclonedx.model
final LicenseChoice licenseChoice = cycloneDxComponent.getLicenseChoice();
if (licenseChoice != null) {
final List<org.cyclonedx.model.License> licenseOptions = new ArrayList<>();
if (licenseChoice.getExpression() != null) {
if (licenseChoice.getExpression() != null && isNotBlank(licenseChoice.getExpression().getValue())) {
final var expressionParser = new SpdxExpressionParser();
final SpdxExpression parsedExpression = expressionParser.parse(licenseChoice.getExpression());
final SpdxExpression parsedExpression = expressionParser.parse(licenseChoice.getExpression().getValue());
if (!Objects.equals(parsedExpression, SpdxExpression.INVALID)) {
// store license expression, but don't overwrite manual changes to the field
if (component.getLicenseExpression() == null) {
component.setLicenseExpression(licenseChoice.getExpression());
component.setLicenseExpression(licenseChoice.getExpression().getValue());
}
// if the expression just consists of one license id, we can add it as another license option
if (parsedExpression.getSpdxLicenseId() != null) {
Expand Down Expand Up @@ -769,7 +771,9 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final
cycloneComponent.setLicenseChoice(licenseChoice);
}
if (component.getLicenseExpression() != null) {
licenseChoice.setExpression(component.getLicenseExpression());
final var licenseExpression = new Expression();
licenseExpression.setValue(component.getLicenseExpression());
licenseChoice.setExpression(licenseExpression);
cycloneComponent.setLicenseChoice(licenseChoice);
}

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.BomUploadEvent;
import org.dependencytrack.event.NewVulnerableDependencyAnalysisEvent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,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.model.ConfigPropertyConstants;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.dependencytrack.parser.cyclonedx;

import org.assertj.core.api.Assertions;
import org.cyclonedx.BomParserFactory;
import org.cyclonedx.exception.ParseException;
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 @@ -999,7 +999,7 @@ public void uploadBomTooLargeViaPutTest() {
{
"status": 400,
"title": "The provided JSON payload could not be mapped",
"detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])"
"detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])"
}
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ public void uploadVexTooLargeViaPutTest() {
{
"status": 400,
"title": "The provided JSON payload could not be mapped",
"detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])"
"detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])"
}
""");
}
Expand Down

0 comments on commit b221329

Please sign in to comment.