Skip to content

Commit

Permalink
Consider rating overrides for findings
Browse files Browse the repository at this point in the history
Additionally, refactor findings query to use JDBI and a single SQL statement, instead of multiple additional queries to enrich the results.

The new query also supports pagination, which the original logic didn't.

Closes DependencyTrack/hyades#966

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Dec 18, 2023
1 parent 1ac8e4c commit eb3eb5f
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 62 deletions.
18 changes: 13 additions & 5 deletions src/main/java/org/dependencytrack/model/Finding.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,27 @@ public Finding(UUID project, Object... o) {
optValue(analysis, "isSuppressed", o[27], false);
}

public Map getComponent() {
public Finding(final Map<String, Object> analysis, final Map<String, Object> attribution,
final Map<String, Object> component, final Map<String, Object> vulnerability) {
this.analysis = analysis;
this.attribution = attribution;
this.component = component;
this.vulnerability = vulnerability;
}

public Map<String, Object> getComponent() {
return component;
}

public Map getVulnerability() {
public Map<String, Object> getVulnerability() {
return vulnerability;
}

public Map getAnalysis() {
public Map<String, Object> getAnalysis() {
return analysis;
}

public Map getAttribution() {
public Map<String, Object> getAttribution() {
return attribution;
}

Expand Down Expand Up @@ -199,7 +207,7 @@ static List<Cwe> getCwes(final Object value) {
}

public String getMatrix() {
return project.toString() + ":" + component.get("uuid") + ":" + vulnerability.get("uuid");
return component.get("project") + ":" + component.get("uuid") + ":" + vulnerability.get("uuid");
}

public void addVulnerabilityAliases(List<VulnerabilityAlias> aliases) {
Expand Down
165 changes: 128 additions & 37 deletions src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
*/
package org.dependencytrack.persistence;

import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import com.github.packageurl.PackageURL;
import org.datanucleus.api.jdo.JDOQuery;
import org.dependencytrack.model.Analysis;
import org.dependencytrack.model.AnalysisComment;
import org.dependencytrack.model.AnalysisJustification;
Expand All @@ -29,17 +28,17 @@
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Finding;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerabilityAlias;
import org.dependencytrack.persistence.jdbi.mapping.FindingRowMapper;
import org.dependencytrack.persistence.jdbi.mapping.PaginatedResultRowReducer;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import static org.dependencytrack.persistence.jdbi.JdbiFactory.jdbi;

public class FindingsQueryManager extends QueryManager implements IQueryManager {


Expand Down Expand Up @@ -324,7 +323,6 @@ void deleteAnalysisTrail(Project project) {
* @param project the project to retrieve findings for
* @return a List of Finding objects
*/
@SuppressWarnings("unchecked")
public List<Finding> getFindings(Project project) {
return getFindings(project, false);
}
Expand All @@ -336,36 +334,129 @@ public List<Finding> getFindings(Project project) {
* @param includeSuppressed determines if suppressed vulnerabilities should be included or not
* @return a List of Finding objects
*/
@SuppressWarnings("unchecked")
public List<Finding> getFindings(Project project, boolean includeSuppressed) {
final Query<Object[]> query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY);
query.setParameters(project.getId());
final List<Object[]> list = query.executeList();
final List<Finding> findings = new ArrayList<>();
for (final Object[] o : list) {
final Finding finding = new Finding(project.getUuid(), o);
final Component component = getObjectByUuid(Component.class, (String) finding.getComponent().get("uuid"));
final Vulnerability vulnerability = getObjectByUuid(Vulnerability.class, (String) finding.getVulnerability().get("uuid"));
final Analysis analysis = getAnalysis(component, vulnerability);
final List<VulnerabilityAlias> aliases = detach(getVulnerabilityAliases(vulnerability));
finding.addVulnerabilityAliases(aliases);
if (includeSuppressed || analysis == null || !analysis.isSuppressed()) { // do not add globally suppressed findings
// These are CLOB fields. Handle these here so that database-specific deserialization doesn't need to be performed (in Finding)
finding.getVulnerability().put("description", vulnerability.getDescription());
finding.getVulnerability().put("recommendation", vulnerability.getRecommendation());
final PackageURL purl = component.getPurl();
if (purl != null) {
final RepositoryType type = RepositoryType.resolve(purl);
if (RepositoryType.UNSUPPORTED != type) {
final RepositoryMetaComponent repoMetaComponent = getRepositoryMetaComponent(type, purl.getNamespace(), purl.getName());
if (repoMetaComponent != null) {
finding.getComponent().put("latestVersion", repoMetaComponent.getLatestVersion());
}
}
}
findings.add(finding);
}
}
return findings;
return getFindingsPage(project, null, includeSuppressed).getList(Finding.class);
}

public PaginatedResult getFindingsPage(final Project project, final Vulnerability.Source source, final boolean includeSuppressed) {
return jdbi(this).withHandle(jdbiHandle -> jdbiHandle.createQuery("""
SELECT
"P"."UUID" AS "projectUuid",
"C"."UUID" AS "componentUuid",
"C"."GROUP" AS "componentGroup",
"C"."NAME" AS "componentName",
"C"."VERSION" AS "componentVersion",
"C"."CPE" AS "componentCpe",
"C"."PURL" AS "componentPurl",
"RMC"."LATEST_VERSION" AS "componentLatestVersion",
"V"."UUID" AS "vulnUuid",
"V"."VULNID" AS "vulnId",
"V"."SOURCE" AS "vulnSource",
"V"."TITLE" AS "vulnTitle",
"V"."SUBTITLE" AS "vulnSubTitle",
"V"."DESCRIPTION" AS "vulnDescription",
"V"."RECOMMENDATION" AS "vulnRecommendation",
CASE
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."CVSSV2SCORE"
ELSE "V"."CVSSV2BASESCORE"
END AS "vulnCvssV2BaseScore",
CASE
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."CVSSV3SCORE"
ELSE "V"."CVSSV3BASESCORE"
END AS "vulnCvssV3BaseScore",
-- TODO: Analysis only has a single score, but OWASP RR defines multiple.
-- How to handle this?
CASE
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."OWASPSCORE"
ELSE "V"."OWASPRRBUSINESSIMPACTSCORE"
END AS "vulnOwaspRrBusinessImpactScore",
CASE
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."OWASPSCORE"
ELSE "V"."OWASPRRLIKELIHOODSCORE"
END AS "vulnOwaspRrLikelihoodScore",
CASE
WHEN "A"."SEVERITY" IS NOT NULL THEN "A"."OWASPSCORE"
ELSE "V"."OWASPRRTECHNICALIMPACTSCORE"
END AS "vulnOwaspRrTechnicalImpactScore",
"CALC_SEVERITY"(
"V"."SEVERITY",
"A"."SEVERITY",
"V"."CVSSV3BASESCORE",
"V"."CVSSV2BASESCORE"
) AS "vulnSeverity",
"V"."EPSSSCORE" AS "vulnEpssScore",
"V"."EPSSPERCENTILE" AS "vulnEpssPercentile",
STRING_TO_ARRAY("V"."CWES", ',') AS "vulnCwes",
"FA"."ANALYZERIDENTITY" AS "analyzerIdentity",
"FA"."ATTRIBUTED_ON" AS "attributedOn",
"FA"."ALT_ID" AS "alternateIdentifier",
"FA"."REFERENCE_URL" AS "referenceUrl",
"A"."STATE" AS "analysisState",
"A"."SUPPRESSED" AS "isSuppressed",
COUNT(*) OVER() AS "totalCount"
FROM
"PROJECT" AS "P"
INNER JOIN
"COMPONENT" AS "C" ON "C"."PROJECT_ID" = "P"."ID"
INNER JOIN
"COMPONENTS_VULNERABILITIES" AS "CV" ON "CV"."COMPONENT_ID" = "C"."ID"
INNER JOIN
"VULNERABILITY" AS "V" ON "V"."ID" = "CV"."VULNERABILITY_ID"
INNER JOIN
"FINDINGATTRIBUTION" AS "FA" ON "FA"."COMPONENT_ID" = "C"."ID" AND "FA"."VULNERABILITY_ID" = "V"."ID"
LEFT JOIN
"ANALYSIS" AS "A" ON "A"."COMPONENT_ID" = "C"."ID" AND "A"."VULNERABILITY_ID" = "V"."ID"
LEFT JOIN
-- TODO: Find a better performing way to join.
-- Perhaps write a SQL function that can parse type, namespace, and name from "C"."PURL"
-- and perform the join on "RMC"."NAMESPACE" and "RMC"."NAME" instead.
"REPOSITORY_META_COMPONENT" AS "RMC"
ON "C"."PURL" LIKE (
'pkg:' || LOWER("RMC"."REPOSITORY_TYPE")
|| CASE WHEN "RMC"."NAMESPACE" IS NOT NULL THEN '/' || "RMC"."NAMESPACE" ELSE '' END
|| '/' || "RMC"."NAME" || '@%'
)
LEFT JOIN LATERAL (
SELECT
CAST(JSONB_AGG(DISTINCT JSONB_STRIP_NULLS(JSONB_BUILD_OBJECT(
'cveId', "VA"."CVE_ID",
'ghsaId', "VA"."GHSA_ID",
'gsdId', "VA"."GSD_ID",
'internalId', "VA"."INTERNAL_ID",
'osvId', "VA"."OSV_ID",
'sonatypeId', "VA"."SONATYPE_ID",
'snykId', "VA"."SNYK_ID",
'vulnDbId', "VA"."VULNDB_ID"
))) AS TEXT) AS "vulnAliases"
FROM
"VULNERABILITYALIAS" AS "VA"
WHERE
("V"."SOURCE" = 'NVD' AND "VA"."CVE_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'GITHUB' AND "VA"."GHSA_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'GSD' AND "VA"."GSD_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'INTERNAL' AND "VA"."INTERNAL_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'OSV' AND "VA"."OSV_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'SONATYPE' AND "VA"."SONATYPE_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'SNYK' AND "VA"."SNYK_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'VULNDB' AND "VA"."VULNDB_ID" = "V"."VULNID")
) AS "vulnAliases" ON TRUE
WHERE
"P"."ID" = :projectId
AND ((:source)::TEXT IS NULL OR ("V"."SOURCE" = :source))
AND (:includeSuppressed OR "A"."SUPPRESSED" IS NULL OR NOT "A"."SUPPRESSED")
<#if pagination.isPaginated()>
OFFSET ${pagination.offset} FETCH NEXT ${pagination.limit} ROWS ONLY
</#if>
""")
.define("pagination", pagination)
.bind("projectId", project.getId())
.bind("source", source)
.bind("includeSuppressed", includeSuppressed)
.registerRowMapper(new FindingRowMapper())
.reduceRows(new PaginatedResultRowReducer<>(Finding.class))
.findFirst()
.orElseGet(PaginatedResult::new)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,10 @@ public List<Finding> getFindings(Project project, boolean includeSuppressed) {
return getFindingsQueryManager().getFindings(project, includeSuppressed);
}

public PaginatedResult getFindingsPage(final Project project, final Vulnerability.Source limitToSource, final boolean includeSuppressed) {
return getFindingsQueryManager().getFindingsPage(project, limitToSource, includeSuppressed);
}

public List<VulnerabilityMetrics> getVulnerabilityMetrics() {
return getMetricsQueryManager().getVulnerabilityMetrics();
}
Expand Down
Loading

0 comments on commit eb3eb5f

Please sign in to comment.