Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ingestion of CycloneDX v1.6 BOMs #3710

Merged
merged 1 commit into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading