Skip to content

Commit

Permalink
Merge pull request #718 from leec94/risk-score
Browse files Browse the repository at this point in the history
Customize risk score calculation
  • Loading branch information
nscuro authored Jun 15, 2024
2 parents a9abb9f + 81e82fc commit bc0a8be
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,13 @@ public enum ConfigPropertyConstants {
TASK_SCHEDULER_INTERNAL_COMPONENT_IDENTIFICATION_CADENCE("task-scheduler", "internal.components.identification.cadence", "6", PropertyType.INTEGER, "Internal component identification cadence (in hours)", ConfigPropertyAccessMode.READ_WRITE),
SEARCH_INDEXES_CONSISTENCY_CHECK_ENABLED("search-indexes", "consistency.check.enabled", "true", PropertyType.BOOLEAN, "Flag to enable lucene indexes periodic consistency check", ConfigPropertyAccessMode.READ_WRITE),
SEARCH_INDEXES_CONSISTENCY_CHECK_CADENCE("search-indexes", "consistency.check.cadence", "4320", PropertyType.INTEGER, "Lucene indexes consistency check cadence (in minutes)", ConfigPropertyAccessMode.READ_WRITE),
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", ConfigPropertyAccessMode.READ_WRITE);
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", ConfigPropertyAccessMode.READ_WRITE),
CUSTOM_RISK_SCORE_HISTORY_ENABLED("risk-score", "weight.history.enabled", "true", PropertyType.BOOLEAN, "Flag to re-calculate risk score history", ConfigPropertyAccessMode.READ_WRITE),
CUSTOM_RISK_SCORE_CRITICAL("risk-score", "weight.critical", "10", PropertyType.INTEGER, "Critical severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE),
CUSTOM_RISK_SCORE_HIGH("risk-score", "weight.high", "5", PropertyType.INTEGER, "High severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE),
CUSTOM_RISK_SCORE_MEDIUM("risk-score", "weight.medium", "3", PropertyType.INTEGER, "Medium severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE),
CUSTOM_RISK_SCORE_LOW("risk-score", "weight.low", "1", PropertyType.INTEGER, "Low severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE),
CUSTOM_RISK_SCORE_UNASSIGNED("risk-score", "weight.unassigned", "5", PropertyType.INTEGER, "Unassigned severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE);

private final String groupName;
private final String propertyName;
Expand Down Expand Up @@ -147,5 +153,5 @@ public String getDescription() {
public ConfigPropertyAccessMode getAccessMode() {
return accessMode;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.dependencytrack.model.ConfigPropertyAccessMode;
import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.persistence.QueryManager;
import org.owasp.security.logging.SecurityMarkers;

import javax.ws.rs.core.Response;
import java.math.BigDecimal;
Expand Down Expand Up @@ -65,6 +66,10 @@ private Response updatePropertyValueInternal(IConfigProperty json, IConfigProper
}

if (property.getPropertyType() == IConfigProperty.PropertyType.BOOLEAN) {
boolean propertyValue = BooleanUtil.valueOf(json.getPropertyValue());
if (ConfigPropertyConstants.CUSTOM_RISK_SCORE_HISTORY_ENABLED.getPropertyName().equals(json.getPropertyName())){
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Attribute \"" + json.getPropertyName() + "\" was changed to value: " + String.valueOf(propertyValue) + " by user " + super.getPrincipal().getName());
}
property.setPropertyValue(String.valueOf(BooleanUtil.valueOf(json.getPropertyValue())));
} else if (property.getPropertyType() == IConfigProperty.PropertyType.INTEGER) {
try {
Expand All @@ -75,6 +80,18 @@ private Response updatePropertyValueInternal(IConfigProperty json, IConfigProper
if(ConfigPropertyConstants.SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD.getPropertyName().equals(json.getPropertyName()) && (propertyValue < 1 || propertyValue > 100)) {
return Response.status(Response.Status.BAD_REQUEST).entity("Lucene index delta threshold ("+json.getPropertyName()+") cannot be inferior to 1 or superior to 100.A value of "+propertyValue+" was provided.").build();
}

if (ConfigPropertyConstants.CUSTOM_RISK_SCORE_CRITICAL.getPropertyName().equals(json.getPropertyName()) ||
ConfigPropertyConstants.CUSTOM_RISK_SCORE_HIGH.getPropertyName().equals(json.getPropertyName()) ||
ConfigPropertyConstants.CUSTOM_RISK_SCORE_MEDIUM.getPropertyName().equals(json.getPropertyName()) ||
ConfigPropertyConstants.CUSTOM_RISK_SCORE_LOW.getPropertyName().equals(json.getPropertyName()) ||
ConfigPropertyConstants.CUSTOM_RISK_SCORE_UNASSIGNED.getPropertyName().equals(json.getPropertyName())
){
if (propertyValue < 1 || propertyValue > 10){
return Response.status(Response.Status.BAD_REQUEST).entity("Risk score \""+json.getPropertyName()+"\" must be between 1 and 10. An invalid value of " + propertyValue + " was provided.").build();
}
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Risk score \"" + json.getPropertyName() + "\" changed to value: " + propertyValue + " by user " + super.getPrincipal().getName());
}
property.setPropertyValue(String.valueOf(propertyValue));
} catch (NumberFormatException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("The property expected an integer and an integer was not sent.").build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
-- The behavior of this function is identical to Metrics#inheritedRiskScore
-- in the API server Java code base.
-- https://github.com/DependencyTrack/dependency-track/blob/1976be1f5cc9d027900f09aed9d1539595aeda3a/src/main/java/org/dependencytrack/metrics/Metrics.java#L31-L33

CREATE OR REPLACE FUNCTION "CALC_RISK_SCORE"(
"critical" INT,
"high" INT,
Expand All @@ -15,5 +16,18 @@ CREATE OR REPLACE FUNCTION "CALC_RISK_SCORE"(
IMMUTABLE
AS
$$
SELECT (("critical" * 10) + ("high" * 5) + ("medium" * 3) + ("low" * 1) + ("unassigned" * 5))::NUMERIC;
WITH "CUSTOM_SCORES" AS (
SELECT "PROPERTYVALUE"::INT AS "value"
, "PROPERTYNAME" AS "name"
FROM "CONFIGPROPERTY"
WHERE "GROUPNAME" = 'risk-score'
AND "PROPERTYTYPE" = 'INTEGER'
)
SELECT (
("critical" * (SELECT "value" FROM "CUSTOM_SCORES" WHERE "name" = 'weight.critical'))
+ ("high" * (SELECT "value" FROM "CUSTOM_SCORES" WHERE "name" = 'weight.high'))
+ ("medium" * (SELECT "value" FROM "CUSTOM_SCORES" WHERE "name" = 'weight.medium'))
+ ("low" * (SELECT "value" FROM "CUSTOM_SCORES" WHERE "name" = 'weight.low'))
+ ("unassigned" * (SELECT "value" FROM "CUSTOM_SCORES" WHERE "name" = 'weight.unassigned'))
)::NUMERIC;
$$;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import alpine.event.framework.Event;
import alpine.event.framework.EventService;
import alpine.event.framework.Subscriber;

import org.dependencytrack.event.ComponentMetricsUpdateEvent;
import org.dependencytrack.event.ComponentPolicyEvaluationEvent;
import org.dependencytrack.event.ProjectMetricsUpdateEvent;
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/dependencytrack/metrics/MetricsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ public void testMetricCalculations() {
double ratio = Metrics.vulnerableComponentRatio(5, 100);
Assert.assertEquals(0.05, ratio, 0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@

import static org.assertj.core.api.Assertions.assertThat;

import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_CRITICAL;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_HIGH;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_MEDIUM;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_LOW;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_UNASSIGNED;

public class ConfigPropertyResourceTest extends ResourceTest {

@ClassRule
Expand Down Expand Up @@ -234,6 +240,116 @@ public void updateConfigPropertyReadOnlyTest() {
assertThat(getPlainTextBody(response)).isEqualTo("The property internal.cluster.id can not be modified");
}

@Test
public void testRiskScoreInvalid(){
qm.createConfigProperty(
CUSTOM_RISK_SCORE_CRITICAL.getGroupName(),
CUSTOM_RISK_SCORE_CRITICAL.getPropertyName(),
CUSTOM_RISK_SCORE_CRITICAL.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_CRITICAL.getPropertyType(),
CUSTOM_RISK_SCORE_CRITICAL.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_HIGH.getGroupName(),
CUSTOM_RISK_SCORE_HIGH.getPropertyName(),
CUSTOM_RISK_SCORE_HIGH.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_HIGH.getPropertyType(),
CUSTOM_RISK_SCORE_HIGH.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_MEDIUM.getGroupName(),
CUSTOM_RISK_SCORE_MEDIUM.getPropertyName(),
CUSTOM_RISK_SCORE_MEDIUM.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_MEDIUM.getPropertyType(),
CUSTOM_RISK_SCORE_MEDIUM.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_LOW.getGroupName(),
CUSTOM_RISK_SCORE_LOW.getPropertyName(),
CUSTOM_RISK_SCORE_LOW.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_LOW.getPropertyType(),
CUSTOM_RISK_SCORE_LOW.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_UNASSIGNED.getGroupName(),
CUSTOM_RISK_SCORE_UNASSIGNED.getPropertyName(),
CUSTOM_RISK_SCORE_UNASSIGNED.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_UNASSIGNED.getPropertyType(),
CUSTOM_RISK_SCORE_UNASSIGNED.getDescription()
);

final Response response = jersey.target(V1_CONFIG_PROPERTY).request()
.header(X_API_KEY, apiKey)
.post(Entity.entity("""
{
"groupName": "risk-score",
"propertyName": "weight.critical",
"propertyValue": "11"
}
""", MediaType.APPLICATION_JSON));

assertThat(response.getStatus()).isEqualTo(400);
assertThat(getPlainTextBody(response)).isEqualTo("Risk score \"weight.critical\" must be between 1 and 10. An invalid value of 11 was provided.");
}

@Test
public void testRiskScoreUpdate(){
qm.createConfigProperty(
CUSTOM_RISK_SCORE_CRITICAL.getGroupName(),
CUSTOM_RISK_SCORE_CRITICAL.getPropertyName(),
CUSTOM_RISK_SCORE_CRITICAL.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_CRITICAL.getPropertyType(),
CUSTOM_RISK_SCORE_CRITICAL.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_HIGH.getGroupName(),
CUSTOM_RISK_SCORE_HIGH.getPropertyName(),
CUSTOM_RISK_SCORE_HIGH.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_HIGH.getPropertyType(),
CUSTOM_RISK_SCORE_HIGH.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_MEDIUM.getGroupName(),
CUSTOM_RISK_SCORE_MEDIUM.getPropertyName(),
CUSTOM_RISK_SCORE_MEDIUM.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_MEDIUM.getPropertyType(),
CUSTOM_RISK_SCORE_MEDIUM.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_LOW.getGroupName(),
CUSTOM_RISK_SCORE_LOW.getPropertyName(),
CUSTOM_RISK_SCORE_LOW.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_LOW.getPropertyType(),
CUSTOM_RISK_SCORE_LOW.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_UNASSIGNED.getGroupName(),
CUSTOM_RISK_SCORE_UNASSIGNED.getPropertyName(),
CUSTOM_RISK_SCORE_UNASSIGNED.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_UNASSIGNED.getPropertyType(),
CUSTOM_RISK_SCORE_UNASSIGNED.getDescription()
);

final Response response = jersey.target(V1_CONFIG_PROPERTY).request()
.header(X_API_KEY, apiKey)
.post(Entity.entity("""
{
"groupName": "risk-score",
"propertyName": "weight.critical",
"propertyValue": "8"
}
""", MediaType.APPLICATION_JSON));

assertThat(response.getStatus()).isEqualTo(200);
JsonObject json = parseJsonObject(response);
Assert.assertNotNull(json);
Assert.assertEquals("risk-score", json.getString("groupName"));
Assert.assertEquals("weight.critical", json.getString("propertyName"));
Assert.assertEquals("8", json.getString("propertyValue"));
Assert.assertEquals("INTEGER", json.getString("propertyType"));
Assert.assertEquals("Critical severity vulnerability weight (between 1-10)", json.getString("description"));
}

@Test
public void updateConfigPropertiesAggregateTest() {
ConfigProperty prop1 = qm.createConfigProperty("my.group", "my.string1", "ABC", IConfigProperty.PropertyType.STRING, "A string");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
import java.util.Date;
import java.util.UUID;

import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_CRITICAL;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_HIGH;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_MEDIUM;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_LOW;
import static org.dependencytrack.model.ConfigPropertyConstants.CUSTOM_RISK_SCORE_UNASSIGNED;

abstract class AbstractMetricsUpdateTaskTest extends PersistenceCapableTest {

@Rule
Expand Down Expand Up @@ -62,4 +68,42 @@ protected PolicyViolation createPolicyViolation(final Component component, final
return qm.persist(policyViolation);
}

public void createTestConfigProperties(){
qm.createConfigProperty(
CUSTOM_RISK_SCORE_CRITICAL.getGroupName(),
CUSTOM_RISK_SCORE_CRITICAL.getPropertyName(),
CUSTOM_RISK_SCORE_CRITICAL.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_CRITICAL.getPropertyType(),
CUSTOM_RISK_SCORE_CRITICAL.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_HIGH.getGroupName(),
CUSTOM_RISK_SCORE_HIGH.getPropertyName(),
CUSTOM_RISK_SCORE_HIGH.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_HIGH.getPropertyType(),
CUSTOM_RISK_SCORE_HIGH.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_MEDIUM.getGroupName(),
CUSTOM_RISK_SCORE_MEDIUM.getPropertyName(),
CUSTOM_RISK_SCORE_MEDIUM.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_MEDIUM.getPropertyType(),
CUSTOM_RISK_SCORE_MEDIUM.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_LOW.getGroupName(),
CUSTOM_RISK_SCORE_LOW.getPropertyName(),
CUSTOM_RISK_SCORE_LOW.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_LOW.getPropertyType(),
CUSTOM_RISK_SCORE_LOW.getDescription()
);
qm.createConfigProperty(
CUSTOM_RISK_SCORE_UNASSIGNED.getGroupName(),
CUSTOM_RISK_SCORE_UNASSIGNED.getPropertyName(),
CUSTOM_RISK_SCORE_UNASSIGNED.getDefaultPropertyValue(),
CUSTOM_RISK_SCORE_UNASSIGNED.getPropertyType(),
CUSTOM_RISK_SCORE_UNASSIGNED.getDescription()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@
import static org.dependencytrack.model.WorkflowStatus.FAILED;
import static org.dependencytrack.model.WorkflowStep.METRICS_UPDATE;


public class ComponentMetricsUpdateTaskTest extends AbstractMetricsUpdateTaskTest {

@Test
public void testUpdateCMetricsEmpty() {
var project = new Project();
project.setName("acme-app");
project = qm.createProject(project, List.of(), false);

// Create risk score configproperties
createTestConfigProperties();

var component = new Component();
component.setProject(project);
Expand Down Expand Up @@ -112,6 +116,9 @@ public void testUpdateMetricsUnchanged() {
project.setName("acme-app");
project = qm.createProject(project, List.of(), false);

// Create risk score configproperties
createTestConfigProperties();

var component = new Component();
component.setProject(project);
component.setName("acme-lib");
Expand All @@ -138,6 +145,9 @@ public void testUpdateMetricsVulnerabilities() {
project.setName("acme-app");
project = qm.createProject(project, List.of(), false);

// Create risk score configproperties
createTestConfigProperties();

var component = new Component();
component.setProject(project);
component.setName("acme-lib");
Expand Down Expand Up @@ -219,6 +229,9 @@ public void testUpdateMetricsVulnerabilitiesWhenSeverityIsOverridden() {
project.setName("acme-app");
project = qm.createProject(project, List.of(), false);

// Create risk score configproperties
createTestConfigProperties();

var component = new Component();
component.setProject(project);
component.setName("acme-lib");
Expand Down Expand Up @@ -304,6 +317,9 @@ public void testUpdateMetricsPolicyViolations() {
project.setName("acme-app");
project = qm.createProject(project, List.of(), false);

// Create risk score configproperties
createTestConfigProperties();

var component = new Component();
component.setProject(project);
component.setName("acme-lib");
Expand Down Expand Up @@ -359,6 +375,9 @@ public void testUpdateMetricsWithDuplicateAliases() {
project.setName("acme-app");
project = qm.createProject(project, List.of(), false);

// Create risk score configproperties
createTestConfigProperties();

var component = new Component();
component.setProject(project);
component.setName("acme-lib");
Expand Down
Loading

0 comments on commit bc0a8be

Please sign in to comment.