From 702e43b07e02d4e728714c837de262e193da070c Mon Sep 17 00:00:00 2001 From: nscuro <nscuro@protonmail.com> Date: Sun, 1 Sep 2024 16:08:51 +0200 Subject: [PATCH 1/2] Support inclusion/exclusion of projects from BOM validation with tags Closes #3891 Signed-off-by: nscuro <nscuro@protonmail.com> --- .../model/BomValidationMode.java | 34 +++ .../model/ConfigPropertyConstants.java | 14 +- .../v1/AbstractConfigPropertyResource.java | 32 ++ .../resources/v1/BomResource.java | 72 ++++- .../upgrade/v4120/v4120Updater.java | 85 ++++++ .../resources/v1/BomResourceTest.java | 275 +++++++++++++++++- .../v1/ConfigPropertyResourceTest.java | 133 +++++++++ .../resources/v1/VexResourceTest.java | 206 ++++++++++++- .../tasks/BomUploadProcessingTaskTest.java | 12 +- 9 files changed, 820 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/BomValidationMode.java diff --git a/src/main/java/org/dependencytrack/model/BomValidationMode.java b/src/main/java/org/dependencytrack/model/BomValidationMode.java new file mode 100644 index 0000000000..f155b4c567 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/BomValidationMode.java @@ -0,0 +1,34 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +/** + * @since 4.12.0 + */ +public enum BomValidationMode { + + ENABLED, + + DISABLED, + + ENABLED_FOR_TAGS, + + DISABLED_FOR_TAGS + +} diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index ea1a7151d5..ced4a71cc2 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -109,13 +109,15 @@ public enum ConfigPropertyConstants { SEARCH_INDEXES_CONSISTENCY_CHECK_ENABLED("search-indexes", "consistency.check.enabled", "true", PropertyType.BOOLEAN, "Flag to enable lucene indexes periodic consistency check"), SEARCH_INDEXES_CONSISTENCY_CHECK_CADENCE("search-indexes", "consistency.check.cadence", "4320", PropertyType.INTEGER, "Lucene indexes consistency check cadence (in minutes)"), SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD("search-indexes", "consistency.check.delta.threshold", "20", PropertyType.INTEGER, "Threshold used to trigger an index rebuild when comparing database table and corresponding lucene index (in percentage). It must be an integer between 1 and 100"), - BOM_VALIDATION_ENABLED("artifact", "bom.validation.enabled", "true", PropertyType.BOOLEAN, "Flag to control bom validation"); + BOM_VALIDATION_MODE("artifact", "bom.validation.mode", BomValidationMode.ENABLED.name(), PropertyType.STRING, "Flag to control the BOM validation mode"), + BOM_VALIDATION_TAGS_INCLUSIVE("artifact", "bom.validation.tags.inclusive", "[]", PropertyType.STRING, "JSON array of tags for which BOM validation shall be performed"), + BOM_VALIDATION_TAGS_EXCLUSIVE("artifact", "bom.validation.tags.exclusive", "[]", PropertyType.STRING, "JSON array of tags for which BOM validation shall NOT be performed"); - private String groupName; - private String propertyName; - private String defaultPropertyValue; - private PropertyType propertyType; - private String description; + private final String groupName; + private final String propertyName; + private final String defaultPropertyValue; + private final PropertyType propertyType; + private final String description; ConfigPropertyConstants(String groupName, String propertyName, String defaultPropertyValue, PropertyType propertyType, String description) { this.groupName = groupName; diff --git a/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java index 3c9fb89bd2..440fa28ab5 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java @@ -24,10 +24,16 @@ import alpine.model.IConfigProperty; import alpine.security.crypto.DataEncryption; import alpine.server.resources.AlpineResource; +import org.dependencytrack.model.BomValidationMode; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.persistence.QueryManager; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonReader; +import jakarta.json.JsonString; import jakarta.ws.rs.core.Response; +import java.io.StringReader; import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URI; @@ -121,6 +127,32 @@ private Response updatePropertyValueInternal(IConfigProperty json, IConfigProper } else { property.setPropertyValue(propertyValue); } + } else if (ConfigPropertyConstants.BOM_VALIDATION_MODE.getPropertyName().equals(json.getPropertyName())) { + try { + BomValidationMode.valueOf(json.getPropertyValue()); + property.setPropertyValue(json.getPropertyValue()); + } catch (IllegalArgumentException e) { + return Response + .status(Response.Status.BAD_REQUEST) + .entity("Value must be any of: %s".formatted(Arrays.stream(BomValidationMode.values()).map(Enum::name).collect(Collectors.joining(", ")))) + .build(); + } + } else if (ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyName().equals(json.getPropertyName()) + || ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE.getPropertyName().equals(json.getPropertyName())) { + try { + final JsonReader jsonReader = Json.createReader(new StringReader(json.getPropertyValue())); + final JsonArray jsonArray = jsonReader.readArray(); + jsonArray.getValuesAs(JsonString::getString); + + // NB: Storing the string representation of the parsed array instead of the original value, + // since this removes any unnecessary whitespace. + property.setPropertyValue(jsonArray.toString()); + } catch (RuntimeException e) { + return Response + .status(Response.Status.BAD_REQUEST) + .entity("Value must be a valid JSON array of strings") + .build(); + } } else { property.setPropertyValue(json.getPropertyValue()); } diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 0da5464604..9c83022282 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -20,6 +20,7 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; +import alpine.model.ConfigProperty; import alpine.notification.Notification; import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; @@ -41,6 +42,7 @@ import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.model.Bom; import org.dependencytrack.model.Bom.Format; +import org.dependencytrack.model.BomValidationMode; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; @@ -63,6 +65,10 @@ import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonReader; +import jakarta.json.JsonString; import jakarta.validation.Validator; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DefaultValue; @@ -79,14 +85,19 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import java.security.Principal; import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.UUID; import static java.util.function.Predicate.not; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE; /** * JAX-RS resources for processing bill-of-material (bom) documents. @@ -524,10 +535,8 @@ private Response process(QueryManager qm, Project project, List<FormDataBodyPart } static void validate(final byte[] bomBytes, final Project project) { - try (QueryManager qm = new QueryManager()) { - if (!qm.isEnabled(ConfigPropertyConstants.BOM_VALIDATION_ENABLED)) { - return; - } + if (!shouldValidate(project)) { + return; } try { @@ -553,6 +562,61 @@ static void validate(final byte[] bomBytes, final Project project) { } } + private static boolean shouldValidate(final Project project) { + try (final var qm = new QueryManager()) { + final ConfigProperty validationModeProperty = qm.getConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName() + ); + + var validationMode = BomValidationMode.valueOf(BOM_VALIDATION_MODE.getDefaultPropertyValue()); + try { + validationMode = BomValidationMode.valueOf(validationModeProperty.getPropertyValue()); + } catch (RuntimeException e) { + LOGGER.warn(""" + No BOM validation mode configured, or configured value is invalid; \ + Assuming default mode %s""".formatted(validationMode), e); + } + + if (validationMode == BomValidationMode.ENABLED) { + LOGGER.debug("Validating BOM because validation is enabled globally"); + return true; + } else if (validationMode == BomValidationMode.DISABLED) { + LOGGER.debug("Not validating BOM because validation is disabled globally"); + return false; + } + + // Other modes depend on tags. Does the project even have tags? + if (project.getTags() == null || project.getTags().isEmpty()) { + return validationMode == BomValidationMode.DISABLED_FOR_TAGS; + } + + final ConfigPropertyConstants tagsPropertyConstant = validationMode == BomValidationMode.ENABLED_FOR_TAGS + ? BOM_VALIDATION_TAGS_INCLUSIVE + : BOM_VALIDATION_TAGS_EXCLUSIVE; + final ConfigProperty tagsProperty = qm.getConfigProperty( + tagsPropertyConstant.getGroupName(), + tagsPropertyConstant.getPropertyName() + ); + + final Set<String> validationModeTags; + try { + final JsonReader jsonParser = Json.createReader(new StringReader(tagsProperty.getPropertyValue())); + final JsonArray jsonArray = jsonParser.readArray(); + validationModeTags = Set.copyOf(jsonArray.getValuesAs(JsonString::getString)); + } catch (RuntimeException e) { + LOGGER.warn("Tags of property %s:%s could not be parsed as JSON array" + .formatted(tagsPropertyConstant.getGroupName(), tagsPropertyConstant.getPropertyName()), e); + return validationMode == BomValidationMode.DISABLED_FOR_TAGS; + } + + final boolean doTagsMatch = project.getTags().stream() + .map(Tag::getName) + .anyMatch(validationModeTags::contains); + return (validationMode == BomValidationMode.ENABLED_FOR_TAGS && doTagsMatch) + || (validationMode == BomValidationMode.DISABLED_FOR_TAGS && !doTagsMatch); + } + } private static void dispatchBomValidationFailedNotification(final Project project, final String bom, final List<String> errors, final Bom.Format bomFormat) { Notification.dispatch(new Notification() diff --git a/src/main/java/org/dependencytrack/upgrade/v4120/v4120Updater.java b/src/main/java/org/dependencytrack/upgrade/v4120/v4120Updater.java index 3502100f8e..99f97b16f8 100644 --- a/src/main/java/org/dependencytrack/upgrade/v4120/v4120Updater.java +++ b/src/main/java/org/dependencytrack/upgrade/v4120/v4120Updater.java @@ -21,11 +21,15 @@ import alpine.common.logging.Logger; import alpine.persistence.AlpineQueryManager; import alpine.server.upgrade.AbstractUpgradeItem; +import org.dependencytrack.model.BomValidationMode; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE; + public class v4120Updater extends AbstractUpgradeItem { private static final Logger LOGGER = Logger.getLogger(v4120Updater.class); @@ -38,6 +42,7 @@ public String getSchemaVersion() { @Override public void executeUpgrade(final AlpineQueryManager qm, final Connection connection) throws Exception { removeExperimentalBomUploadProcessingV2ConfigProperty(connection); + migrateBomValidationConfigProperty(connection); } private static void removeExperimentalBomUploadProcessingV2ConfigProperty(final Connection connection) throws SQLException { @@ -58,4 +63,84 @@ private static void removeExperimentalBomUploadProcessingV2ConfigProperty(final } } + private static void migrateBomValidationConfigProperty(final Connection connection) throws SQLException { + final boolean shouldReEnableAutoCommit = connection.getAutoCommit(); + connection.setAutoCommit(false); + boolean committed = false; + + final String bomValidationEnabledGroupName = "artifact"; + final String bomValidationEnabledPropertyName = "bom.validation.enabled"; + + LOGGER.info("Migrating ConfigProperty %s:%s to %s:%s" + .formatted(bomValidationEnabledGroupName, bomValidationEnabledPropertyName, + BOM_VALIDATION_MODE.getGroupName(), BOM_VALIDATION_MODE.getPropertyName())); + + try { + LOGGER.debug("Determining current value of ConfigProperty %s:%s" + .formatted(bomValidationEnabledGroupName, bomValidationEnabledPropertyName)); + final String validationEnabledValue; + try (final PreparedStatement ps = connection.prepareStatement(""" + SELECT "PROPERTYVALUE" + FROM "CONFIGPROPERTY" + WHERE "GROUPNAME" = ? + AND "PROPERTYNAME" = ? + """)) { + ps.setString(1, bomValidationEnabledGroupName); + ps.setString(2, bomValidationEnabledPropertyName); + final ResultSet rs = ps.executeQuery(); + if (rs.next()) { + validationEnabledValue = rs.getString(1); + } else { + validationEnabledValue = "true"; + } + } + + final BomValidationMode validationModeValue = "false".equals(validationEnabledValue) + ? BomValidationMode.DISABLED + : BomValidationMode.ENABLED; + + LOGGER.debug("Creating ConfigProperty %s:%s with value %s" + .formatted(BOM_VALIDATION_MODE.getGroupName(), BOM_VALIDATION_MODE.getPropertyName(), validationModeValue)); + try (final PreparedStatement ps = connection.prepareStatement(""" + INSERT INTO "CONFIGPROPERTY" ( + "DESCRIPTION" + , "GROUPNAME" + , "PROPERTYNAME" + , "PROPERTYTYPE" + , "PROPERTYVALUE" + ) VALUES (?, ?, ?, ?, ?) + """)) { + ps.setString(1, BOM_VALIDATION_MODE.getDescription()); + ps.setString(2, BOM_VALIDATION_MODE.getGroupName()); + ps.setString(3, BOM_VALIDATION_MODE.getPropertyName()); + ps.setString(4, BOM_VALIDATION_MODE.getPropertyType().name()); + ps.setString(5, validationModeValue.name()); + ps.executeUpdate(); + } + + LOGGER.debug("Removing ConfigProperty %s:%s".formatted(bomValidationEnabledGroupName, bomValidationEnabledPropertyName)); + try (final PreparedStatement ps = connection.prepareStatement(""" + DELETE + FROM "CONFIGPROPERTY" + WHERE "GROUPNAME" = ? + AND "PROPERTYNAME" = ? + """)) { + ps.setString(1, bomValidationEnabledGroupName); + ps.setString(2, bomValidationEnabledPropertyName); + ps.executeUpdate(); + } + + connection.commit(); + committed = true; + } finally { + if (!committed) { + connection.rollback(); + } + + if (shouldReEnableAutoCommit) { + connection.setAutoCommit(true); + } + } + } + } diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index ecbbccfcd8..48b5015910 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -29,6 +29,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.AnalysisResponse; import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.BomValidationMode; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentProperty; @@ -61,6 +62,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -72,7 +74,9 @@ import static org.apache.commons.io.IOUtils.resourceToString; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE; import static org.hamcrest.CoreMatchers.equalTo; public class BomResourceTest extends ResourceTest { @@ -1018,11 +1022,11 @@ public void uploadBomInvalidJsonTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); qm.createConfigProperty( - BOM_VALIDATION_ENABLED.getGroupName(), - BOM_VALIDATION_ENABLED.getPropertyName(), - "true", - BOM_VALIDATION_ENABLED.getPropertyType(), - null + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.ENABLED.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() ); final var project = new Project(); @@ -1074,11 +1078,11 @@ public void uploadBomInvalidXmlTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); qm.createConfigProperty( - BOM_VALIDATION_ENABLED.getGroupName(), - BOM_VALIDATION_ENABLED.getPropertyName(), - "true", - BOM_VALIDATION_ENABLED.getPropertyType(), - null + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.ENABLED.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() ); final var project = new Project(); @@ -1122,6 +1126,255 @@ public void uploadBomInvalidXmlTest() { """); } + @Test + public void uploadBomWithValidationModeDisabledTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + qm.createConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.DISABLED.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final String encodedBom = Base64.getEncoder().encodeToString(""" + { + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "foo", + "name": "acme-library", + "version": "1.0.0" + } + ] + } + """.getBytes()); + + final Response response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void uploadBomWithValidationModeEnabledForTagsTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + qm.createConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.ENABLED_FOR_TAGS.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() + ); + qm.createConfigProperty( + BOM_VALIDATION_TAGS_INCLUSIVE.getGroupName(), + BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyName(), + "[\"foo\"]", + BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyType(), + BOM_VALIDATION_TAGS_INCLUSIVE.getDescription() + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + qm.bind(project, List.of(qm.createTag("foo"))); + + final String encodedBom = Base64.getEncoder().encodeToString(""" + { + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "foo", + "name": "acme-library", + "version": "1.0.0" + } + ] + } + """.getBytes()); + + Response response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + + qm.bind(project, Collections.emptyList()); + + response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void uploadBomWithValidationModeDisabledForTagsTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + qm.createConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.DISABLED_FOR_TAGS.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() + ); + qm.createConfigProperty( + BOM_VALIDATION_TAGS_EXCLUSIVE.getGroupName(), + BOM_VALIDATION_TAGS_EXCLUSIVE.getPropertyName(), + "[\"foo\"]", + BOM_VALIDATION_TAGS_EXCLUSIVE.getPropertyType(), + BOM_VALIDATION_TAGS_EXCLUSIVE.getDescription() + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + qm.bind(project, List.of(qm.createTag("foo"))); + + final String encodedBom = Base64.getEncoder().encodeToString(""" + { + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "foo", + "name": "acme-library", + "version": "1.0.0" + } + ] + } + """.getBytes()); + + Response response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + + qm.bind(project, Collections.emptyList()); + + response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + } + + @Test + public void uploadBomWithValidationTagsInvalidTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + qm.createConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.ENABLED_FOR_TAGS.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() + ); + qm.createConfigProperty( + BOM_VALIDATION_TAGS_INCLUSIVE.getGroupName(), + BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyName(), + "invalid", + BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyType(), + BOM_VALIDATION_TAGS_INCLUSIVE.getDescription() + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + qm.bind(project, List.of(qm.createTag("foo"))); + + final String encodedBom = Base64.getEncoder().encodeToString(""" + { + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "foo", + "name": "acme-library", + "version": "1.0.0" + } + ] + } + """.getBytes()); + + // With validation mode ENABLED_FOR_TAGS, and invalid tags, + // should fall back to NOT validating. + Response response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + + qm.bind(project, Collections.emptyList()); + + // Removal of the project tag should not make a difference. + response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void uploadBomTooLargeViaPutTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); diff --git a/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java index ed2a63d868..b316c03b79 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java @@ -37,6 +37,9 @@ import jakarta.ws.rs.core.Response; import java.util.Arrays; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + public class ConfigPropertyResourceTest extends ResourceTest { @ClassRule @@ -250,4 +253,134 @@ public void updateConfigPropertyOsvEcosystemTest() { Assert.assertEquals(ConfigPropertyConstants.VULNERABILITY_SOURCE_GOOGLE_OSV_ENABLED.getPropertyName(), json.getString("propertyName")); Assert.assertEquals("maven;npm", json.getString("propertyValue")); } + + @Test + public void updateConfigPropertyBomValidationModeTest() { + qm.createConfigProperty( + ConfigPropertyConstants.BOM_VALIDATION_MODE.getGroupName(), + ConfigPropertyConstants.BOM_VALIDATION_MODE.getPropertyName(), + ConfigPropertyConstants.BOM_VALIDATION_MODE.getDefaultPropertyValue(), + ConfigPropertyConstants.BOM_VALIDATION_MODE.getPropertyType(), + ConfigPropertyConstants.BOM_VALIDATION_MODE.getDescription() + ); + + Response response = jersey.target(V1_CONFIG_PROPERTY).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.mode", + "propertyValue": "ENABLED_FOR_TAGS" + } + """, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.mode", + "propertyValue": "ENABLED_FOR_TAGS", + "propertyType": "STRING", + "description": "${json-unit.any-string}" + } + """); + + response = jersey.target(V1_CONFIG_PROPERTY).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.mode", + "propertyValue": "foo" + } + """, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(getPlainTextBody(response)).isEqualTo("Value must be any of: ENABLED, DISABLED, ENABLED_FOR_TAGS, DISABLED_FOR_TAGS"); + } + + @Test + public void updateConfigPropertyBomValidationTagsExclusiveTest() { + qm.createConfigProperty( + ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE.getGroupName(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE.getPropertyName(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE.getDefaultPropertyValue(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE.getPropertyType(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE.getDescription() + ); + + Response response = jersey.target(V1_CONFIG_PROPERTY).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.tags.exclusive", + "propertyValue": "[\\"foo\\"]" + } + """, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.tags.exclusive", + "propertyValue": "[\\"foo\\"]", + "propertyType": "STRING", + "description": "${json-unit.any-string}" + } + """); + + response = jersey.target(V1_CONFIG_PROPERTY).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.tags.exclusive", + "propertyValue": "foo" + } + """, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(getPlainTextBody(response)).isEqualTo("Value must be a valid JSON array of strings"); + } + + @Test + public void updateConfigPropertyBomValidationTagsInclusiveTest() { + qm.createConfigProperty( + ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE.getGroupName(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyName(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE.getDefaultPropertyValue(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyType(), + ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE.getDescription() + ); + + Response response = jersey.target(V1_CONFIG_PROPERTY).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.tags.inclusive", + "propertyValue": "[\\"foo\\"]" + } + """, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.tags.inclusive", + "propertyValue": "[\\"foo\\"]", + "propertyType": "STRING", + "description": "${json-unit.any-string}" + } + """); + + response = jersey.target(V1_CONFIG_PROPERTY).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(/* language=JSON */ """ + { + "groupName": "artifact", + "propertyName": "bom.validation.tags.inclusive", + "propertyValue": "foo" + } + """, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(getPlainTextBody(response)).isEqualTo("Value must be a valid JSON array of strings"); + } + } diff --git a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index fbe8eead8a..c34c62b82d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -26,6 +26,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.AnalysisResponse; import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.BomValidationMode; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; import org.dependencytrack.model.Project; @@ -43,11 +44,15 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.Base64; +import java.util.Collections; +import java.util.List; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE; import static org.hamcrest.CoreMatchers.equalTo; public class VexResourceTest extends ResourceTest { @@ -438,11 +443,11 @@ public void uploadVexInvalidJsonTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); qm.createConfigProperty( - BOM_VALIDATION_ENABLED.getGroupName(), - BOM_VALIDATION_ENABLED.getPropertyName(), - "true", - BOM_VALIDATION_ENABLED.getPropertyType(), - null + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.ENABLED.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() ); final var project = new Project(); @@ -494,11 +499,11 @@ public void uploadVexInvalidXmlTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); qm.createConfigProperty( - BOM_VALIDATION_ENABLED.getGroupName(), - BOM_VALIDATION_ENABLED.getPropertyName(), - "true", - BOM_VALIDATION_ENABLED.getPropertyType(), - null + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.ENABLED.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() ); final var project = new Project(); @@ -542,6 +547,185 @@ public void uploadVexInvalidXmlTest() { """); } + @Test + public void uploadVexWithValidationModeDisabledTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + qm.createConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.DISABLED.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final String encodedBom = Base64.getEncoder().encodeToString(""" + { + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "foo", + "name": "acme-library", + "version": "1.0.0" + } + ] + } + """.getBytes()); + + final Response response = jersey.target(V1_VEX).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "vex": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void uploadVexWithValidationModeEnabledForTagsTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + qm.createConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.ENABLED_FOR_TAGS.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() + ); + qm.createConfigProperty( + BOM_VALIDATION_TAGS_INCLUSIVE.getGroupName(), + BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyName(), + "[\"foo\"]", + BOM_VALIDATION_TAGS_INCLUSIVE.getPropertyType(), + BOM_VALIDATION_TAGS_INCLUSIVE.getDescription() + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + qm.bind(project, List.of(qm.createTag("foo"))); + + final String encodedBom = Base64.getEncoder().encodeToString(""" + { + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "foo", + "name": "acme-library", + "version": "1.0.0" + } + ] + } + """.getBytes()); + + Response response = jersey.target(V1_VEX).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "vex": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + + qm.bind(project, Collections.emptyList()); + + response = jersey.target(V1_VEX).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "vex": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void uploadVexWithValidationModeDisabledForTagsTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + qm.createConfigProperty( + BOM_VALIDATION_MODE.getGroupName(), + BOM_VALIDATION_MODE.getPropertyName(), + BomValidationMode.DISABLED_FOR_TAGS.name(), + BOM_VALIDATION_MODE.getPropertyType(), + BOM_VALIDATION_MODE.getDescription() + ); + qm.createConfigProperty( + BOM_VALIDATION_TAGS_EXCLUSIVE.getGroupName(), + BOM_VALIDATION_TAGS_EXCLUSIVE.getPropertyName(), + "[\"foo\"]", + BOM_VALIDATION_TAGS_EXCLUSIVE.getPropertyType(), + BOM_VALIDATION_TAGS_EXCLUSIVE.getDescription() + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + qm.bind(project, List.of(qm.createTag("foo"))); + + final String encodedBom = Base64.getEncoder().encodeToString(""" + { + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "foo", + "name": "acme-library", + "version": "1.0.0" + } + ] + } + """.getBytes()); + + Response response = jersey.target(V1_VEX).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "vex": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(200); + + qm.bind(project, Collections.emptyList()); + + response = jersey.target(V1_VEX).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "vex": "%s" + } + """.formatted(encodedBom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + } + @Test public void uploadVexTooLargeViaPutTest() { final var project = new Project(); diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 9bb3896501..03d128be9f 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -25,8 +25,6 @@ import alpine.notification.NotificationLevel; import alpine.notification.NotificationService; import alpine.notification.Subscription; -import java.util.Arrays; - import org.awaitility.core.ConditionTimeoutException; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.event.BomUploadEvent; @@ -58,6 +56,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -70,7 +69,6 @@ import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; import static org.dependencytrack.assertion.Assertions.assertConditionWithTimeout; -import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; public class BomUploadProcessingTaskTest extends PersistenceCapableTest { @@ -293,14 +291,6 @@ public void informWithEmptyBomTest() throws Exception { @Test public void informWithInvalidCycloneDxBomTest() throws Exception { - qm.createConfigProperty( - BOM_VALIDATION_ENABLED.getGroupName(), - BOM_VALIDATION_ENABLED.getPropertyName(), - "true", - BOM_VALIDATION_ENABLED.getPropertyType(), - null - ); - final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); final byte[] bomBytes = """ From ea1bf331afca25a7f2927887f12caea497dc2ded Mon Sep 17 00:00:00 2001 From: nscuro <nscuro@protonmail.com> Date: Sun, 1 Sep 2024 18:51:00 +0200 Subject: [PATCH 2/2] Exclude `upgrade` package from test coverage Signed-off-by: nscuro <nscuro@protonmail.com> --- pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pom.xml b/pom.xml index 5ba49c6618..cf0b79cf18 100644 --- a/pom.xml +++ b/pom.xml @@ -580,6 +580,15 @@ </dependency> </dependencies> </plugin> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <configuration> + <excludes> + <exclude>org/dependencytrack/upgrade/**/*</exclude> + </excludes> + </configuration> + </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId>