Skip to content

Commit

Permalink
Support inclusion/exclusion of projects from BOM validation with tags
Browse files Browse the repository at this point in the history
Closes #3891

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Sep 1, 2024
1 parent bd17623 commit ebb6ce5
Show file tree
Hide file tree
Showing 9 changed files with 748 additions and 43 deletions.
34 changes: 34 additions & 0 deletions src/main/java/org/dependencytrack/model/BomValidationMode.java
Original file line number Diff line number Diff line change
@@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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, ""),
BOM_VALIDATION_TAGS_INCLUSIVE("artifact", "bom.validation.tags.inclusive", null, PropertyType.STRING, ""),
BOM_VALIDATION_TAGS_EXCLUSIVE("artifact", "bom.validation.tags.exclusive", null, PropertyType.STRING, "");

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand Down
70 changes: 66 additions & 4 deletions src/main/java/org/dependencytrack/resources/v1/BomResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -553,6 +562,59 @@ 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()
);

final var validationMode = BomValidationMode.valueOf(validationModeProperty.getPropertyValue());
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()
);
if (tagsProperty == null || tagsProperty.getPropertyValue() == null) {
LOGGER.warn("%s:%s is configured as %s, but %s:%s does not contain any tags"
.formatted(BOM_VALIDATION_MODE.getGroupName(), BOM_VALIDATION_MODE.getPropertyName(),
validationMode, tagsPropertyConstant.getGroupName(), tagsPropertyConstant.getPropertyName()));
return validationMode == BomValidationMode.DISABLED_FOR_TAGS;
}

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()
Expand Down
85 changes: 85 additions & 0 deletions src/main/java/org/dependencytrack/upgrade/v4120/v4120Updater.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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);
}
}
}

}
Loading

0 comments on commit ebb6ce5

Please sign in to comment.