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>