Skip to content

Commit

Permalink
WIP: Consider rating overrides for findings
Browse files Browse the repository at this point in the history
Closes DependencyTrack/hyades#966

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Dec 14, 2023
1 parent fd18aca commit 288f157
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 18 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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.dependencytrack.persistence;

import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import com.github.packageurl.PackageURL;
import org.datanucleus.api.jdo.JDOQuery;
Expand All @@ -33,13 +34,17 @@
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 @@ -368,4 +373,113 @@ public List<Finding> getFindings(Project project, boolean includeSuppressed) {
}
return findings;
}

public PaginatedResult getFindingsPage(final Project project, final Vulnerability.Source limitToSource, 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",
"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 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))
<#if pagination.isPaginated()>
OFFSET ${pagination.offset} FETCH NEXT ${pagination.limit} ROWS ONLY
</#if>
""")
.define("pagination", pagination)
.bind("projectId", project.getId())
.bind("source", limitToSource)
.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
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.dependencytrack.persistence.jdbi.mapping;

import com.fasterxml.jackson.core.type.TypeReference;
import org.dependencytrack.model.Cwe;
import org.dependencytrack.model.Finding;
import org.dependencytrack.model.Severity;
import org.dependencytrack.model.VulnerabilityAlias;
import org.dependencytrack.parser.common.resolver.CweResolver;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.json;
import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet;
import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.stringArray;

public class FindingRowMapper implements RowMapper<Finding> {

private static final TypeReference<List<VulnerabilityAlias>> VULNERABILITY_ALIASES_TYPE_REF = new TypeReference<>() {
};

@Override
public Finding map(final ResultSet rs, final StatementContext ctx) throws SQLException {
final var analysis = new HashMap<String, Object>();
final var attribution = new HashMap<String, Object>();
final var component = new HashMap<String, Object>();
final var vuln = new HashMap<String, Object>();

maybeSet(rs, "projectUuid", ResultSet::getString, value -> component.put("project", value));
maybeSet(rs, "componentUuid", ResultSet::getString, value -> component.put("uuid", value));
maybeSet(rs, "componentGroup", ResultSet::getString, value -> component.put("group", value));
maybeSet(rs, "componentName", ResultSet::getString, value -> component.put("name", value));
maybeSet(rs, "componentVersion", ResultSet::getString, value -> component.put("version", value));
maybeSet(rs, "componentCpe", ResultSet::getString, value -> component.put("cpe", value));
maybeSet(rs, "componentPurl", ResultSet::getString, value -> component.put("purl", value));
maybeSet(rs, "vulnUuid", ResultSet::getString, value -> vuln.put("uuid", value));
maybeSet(rs, "vulnId", ResultSet::getString, value -> vuln.put("vulnId", value));
maybeSet(rs, "vulnSource", ResultSet::getString, value -> vuln.put("source", value));
maybeSet(rs, "vulnTitle", ResultSet::getString, value -> vuln.put("title", value));
maybeSet(rs, "vulnSubTitle", ResultSet::getString, value -> vuln.put("subtitle", value));
maybeSet(rs, "vulnDescription", ResultSet::getString, value -> vuln.put("description", value));
maybeSet(rs, "vulnRecommendation", ResultSet::getString, value -> vuln.put("recommendation", value));
maybeSet(rs, "vulnCvssV2BaseScore", RowMapperUtil::nullableDouble, value -> vuln.put("cvssV2BaseScore", value));
maybeSet(rs, "vulnCvssV3BaseScore", RowMapperUtil::nullableDouble, value -> vuln.put("cvssV3BaseScore", value));
maybeSet(rs, "vulnOwaspRrBusinessImpactScore", RowMapperUtil::nullableDouble, value -> vuln.put("owaspBusinessImpactScore", value));
maybeSet(rs, "vulnOwaspRrLikelihoodScore", RowMapperUtil::nullableDouble, value -> vuln.put("owaspLikelihoodScore", value));
maybeSet(rs, "vulnOwaspRrTechnicalImpactScore", RowMapperUtil::nullableDouble, value -> vuln.put("owaspTechnicalImpactScore", value));
maybeSet(rs, "vulnSeverity", ResultSet::getString, value -> {
final Severity severity = Severity.valueOf(value);
vuln.put("severity", severity.name());
vuln.put("severityRank", severity.ordinal());
});
maybeSet(rs, "vulnEpssScore", RowMapperUtil::nullableDouble, value -> vuln.put("epssScore", value));
maybeSet(rs, "vulnEpssPercentile", RowMapperUtil::nullableDouble, value -> vuln.put("epssPercentile", value));
maybeSet(rs, "vulnCwes", FindingRowMapper::maybeConvertCwes, value -> vuln.put("cwes", value));
maybeSet(rs, "vulnAliases", FindingRowMapper::maybeConvertAliases, value -> vuln.put("aliases", value));
maybeSet(rs, "analyzerIdentity", ResultSet::getString, value -> attribution.put("analyzerIdentity", value));
maybeSet(rs, "attributedOn", ResultSet::getTimestamp, value -> attribution.put("attributedOn", value));
maybeSet(rs, "alternateIdentifier", ResultSet::getString, value -> attribution.put("alternateIdentifier", value));
maybeSet(rs, "referenceUrl", ResultSet::getString, value -> attribution.put("referenceUrl", value));
maybeSet(rs, "analysisState", ResultSet::getString, value -> analysis.put("state", value));
analysis.put("isSuppressed", rs.getBoolean("isSuppressed"));

return new Finding(analysis, attribution, component, vuln);
}

private static List<Cwe> maybeConvertCwes(final ResultSet rs, final String columnName) throws SQLException {
return stringArray(rs, columnName).stream()
.map(CweResolver.getInstance()::lookup)
.filter(Objects::nonNull)
.toList();
}

private static Set<Map<String, String>> maybeConvertAliases(final ResultSet rs, final String columnName) throws SQLException {
final List<VulnerabilityAlias> aliases = json(rs, columnName, VULNERABILITY_ALIASES_TYPE_REF);
if (aliases == null) {
return Collections.emptySet();
}

final Set<Map<String, String>> uniqueAliases = new HashSet<>();
for (final VulnerabilityAlias alias : aliases) {
Map<String, String> map = new HashMap<>();
if (alias.getCveId() != null && !alias.getCveId().isBlank()) {
map.put("cveId", alias.getCveId());
}
if (alias.getGhsaId() != null && !alias.getGhsaId().isBlank()) {
map.put("ghsaId", alias.getGhsaId());
}
if (alias.getSonatypeId() != null && !alias.getSonatypeId().isBlank()) {
map.put("sonatypeId", alias.getSonatypeId());
}
if (alias.getOsvId() != null && !alias.getOsvId().isBlank()) {
map.put("osvId", alias.getOsvId());
}
if (alias.getSnykId() != null && !alias.getSnykId().isBlank()) {
map.put("snykId", alias.getSnykId());
}
if (alias.getVulnDbId() != null && !alias.getVulnDbId().isBlank()) {
map.put("vulnDbId", alias.getVulnDbId());
}
uniqueAliases.add(map);
}

return uniqueAliases;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.dependencytrack.persistence.jdbi.mapping;

import alpine.persistence.PaginatedResult;
import org.jdbi.v3.core.result.RowReducer;
import org.jdbi.v3.core.result.RowView;
import org.jdbi.v3.core.result.UnableToProduceResultException;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class PaginatedResultRowReducer<T> implements RowReducer<PaginatedResultRowReducer.ResultContainer<T>, PaginatedResult> {

public static final class ResultContainer<T> {

private long totalCount;
private final List<T> results = new ArrayList<>();

private void addResult(final T result) {
results.add(result);
}

}

private final Class<T> elementClass;

public PaginatedResultRowReducer(final Class<T> elementClass) {
this.elementClass = elementClass;
}

@Override
public ResultContainer<T> container() {
return new ResultContainer<>();
}

@Override
public void accumulate(final ResultContainer<T> container, final RowView rowView) {
final Long totalCount = rowView.getColumn("totalCount", Long.class);
if (totalCount == null) {
throw new UnableToProduceResultException("Result does not contain a totalCount column");
}

container.totalCount = totalCount;
container.addResult(rowView.getRow(elementClass));
}

@Override
public Stream<PaginatedResult> stream(final ResultContainer<T> container) {
return Stream.of(new PaginatedResult().objects(container.results).total(container.totalCount));
}

}
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.persistence.PaginatedResult;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
import io.swagger.annotations.Api;
Expand Down Expand Up @@ -50,7 +51,6 @@
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
* JAX-RS resources for processing findings.
Expand Down Expand Up @@ -88,13 +88,8 @@ public Response getFindingsByProject(@PathParam("uuid") String uuid,
final Project project = qm.getObjectByUuid(Project.class, uuid);
if (project != null) {
if (qm.hasAccess(super.getPrincipal(), project)) {
final List<Finding> findings = qm.getFindings(project, suppressed);
if (source != null) {
final List<Finding> filteredList = findings.stream().filter(finding -> source.name().equals(finding.getVulnerability().get("source"))).collect(Collectors.toList());
return Response.ok(filteredList).header(TOTAL_COUNT_HEADER, filteredList.size()).build();
} else {
return Response.ok(findings).header(TOTAL_COUNT_HEADER, findings.size()).build();
}
final PaginatedResult findings = qm.getFindingsPage(project, source, suppressed);
return Response.ok(findings.getObjects()).header(TOTAL_COUNT_HEADER, findings.getTotal()).build();
} else {
return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build();
}
Expand Down
Loading

0 comments on commit 288f157

Please sign in to comment.