Skip to content

Commit

Permalink
Store computed severities in the database (#706)
Browse files Browse the repository at this point in the history
  • Loading branch information
nscuro authored Jun 13, 2024
1 parent df57c53 commit 47105cd
Show file tree
Hide file tree
Showing 19 changed files with 327 additions and 154 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,6 @@ private Vulnerability syncVulnerability(final QueryManager qm, final Vulnerabili
differ.applyIfChanged("published", Vulnerability::getPublished, existingVuln::setPublished);
differ.applyIfChanged("updated", Vulnerability::getUpdated, existingVuln::setUpdated);
differ.applyIfChanged("cwes", Vulnerability::getCwes, existingVuln::setCwes);
// Calling setSeverity nulls all CVSS and OWASP RR fields. getSeverity calculates the severity on-the-fly,
// and will return UNASSIGNED even when no severity is set explicitly. Thus, calling setSeverity
// must happen before CVSS and OWASP RR fields are set, to avoid null-ing them again.
differ.applyIfChanged("severity", Vulnerability::getSeverity, existingVuln::setSeverity);
differ.applyIfChanged("cvssV2BaseScore", Vulnerability::getCvssV2BaseScore, existingVuln::setCvssV2BaseScore);
differ.applyIfChanged("cvssV2ImpactSubScore", Vulnerability::getCvssV2ImpactSubScore, existingVuln::setCvssV2ImpactSubScore);
Expand Down
28 changes: 1 addition & 27 deletions src/main/java/org/dependencytrack/model/Vulnerability.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.dependencytrack.resources.v1.serializers.CweSerializer;
import org.dependencytrack.resources.v1.serializers.Iso8601DateSerializer;
import org.dependencytrack.resources.v1.vo.AffectedComponent;
import org.dependencytrack.util.VulnerabilityUtil;

import javax.jdo.annotations.Column;
import javax.jdo.annotations.Convert;
Expand Down Expand Up @@ -349,36 +348,11 @@ public void setId(long id) {
this.id = id;
}

/**
* Returns the value of the severity field (if specified), otherwise, will
* return the severity based on the numerical CVSS or OWASP score.
*
* This method properly accounts for vulnerabilities that may have a subset or all of (CVSSv2, CVSSv3, OWASP RR)
* score. The highest severity is returned.
* @return the severity of the vulnerability
* @see VulnerabilityUtil#getSeverity(BigDecimal, BigDecimal, BigDecimal, BigDecimal, BigDecimal)
*/
public Severity getSeverity() {
return (this.severity != null) ? severity : VulnerabilityUtil.getSeverity(cvssV2BaseScore, cvssV3BaseScore, owaspRRLikelihoodScore, owaspRRTechnicalImpactScore, owaspRRBusinessImpactScore);
return severity;
}

/**
* Sets the severity. This should only be set if CVSSv2, CVSSv3 or OWASP RR scores
* are not used.
* @param severity the severity of the vulnerability
*/
public void setSeverity(Severity severity) {
cvssV2BaseScore = null;
cvssV2ImpactSubScore = null;
cvssV2ExploitabilitySubScore = null;
cvssV2Vector = null;
cvssV3BaseScore = null;
cvssV3ImpactSubScore = null;
cvssV3ExploitabilitySubScore = null;
cvssV3Vector = null;
owaspRRLikelihoodScore = null;
owaspRRBusinessImpactScore = null;
owaspRRTechnicalImpactScore = null;
this.severity = severity;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.dependencytrack.parser.common.resolver.CweResolver;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.proto.vulnanalysis.v1.Scanner;
import org.dependencytrack.util.VulnerabilityUtil;
import us.springett.cvss.Cvss;
import us.springett.cvss.Score;
import us.springett.owasp.riskrating.MissingFactorException;
Expand Down Expand Up @@ -170,6 +171,14 @@ public static Vulnerability convert(final QueryManager qm, final Bom bom,
}
}
}
vuln.setSeverity(VulnerabilityUtil.getSeverity(
vuln.getSeverity(),
vuln.getCvssV2BaseScore(),
vuln.getCvssV3BaseScore(),
vuln.getOwaspRRLikelihoodScore(),
vuln.getOwaspRRTechnicalImpactScore(),
vuln.getOwaspRRBusinessImpactScore()
));

// There can be cases where ratings do not have a known method, and the source only assigned
// a severity. Such ratings are inferior to those with proper method and vector, but we'll use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,7 @@ public interface NotificationSubjectDao extends SqlObject {
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."OWASPVECTOR"
ELSE "V"."OWASPRRVECTOR"
END AS "vulnOwaspRrVector",
"CALC_SEVERITY"(
"V"."SEVERITY",
"A"."SEVERITY",
"V"."CVSSV3BASESCORE",
"V"."CVSSV2BASESCORE"
) AS "vulnSeverity",
COALESCE("A"."SEVERITY", "V"."SEVERITY") AS "vulnSeverity",
STRING_TO_ARRAY("V"."CWES", ',') AS "vulnCwes",
"vulnAliasesJson",
:vulnAnalysisLevel AS "vulnAnalysisLevel",
Expand Down Expand Up @@ -249,12 +244,7 @@ List<NewVulnerabilitySubject> getForNewVulnerabilities(final UUID componentUuid,
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."OWASPVECTOR"
ELSE "V"."OWASPRRVECTOR"
END AS "vulnOwaspRrVector",
"CALC_SEVERITY"(
"V"."SEVERITY",
"A"."SEVERITY",
"V"."CVSSV3BASESCORE",
"V"."CVSSV2BASESCORE"
) AS "vulnSeverity",
COALESCE("A"."SEVERITY", "V"."SEVERITY") AS "vulnSeverity",
STRING_TO_ARRAY("V"."CWES", ',') AS "vulnCwes",
"vulnAliasesJson"
FROM
Expand Down Expand Up @@ -364,12 +354,7 @@ LEFT JOIN LATERAL (
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."OWASPVECTOR"
ELSE "V"."OWASPRRVECTOR"
END AS "vulnOwaspRrVector",
"CALC_SEVERITY"(
"V"."SEVERITY",
"A"."SEVERITY",
"V"."CVSSV3BASESCORE",
"V"."CVSSV2BASESCORE"
) AS "vulnSeverity",
COALESCE("A"."SEVERITY", "V"."SEVERITY") AS "vulnSeverity",
STRING_TO_ARRAY("V"."CWES", ',') AS "vulnCwes",
"vulnAliasesJson",
:isSuppressed AS "isVulnAnalysisSuppressed",
Expand Down Expand Up @@ -519,7 +504,7 @@ default Optional<ProjectVulnAnalysisCompleteSubject> getForProjectVulnAnalysisCo
THEN "A"."OWASPVECTOR"
ELSE "V"."OWASPRRVECTOR"
END AS "vulnOwaspRrVector"
, "CALC_SEVERITY"("V"."SEVERITY", "A"."SEVERITY", "V"."CVSSV3BASESCORE", "V"."CVSSV2BASESCORE") AS "vulnSeverity"
, COALESCE("A"."SEVERITY", "V"."SEVERITY") AS "vulnSeverity"
, STRING_TO_ARRAY("V"."CWES", ',') AS "vulnCwes"
, "vulnAliasesJson"
FROM "COMPONENT" AS "C"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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.persistence.migration.change.v550;

import liquibase.change.custom.CustomTaskChange;
import liquibase.database.Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.CustomChangeException;
import liquibase.exception.DatabaseException;
import liquibase.exception.SetupException;
import liquibase.exception.ValidationErrors;
import liquibase.resource.ResourceAccessor;
import org.dependencytrack.model.Severity;
import org.dependencytrack.util.VulnerabilityUtil;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class ComputeSeveritiesChange implements CustomTaskChange {

private int batchSize;
private int numBatches;
private int numUpdates;

@Override
public void setUp() throws SetupException {
}

@Override
public void execute(final Database database) throws CustomChangeException {
final var connection = (JdbcConnection) database.getConnection();

// NB: When generating the schema via `mvn liquibase:updateSQL`, none of the changesets
// is actually applied. If we don't perform a preliminary check here, schema generation fails.
try (final PreparedStatement ps = connection.prepareStatement("""
SELECT 1
FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name = 'VULNERABILITY'
""")) {
if (!ps.executeQuery().next()) {
// Probably running within `mvn liquibase:updateSQL`.
return;
}
} catch (DatabaseException | SQLException e) {
throw new CustomChangeException("Failed to check for databasechangelog table", e);
}

try (final PreparedStatement selectStatement = connection.prepareStatement("""
SELECT "CVSSV2BASESCORE"
, "CVSSV3BASESCORE"
, "OWASPRRLIKELIHOODSCORE"
, "OWASPRRTECHNICALIMPACTSCORE"
, "OWASPRRBUSINESSIMPACTSCORE"
, "VULNID"
FROM "VULNERABILITY"
WHERE "SEVERITY" IS NULL
""");
final PreparedStatement updateStatement = connection.prepareStatement("""
UPDATE "VULNERABILITY" SET "SEVERITY" = ? WHERE "VULNID" = ?
""")) {
final ResultSet rs = selectStatement.executeQuery();
while (rs.next()) {
final String vulnId = rs.getString(6);
final Severity severity = VulnerabilityUtil.getSeverity(
rs.getBigDecimal(1),
rs.getBigDecimal(2),
rs.getBigDecimal(3),
rs.getBigDecimal(4),
rs.getBigDecimal(5)
);

updateStatement.setString(1, severity.name());
updateStatement.setString(2, vulnId);
updateStatement.addBatch();
if (++batchSize == 500) {
updateStatement.executeBatch();
numUpdates += batchSize;
numBatches++;
batchSize = 0;
}
}

if (batchSize > 0) {
updateStatement.executeBatch();
numUpdates += batchSize;
numBatches++;
}
} catch (DatabaseException | SQLException e) {
throw new CustomChangeException("Failed to update severities", e);
}
}

@Override
public String getConfirmationMessage() {
return "Updated %d vulnerabilities in %d batches".formatted(numUpdates, numBatches);
}

@Override
public void setFileOpener(final ResourceAccessor resourceAccessor) {
}

@Override
public ValidationErrors validate(final Database database) {
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ public Response createVulnerability(Vulnerability jsonVulnerability) {
}
}
}
recalculateScoresFromVector(jsonVulnerability);
recalculateScoresAndSeverityFromVectors(jsonVulnerability);
jsonVulnerability.setSource(Vulnerability.Source.INTERNAL);
vulnerability = qm.createVulnerability(jsonVulnerability, true);
qm.persist(vsList);
Expand Down Expand Up @@ -391,7 +391,7 @@ public Response updateVulnerability(Vulnerability jsonVuln) {
final List<Tag> resolvedTags = qm.resolveTags(jsonVuln.getTags());
qm.bind(vulnerability, resolvedTags);

recalculateScoresFromVector(jsonVuln);
recalculateScoresAndSeverityFromVectors(jsonVuln);
vulnerability = qm.updateVulnerability(jsonVuln, true);
qm.persist(vsList);
vsList = qm.reconcileVulnerableSoftware(vulnerability, vsListOld, vsList, Vulnerability.Source.INTERNAL);
Expand Down Expand Up @@ -460,7 +460,7 @@ public Response generateInternalVulnerabilityIdentifier() {
return Response.ok(vulnId).build();
}

public void recalculateScoresFromVector(Vulnerability vuln) throws MissingFactorException {
public void recalculateScoresAndSeverityFromVectors(Vulnerability vuln) throws MissingFactorException {
// Recalculate V2 score based on vector passed to resource and normalize vector
final Cvss v2 = Cvss.fromVector(vuln.getCvssV2Vector());
if (v2 != null) {
Expand Down Expand Up @@ -489,6 +489,15 @@ public void recalculateScoresFromVector(Vulnerability vuln) throws MissingFactor
vuln.setOwaspRRTechnicalImpactScore(BigDecimal.valueOf(score.getTechnicalImpactScore()));
vuln.setOwaspRRBusinessImpactScore(BigDecimal.valueOf(score.getBusinessImpactScore()));
}

vuln.setSeverity(VulnerabilityUtil.getSeverity(
vuln.getSeverity(),
vuln.getCvssV2BaseScore(),
vuln.getCvssV3BaseScore(),
vuln.getOwaspRRLikelihoodScore(),
vuln.getOwaspRRTechnicalImpactScore(),
vuln.getOwaspRRBusinessImpactScore()
));
}

@POST
Expand Down
9 changes: 0 additions & 9 deletions src/main/resources/migration/changelog-procedures.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd
http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="function_cvssv2-to-severity" author="nscuro@protonmail.com" runOnChange="true">
<createProcedure path="procedures/function_cvssv2-to-severity.sql" relativeToChangelogFile="true"/>
</changeSet>
<changeSet id="function_cvssv3-to-severity" author="nscuro@protonmail.com" runOnChange="true">
<createProcedure path="procedures/function_cvssv3-to-severity.sql" relativeToChangelogFile="true"/>
</changeSet>
<changeSet id="function_calc-severity" author="nscuro@protonmail.com" runOnChange="true">
<createProcedure path="procedures/function_calc-severity.sql" relativeToChangelogFile="true"/>
</changeSet>
<changeSet id="function_calc-risk-score" author="nscuro@protonmail.com" runOnChange="true">
<createProcedure path="procedures/function_calc-risk-score.sql" relativeToChangelogFile="true"/>
</changeSet>
Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/migration/changelog-v5.5.0.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,13 @@
CHECK ("PROPERTYTYPE" IS NULL OR "PROPERTYTYPE"::TEXT = ANY(ARRAY['BOOLEAN', 'INTEGER', 'NUMBER', 'STRING', 'ENCRYPTEDSTRING', 'TIMESTAMP', 'URL', 'UUID']));
</sql>
</changeSet>

<changeSet id="v5.5.0-11" author="nscuro">
<customChange class="org.dependencytrack.persistence.migration.change.v550.ComputeSeveritiesChange"/>
<sql splitStatements="true">
DROP FUNCTION IF EXISTS "CVSSV2_TO_SEVERITY";
DROP FUNCTION IF EXISTS "CVSSV3_TO_SEVERITY";
DROP FUNCTION IF EXISTS "CALC_SEVERITY";
</sql>
</changeSet>
</databaseChangeLog>
27 changes: 0 additions & 27 deletions src/main/resources/migration/procedures/function_calc-severity.sql

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit 47105cd

Please sign in to comment.