From b4800ed5b9173522cf1f09c4f9d0e29fe99bf8ff Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 14 Dec 2023 19:45:20 +0100 Subject: [PATCH] Consider rating overrides for findings 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 https://github.com/DependencyTrack/hyades/issues/966 Signed-off-by: nscuro --- .../org/dependencytrack/model/Finding.java | 18 +- .../persistence/FindingsQueryManager.java | 165 ++++++++++++++---- .../persistence/QueryManager.java | 4 + .../jdbi/mapping/FindingRowMapper.java | 117 +++++++++++++ .../mapping/PaginatedResultRowReducer.java | 52 ++++++ .../resources/v1/FindingResource.java | 11 +- .../dependencytrack/PostgresResourceTest.java | 77 ++++++++ .../resources/v1/FindingResourceTest.java | 17 +- 8 files changed, 399 insertions(+), 62 deletions(-) create mode 100644 src/main/java/org/dependencytrack/persistence/jdbi/mapping/FindingRowMapper.java create mode 100644 src/main/java/org/dependencytrack/persistence/jdbi/mapping/PaginatedResultRowReducer.java create mode 100644 src/test/java/org/dependencytrack/PostgresResourceTest.java diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index 4802e6fcc..03bd85fac 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -145,19 +145,27 @@ public Finding(UUID project, Object... o) { optValue(analysis, "isSuppressed", o[27], false); } - public Map getComponent() { + public Finding(final Map analysis, final Map attribution, + final Map component, final Map vulnerability) { + this.analysis = analysis; + this.attribution = attribution; + this.component = component; + this.vulnerability = vulnerability; + } + + public Map getComponent() { return component; } - public Map getVulnerability() { + public Map getVulnerability() { return vulnerability; } - public Map getAnalysis() { + public Map getAnalysis() { return analysis; } - public Map getAttribution() { + public Map getAttribution() { return attribution; } @@ -199,7 +207,7 @@ static List 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 aliases) { diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java index 50aa9ede1..7829b46f6 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java @@ -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; @@ -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 { @@ -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 getFindings(Project project) { return getFindings(project, false); } @@ -336,36 +334,129 @@ public List getFindings(Project project) { * @param includeSuppressed determines if suppressed vulnerabilities should be included or not * @return a List of Finding objects */ - @SuppressWarnings("unchecked") public List getFindings(Project project, boolean includeSuppressed) { - final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY); - query.setParameters(project.getId()); - final List list = query.executeList(); - final List 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 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 + + """) + .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) + ); + } + } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 2d7efeed8..f59c69429 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1209,6 +1209,10 @@ public List 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 getVulnerabilityMetrics() { return getMetricsQueryManager().getVulnerabilityMetrics(); } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/FindingRowMapper.java b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/FindingRowMapper.java new file mode 100644 index 000000000..af8569c18 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/FindingRowMapper.java @@ -0,0 +1,117 @@ +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 { + + private static final TypeReference> VULNERABILITY_ALIASES_TYPE_REF = new TypeReference<>() { + }; + + @Override + public Finding map(final ResultSet rs, final StatementContext ctx) throws SQLException { + final var analysis = new HashMap(); + final var attribution = new HashMap(); + final var component = new HashMap(); + final var vuln = new HashMap(); + + 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, "componentLatestVersion", ResultSet::getString, value -> component.put("latestVersion", 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 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> maybeConvertAliases(final ResultSet rs, final String columnName) throws SQLException { + final List aliases = json(rs, columnName, VULNERABILITY_ALIASES_TYPE_REF); + if (aliases == null) { + return Collections.emptySet(); + } + + final Set> uniqueAliases = new HashSet<>(); + for (final VulnerabilityAlias alias : aliases) { + Map 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; + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/PaginatedResultRowReducer.java b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/PaginatedResultRowReducer.java new file mode 100644 index 000000000..5fc12ca8c --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/PaginatedResultRowReducer.java @@ -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 implements RowReducer, PaginatedResult> { + + public static final class ResultContainer { + + private long totalCount; + private final List results = new ArrayList<>(); + + private void addResult(final T result) { + results.add(result); + } + + } + + private final Class elementClass; + + public PaginatedResultRowReducer(final Class elementClass) { + this.elementClass = elementClass; + } + + @Override + public ResultContainer container() { + return new ResultContainer<>(); + } + + @Override + public void accumulate(final ResultContainer 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 stream(final ResultContainer container) { + return Stream.of(new PaginatedResult().objects(container.results).total(container.totalCount)); + } + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 220a67797..5e6dfd562 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -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; @@ -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. @@ -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 findings = qm.getFindings(project, suppressed); - if (source != null) { - final List 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(); } diff --git a/src/test/java/org/dependencytrack/PostgresResourceTest.java b/src/test/java/org/dependencytrack/PostgresResourceTest.java new file mode 100644 index 000000000..da5f448ce --- /dev/null +++ b/src/test/java/org/dependencytrack/PostgresResourceTest.java @@ -0,0 +1,77 @@ +package org.dependencytrack; + +import alpine.Config; +import alpine.server.persistence.PersistenceManagerFactory; +import org.datanucleus.api.jdo.JDOPersistenceManagerFactory; +import org.dependencytrack.persistence.migration.MigrationInitializer; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +import javax.jdo.JDOHelper; +import java.sql.Connection; +import java.sql.Statement; + +public abstract class PostgresResourceTest extends ResourceTest { + + protected static PostgreSQLContainer postgresContainer; + + + @BeforeClass + public static void setUpClass() throws Exception { + Config.enableUnitTests(); + + postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:11-alpine")) + .withUsername("dtrack") + .withPassword("dtrack") + .withDatabaseName("dtrack"); + postgresContainer.start(); + + final var dataSource = new PGSimpleDataSource(); + dataSource.setUrl(postgresContainer.getJdbcUrl()); + dataSource.setUser(postgresContainer.getUsername()); + dataSource.setPassword(postgresContainer.getPassword()); + MigrationInitializer.runMigration(dataSource, /* silent */ true); + } + + @Override + public void before() throws Exception { + // Truncate all tables to ensure each test starts from a clean slate. + // https://stackoverflow.com/a/63227261 + try (final Connection connection = postgresContainer.createConnection(""); + final Statement statement = connection.createStatement()) { + statement.execute(""" + DO $$ DECLARE + r RECORD; + BEGIN + FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = CURRENT_SCHEMA()) LOOP + EXECUTE 'TRUNCATE TABLE ' || QUOTE_IDENT(r.tablename) || ' CASCADE'; + END LOOP; + END $$; + """); + } + + PersistenceManagerFactory.setJdoPersistenceManagerFactory(createPmf()); + + super.before(); + } + + @AfterClass + public static void tearDownClass() { + if (postgresContainer != null) { + postgresContainer.stop(); + } + } + + protected JDOPersistenceManagerFactory createPmf() { + final var dnProps = TestUtil.getDatanucleusProperties(postgresContainer.getJdbcUrl(), + postgresContainer.getDriverClassName(), + postgresContainer.getUsername(), + postgresContainer.getPassword()); + + return (JDOPersistenceManagerFactory) JDOHelper.getPersistenceManagerFactory(dnProps, "Alpine"); + } + +} diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 3b8154fb8..a266c2564 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -22,7 +22,7 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.assertj.core.api.Assertions; -import org.dependencytrack.ResourceTest; +import org.dependencytrack.PostgresResourceTest; import org.dependencytrack.model.AnalyzerIdentity; import org.dependencytrack.model.Component; import org.dependencytrack.model.Project; @@ -52,7 +52,7 @@ import static org.dependencytrack.model.WorkflowStatus.PENDING; import static org.junit.Assert.assertEquals; -public class FindingResourceTest extends ResourceTest { +public class FindingResourceTest extends PostgresResourceTest { @Override protected DeploymentContext configureDeployment() { @@ -99,7 +99,6 @@ public void getFindingsByProjectTest() { assertEquals("1.0", json.getJsonObject(0).getJsonObject("component").getString("version")); assertEquals("Vuln-1", json.getJsonObject(0).getJsonObject("vulnerability").getString("vulnId")); assertEquals(Severity.CRITICAL.name(), json.getJsonObject(0).getJsonObject("vulnerability").getString("severity")); - assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getInt("cweId")); assertEquals(2, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").size()); assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); assertEquals(666, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); @@ -109,7 +108,6 @@ public void getFindingsByProjectTest() { assertEquals("1.0", json.getJsonObject(1).getJsonObject("component").getString("version")); assertEquals("Vuln-2", json.getJsonObject(1).getJsonObject("vulnerability").getString("vulnId")); assertEquals(Severity.HIGH.name(), json.getJsonObject(1).getJsonObject("vulnerability").getString("severity")); - assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getInt("cweId")); assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").size()); assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); assertEquals(666, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); @@ -119,7 +117,6 @@ public void getFindingsByProjectTest() { assertEquals("1.0", json.getJsonObject(2).getJsonObject("component").getString("version")); assertEquals("Vuln-3", json.getJsonObject(2).getJsonObject("vulnerability").getString("vulnId")); assertEquals(Severity.MEDIUM.name(), json.getJsonObject(2).getJsonObject("vulnerability").getString("severity")); - assertEquals(80, json.getJsonObject(2).getJsonObject("vulnerability").getInt("cweId")); assertEquals(2, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").size()); assertEquals(80, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); assertEquals(666, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); @@ -220,7 +217,7 @@ public void getFindingsByProjectWithComponentLatestVersionTest() { Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); Project p2 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); Component c1 = createComponent(p1, "Component A", "1.0"); - c1.setPurl("pkg:/maven/org.acme/component-a@1.0.0"); + c1.setPurl("pkg:maven/org.acme/component-a@1.0.0"); RepositoryMetaComponent r1 = new RepositoryMetaComponent(); Date d1 = new Date(); r1.setLastCheck(d1); @@ -231,7 +228,7 @@ public void getFindingsByProjectWithComponentLatestVersionTest() { qm.persist(r1); Component c2 = createComponent(p1, "Component B", "1.0"); - c2.setPurl("pkg:/maven/org.acme/component-b@1.0.0"); + c2.setPurl("pkg:maven/org.acme/component-b@1.0.0"); RepositoryMetaComponent r2 = new RepositoryMetaComponent(); Date d2 = new Date(); r2.setLastCheck(d2); @@ -245,7 +242,7 @@ public void getFindingsByProjectWithComponentLatestVersionTest() { Component c4 = createComponent(p2, "Component D", "1.0"); Component c5 = createComponent(p2, "Component E", "1.0"); - c5.setPurl("pkg:/maven/org.acme/component-e@1.0.0"); + c5.setPurl("pkg:maven/org.acme/component-e@1.0.0"); RepositoryMetaComponent r3 = new RepositoryMetaComponent(); Date d3 = new Date(); r3.setLastCheck(d3); @@ -276,7 +273,6 @@ public void getFindingsByProjectWithComponentLatestVersionTest() { assertEquals("1.0", json.getJsonObject(0).getJsonObject("component").getString("version")); assertEquals("Vuln-1", json.getJsonObject(0).getJsonObject("vulnerability").getString("vulnId")); assertEquals(Severity.CRITICAL.name(), json.getJsonObject(0).getJsonObject("vulnerability").getString("severity")); - assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getInt("cweId")); assertEquals(2, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").size()); assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); assertEquals(666, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); @@ -287,7 +283,6 @@ public void getFindingsByProjectWithComponentLatestVersionTest() { assertEquals("1.0", json.getJsonObject(1).getJsonObject("component").getString("version")); assertEquals("Vuln-2", json.getJsonObject(1).getJsonObject("vulnerability").getString("vulnId")); assertEquals(Severity.HIGH.name(), json.getJsonObject(1).getJsonObject("vulnerability").getString("severity")); - assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getInt("cweId")); assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").size()); assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); assertEquals(666, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); @@ -298,7 +293,6 @@ public void getFindingsByProjectWithComponentLatestVersionTest() { assertEquals("1.0", json.getJsonObject(2).getJsonObject("component").getString("version")); assertEquals("Vuln-3", json.getJsonObject(2).getJsonObject("vulnerability").getString("vulnId")); assertEquals(Severity.MEDIUM.name(), json.getJsonObject(2).getJsonObject("vulnerability").getString("severity")); - assertEquals(80, json.getJsonObject(2).getJsonObject("vulnerability").getInt("cweId")); assertEquals(2, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").size()); assertEquals(80, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); assertEquals(666, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); @@ -327,7 +321,6 @@ public void getFindingsByProjectWithComponentLatestVersionWithoutRepositoryMetaC assertEquals("1.0", json.getJsonObject(0).getJsonObject("component").getString("version")); assertEquals("Vuln-1", json.getJsonObject(0).getJsonObject("vulnerability").getString("vulnId")); assertEquals(Severity.CRITICAL.name(), json.getJsonObject(0).getJsonObject("vulnerability").getString("severity")); - assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getInt("cweId")); assertEquals(2, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").size()); assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); assertEquals(666, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId"));