From 8c2dc7b236050086025172ef413e0b152114ff4c Mon Sep 17 00:00:00 2001 From: RBickert Date: Mon, 30 Jan 2023 08:47:36 +0100 Subject: [PATCH 01/16] Global Audit View: Vulnerabilities Adds two new API methods to the FindingResource, which return a filtered list (ACL and optional other filters) of every finding, either by occurrence or grouped by vulnerability, to allow users to quickly get every finding for all of their projects. Signed-off-by: RBickert --- .../org/dependencytrack/model/Finding.java | 49 ++- .../dependencytrack/model/GroupedFinding.java | 91 +++++ .../persistence/FindingsQueryManager.java | 359 ++++++++++++++++++ .../persistence/QueryManager.java | 9 + .../resources/v1/FindingResource.java | 113 ++++++ 5 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/GroupedFinding.java diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index c6c35ee09..75ac6c466 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -89,6 +89,46 @@ public class Finding implements Serializable { "LEFT JOIN \"ANALYSIS\" ON (\"COMPONENT\".\"ID\" = \"ANALYSIS\".\"COMPONENT_ID\") AND (\"VULNERABILITY\".\"ID\" = \"ANALYSIS\".\"VULNERABILITY_ID\") AND (\"COMPONENT\".\"PROJECT_ID\" = \"ANALYSIS\".\"PROJECT_ID\") " + "WHERE \"COMPONENT\".\"PROJECT_ID\" = ?"; + public static final String QUERY_ALL_FINDINGS = "SELECT " + + "\"COMPONENT\".\"UUID\"," + + "\"COMPONENT\".\"NAME\"," + + "\"COMPONENT\".\"GROUP\"," + + "\"COMPONENT\".\"VERSION\"," + + "\"COMPONENT\".\"PURL\"," + + "\"COMPONENT\".\"CPE\"," + + "\"VULNERABILITY\".\"UUID\"," + + "\"VULNERABILITY\".\"SOURCE\"," + + "\"VULNERABILITY\".\"VULNID\"," + + "\"VULNERABILITY\".\"TITLE\"," + + "\"VULNERABILITY\".\"SUBTITLE\"," + + "\"VULNERABILITY\".\"DESCRIPTION\"," + + "\"VULNERABILITY\".\"RECOMMENDATION\"," + + "\"VULNERABILITY\".\"SEVERITY\"," + + "\"VULNERABILITY\".\"CVSSV2BASESCORE\"," + + "\"VULNERABILITY\".\"CVSSV3BASESCORE\"," + + "\"VULNERABILITY\".\"OWASPRRLIKELIHOODSCORE\"," + + "\"VULNERABILITY\".\"OWASPRRTECHNICALIMPACTSCORE\"," + + "\"VULNERABILITY\".\"OWASPRRBUSINESSIMPACTSCORE\"," + + "\"VULNERABILITY\".\"EPSSSCORE\"," + + "\"VULNERABILITY\".\"EPSSPERCENTILE\"," + + "\"VULNERABILITY\".\"CWES\"," + + "\"FINDINGATTRIBUTION\".\"ANALYZERIDENTITY\"," + + "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"," + + "\"FINDINGATTRIBUTION\".\"ALT_ID\"," + + "\"FINDINGATTRIBUTION\".\"REFERENCE_URL\"," + + "\"ANALYSIS\".\"STATE\"," + + "\"ANALYSIS\".\"SUPPRESSED\"," + + "\"VULNERABILITY\".\"PUBLISHED\"," + + "\"PROJECT\".\"UUID\"," + + "\"PROJECT\".\"NAME\"," + + "\"PROJECT\".\"VERSION\"" + + "FROM \"COMPONENT\" " + + "INNER JOIN \"COMPONENTS_VULNERABILITIES\" ON (\"COMPONENT\".\"ID\" = \"COMPONENTS_VULNERABILITIES\".\"COMPONENT_ID\") " + + "INNER JOIN \"VULNERABILITY\" ON (\"COMPONENTS_VULNERABILITIES\".\"VULNERABILITY_ID\" = \"VULNERABILITY\".\"ID\") " + + "INNER JOIN \"FINDINGATTRIBUTION\" ON (\"COMPONENT\".\"ID\" = \"FINDINGATTRIBUTION\".\"COMPONENT_ID\") AND (\"VULNERABILITY\".\"ID\" = \"FINDINGATTRIBUTION\".\"VULNERABILITY_ID\")" + + "LEFT JOIN \"ANALYSIS\" ON (\"COMPONENT\".\"ID\" = \"ANALYSIS\".\"COMPONENT_ID\") AND (\"VULNERABILITY\".\"ID\" = \"ANALYSIS\".\"VULNERABILITY_ID\") AND (\"COMPONENT\".\"PROJECT_ID\" = \"ANALYSIS\".\"PROJECT_ID\") " + + "INNER JOIN \"PROJECT\" ON (\"COMPONENT\".\"PROJECT_ID\" = \"PROJECT\".\"ID\")"; + private UUID project; private Map component = new LinkedHashMap<>(); private Map vulnerability = new LinkedHashMap<>(); @@ -98,8 +138,8 @@ public class Finding implements Serializable { /** * Constructs a new Finding object. The generic Object array passed as an argument is the * individual values for each row in a resultset. The order of these must match the order - * of the columns being queried in {@link #QUERY}. - * @param o An array of values specific to an individual row returned from {@link #QUERY} + * of the columns being queried in {@link #QUERY} or {@link #QUERY_ALL_FINDINGS}. + * @param o An array of values specific to an individual row returned from {@link #QUERY} or {@link #QUERY_ALL_FINDINGS} */ public Finding(UUID project, Object... o) { this.project = project; @@ -142,6 +182,11 @@ public Finding(UUID project, Object... o) { optValue(analysis, "state", o[26]); optValue(analysis, "isSuppressed", o[27], false); + if (o.length > 30) { + optValue(vulnerability, "published", o[28]); + optValue(component, "projectName", o[30]); + optValue(component, "projectVersion", o[31]); + } } public Map getComponent() { diff --git a/src/main/java/org/dependencytrack/model/GroupedFinding.java b/src/main/java/org/dependencytrack/model/GroupedFinding.java new file mode 100644 index 000000000..d55c7ffc8 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/GroupedFinding.java @@ -0,0 +1,91 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The GroupedFinding object is a metadata/value object that combines data from multiple tables. The object can + * only be queried on, not updated or deleted. Modifications to data in the GroupedFinding object need to be made + * to the original source object needing modified. + * + * @since 4.8.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class GroupedFinding implements Serializable { + + private static final long serialVersionUID = 2246518534279822243L; + + public static final String QUERY = """ + SELECT + "VULNERABILITY"."SOURCE", + "VULNERABILITY"."VULNID", + "VULNERABILITY"."TITLE", + "VULNERABILITY"."SEVERITY", + "FINDINGATTRIBUTION"."ANALYZERIDENTITY", + "VULNERABILITY"."PUBLISHED", + "VULNERABILITY"."CWES", + "VULNERABILITY"."CVSSV3BASESCORE", + COUNT(DISTINCT "PROJECT"."ID") AS "AFFECTED_PROJECT_COUNT", + MIN("AFFECTEDVERSIONATTRIBUTION"."FIRST_SEEN") AS "FIRST_OCCURRENCE", + MAX("AFFECTEDVERSIONATTRIBUTION"."LAST_SEEN") AS "LAST_OCCURRENCE" + FROM COMPONENT + INNER JOIN "COMPONENTS_VULNERABILITIES" ON ("COMPONENT"."ID" = "COMPONENTS_VULNERABILITIES"."COMPONENT_ID") + INNER JOIN "VULNERABILITY" ON ("COMPONENTS_VULNERABILITIES"."VULNERABILITY_ID" = "VULNERABILITY"."ID") + INNER JOIN "FINDINGATTRIBUTION" ON ("COMPONENT"."ID" = "FINDINGATTRIBUTION"."COMPONENT_ID") AND ("VULNERABILITY"."ID" = "FINDINGATTRIBUTION"."VULNERABILITY_ID") + LEFT JOIN "ANALYSIS" ON ("COMPONENT"."ID" = "ANALYSIS"."COMPONENT_ID") AND ("VULNERABILITY"."ID" = "ANALYSIS"."VULNERABILITY_ID") AND ("COMPONENT"."PROJECT_ID" = "ANALYSIS"."PROJECT_ID") + INNER JOIN "PROJECT" ON ("COMPONENT"."PROJECT_ID" = "PROJECT"."ID") + LEFT JOIN "AFFECTEDVERSIONATTRIBUTION" ON ("VULNERABILITY"."ID" = "AFFECTEDVERSIONATTRIBUTION"."VULNERABILITY") + """; + + private Map vulnerability = new LinkedHashMap<>(); + private Map attribution = new LinkedHashMap<>(); + + public GroupedFinding(Object ...o) { + optValue(vulnerability, "source", o[0]); + optValue(vulnerability, "vulnId", o[1]); + optValue(vulnerability, "title", o[2]); + optValue(vulnerability, "severity", o[3]); + optValue(attribution, "analyzerIdentity", o[4]); + optValue(vulnerability, "published", o[5]); + optValue(vulnerability, "cwes", Finding.getCwes(o[6])); + optValue(vulnerability, "cvssV3BaseScore", o[7]); + optValue(vulnerability, "affectedProjectCount", o[8]); + optValue(attribution, "firstOccurrence", o[9]); + optValue(attribution, "lastOccurrence", o[10]); + } + + public Map getVulnerability() { + return vulnerability; + } + + public Map getAttribution() { + return attribution; + } + + private void optValue(Map map, String key, Object value) { + if (value != null) { + map.put(key, value); + } + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java index a22c4031e..d0decf63c 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java @@ -18,7 +18,12 @@ */ package org.dependencytrack.persistence; +import alpine.common.logging.Logger; +import alpine.model.ApiKey; +import alpine.model.Team; +import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; +import alpine.server.util.DbUtil; import com.github.packageurl.PackageURL; import org.datanucleus.api.jdo.JDOQuery; import org.dependencytrack.model.Analysis; @@ -27,7 +32,9 @@ import org.dependencytrack.model.AnalysisResponse; import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Component; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Finding; +import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.Project; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; @@ -36,12 +43,36 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.UUID; public class FindingsQueryManager extends QueryManager implements IQueryManager { + private static final Logger LOGGER = Logger.getLogger(FindingsQueryManager.class); + + public static final String QUERY_ACL_1 = """ + "DESCENDANTS" ("ID", "NAME") AS + (SELECT "PROJECT"."ID", + "PROJECT"."NAME" + FROM "PROJECT" + """; + + public static final String QUERY_ACL_2 = """ + UNION ALL + SELECT "CHILD"."ID", + "CHILD"."NAME" + FROM "PROJECT" "CHILD" + JOIN "DESCENDANTS" + ON "DESCENDANTS"."ID" = "CHILD"."PARENT_PROJECT_ID") + SELECT "DESCENDANTS"."ID", "DESCENDANTS"."NAME" FROM "DESCENDANTS" + """; /** * Constructs a new QueryManager. @@ -295,4 +326,332 @@ public List getFindings(Project project, boolean includeSuppressed) { } return findings; } + + /** + * Returns a List of all Finding objects filtered by ACL and other optional filters. + * @param filters determines the filters to apply on the list of Finding objects + * @param showSuppressed determines if suppressed vulnerabilities should be included or not + * @param showInactive determines if inactive projects should be included or not + * @return a List of Finding objects + */ + public List getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { + StringBuilder queryFilter = new StringBuilder(); + Map params = new HashMap<>(); + if (showInactive) { + queryFilter.append(" WHERE (\"PROJECT\".\"ACTIVE\" = :active OR \"PROJECT\".\"ACTIVE\" IS NULL)"); + params.put("active", true); + } + if (!showSuppressed) { + if (queryFilter.length() == 0) { + queryFilter.append(" WHERE "); + } else { + queryFilter.append(" AND "); + } + queryFilter.append("(\"ANALYSIS\".\"SUPPRESSED\" = :showSuppressed OR \"ANALYSIS\".\"SUPPRESSED\" IS NULL)"); + params.put("showSuppressed", false); + } + processFilters(filters, queryFilter, params, false); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter); + query.setNamedParameters(params); + final List list = query.executeList(); + final List findings = new ArrayList<>(); + for (final Object[] o: list) { + final Finding finding = new Finding(UUID.fromString((String) o[29]), 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)); + aliases.forEach(alias -> alias.setUuid(null)); + finding.getVulnerability().put("aliases", aliases); + // 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; + } + + /** + * Returns a List of all Finding objects filtered by ACL and other optional filters. The resulting list is grouped by vulnerability. + * @param filters determines the filters to apply on the list of Finding objects + * @param showInactive determines if inactive projects should be included or not + * @return a List of Finding objects + */ + public List getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { + StringBuilder queryFilter = new StringBuilder(); + Map params = new HashMap<>(); + if (showInactive) { + queryFilter.append(" WHERE (\"PROJECT\".\"ACTIVE\" = :active OR \"PROJECT\".\"ACTIVE\" IS NULL)"); + params.put("active", true); + } + processFilters(filters, queryFilter, params, true); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, GroupedFinding.QUERY + queryFilter); + query.setNamedParameters(params); + final List list = query.executeList(); + final List findings = new ArrayList<>(); + for (Object[] o : list) { + final GroupedFinding finding = new GroupedFinding(o); + findings.add(finding); + } + return findings; + } + + private void processFilters(Map filters, StringBuilder queryFilter, Map params, boolean isGroupedByVulnerabilities) { + for (String filter : filters.keySet()) { + switch (filter) { + case "severity" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"SEVERITY\""); + case "analysisStatus" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"ANALYSIS\".\"STATE\""); + case "vendorResponse" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"ANALYSIS\".\"RESPONSE\""); + case "publishDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"PUBLISHED\"", true, true, false); + case "publishDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"PUBLISHED\"", false, true, false); + case "attributedOnDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", true, true, false); + case "attributedOnDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", false, true, false); + case "textSearchField" -> processInputFilter(queryFilter, params, filter, filters.get(filter), filters.get("textSearchInput")); + case "cvssFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", true, false, false); + case "cvssTo" -> processRangeFilter(queryFilter, params,filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", false, false, false); + } + } + preprocessACLs(queryFilter, params); + if (isGroupedByVulnerabilities) { + queryFilter.append(" GROUP BY \"VULNERABILITY\".\"ID\""); + StringBuilder aggregateFilter = new StringBuilder(); + processAggregateFilters(filters, aggregateFilter, params); + queryFilter.append(aggregateFilter); + } + } + + private void processAggregateFilters(Map filters, StringBuilder queryFilter, Map params) { + for (String filter : filters.keySet()) { + switch (filter) { + case "occurrencesFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", true, false, true); + case "occurrencesTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", false, false, true); + case "aggregatedAttributedOnDateFrom" -> { + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", true, true); + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", true, false); + } + case "aggregatedAttributedOnDateTo" -> { + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", false, true); + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", false, false); + } + } + } + } + + private void processArrayFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column) { + if (filter != null && !filter.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(" WHERE ("); + } else { + queryFilter.append(" AND ("); + } + String[] filters = filter.split(","); + for (int i = 0, length = filters.length; i < length; i++) { + queryFilter.append(column).append(" = :").append(paramName).append(i); + params.put(paramName + i, filters[i].toUpperCase()); + if (i < length-1) { + queryFilter.append(" OR "); + } + } + queryFilter.append(")"); + } + } + + private void processRangeFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column, boolean fromValue, boolean isDate, boolean isAggregateFilter) { + if (filter != null && !filter.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(isAggregateFilter ? " HAVING (" : " WHERE ("); + } else { + queryFilter.append(" AND ("); + } + queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); + String value = filter; + if (isDate) { + value += (fromValue ? " 00:00:00" : " 23:59:59"); + } + params.put(paramName, value); + queryFilter.append(")"); + } + } + + private void processAggregatedDateRangeFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column, boolean fromValue, boolean isMin) { + if (filter != null && !filter.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(" HAVING ("); + } else { + queryFilter.append(isMin ? " AND (" : " OR "); + } + queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); + String value = filter; + value += (fromValue ? " 00:00:00" : " 23:59:59"); + params.put(paramName, value); + if (!isMin) { + queryFilter.append(")"); + } + } + } + + private void processInputFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String input) { + if (filter != null && !filter.isEmpty() && input != null && !input.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(" WHERE ("); + } else { + queryFilter.append(" AND ("); + } + String[] filters = filter.split(","); + for (int i = 0, length = filters.length; i < length; i++) { + switch (filters[i].toUpperCase()) { + case "VULNERABILITY_ID" -> queryFilter.append("\"VULNERABILITY\".\"VULNID\""); + case "VULNERABILITY_TITLE" -> queryFilter.append("\"VULNERABILITY\".\"TITLE\""); + case "COMPONENT_NAME" -> queryFilter.append("\"COMPONENT\".\"NAME\""); + case "COMPONENT_VERSION" -> queryFilter.append("\"COMPONENT\".\"VERSION\""); + case "PROJECT_NAME" -> queryFilter.append("concat(\"PROJECT\".\"NAME\", ' ', \"PROJECT\".\"VERSION\")"); + } + queryFilter.append(" LIKE :").append(paramName); + if (i < length-1) { + queryFilter.append(" OR "); + } + } + if (filters.length > 0) { + params.put(paramName, "%" + input + "%"); + } + queryFilter.append(")"); + } + } + + private void preprocessACLs(StringBuilder queryFilter, final Map params) { + if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { + final List teams; + if (super.principal instanceof UserPrincipal) { + final UserPrincipal userPrincipal = ((UserPrincipal) super.principal); + teams = userPrincipal.getTeams(); + if (super.hasAccessManagementPermission(userPrincipal)) { + return; + } + } else { + final ApiKey apiKey = ((ApiKey) super.principal); + teams = apiKey.getTeams(); + if (super.hasAccessManagementPermission(apiKey)) { + return; + } + } + + // Query every project that the teams have access to + final Map tempParams = new HashMap<>(); + final Query queryAclProjects = pm.newQuery(Project.class); + if (teams != null && teams.size() > 0) { + final StringBuilder stringBuilderAclProjects = new StringBuilder(); + for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) { + final Team team = super.getObjectById(Team.class, teams.get(i).getId()); + stringBuilderAclProjects.append(" accessTeams.contains(:team").append(i).append(") "); + tempParams.put("team" + i, team); + if (i < teamsSize - 1) { + stringBuilderAclProjects.append(" || "); + } + } + queryAclProjects.setFilter(stringBuilderAclProjects.toString()); + } else { + params.put("false", false); + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND :false"); + } else { + queryFilter.append("WHERE :false"); + } + } + List result = (List) queryAclProjects.executeWithMap(tempParams); + // Query the descendants of the projects that the teams have access to + if (result != null && !result.isEmpty()) { + final StringBuilder stringBuilderDescendants = new StringBuilder(); + final List parameters = new ArrayList<>(); + stringBuilderDescendants.append("WHERE"); + int i = 0, teamSize = result.size(); + for (Project project : result) { + stringBuilderDescendants.append(" \"ID\" = ?").append(" "); + parameters.add(project.getId()); + if (i < teamSize - 1) { + stringBuilderDescendants.append(" OR"); + } + i++; + } + stringBuilderDescendants.append("\n"); + final List results = new ArrayList<>(); + + // Querying the descendants of projects requires a CTE (Common Table Expression), which needs to be at the top-level of the query for Microsoft SQL Server. + // Because of JDO, queries are only allowed to start with "SELECT", so the "WITH" clause for the CTE in MSSQL cannot be at top level. + // Activating the JDO property that queries don't have to start with "SELECT" does not help in this case, because JDO queries that do not start with "SELECT" only return "true", so no data can be fetched this way. + // To circumvent this problem, the query is executed via the direct connection to the database and not via JDO. + Connection connection = null; + PreparedStatement preparedStatement = null; + ResultSet rs = null; + try { + connection = (Connection) pm.getDataStoreConnection(); + if (DbUtil.isMssql() || DbUtil.isOracle()) { // Microsoft SQL Server and Oracle DB already imply the "RECURSIVE" keyword in the "WITH" clause, therefore it is not needed in the query + preparedStatement = connection.prepareStatement("WITH " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); + } else { // Other Databases need the "RECURSIVE" keyword in the "WITH" clause to correctly execute the query + preparedStatement = connection.prepareStatement("WITH RECURSIVE " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); + } + int j = 1; + for (Long id : parameters) { + preparedStatement.setLong(j, id); + j++; + } + preparedStatement.execute(); + rs = preparedStatement.getResultSet(); + while (rs.next()) { + results.add(rs.getLong(1)); + } + } catch (Exception e) { + LOGGER.error(e.getMessage()); + params.put("false", false); + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND :false"); + } else { + queryFilter.append("WHERE :false"); + } + return; + } finally { + DbUtil.close(rs); + DbUtil.close(preparedStatement); + DbUtil.close(connection); + } + + // Add queried projects and descendants to the input filter of the query + if (results != null && !results.isEmpty()) { + final StringBuilder stringBuilderInputFilter = new StringBuilder(); + int j = 0; + int resultSize = results.size(); + for (Long id : results) { + stringBuilderInputFilter.append(" \"PROJECT\".\"ID\" = :id").append(j); + params.put("id" + j, id); + if (j < resultSize - 1) { + stringBuilderInputFilter.append(" OR "); + } + j++; + } + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND (").append(stringBuilderInputFilter).append(")"); + } else { + queryFilter.append("WHERE (").append(stringBuilderInputFilter).append(")"); + } + } + } else { + params.put("false", false); + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND :false"); + } else { + queryFilter.append("WHERE :false"); + } + } + } + } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 459546b36..8611c8985 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -48,6 +48,7 @@ import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.Finding; import org.dependencytrack.model.FindingAttribution; +import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; import org.dependencytrack.model.NotificationPublisher; @@ -1032,6 +1033,14 @@ public List getFindings(Project project, boolean includeSuppressed) { return getFindingsQueryManager().getFindings(project, includeSuppressed); } + public List getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { + return getFindingsQueryManager().getAllFindings(filters, showSuppressed, showInactive); + } + + public List getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { + return getFindingsQueryManager().getAllFindingsGroupedByVulnerability(filters, showInactive); + } + public List getVulnerabilityMetrics() { return getMetricsQueryManager().getVulnerabilityMetrics(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index cbcfc8fe4..7fe0ad306 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -36,6 +36,7 @@ import org.dependencytrack.integrations.FindingPackagingFormat; import org.dependencytrack.model.Component; import org.dependencytrack.model.Finding; +import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.Project; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.persistence.QueryManager; @@ -48,7 +49,9 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @@ -177,5 +180,115 @@ public Response analyzeProject( } } + @GET + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Returns a list of all findings", + response = Finding.class, + responseContainer = "List", + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings") + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + }) + @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) + public Response getAllFindings(@ApiParam(value = "Show inactive projects") + @QueryParam("showInactive") boolean showInactive, + @ApiParam(value = "Show suppressed findings") + @QueryParam("showSuppressed") boolean showSuppressed, + @ApiParam(value = "Filter by severity") + @QueryParam("severity") String severity, + @ApiParam(value = "Filter by analysis status") + @QueryParam("analysisStatus") String analysisStatus, + @ApiParam(value = "Filter by vendor response") + @QueryParam("vendorResponse") String vendorResponse, + @ApiParam(value = "Filter published from this date") + @QueryParam("publishDateFrom") String publishDateFrom, + @ApiParam(value = "Filter published to this date") + @QueryParam("publishDateTo") String publishDateTo, + @ApiParam(value = "Filter attributed on from this date") + @QueryParam("attributedOnDateFrom") String attributedOnDateFrom, + @ApiParam(value = "Filter attributed on to this date") + @QueryParam("attributedOnDateTo") String attributedOnDateTo, + @ApiParam(value = "Filter the text input in these fields") + @QueryParam("textSearchField") String textSearchField, + @ApiParam(value = "Filter by this text input") + @QueryParam("textSearchInput") String textSearchInput, + @ApiParam(value = "Filter CVSS from this value") + @QueryParam("cvssFrom") String cvssFrom, + @ApiParam(value = "Filter CVSS from this Value") + @QueryParam("cvssTo") String cvssTo) { + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + final Map filters = new HashMap<>(); + filters.put("severity", severity); + filters.put("analysisStatus", analysisStatus); + filters.put("vendorResponse", vendorResponse); + filters.put("publishDateFrom", publishDateFrom); + filters.put("publishDateTo", publishDateTo); + filters.put("attributedOnDateFrom", attributedOnDateFrom); + filters.put("attributedOnDateTo", attributedOnDateTo); + filters.put("textSearchField", textSearchField); + filters.put("textSearchInput", textSearchInput); + filters.put("cvssFrom", cvssFrom); + filters.put("cvssTo", cvssTo); + final List findings = qm.getAllFindings(filters, showSuppressed, showInactive); + return Response.ok(findings).header(TOTAL_COUNT_HEADER, findings.size()).build(); + } + } + + @GET + @Path("/grouped") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Returns a list of all findings grouped by vulnerability", + response = GroupedFinding.class, + responseContainer = "List", + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings") + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + }) + @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) + public Response getAllFindings(@ApiParam(value = "Show inactive projects") + @QueryParam("showInactive") boolean showInactive, + @ApiParam(value = "Filter by severity") + @QueryParam("severity") String severity, + @ApiParam(value = "Filter published from this date") + @QueryParam("publishDateFrom") String publishDateFrom, + @ApiParam(value = "Filter published to this date") + @QueryParam("publishDateTo") String publishDateTo, + @ApiParam(value = "Filter the text input in these fields") + @QueryParam("textSearchField") String textSearchField, + @ApiParam(value = "Filter by this text input") + @QueryParam("textSearchInput") String textSearchInput, + @ApiParam(value = "Filter CVSS from this value") + @QueryParam("cvssFrom") String cvssFrom, + @ApiParam(value = "Filter CVSS to this value") + @QueryParam("cvssTo") String cvssTo, + @ApiParam(value = "Filter occurrences in projects from this value") + @QueryParam("occurrencesFrom") String occurrencesFrom, + @ApiParam(value = "Filter occurrences in projects to this value") + @QueryParam("occurrencesTo") String occurrencesTo, + @ApiParam(value = "Filter first attributed on and last attributed on from this date") + @QueryParam("aggregatedAttributedOnDateFrom") String aggregatedAttributedOnDateFrom, + @ApiParam(value = "Filter first attributed on and last attributed on to this date") + @QueryParam("aggregatedAttributedOnDateTo") String aggregatedAttributedOnDateTo) { + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + final Map filters = new HashMap<>(); + filters.put("severity", severity); + filters.put("publishDateFrom", publishDateFrom); + filters.put("publishDateTo", publishDateTo); + filters.put("textSearchField", textSearchField); + filters.put("textSearchInput", textSearchInput); + filters.put("cvssFrom", cvssFrom); + filters.put("cvssTo", cvssTo); + filters.put("occurrencesFrom", occurrencesFrom); + filters.put("occurrencesTo", occurrencesTo); + filters.put("aggregatedAttributedOnDateFrom", aggregatedAttributedOnDateFrom); + filters.put("aggregatedAttributedOnDateTo", aggregatedAttributedOnDateTo); + final List findings = qm.getAllFindingsGroupedByVulnerability(filters, showInactive); + return Response.ok(findings).header(TOTAL_COUNT_HEADER, findings.size()).build(); + } + } } From 1f94afeda52fd8d18253a16f8784c4b0706b51f9 Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 7 Feb 2023 10:54:22 +0100 Subject: [PATCH 02/16] Add tests Adds test for the new class `GroupedFinding` and for the new methods in the `FindingResource`. Signed-off-by: RBickert --- .../model/GroupedFindingTest.java | 60 ++++ .../resources/v1/FindingResourceTest.java | 270 ++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/test/java/org/dependencytrack/model/GroupedFindingTest.java diff --git a/src/test/java/org/dependencytrack/model/GroupedFindingTest.java b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java new file mode 100644 index 000000000..f4ae216e4 --- /dev/null +++ b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java @@ -0,0 +1,60 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.model; + +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.tasks.scanners.AnalyzerIdentity; +import org.junit.Assert; +import org.junit.Test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.Map; + +public class GroupedFindingTest extends PersistenceCapableTest { + private Date published = new Date(); + + private Date firstOccurrence = new Date(); + + private Date lastOccurrence = new Date(); + + private GroupedFinding groupedFinding = new GroupedFinding("vuln-source", "vuln-vulnId", "vuln-title", + Severity.HIGH, AnalyzerIdentity.INTERNAL_ANALYZER, published, null, BigDecimal.valueOf(8.4), 3, firstOccurrence, lastOccurrence); + + + @Test + public void testVulnerability() { + Map map = groupedFinding.getVulnerability(); + Assert.assertEquals("vuln-source", map.get("source")); + Assert.assertEquals("vuln-vulnId", map.get("vulnId")); + Assert.assertEquals("vuln-title", map.get("title")); + Assert.assertEquals(Severity.HIGH, map.get("severity")); + Assert.assertEquals(published, map.get("published")); + Assert.assertEquals(BigDecimal.valueOf(8.4), map.get("cvssV3BaseScore")); + Assert.assertEquals(3, map.get("affectedProjectCount")); + } + + @Test + public void testAttribution() { + Map map = groupedFinding.getAttribution(); + Assert.assertEquals(AnalyzerIdentity.INTERNAL_ANALYZER, map.get("analyzerIdentity")); + Assert.assertEquals(firstOccurrence, map.get("firstOccurrence")); + Assert.assertEquals(lastOccurrence, map.get("lastOccurrence")); + } +} diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index d6c499e50..f84013877 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -19,15 +19,19 @@ package org.dependencytrack.resources.v1; import alpine.Config; +import alpine.model.ConfigProperty; +import alpine.model.Team; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Component; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.persistence.CweImporter; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; @@ -321,6 +325,272 @@ public void getFindingsByProjectWithComponentLatestVersionWithoutRepositoryMetaC Assert.assertThrows(NullPointerException.class, () -> json.getJsonObject(0).getJsonObject("component").getString("latestVersion")); } + @Test + public void getAllFindings() { + Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Project p1_child = qm.createProject("Acme Example", null, "1.0", null, p1, 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"); + Component c2 = createComponent(p1, "Component B", "1.0"); + Component c3 = createComponent(p1_child, "Component C", "1.0"); + Component c4 = createComponent(p2, "Component D", "1.0"); + Component c5 = createComponent(p2, "Component E", "1.0"); + Component c6 = createComponent(p2, "Component F", "1.0"); + Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL); + Vulnerability v2 = createVulnerability("Vuln-2", Severity.HIGH); + Vulnerability v3 = createVulnerability("Vuln-3", Severity.MEDIUM); + Vulnerability v4 = createVulnerability("Vuln-4", Severity.LOW); + Date date = new Date(); + v1.setPublished(date); + v2.setPublished(date); + v3.setPublished(date); + v4.setPublished(date); + qm.addVulnerability(v1, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c3, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); + qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); + Response response = target(V1_FINDING).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(String.valueOf(5), response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(5, json.size()); + Assert.assertEquals(date.getTime() ,json.getJsonObject(0).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1.getName() ,json.getJsonObject(0).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(0).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(0).getJsonObject("component").getString("project")); + Assert.assertEquals(date.getTime() ,json.getJsonObject(1).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1.getName() ,json.getJsonObject(1).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(1).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(1).getJsonObject("component").getString("project")); + Assert.assertEquals(date.getTime() ,json.getJsonObject(2).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1_child.getName() ,json.getJsonObject(2).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1_child.getVersion() ,json.getJsonObject(2).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1_child.getUuid().toString(), json.getJsonObject(2).getJsonObject("component").getString("project")); + Assert.assertEquals(date.getTime() ,json.getJsonObject(3).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1.getName() ,json.getJsonObject(3).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(3).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(3).getJsonObject("component").getString("project")); + Assert.assertEquals(date.getTime() ,json.getJsonObject(4).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p2.getName() ,json.getJsonObject(4).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p2.getVersion() ,json.getJsonObject(4).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p2.getUuid().toString(), json.getJsonObject(4).getJsonObject("component").getString("project")); + } + + @Test + public void getAllFindingsWithAclEnabled() { + Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Project p1_child = qm.createProject("Acme Example", null, "1.0", null, p1, null, true, false); + Project p2 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Team team = qm.createTeam("Team Acme", true); + p1.addAccessTeam(team); + Component c1 = createComponent(p1, "Component A", "1.0"); + Component c2 = createComponent(p1, "Component B", "1.0"); + Component c3 = createComponent(p1_child, "Component C", "1.0"); + Component c4 = createComponent(p2, "Component D", "1.0"); + Component c5 = createComponent(p2, "Component E", "1.0"); + Component c6 = createComponent(p2, "Component F", "1.0"); + Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL); + Vulnerability v2 = createVulnerability("Vuln-2", Severity.HIGH); + Vulnerability v3 = createVulnerability("Vuln-3", Severity.MEDIUM); + Vulnerability v4 = createVulnerability("Vuln-4", Severity.LOW); + Date date = new Date(); + v1.setPublished(date); + v2.setPublished(date); + v3.setPublished(date); + v4.setPublished(date); + qm.addVulnerability(v1, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c3, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); + qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); + ConfigProperty aclToggle = qm.getConfigProperty(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName()); + if (aclToggle == null) { + qm.createConfigProperty(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), "true", ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getDescription()); + } else { + aclToggle.setPropertyValue("true"); + qm.persist(aclToggle); + } + Response response = target(V1_FINDING).request() + .header(X_API_KEY, team.getApiKeys().get(0).getKey()) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(String.valueOf(4), response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(4, json.size()); + Assert.assertEquals(date.getTime() ,json.getJsonObject(0).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1.getName() ,json.getJsonObject(0).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(0).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(0).getJsonObject("component").getString("project")); + Assert.assertEquals(date.getTime() ,json.getJsonObject(1).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1.getName() ,json.getJsonObject(1).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(1).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(1).getJsonObject("component").getString("project")); + Assert.assertEquals(date.getTime() ,json.getJsonObject(2).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1_child.getName() ,json.getJsonObject(2).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1_child.getVersion() ,json.getJsonObject(2).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1_child.getUuid().toString(), json.getJsonObject(2).getJsonObject("component").getString("project")); + Assert.assertEquals(date.getTime() ,json.getJsonObject(3).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1.getName() ,json.getJsonObject(3).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(3).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(3).getJsonObject("component").getString("project")); + } + + @Test + public void getAllFindingsGroupedByVulnerability() { + Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Project p1_child = qm.createProject("Acme Example", null, "1.0", null, p1, 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"); + Component c2 = createComponent(p1, "Component B", "1.0"); + Component c3 = createComponent(p1_child, "Component C", "1.0"); + Component c4 = createComponent(p2, "Component D", "1.0"); + Component c5 = createComponent(p2, "Component E", "1.0"); + Component c6 = createComponent(p2, "Component F", "1.0"); + Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL); + Vulnerability v2 = createVulnerability("Vuln-2", Severity.HIGH); + Vulnerability v3 = createVulnerability("Vuln-3", Severity.MEDIUM); + Vulnerability v4 = createVulnerability("Vuln-4", Severity.LOW); + Date date = new Date(); + v1.setPublished(date); + v2.setPublished(date); + v3.setPublished(date); + v4.setPublished(date); + qm.addVulnerability(v1, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c3, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c4, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c6, AnalyzerIdentity.NONE); + qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); + Response response = target(V1_FINDING + "/grouped").request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(String.valueOf(4), response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(4, json.size()); + Assert.assertEquals("INTERNAL", json.getJsonObject(0).getJsonObject("vulnerability").getString("source")); + Assert.assertEquals("Vuln-1", json.getJsonObject(0).getJsonObject("vulnerability").getString("vulnId")); + Assert.assertEquals(Severity.CRITICAL.name(), json.getJsonObject(0).getJsonObject("vulnerability").getString("severity")); + Assert.assertEquals("NONE", json.getJsonObject(0).getJsonObject("attribution").getString("analyzerIdentity")); + Assert.assertEquals(date.getTime(), json.getJsonObject(0).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(2, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").size()); + Assert.assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); + Assert.assertEquals(666, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); + Assert.assertEquals(1, json.getJsonObject(0).getJsonObject("vulnerability").getInt("affectedProjectCount")); + + Assert.assertEquals("INTERNAL", json.getJsonObject(1).getJsonObject("vulnerability").getString("source")); + Assert.assertEquals("Vuln-2", json.getJsonObject(1).getJsonObject("vulnerability").getString("vulnId")); + Assert.assertEquals(Severity.HIGH.name(), json.getJsonObject(1).getJsonObject("vulnerability").getString("severity")); + Assert.assertEquals("NONE", json.getJsonObject(1).getJsonObject("attribution").getString("analyzerIdentity")); + Assert.assertEquals(date.getTime(), json.getJsonObject(1).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").size()); + Assert.assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); + Assert.assertEquals(666, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); + Assert.assertEquals(3, json.getJsonObject(1).getJsonObject("vulnerability").getInt("affectedProjectCount")); + + Assert.assertEquals("INTERNAL", json.getJsonObject(2).getJsonObject("vulnerability").getString("source")); + Assert.assertEquals("Vuln-3", json.getJsonObject(2).getJsonObject("vulnerability").getString("vulnId")); + Assert.assertEquals(Severity.MEDIUM.name(), json.getJsonObject(2).getJsonObject("vulnerability").getString("severity")); + Assert.assertEquals("NONE", json.getJsonObject(2).getJsonObject("attribution").getString("analyzerIdentity")); + Assert.assertEquals(date.getTime(), json.getJsonObject(2).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(2, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").size()); + Assert.assertEquals(80, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); + Assert.assertEquals(666, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); + Assert.assertEquals(2, json.getJsonObject(2).getJsonObject("vulnerability").getInt("affectedProjectCount")); + + Assert.assertEquals("INTERNAL", json.getJsonObject(3).getJsonObject("vulnerability").getString("source")); + Assert.assertEquals("Vuln-4", json.getJsonObject(3).getJsonObject("vulnerability").getString("vulnId")); + Assert.assertEquals(Severity.LOW.name(), json.getJsonObject(3).getJsonObject("vulnerability").getString("severity")); + Assert.assertEquals("NONE", json.getJsonObject(3).getJsonObject("attribution").getString("analyzerIdentity")); + Assert.assertEquals(date.getTime(), json.getJsonObject(3).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(2, json.getJsonObject(3).getJsonObject("vulnerability").getJsonArray("cwes").size()); + Assert.assertEquals(80, json.getJsonObject(3).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); + Assert.assertEquals(666, json.getJsonObject(3).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); + Assert.assertEquals(1, json.getJsonObject(3).getJsonObject("vulnerability").getInt("affectedProjectCount")); + } + + @Test + public void getAllFindingsGroupedByVulnerabilityWithAclEnabled() { + Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Project p1_child = qm.createProject("Acme Example", null, "1.0", null, p1, null, true, false); + Project p2 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Team team = qm.createTeam("Team Acme", true); + p1.addAccessTeam(team); + Component c1 = createComponent(p1, "Component A", "1.0"); + Component c2 = createComponent(p1, "Component B", "1.0"); + Component c3 = createComponent(p1_child, "Component C", "1.0"); + Component c4 = createComponent(p2, "Component D", "1.0"); + Component c5 = createComponent(p2, "Component E", "1.0"); + Component c6 = createComponent(p2, "Component F", "1.0"); + Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL); + Vulnerability v2 = createVulnerability("Vuln-2", Severity.HIGH); + Vulnerability v3 = createVulnerability("Vuln-3", Severity.MEDIUM); + Vulnerability v4 = createVulnerability("Vuln-4", Severity.LOW); + Date date = new Date(); + v1.setPublished(date); + v2.setPublished(date); + v3.setPublished(date); + v4.setPublished(date); + qm.addVulnerability(v1, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c3, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c4, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c6, AnalyzerIdentity.NONE); + qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); + ConfigProperty aclToggle = qm.getConfigProperty(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName()); + if (aclToggle == null) { + qm.createConfigProperty(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), "true", ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getDescription()); + } else { + aclToggle.setPropertyValue("true"); + qm.persist(aclToggle); + } + Response response = target(V1_FINDING + "/grouped").request() + .header(X_API_KEY, team.getApiKeys().get(0).getKey()) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(String.valueOf(3), response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(3, json.size()); + Assert.assertEquals("INTERNAL", json.getJsonObject(0).getJsonObject("vulnerability").getString("source")); + Assert.assertEquals("Vuln-1", json.getJsonObject(0).getJsonObject("vulnerability").getString("vulnId")); + Assert.assertEquals(Severity.CRITICAL.name(), json.getJsonObject(0).getJsonObject("vulnerability").getString("severity")); + Assert.assertEquals("NONE", json.getJsonObject(0).getJsonObject("attribution").getString("analyzerIdentity")); + Assert.assertEquals(date.getTime(), json.getJsonObject(0).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(2, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").size()); + Assert.assertEquals(80, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); + Assert.assertEquals(666, json.getJsonObject(0).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); + Assert.assertEquals(1, json.getJsonObject(0).getJsonObject("vulnerability").getInt("affectedProjectCount")); + + Assert.assertEquals("INTERNAL", json.getJsonObject(1).getJsonObject("vulnerability").getString("source")); + Assert.assertEquals("Vuln-2", json.getJsonObject(1).getJsonObject("vulnerability").getString("vulnId")); + Assert.assertEquals(Severity.HIGH.name(), json.getJsonObject(1).getJsonObject("vulnerability").getString("severity")); + Assert.assertEquals("NONE", json.getJsonObject(1).getJsonObject("attribution").getString("analyzerIdentity")); + Assert.assertEquals(date.getTime(), json.getJsonObject(1).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").size()); + Assert.assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); + Assert.assertEquals(666, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); + Assert.assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getInt("affectedProjectCount")); + + Assert.assertEquals("INTERNAL", json.getJsonObject(2).getJsonObject("vulnerability").getString("source")); + Assert.assertEquals("Vuln-3", json.getJsonObject(2).getJsonObject("vulnerability").getString("vulnId")); + Assert.assertEquals(Severity.MEDIUM.name(), json.getJsonObject(2).getJsonObject("vulnerability").getString("severity")); + Assert.assertEquals("NONE", json.getJsonObject(2).getJsonObject("attribution").getString("analyzerIdentity")); + Assert.assertEquals(date.getTime(), json.getJsonObject(2).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(2, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").size()); + Assert.assertEquals(80, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); + Assert.assertEquals(666, json.getJsonObject(2).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); + Assert.assertEquals(1, json.getJsonObject(2).getJsonObject("vulnerability").getInt("affectedProjectCount")); + } + private Component createComponent(Project project, String name, String version) { Component component = new Component(); component.setProject(project); From ace17281bf1e4d6eb362c6a226bf0e7e0080a1c9 Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 7 Feb 2023 16:38:57 +0100 Subject: [PATCH 03/16] Fix for PostgreSQL and MSSQL Signed-off-by: RBickert --- .../dependencytrack/model/GroupedFinding.java | 2 +- .../persistence/FindingsQueryManager.java | 44 ++++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/GroupedFinding.java b/src/main/java/org/dependencytrack/model/GroupedFinding.java index d55c7ffc8..3cfef7942 100644 --- a/src/main/java/org/dependencytrack/model/GroupedFinding.java +++ b/src/main/java/org/dependencytrack/model/GroupedFinding.java @@ -48,7 +48,7 @@ public class GroupedFinding implements Serializable { COUNT(DISTINCT "PROJECT"."ID") AS "AFFECTED_PROJECT_COUNT", MIN("AFFECTEDVERSIONATTRIBUTION"."FIRST_SEEN") AS "FIRST_OCCURRENCE", MAX("AFFECTEDVERSIONATTRIBUTION"."LAST_SEEN") AS "LAST_OCCURRENCE" - FROM COMPONENT + FROM "COMPONENT" INNER JOIN "COMPONENTS_VULNERABILITIES" ON ("COMPONENT"."ID" = "COMPONENTS_VULNERABILITIES"."COMPONENT_ID") INNER JOIN "VULNERABILITY" ON ("COMPONENTS_VULNERABILITIES"."VULNERABILITY_ID" = "VULNERABILITY"."ID") INNER JOIN "FINDINGATTRIBUTION" ON ("COMPONENT"."ID" = "FINDINGATTRIBUTION"."COMPONENT_ID") AND ("VULNERABILITY"."ID" = "FINDINGATTRIBUTION"."VULNERABILITY_ID") diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java index d0decf63c..8ee2cb9f8 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java @@ -424,7 +424,17 @@ private void processFilters(Map filters, StringBuilder queryFilt } preprocessACLs(queryFilter, params); if (isGroupedByVulnerabilities) { - queryFilter.append(" GROUP BY \"VULNERABILITY\".\"ID\""); + queryFilter.append(""" + GROUP BY "VULNERABILITY"."ID",\s + "VULNERABILITY"."SOURCE",\s + "VULNERABILITY"."VULNID",\s + "VULNERABILITY"."TITLE",\s + "VULNERABILITY"."SEVERITY", + "FINDINGATTRIBUTION"."ANALYZERIDENTITY", + "VULNERABILITY"."PUBLISHED", + "VULNERABILITY"."CWES", + "VULNERABILITY"."CVSSV3BASESCORE" + """); StringBuilder aggregateFilter = new StringBuilder(); processAggregateFilters(filters, aggregateFilter, params); queryFilter.append(aggregateFilter); @@ -474,12 +484,21 @@ private void processRangeFilter(StringBuilder queryFilter, Map p } else { queryFilter.append(" AND ("); } - queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); - String value = filter; - if (isDate) { - value += (fromValue ? " 00:00:00" : " 23:59:59"); + if (DbUtil.isPostgreSQL()) { + queryFilter.append(column).append(fromValue ? " >= " : " <= "); + if (isDate) { + queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); + } else { + queryFilter.append("CAST('").append(filter).append("' AS NUMERIC)"); + } + } else { + queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); + String value = filter; + if (isDate) { + value += (fromValue ? " 00:00:00" : " 23:59:59"); + } + params.put(paramName, value); } - params.put(paramName, value); queryFilter.append(")"); } } @@ -491,10 +510,15 @@ private void processAggregatedDateRangeFilter(StringBuilder queryFilter, Map= :" : " <= :").append(paramName); - String value = filter; - value += (fromValue ? " 00:00:00" : " 23:59:59"); - params.put(paramName, value); + if (DbUtil.isPostgreSQL()) { + queryFilter.append(column).append(fromValue ? " >= " : " <= "); + queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); + } else { + queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); + String value = filter; + value += (fromValue ? " 00:00:00" : " 23:59:59"); + params.put(paramName, value); + } if (!isMin) { queryFilter.append(")"); } From 38f9034332c0e5d5e20b10cf406f096911c0f5af Mon Sep 17 00:00:00 2001 From: RBickert Date: Wed, 8 Feb 2023 10:30:31 +0100 Subject: [PATCH 04/16] Put logic for new API methods in dedicated class Calculate severity if NULL in database Adjust tests Signed-off-by: RBickert --- .../dependencytrack/model/GroupedFinding.java | 25 +- .../persistence/FindingsQueryManager.java | 383 ---------------- .../FindingsSearchQueryManager.java | 429 ++++++++++++++++++ .../persistence/QueryManager.java | 17 +- .../model/GroupedFindingTest.java | 2 +- 5 files changed, 461 insertions(+), 395 deletions(-) create mode 100644 src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java diff --git a/src/main/java/org/dependencytrack/model/GroupedFinding.java b/src/main/java/org/dependencytrack/model/GroupedFinding.java index 3cfef7942..2ecd68729 100644 --- a/src/main/java/org/dependencytrack/model/GroupedFinding.java +++ b/src/main/java/org/dependencytrack/model/GroupedFinding.java @@ -19,7 +19,10 @@ package org.dependencytrack.model; import com.fasterxml.jackson.annotation.JsonInclude; +import org.dependencytrack.util.VulnerabilityUtil; + import java.io.Serializable; +import java.math.BigDecimal; import java.util.LinkedHashMap; import java.util.Map; @@ -41,10 +44,14 @@ public class GroupedFinding implements Serializable { "VULNERABILITY"."VULNID", "VULNERABILITY"."TITLE", "VULNERABILITY"."SEVERITY", + "VULNERABILITY"."CVSSV2BASESCORE", + "VULNERABILITY"."CVSSV3BASESCORE", + "VULNERABILITY"."OWASPRRLIKELIHOODSCORE", + "VULNERABILITY"."OWASPRRTECHNICALIMPACTSCORE", + "VULNERABILITY"."OWASPRRBUSINESSIMPACTSCORE", "FINDINGATTRIBUTION"."ANALYZERIDENTITY", "VULNERABILITY"."PUBLISHED", "VULNERABILITY"."CWES", - "VULNERABILITY"."CVSSV3BASESCORE", COUNT(DISTINCT "PROJECT"."ID") AS "AFFECTED_PROJECT_COUNT", MIN("AFFECTEDVERSIONATTRIBUTION"."FIRST_SEEN") AS "FIRST_OCCURRENCE", MAX("AFFECTEDVERSIONATTRIBUTION"."LAST_SEEN") AS "LAST_OCCURRENCE" @@ -64,14 +71,14 @@ public GroupedFinding(Object ...o) { optValue(vulnerability, "source", o[0]); optValue(vulnerability, "vulnId", o[1]); optValue(vulnerability, "title", o[2]); - optValue(vulnerability, "severity", o[3]); - optValue(attribution, "analyzerIdentity", o[4]); - optValue(vulnerability, "published", o[5]); - optValue(vulnerability, "cwes", Finding.getCwes(o[6])); - optValue(vulnerability, "cvssV3BaseScore", o[7]); - optValue(vulnerability, "affectedProjectCount", o[8]); - optValue(attribution, "firstOccurrence", o[9]); - optValue(attribution, "lastOccurrence", o[10]); + optValue(vulnerability, "severity", VulnerabilityUtil.getSeverity(o[3], (BigDecimal) o[4], (BigDecimal) o[5], (BigDecimal) o[6], (BigDecimal) o[7], (BigDecimal) o[8])); + optValue(vulnerability, "cvssV3BaseScore", o[5]); + optValue(attribution, "analyzerIdentity", o[9]); + optValue(vulnerability, "published", o[10]); + optValue(vulnerability, "cwes", Finding.getCwes(o[11])); + optValue(vulnerability, "affectedProjectCount", o[12]); + optValue(attribution, "firstOccurrence", o[13]); + optValue(attribution, "lastOccurrence", o[14]); } public Map getVulnerability() { diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java index 8ee2cb9f8..a22c4031e 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java @@ -18,12 +18,7 @@ */ package org.dependencytrack.persistence; -import alpine.common.logging.Logger; -import alpine.model.ApiKey; -import alpine.model.Team; -import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; -import alpine.server.util.DbUtil; import com.github.packageurl.PackageURL; import org.datanucleus.api.jdo.JDOQuery; import org.dependencytrack.model.Analysis; @@ -32,9 +27,7 @@ import org.dependencytrack.model.AnalysisResponse; import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Component; -import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Finding; -import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.Project; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; @@ -43,36 +36,12 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Map; -import java.util.HashMap; -import java.util.UUID; public class FindingsQueryManager extends QueryManager implements IQueryManager { - private static final Logger LOGGER = Logger.getLogger(FindingsQueryManager.class); - - public static final String QUERY_ACL_1 = """ - "DESCENDANTS" ("ID", "NAME") AS - (SELECT "PROJECT"."ID", - "PROJECT"."NAME" - FROM "PROJECT" - """; - - public static final String QUERY_ACL_2 = """ - UNION ALL - SELECT "CHILD"."ID", - "CHILD"."NAME" - FROM "PROJECT" "CHILD" - JOIN "DESCENDANTS" - ON "DESCENDANTS"."ID" = "CHILD"."PARENT_PROJECT_ID") - SELECT "DESCENDANTS"."ID", "DESCENDANTS"."NAME" FROM "DESCENDANTS" - """; /** * Constructs a new QueryManager. @@ -326,356 +295,4 @@ public List getFindings(Project project, boolean includeSuppressed) { } return findings; } - - /** - * Returns a List of all Finding objects filtered by ACL and other optional filters. - * @param filters determines the filters to apply on the list of Finding objects - * @param showSuppressed determines if suppressed vulnerabilities should be included or not - * @param showInactive determines if inactive projects should be included or not - * @return a List of Finding objects - */ - public List getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { - StringBuilder queryFilter = new StringBuilder(); - Map params = new HashMap<>(); - if (showInactive) { - queryFilter.append(" WHERE (\"PROJECT\".\"ACTIVE\" = :active OR \"PROJECT\".\"ACTIVE\" IS NULL)"); - params.put("active", true); - } - if (!showSuppressed) { - if (queryFilter.length() == 0) { - queryFilter.append(" WHERE "); - } else { - queryFilter.append(" AND "); - } - queryFilter.append("(\"ANALYSIS\".\"SUPPRESSED\" = :showSuppressed OR \"ANALYSIS\".\"SUPPRESSED\" IS NULL)"); - params.put("showSuppressed", false); - } - processFilters(filters, queryFilter, params, false); - final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter); - query.setNamedParameters(params); - final List list = query.executeList(); - final List findings = new ArrayList<>(); - for (final Object[] o: list) { - final Finding finding = new Finding(UUID.fromString((String) o[29]), 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)); - aliases.forEach(alias -> alias.setUuid(null)); - finding.getVulnerability().put("aliases", aliases); - // 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; - } - - /** - * Returns a List of all Finding objects filtered by ACL and other optional filters. The resulting list is grouped by vulnerability. - * @param filters determines the filters to apply on the list of Finding objects - * @param showInactive determines if inactive projects should be included or not - * @return a List of Finding objects - */ - public List getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { - StringBuilder queryFilter = new StringBuilder(); - Map params = new HashMap<>(); - if (showInactive) { - queryFilter.append(" WHERE (\"PROJECT\".\"ACTIVE\" = :active OR \"PROJECT\".\"ACTIVE\" IS NULL)"); - params.put("active", true); - } - processFilters(filters, queryFilter, params, true); - final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, GroupedFinding.QUERY + queryFilter); - query.setNamedParameters(params); - final List list = query.executeList(); - final List findings = new ArrayList<>(); - for (Object[] o : list) { - final GroupedFinding finding = new GroupedFinding(o); - findings.add(finding); - } - return findings; - } - - private void processFilters(Map filters, StringBuilder queryFilter, Map params, boolean isGroupedByVulnerabilities) { - for (String filter : filters.keySet()) { - switch (filter) { - case "severity" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"SEVERITY\""); - case "analysisStatus" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"ANALYSIS\".\"STATE\""); - case "vendorResponse" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"ANALYSIS\".\"RESPONSE\""); - case "publishDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"PUBLISHED\"", true, true, false); - case "publishDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"PUBLISHED\"", false, true, false); - case "attributedOnDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", true, true, false); - case "attributedOnDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", false, true, false); - case "textSearchField" -> processInputFilter(queryFilter, params, filter, filters.get(filter), filters.get("textSearchInput")); - case "cvssFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", true, false, false); - case "cvssTo" -> processRangeFilter(queryFilter, params,filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", false, false, false); - } - } - preprocessACLs(queryFilter, params); - if (isGroupedByVulnerabilities) { - queryFilter.append(""" - GROUP BY "VULNERABILITY"."ID",\s - "VULNERABILITY"."SOURCE",\s - "VULNERABILITY"."VULNID",\s - "VULNERABILITY"."TITLE",\s - "VULNERABILITY"."SEVERITY", - "FINDINGATTRIBUTION"."ANALYZERIDENTITY", - "VULNERABILITY"."PUBLISHED", - "VULNERABILITY"."CWES", - "VULNERABILITY"."CVSSV3BASESCORE" - """); - StringBuilder aggregateFilter = new StringBuilder(); - processAggregateFilters(filters, aggregateFilter, params); - queryFilter.append(aggregateFilter); - } - } - - private void processAggregateFilters(Map filters, StringBuilder queryFilter, Map params) { - for (String filter : filters.keySet()) { - switch (filter) { - case "occurrencesFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", true, false, true); - case "occurrencesTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", false, false, true); - case "aggregatedAttributedOnDateFrom" -> { - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", true, true); - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", true, false); - } - case "aggregatedAttributedOnDateTo" -> { - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", false, true); - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", false, false); - } - } - } - } - - private void processArrayFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column) { - if (filter != null && !filter.isEmpty()) { - if (queryFilter.length() == 0) { - queryFilter.append(" WHERE ("); - } else { - queryFilter.append(" AND ("); - } - String[] filters = filter.split(","); - for (int i = 0, length = filters.length; i < length; i++) { - queryFilter.append(column).append(" = :").append(paramName).append(i); - params.put(paramName + i, filters[i].toUpperCase()); - if (i < length-1) { - queryFilter.append(" OR "); - } - } - queryFilter.append(")"); - } - } - - private void processRangeFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column, boolean fromValue, boolean isDate, boolean isAggregateFilter) { - if (filter != null && !filter.isEmpty()) { - if (queryFilter.length() == 0) { - queryFilter.append(isAggregateFilter ? " HAVING (" : " WHERE ("); - } else { - queryFilter.append(" AND ("); - } - if (DbUtil.isPostgreSQL()) { - queryFilter.append(column).append(fromValue ? " >= " : " <= "); - if (isDate) { - queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); - } else { - queryFilter.append("CAST('").append(filter).append("' AS NUMERIC)"); - } - } else { - queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); - String value = filter; - if (isDate) { - value += (fromValue ? " 00:00:00" : " 23:59:59"); - } - params.put(paramName, value); - } - queryFilter.append(")"); - } - } - - private void processAggregatedDateRangeFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column, boolean fromValue, boolean isMin) { - if (filter != null && !filter.isEmpty()) { - if (queryFilter.length() == 0) { - queryFilter.append(" HAVING ("); - } else { - queryFilter.append(isMin ? " AND (" : " OR "); - } - if (DbUtil.isPostgreSQL()) { - queryFilter.append(column).append(fromValue ? " >= " : " <= "); - queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); - } else { - queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); - String value = filter; - value += (fromValue ? " 00:00:00" : " 23:59:59"); - params.put(paramName, value); - } - if (!isMin) { - queryFilter.append(")"); - } - } - } - - private void processInputFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String input) { - if (filter != null && !filter.isEmpty() && input != null && !input.isEmpty()) { - if (queryFilter.length() == 0) { - queryFilter.append(" WHERE ("); - } else { - queryFilter.append(" AND ("); - } - String[] filters = filter.split(","); - for (int i = 0, length = filters.length; i < length; i++) { - switch (filters[i].toUpperCase()) { - case "VULNERABILITY_ID" -> queryFilter.append("\"VULNERABILITY\".\"VULNID\""); - case "VULNERABILITY_TITLE" -> queryFilter.append("\"VULNERABILITY\".\"TITLE\""); - case "COMPONENT_NAME" -> queryFilter.append("\"COMPONENT\".\"NAME\""); - case "COMPONENT_VERSION" -> queryFilter.append("\"COMPONENT\".\"VERSION\""); - case "PROJECT_NAME" -> queryFilter.append("concat(\"PROJECT\".\"NAME\", ' ', \"PROJECT\".\"VERSION\")"); - } - queryFilter.append(" LIKE :").append(paramName); - if (i < length-1) { - queryFilter.append(" OR "); - } - } - if (filters.length > 0) { - params.put(paramName, "%" + input + "%"); - } - queryFilter.append(")"); - } - } - - private void preprocessACLs(StringBuilder queryFilter, final Map params) { - if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { - final List teams; - if (super.principal instanceof UserPrincipal) { - final UserPrincipal userPrincipal = ((UserPrincipal) super.principal); - teams = userPrincipal.getTeams(); - if (super.hasAccessManagementPermission(userPrincipal)) { - return; - } - } else { - final ApiKey apiKey = ((ApiKey) super.principal); - teams = apiKey.getTeams(); - if (super.hasAccessManagementPermission(apiKey)) { - return; - } - } - - // Query every project that the teams have access to - final Map tempParams = new HashMap<>(); - final Query queryAclProjects = pm.newQuery(Project.class); - if (teams != null && teams.size() > 0) { - final StringBuilder stringBuilderAclProjects = new StringBuilder(); - for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) { - final Team team = super.getObjectById(Team.class, teams.get(i).getId()); - stringBuilderAclProjects.append(" accessTeams.contains(:team").append(i).append(") "); - tempParams.put("team" + i, team); - if (i < teamsSize - 1) { - stringBuilderAclProjects.append(" || "); - } - } - queryAclProjects.setFilter(stringBuilderAclProjects.toString()); - } else { - params.put("false", false); - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND :false"); - } else { - queryFilter.append("WHERE :false"); - } - } - List result = (List) queryAclProjects.executeWithMap(tempParams); - // Query the descendants of the projects that the teams have access to - if (result != null && !result.isEmpty()) { - final StringBuilder stringBuilderDescendants = new StringBuilder(); - final List parameters = new ArrayList<>(); - stringBuilderDescendants.append("WHERE"); - int i = 0, teamSize = result.size(); - for (Project project : result) { - stringBuilderDescendants.append(" \"ID\" = ?").append(" "); - parameters.add(project.getId()); - if (i < teamSize - 1) { - stringBuilderDescendants.append(" OR"); - } - i++; - } - stringBuilderDescendants.append("\n"); - final List results = new ArrayList<>(); - - // Querying the descendants of projects requires a CTE (Common Table Expression), which needs to be at the top-level of the query for Microsoft SQL Server. - // Because of JDO, queries are only allowed to start with "SELECT", so the "WITH" clause for the CTE in MSSQL cannot be at top level. - // Activating the JDO property that queries don't have to start with "SELECT" does not help in this case, because JDO queries that do not start with "SELECT" only return "true", so no data can be fetched this way. - // To circumvent this problem, the query is executed via the direct connection to the database and not via JDO. - Connection connection = null; - PreparedStatement preparedStatement = null; - ResultSet rs = null; - try { - connection = (Connection) pm.getDataStoreConnection(); - if (DbUtil.isMssql() || DbUtil.isOracle()) { // Microsoft SQL Server and Oracle DB already imply the "RECURSIVE" keyword in the "WITH" clause, therefore it is not needed in the query - preparedStatement = connection.prepareStatement("WITH " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); - } else { // Other Databases need the "RECURSIVE" keyword in the "WITH" clause to correctly execute the query - preparedStatement = connection.prepareStatement("WITH RECURSIVE " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); - } - int j = 1; - for (Long id : parameters) { - preparedStatement.setLong(j, id); - j++; - } - preparedStatement.execute(); - rs = preparedStatement.getResultSet(); - while (rs.next()) { - results.add(rs.getLong(1)); - } - } catch (Exception e) { - LOGGER.error(e.getMessage()); - params.put("false", false); - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND :false"); - } else { - queryFilter.append("WHERE :false"); - } - return; - } finally { - DbUtil.close(rs); - DbUtil.close(preparedStatement); - DbUtil.close(connection); - } - - // Add queried projects and descendants to the input filter of the query - if (results != null && !results.isEmpty()) { - final StringBuilder stringBuilderInputFilter = new StringBuilder(); - int j = 0; - int resultSize = results.size(); - for (Long id : results) { - stringBuilderInputFilter.append(" \"PROJECT\".\"ID\" = :id").append(j); - params.put("id" + j, id); - if (j < resultSize - 1) { - stringBuilderInputFilter.append(" OR "); - } - j++; - } - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND (").append(stringBuilderInputFilter).append(")"); - } else { - queryFilter.append("WHERE (").append(stringBuilderInputFilter).append(")"); - } - } - } else { - params.put("false", false); - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND :false"); - } else { - queryFilter.append("WHERE :false"); - } - } - } - } } diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java new file mode 100644 index 000000000..f02cb0ec2 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -0,0 +1,429 @@ +package org.dependencytrack.persistence; + +import alpine.common.logging.Logger; +import alpine.model.ApiKey; +import alpine.model.Team; +import alpine.model.UserPrincipal; +import alpine.resources.AlpineRequest; +import alpine.server.util.DbUtil; +import com.github.packageurl.PackageURL; +import org.datanucleus.api.jdo.JDOQuery; +import org.dependencytrack.model.Analysis; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.Finding; +import org.dependencytrack.model.GroupedFinding; +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 javax.jdo.PersistenceManager; +import javax.jdo.Query; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class FindingsSearchQueryManager extends QueryManager implements IQueryManager { + + private static final Logger LOGGER = Logger.getLogger(FindingsSearchQueryManager.class); + + /** + * Constructs a new QueryManager. + * @param pm a PersistenceManager object + */ + FindingsSearchQueryManager(final PersistenceManager pm) { + super(pm); + } + + /** + * Constructs a new QueryManager. + * @param pm a PersistenceManager object + * @param request an AlpineRequest object + */ + FindingsSearchQueryManager(final PersistenceManager pm, final AlpineRequest request) { + super(pm, request); + } + + public static final String QUERY_ACL_1 = """ + "DESCENDANTS" ("ID", "NAME") AS + (SELECT "PROJECT"."ID", + "PROJECT"."NAME" + FROM "PROJECT" + """; + + public static final String QUERY_ACL_2 = """ + UNION ALL + SELECT "CHILD"."ID", + "CHILD"."NAME" + FROM "PROJECT" "CHILD" + JOIN "DESCENDANTS" + ON "DESCENDANTS"."ID" = "CHILD"."PARENT_PROJECT_ID") + SELECT "DESCENDANTS"."ID", "DESCENDANTS"."NAME" FROM "DESCENDANTS" + """; + + /** + * Returns a List of all Finding objects filtered by ACL and other optional filters. + * @param filters determines the filters to apply on the list of Finding objects + * @param showSuppressed determines if suppressed vulnerabilities should be included or not + * @param showInactive determines if inactive projects should be included or not + * @return a List of Finding objects + */ + public List getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { + StringBuilder queryFilter = new StringBuilder(); + Map params = new HashMap<>(); + if (showInactive) { + queryFilter.append(" WHERE (\"PROJECT\".\"ACTIVE\" = :active OR \"PROJECT\".\"ACTIVE\" IS NULL)"); + params.put("active", true); + } + if (!showSuppressed) { + if (queryFilter.length() == 0) { + queryFilter.append(" WHERE "); + } else { + queryFilter.append(" AND "); + } + queryFilter.append("(\"ANALYSIS\".\"SUPPRESSED\" = :showSuppressed OR \"ANALYSIS\".\"SUPPRESSED\" IS NULL)"); + params.put("showSuppressed", false); + } + processFilters(filters, queryFilter, params, false); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter); + query.setNamedParameters(params); + final List list = query.executeList(); + final List findings = new ArrayList<>(); + for (final Object[] o: list) { + final Finding finding = new Finding(UUID.fromString((String) o[29]), 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)); + aliases.forEach(alias -> alias.setUuid(null)); + finding.getVulnerability().put("aliases", aliases); + // 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; + } + + /** + * Returns a List of all Finding objects filtered by ACL and other optional filters. The resulting list is grouped by vulnerability. + * @param filters determines the filters to apply on the list of Finding objects + * @param showInactive determines if inactive projects should be included or not + * @return a List of Finding objects + */ + public List getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { + StringBuilder queryFilter = new StringBuilder(); + Map params = new HashMap<>(); + if (showInactive) { + queryFilter.append(" WHERE (\"PROJECT\".\"ACTIVE\" = :active OR \"PROJECT\".\"ACTIVE\" IS NULL)"); + params.put("active", true); + } + processFilters(filters, queryFilter, params, true); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, GroupedFinding.QUERY + queryFilter); + query.setNamedParameters(params); + final List list = query.executeList(); + final List findings = new ArrayList<>(); + for (Object[] o : list) { + final GroupedFinding finding = new GroupedFinding(o); + findings.add(finding); + } + return findings; + } + + private void processFilters(Map filters, StringBuilder queryFilter, Map params, boolean isGroupedByVulnerabilities) { + for (String filter : filters.keySet()) { + switch (filter) { + case "severity" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"SEVERITY\""); + case "analysisStatus" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"ANALYSIS\".\"STATE\""); + case "vendorResponse" -> processArrayFilter(queryFilter, params, filter, filters.get(filter), "\"ANALYSIS\".\"RESPONSE\""); + case "publishDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"PUBLISHED\"", true, true, false); + case "publishDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"PUBLISHED\"", false, true, false); + case "attributedOnDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", true, true, false); + case "attributedOnDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", false, true, false); + case "textSearchField" -> processInputFilter(queryFilter, params, filter, filters.get(filter), filters.get("textSearchInput")); + case "cvssFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", true, false, false); + case "cvssTo" -> processRangeFilter(queryFilter, params,filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", false, false, false); + } + } + preprocessACLs(queryFilter, params); + if (isGroupedByVulnerabilities) { + queryFilter.append(""" + GROUP BY "VULNERABILITY"."ID",\s + "VULNERABILITY"."SOURCE",\s + "VULNERABILITY"."VULNID",\s + "VULNERABILITY"."TITLE",\s + "VULNERABILITY"."SEVERITY", + "VULNERABILITY"."CVSSV2BASESCORE", + "VULNERABILITY"."CVSSV3BASESCORE", + "VULNERABILITY"."OWASPRRLIKELIHOODSCORE", + "VULNERABILITY"."OWASPRRTECHNICALIMPACTSCORE", + "VULNERABILITY"."OWASPRRBUSINESSIMPACTSCORE", + "FINDINGATTRIBUTION"."ANALYZERIDENTITY", + "VULNERABILITY"."PUBLISHED", + "VULNERABILITY"."CWES" + """); + StringBuilder aggregateFilter = new StringBuilder(); + processAggregateFilters(filters, aggregateFilter, params); + queryFilter.append(aggregateFilter); + } + } + + private void processAggregateFilters(Map filters, StringBuilder queryFilter, Map params) { + for (String filter : filters.keySet()) { + switch (filter) { + case "occurrencesFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", true, false, true); + case "occurrencesTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", false, false, true); + case "aggregatedAttributedOnDateFrom" -> { + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", true, true); + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", true, false); + } + case "aggregatedAttributedOnDateTo" -> { + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", false, true); + processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", false, false); + } + } + } + } + + private void processArrayFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column) { + if (filter != null && !filter.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(" WHERE ("); + } else { + queryFilter.append(" AND ("); + } + String[] filters = filter.split(","); + for (int i = 0, length = filters.length; i < length; i++) { + queryFilter.append(column).append(" = :").append(paramName).append(i); + params.put(paramName + i, filters[i].toUpperCase()); + if (filters[i].equals("NOT_SET") && (paramName.equals("analysisStatus") || paramName.equals("vendorResponse"))) { + queryFilter.append(" OR ").append(column).append(" IS NULL"); + } + if (i < length-1) { + queryFilter.append(" OR "); + } + } + queryFilter.append(")"); + } + } + + private void processRangeFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column, boolean fromValue, boolean isDate, boolean isAggregateFilter) { + if (filter != null && !filter.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(isAggregateFilter ? " HAVING (" : " WHERE ("); + } else { + queryFilter.append(" AND ("); + } + if (DbUtil.isPostgreSQL()) { + queryFilter.append(column).append(fromValue ? " >= " : " <= "); + if (isDate) { + queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); + } else { + queryFilter.append("CAST('").append(filter).append("' AS NUMERIC)"); + } + } else { + queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); + String value = filter; + if (isDate) { + value += (fromValue ? " 00:00:00" : " 23:59:59"); + } + params.put(paramName, value); + } + queryFilter.append(")"); + } + } + + private void processAggregatedDateRangeFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String column, boolean fromValue, boolean isMin) { + if (filter != null && !filter.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(" HAVING ("); + } else { + queryFilter.append(isMin ? " AND (" : " OR "); + } + if (DbUtil.isPostgreSQL()) { + queryFilter.append(column).append(fromValue ? " >= " : " <= "); + queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); + } else { + queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); + String value = filter; + value += (fromValue ? " 00:00:00" : " 23:59:59"); + params.put(paramName, value); + } + if (!isMin) { + queryFilter.append(")"); + } + } + } + + private void processInputFilter(StringBuilder queryFilter, Map params, String paramName, String filter, String input) { + if (filter != null && !filter.isEmpty() && input != null && !input.isEmpty()) { + if (queryFilter.length() == 0) { + queryFilter.append(" WHERE ("); + } else { + queryFilter.append(" AND ("); + } + String[] filters = filter.split(","); + for (int i = 0, length = filters.length; i < length; i++) { + switch (filters[i].toUpperCase()) { + case "VULNERABILITY_ID" -> queryFilter.append("\"VULNERABILITY\".\"VULNID\""); + case "VULNERABILITY_TITLE" -> queryFilter.append("\"VULNERABILITY\".\"TITLE\""); + case "COMPONENT_NAME" -> queryFilter.append("\"COMPONENT\".\"NAME\""); + case "COMPONENT_VERSION" -> queryFilter.append("\"COMPONENT\".\"VERSION\""); + case "PROJECT_NAME" -> queryFilter.append("concat(\"PROJECT\".\"NAME\", ' ', \"PROJECT\".\"VERSION\")"); + } + queryFilter.append(" LIKE :").append(paramName); + if (i < length-1) { + queryFilter.append(" OR "); + } + } + if (filters.length > 0) { + params.put(paramName, "%" + input + "%"); + } + queryFilter.append(")"); + } + } + + private void preprocessACLs(StringBuilder queryFilter, final Map params) { + if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { + final List teams; + if (super.principal instanceof UserPrincipal) { + final UserPrincipal userPrincipal = ((UserPrincipal) super.principal); + teams = userPrincipal.getTeams(); + if (super.hasAccessManagementPermission(userPrincipal)) { + return; + } + } else { + final ApiKey apiKey = ((ApiKey) super.principal); + teams = apiKey.getTeams(); + if (super.hasAccessManagementPermission(apiKey)) { + return; + } + } + + // Query every project that the teams have access to + final Map tempParams = new HashMap<>(); + final Query queryAclProjects = pm.newQuery(Project.class); + if (teams != null && teams.size() > 0) { + final StringBuilder stringBuilderAclProjects = new StringBuilder(); + for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) { + final Team team = super.getObjectById(Team.class, teams.get(i).getId()); + stringBuilderAclProjects.append(" accessTeams.contains(:team").append(i).append(") "); + tempParams.put("team" + i, team); + if (i < teamsSize - 1) { + stringBuilderAclProjects.append(" || "); + } + } + queryAclProjects.setFilter(stringBuilderAclProjects.toString()); + } else { + params.put("false", false); + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND :false"); + } else { + queryFilter.append("WHERE :false"); + } + } + List result = (List) queryAclProjects.executeWithMap(tempParams); + // Query the descendants of the projects that the teams have access to + if (result != null && !result.isEmpty()) { + final StringBuilder stringBuilderDescendants = new StringBuilder(); + final List parameters = new ArrayList<>(); + stringBuilderDescendants.append("WHERE"); + int i = 0, teamSize = result.size(); + for (Project project : result) { + stringBuilderDescendants.append(" \"ID\" = ?").append(" "); + parameters.add(project.getId()); + if (i < teamSize - 1) { + stringBuilderDescendants.append(" OR"); + } + i++; + } + stringBuilderDescendants.append("\n"); + final List results = new ArrayList<>(); + + // Querying the descendants of projects requires a CTE (Common Table Expression), which needs to be at the top-level of the query for Microsoft SQL Server. + // Because of JDO, queries are only allowed to start with "SELECT", so the "WITH" clause for the CTE in MSSQL cannot be at top level. + // Activating the JDO property that queries don't have to start with "SELECT" does not help in this case, because JDO queries that do not start with "SELECT" only return "true", so no data can be fetched this way. + // To circumvent this problem, the query is executed via the direct connection to the database and not via JDO. + Connection connection = null; + PreparedStatement preparedStatement = null; + ResultSet rs = null; + try { + connection = (Connection) pm.getDataStoreConnection(); + if (DbUtil.isMssql() || DbUtil.isOracle()) { // Microsoft SQL Server and Oracle DB already imply the "RECURSIVE" keyword in the "WITH" clause, therefore it is not needed in the query + preparedStatement = connection.prepareStatement("WITH " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); + } else { // Other Databases need the "RECURSIVE" keyword in the "WITH" clause to correctly execute the query + preparedStatement = connection.prepareStatement("WITH RECURSIVE " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); + } + int j = 1; + for (Long id : parameters) { + preparedStatement.setLong(j, id); + j++; + } + preparedStatement.execute(); + rs = preparedStatement.getResultSet(); + while (rs.next()) { + results.add(rs.getLong(1)); + } + } catch (Exception e) { + LOGGER.error(e.getMessage()); + params.put("false", false); + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND :false"); + } else { + queryFilter.append("WHERE :false"); + } + return; + } finally { + DbUtil.close(rs); + DbUtil.close(preparedStatement); + DbUtil.close(connection); + } + + // Add queried projects and descendants to the input filter of the query + if (results != null && !results.isEmpty()) { + final StringBuilder stringBuilderInputFilter = new StringBuilder(); + int j = 0; + int resultSize = results.size(); + for (Long id : results) { + stringBuilderInputFilter.append(" \"PROJECT\".\"ID\" = :id").append(j); + params.put("id" + j, id); + if (j < resultSize - 1) { + stringBuilderInputFilter.append(" OR "); + } + j++; + } + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND (").append(stringBuilderInputFilter).append(")"); + } else { + queryFilter.append("WHERE (").append(stringBuilderInputFilter).append(")"); + } + } + } else { + params.put("false", false); + if (queryFilter != null && !queryFilter.isEmpty()) { + queryFilter.append(" AND :false"); + } else { + queryFilter.append("WHERE :false"); + } + } + } + } +} diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 8611c8985..d05deeb92 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -110,6 +110,8 @@ public class QueryManager extends AlpineQueryManager { private CacheQueryManager cacheQueryManager; private ComponentQueryManager componentQueryManager; private FindingsQueryManager findingsQueryManager; + + private FindingsSearchQueryManager findingsSearchQueryManager; private LicenseQueryManager licenseQueryManager; private MetricsQueryManager metricsQueryManager; private NotificationQueryManager notificationQueryManager; @@ -277,6 +279,17 @@ private FindingsQueryManager getFindingsQueryManager() { return findingsQueryManager; } + /** + * Lazy instantiation of FindingsSearchQueryManager. + * @return a FindingsSearchQueryManager object + */ + private FindingsSearchQueryManager getFindingsSearchQueryManager() { + if (findingsSearchQueryManager == null) { + findingsSearchQueryManager = (request == null) ? new FindingsSearchQueryManager(getPersistenceManager()) : new FindingsSearchQueryManager(getPersistenceManager(), request); + } + return findingsSearchQueryManager; + } + /** * Lazy instantiation of MetricsQueryManager. * @return a MetricsQueryManager object @@ -1034,11 +1047,11 @@ public List getFindings(Project project, boolean includeSuppressed) { } public List getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { - return getFindingsQueryManager().getAllFindings(filters, showSuppressed, showInactive); + return getFindingsSearchQueryManager().getAllFindings(filters, showSuppressed, showInactive); } public List getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { - return getFindingsQueryManager().getAllFindingsGroupedByVulnerability(filters, showInactive); + return getFindingsSearchQueryManager().getAllFindingsGroupedByVulnerability(filters, showInactive); } public List getVulnerabilityMetrics() { diff --git a/src/test/java/org/dependencytrack/model/GroupedFindingTest.java b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java index f4ae216e4..722c182eb 100644 --- a/src/test/java/org/dependencytrack/model/GroupedFindingTest.java +++ b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java @@ -35,7 +35,7 @@ public class GroupedFindingTest extends PersistenceCapableTest { private Date lastOccurrence = new Date(); private GroupedFinding groupedFinding = new GroupedFinding("vuln-source", "vuln-vulnId", "vuln-title", - Severity.HIGH, AnalyzerIdentity.INTERNAL_ANALYZER, published, null, BigDecimal.valueOf(8.4), 3, firstOccurrence, lastOccurrence); + Severity.HIGH, null, BigDecimal.valueOf(8.4), null, null, null, AnalyzerIdentity.INTERNAL_ANALYZER, published, null, 3, firstOccurrence, lastOccurrence); @Test From 5a9d3b90aeb1fda0fba360fe8ecce6a58136b7ec Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 1 Aug 2023 12:02:56 +0200 Subject: [PATCH 05/16] Integrate pagination and ordering in backend Integrates server side pagination and ordering in FindingsSearchQueryManager to reduce the Frontend traffic by only sending the necessary data Signed-off-by: RBickert --- .../FindingsSearchQueryManager.java | 49 ++++++++++++++++--- .../persistence/QueryManager.java | 4 +- .../resources/v1/FindingResource.java | 9 ++-- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index f02cb0ec2..32892ecfe 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -4,6 +4,7 @@ import alpine.model.ApiKey; import alpine.model.Team; import alpine.model.UserPrincipal; +import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; import alpine.server.util.DbUtil; import com.github.packageurl.PackageURL; @@ -34,6 +35,30 @@ public class FindingsSearchQueryManager extends QueryManager implements IQueryMa private static final Logger LOGGER = Logger.getLogger(FindingsSearchQueryManager.class); + private static final Map sortingAttributes = Map.ofEntries( + Map.entry("vulnerability.vulnId", "\"VULNERABILITY\".\"VULNID\""), + Map.entry("vulnerability.title", "\"VULNERABILITY\".\"TITLE\""), + Map.entry("vulnerability.severity", """ + CASE WHEN \"VULNERABILITY\".\"SEVERITY\" = 'UNASSIGNED' THEN 0 WHEN \"VULNERABILITY\".\"SEVERITY\" = 'LOW' THEN 3 + WHEN \"VULNERABILITY\".\"SEVERITY\" = 'MEDIUM' THEN 6 WHEN \"VULNERABILITY\".\"SEVERITY\" = 'HIGH' THEN 8 + WHEN \"VULNERABILITY\".\"SEVERITY\" = 'CRITICAL' THEN 10 ELSE + CASE WHEN \"VULNERABILITY\".\"CVSSV3BASESCORE\" IS NOT NULL THEN \"VULNERABILITY\".\"CVSSV3BASESCORE\" + ELSE \"VULNERABILITY\".\"CVSSV2BASESCORE\" END END + """), + Map.entry("attribution.analyzerIdentity", "\"FINDINGATTRIBUTION\".\"ANALYZERIDENTITY\""), + Map.entry("vulnerability.published", "\"VULNERABILITY\".\"PUBLISHED\""), + Map.entry("vulnerability.cvssV3BaseScore", "\"VULNERABILITY\".\"CVSSV3BASESCORE\""), + Map.entry("component.projectName", "concat(\"PROJECT\".\"NAME\", ' ', \"PROJECT\".\"VERSION\")"), + Map.entry("component.name", "\"COMPONENT\".\"NAME\""), + Map.entry("component.version", "\"COMPONENT\".\"VERSION\""), + Map.entry("analysis.state", "\"ANALYSIS\".\"STATE\""), + Map.entry("analysis.isSuppressed", "\"ANALYSIS\".\"SUPPRESSED\""), + Map.entry("attribution.attributedOn", "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\""), + Map.entry("vulnerability.affectedProjectCount", "COUNT(DISTINCT \"PROJECT\".\"ID\")"), + Map.entry("attribution.firstOccurrence", "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")"), + Map.entry("attribution.lastOccurrence", "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")") + ); + /** * Constructs a new QueryManager. * @param pm a PersistenceManager object @@ -75,7 +100,7 @@ public class FindingsSearchQueryManager extends QueryManager implements IQueryMa * @param showInactive determines if inactive projects should be included or not * @return a List of Finding objects */ - public List getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { + public PaginatedResult getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { StringBuilder queryFilter = new StringBuilder(); Map params = new HashMap<>(); if (showInactive) { @@ -92,9 +117,12 @@ public List getAllFindings(final Map filters, final boo params.put("showSuppressed", false); } processFilters(filters, queryFilter, params, false); - final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter + " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().toLowerCase().equals("descending") ? " DESC" : "ASC")); + PaginatedResult result = new PaginatedResult(); query.setNamedParameters(params); - final List list = query.executeList(); + final List totalList = query.executeList(); + result.setTotal(totalList.size()); + final List list = totalList.subList(this.pagination.getOffset(), (this.pagination.getOffset() + this.pagination.getLimit() >= totalList.size()) ? totalList.size() : this.pagination.getOffset() + this.pagination.getLimit()); final List findings = new ArrayList<>(); for (final Object[] o: list) { final Finding finding = new Finding(UUID.fromString((String) o[29]), o); @@ -120,7 +148,8 @@ public List getAllFindings(final Map filters, final boo } findings.add(finding); } - return findings; + result.setObjects(findings); + return result; } /** @@ -129,7 +158,7 @@ public List getAllFindings(final Map filters, final boo * @param showInactive determines if inactive projects should be included or not * @return a List of Finding objects */ - public List getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { + public PaginatedResult getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { StringBuilder queryFilter = new StringBuilder(); Map params = new HashMap<>(); if (showInactive) { @@ -137,15 +166,19 @@ public List getAllFindingsGroupedByVulnerability(final Map query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, GroupedFinding.QUERY + queryFilter); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, GroupedFinding.QUERY + queryFilter + " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().toLowerCase().equals("descending") ? " DESC" : "ASC")); + PaginatedResult result = new PaginatedResult(); query.setNamedParameters(params); - final List list = query.executeList(); + final List totalList = query.executeList(); + result.setTotal(totalList.size()); + final List list = totalList.subList(this.pagination.getOffset(), (this.pagination.getOffset() + this.pagination.getLimit() >= totalList.size()) ? totalList.size() : this.pagination.getOffset() + this.pagination.getLimit()); final List findings = new ArrayList<>(); for (Object[] o : list) { final GroupedFinding finding = new GroupedFinding(o); findings.add(finding); } - return findings; + result.setObjects(findings); + return result; } private void processFilters(Map filters, StringBuilder queryFilter, Map params, boolean isGroupedByVulnerabilities) { diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index d05deeb92..97cee098d 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1046,11 +1046,11 @@ public List getFindings(Project project, boolean includeSuppressed) { return getFindingsQueryManager().getFindings(project, includeSuppressed); } - public List getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { + public PaginatedResult getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { return getFindingsSearchQueryManager().getAllFindings(filters, showSuppressed, showInactive); } - public List getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { + public PaginatedResult getAllFindingsGroupedByVulnerability(final Map filters, final boolean showInactive) { return getFindingsSearchQueryManager().getAllFindingsGroupedByVulnerability(filters, showInactive); } diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 7fe0ad306..73b92eb86 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; @@ -231,8 +232,8 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") filters.put("textSearchInput", textSearchInput); filters.put("cvssFrom", cvssFrom); filters.put("cvssTo", cvssTo); - final List findings = qm.getAllFindings(filters, showSuppressed, showInactive); - return Response.ok(findings).header(TOTAL_COUNT_HEADER, findings.size()).build(); + final PaginatedResult result = qm.getAllFindings(filters, showSuppressed, showInactive); + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } } @@ -286,8 +287,8 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") filters.put("occurrencesTo", occurrencesTo); filters.put("aggregatedAttributedOnDateFrom", aggregatedAttributedOnDateFrom); filters.put("aggregatedAttributedOnDateTo", aggregatedAttributedOnDateTo); - final List findings = qm.getAllFindingsGroupedByVulnerability(filters, showInactive); - return Response.ok(findings).header(TOTAL_COUNT_HEADER, findings.size()).build(); + final PaginatedResult result = qm.getAllFindingsGroupedByVulnerability(filters, showInactive); + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } } From bec9ef8c137aad5302afbe538e9f87c88ccb7453 Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 8 Aug 2023 10:58:29 +0200 Subject: [PATCH 06/16] Fix checkstyle errors Signed-off-by: RBickert --- .../FindingsSearchQueryManager.java | 18 ++++++++++++++++++ .../persistence/QueryManager.java | 1 - 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index 32892ecfe..98957bc40 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -1,3 +1,21 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ package org.dependencytrack.persistence; import alpine.common.logging.Logger; diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 97cee098d..2f9051609 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -48,7 +48,6 @@ import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.Finding; import org.dependencytrack.model.FindingAttribution; -import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; import org.dependencytrack.model.NotificationPublisher; From d8e1dbb586431a392c27028e19c7b2eefba375b0 Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 8 Aug 2023 17:26:48 +0200 Subject: [PATCH 07/16] Change from hierarchic ACL to simple ACL Signed-off-by: RBickert --- .../org/dependencytrack/model/Finding.java | 3 +- .../dependencytrack/model/GroupedFinding.java | 1 + .../FindingsSearchQueryManager.java | 117 +----------------- 3 files changed, 9 insertions(+), 112 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index 75ac6c466..11081567d 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -127,7 +127,8 @@ public class Finding implements Serializable { "INNER JOIN \"VULNERABILITY\" ON (\"COMPONENTS_VULNERABILITIES\".\"VULNERABILITY_ID\" = \"VULNERABILITY\".\"ID\") " + "INNER JOIN \"FINDINGATTRIBUTION\" ON (\"COMPONENT\".\"ID\" = \"FINDINGATTRIBUTION\".\"COMPONENT_ID\") AND (\"VULNERABILITY\".\"ID\" = \"FINDINGATTRIBUTION\".\"VULNERABILITY_ID\")" + "LEFT JOIN \"ANALYSIS\" ON (\"COMPONENT\".\"ID\" = \"ANALYSIS\".\"COMPONENT_ID\") AND (\"VULNERABILITY\".\"ID\" = \"ANALYSIS\".\"VULNERABILITY_ID\") AND (\"COMPONENT\".\"PROJECT_ID\" = \"ANALYSIS\".\"PROJECT_ID\") " + - "INNER JOIN \"PROJECT\" ON (\"COMPONENT\".\"PROJECT_ID\" = \"PROJECT\".\"ID\")"; + "INNER JOIN \"PROJECT\" ON (\"COMPONENT\".\"PROJECT_ID\" = \"PROJECT\".\"ID\") " + + "LEFT JOIN \"PROJECT_ACCESS_TEAMS\" ON (\"PROJECT\".\"ID\" = \"PROJECT_ACCESS_TEAMS\".\"PROJECT_ID\")"; private UUID project; private Map component = new LinkedHashMap<>(); diff --git a/src/main/java/org/dependencytrack/model/GroupedFinding.java b/src/main/java/org/dependencytrack/model/GroupedFinding.java index 2ecd68729..57c539fb9 100644 --- a/src/main/java/org/dependencytrack/model/GroupedFinding.java +++ b/src/main/java/org/dependencytrack/model/GroupedFinding.java @@ -62,6 +62,7 @@ public class GroupedFinding implements Serializable { LEFT JOIN "ANALYSIS" ON ("COMPONENT"."ID" = "ANALYSIS"."COMPONENT_ID") AND ("VULNERABILITY"."ID" = "ANALYSIS"."VULNERABILITY_ID") AND ("COMPONENT"."PROJECT_ID" = "ANALYSIS"."PROJECT_ID") INNER JOIN "PROJECT" ON ("COMPONENT"."PROJECT_ID" = "PROJECT"."ID") LEFT JOIN "AFFECTEDVERSIONATTRIBUTION" ON ("VULNERABILITY"."ID" = "AFFECTEDVERSIONATTRIBUTION"."VULNERABILITY") + LEFT JOIN "PROJECT_ACCESS_TEAMS" ON ("PROJECT"."ID" = "PROJECT_ACCESS_TEAMS"."PROJECT_ID") """; private Map vulnerability = new LinkedHashMap<>(); diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index 98957bc40..778337868 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -32,7 +32,6 @@ import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Finding; import org.dependencytrack.model.GroupedFinding; -import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.model.Vulnerability; @@ -40,9 +39,6 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -94,23 +90,6 @@ public class FindingsSearchQueryManager extends QueryManager implements IQueryMa super(pm, request); } - public static final String QUERY_ACL_1 = """ - "DESCENDANTS" ("ID", "NAME") AS - (SELECT "PROJECT"."ID", - "PROJECT"."NAME" - FROM "PROJECT" - """; - - public static final String QUERY_ACL_2 = """ - UNION ALL - SELECT "CHILD"."ID", - "CHILD"."NAME" - FROM "PROJECT" "CHILD" - JOIN "DESCENDANTS" - ON "DESCENDANTS"."ID" = "CHILD"."PARENT_PROJECT_ID") - SELECT "DESCENDANTS"."ID", "DESCENDANTS"."NAME" FROM "DESCENDANTS" - """; - /** * Returns a List of all Finding objects filtered by ACL and other optional filters. * @param filters determines the filters to apply on the list of Finding objects @@ -368,104 +347,20 @@ private void preprocessACLs(StringBuilder queryFilter, final Map return; } } - - // Query every project that the teams have access to - final Map tempParams = new HashMap<>(); - final Query queryAclProjects = pm.newQuery(Project.class); if (teams != null && teams.size() > 0) { - final StringBuilder stringBuilderAclProjects = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) { final Team team = super.getObjectById(Team.class, teams.get(i).getId()); - stringBuilderAclProjects.append(" accessTeams.contains(:team").append(i).append(") "); - tempParams.put("team" + i, team); + sb.append(" \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = :team").append(i); + params.put("team" + i, team.getId()); if (i < teamsSize - 1) { - stringBuilderAclProjects.append(" || "); + sb.append(" OR "); } } - queryAclProjects.setFilter(stringBuilderAclProjects.toString()); - } else { - params.put("false", false); if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND :false"); + queryFilter.append(" AND (" + sb.toString() + ")"); } else { - queryFilter.append("WHERE :false"); - } - } - List result = (List) queryAclProjects.executeWithMap(tempParams); - // Query the descendants of the projects that the teams have access to - if (result != null && !result.isEmpty()) { - final StringBuilder stringBuilderDescendants = new StringBuilder(); - final List parameters = new ArrayList<>(); - stringBuilderDescendants.append("WHERE"); - int i = 0, teamSize = result.size(); - for (Project project : result) { - stringBuilderDescendants.append(" \"ID\" = ?").append(" "); - parameters.add(project.getId()); - if (i < teamSize - 1) { - stringBuilderDescendants.append(" OR"); - } - i++; - } - stringBuilderDescendants.append("\n"); - final List results = new ArrayList<>(); - - // Querying the descendants of projects requires a CTE (Common Table Expression), which needs to be at the top-level of the query for Microsoft SQL Server. - // Because of JDO, queries are only allowed to start with "SELECT", so the "WITH" clause for the CTE in MSSQL cannot be at top level. - // Activating the JDO property that queries don't have to start with "SELECT" does not help in this case, because JDO queries that do not start with "SELECT" only return "true", so no data can be fetched this way. - // To circumvent this problem, the query is executed via the direct connection to the database and not via JDO. - Connection connection = null; - PreparedStatement preparedStatement = null; - ResultSet rs = null; - try { - connection = (Connection) pm.getDataStoreConnection(); - if (DbUtil.isMssql() || DbUtil.isOracle()) { // Microsoft SQL Server and Oracle DB already imply the "RECURSIVE" keyword in the "WITH" clause, therefore it is not needed in the query - preparedStatement = connection.prepareStatement("WITH " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); - } else { // Other Databases need the "RECURSIVE" keyword in the "WITH" clause to correctly execute the query - preparedStatement = connection.prepareStatement("WITH RECURSIVE " + QUERY_ACL_1 + stringBuilderDescendants + QUERY_ACL_2); - } - int j = 1; - for (Long id : parameters) { - preparedStatement.setLong(j, id); - j++; - } - preparedStatement.execute(); - rs = preparedStatement.getResultSet(); - while (rs.next()) { - results.add(rs.getLong(1)); - } - } catch (Exception e) { - LOGGER.error(e.getMessage()); - params.put("false", false); - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND :false"); - } else { - queryFilter.append("WHERE :false"); - } - return; - } finally { - DbUtil.close(rs); - DbUtil.close(preparedStatement); - DbUtil.close(connection); - } - - // Add queried projects and descendants to the input filter of the query - if (results != null && !results.isEmpty()) { - final StringBuilder stringBuilderInputFilter = new StringBuilder(); - int j = 0; - int resultSize = results.size(); - for (Long id : results) { - stringBuilderInputFilter.append(" \"PROJECT\".\"ID\" = :id").append(j); - params.put("id" + j, id); - if (j < resultSize - 1) { - stringBuilderInputFilter.append(" OR "); - } - j++; - } - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND (").append(stringBuilderInputFilter).append(")"); - } else { - queryFilter.append("WHERE (").append(stringBuilderInputFilter).append(")"); - } + queryFilter.append("WHERE (" + sb.toString() + ")"); } } else { params.put("false", false); From e23a159c319fadb0cebbbc5b9f58c840d3f2c91c Mon Sep 17 00:00:00 2001 From: RBickert Date: Wed, 8 Feb 2023 17:10:02 +0100 Subject: [PATCH 08/16] Fix possible SQL injection for PostgreSQL Signed-off-by: RBickert --- .../persistence/FindingsSearchQueryManager.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index 778337868..e470ac79a 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -262,21 +262,22 @@ private void processRangeFilter(StringBuilder queryFilter, Map p } else { queryFilter.append(" AND ("); } + String value = filter; if (DbUtil.isPostgreSQL()) { queryFilter.append(column).append(fromValue ? " >= " : " <= "); if (isDate) { - queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); + queryFilter.append("TO_TIMESTAMP(:").append(paramName).append(", 'YYYY-MM-DD HH24:MI:SS')"); + value += (fromValue ? " 00:00:00" : " 23:59:59"); } else { - queryFilter.append("CAST('").append(filter).append("' AS NUMERIC)"); + queryFilter.append("CAST(:").append(paramName).append(" AS NUMERIC)"); } } else { queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); - String value = filter; if (isDate) { value += (fromValue ? " 00:00:00" : " 23:59:59"); } - params.put(paramName, value); } + params.put(paramName, value); queryFilter.append(")"); } } @@ -290,13 +291,11 @@ private void processAggregatedDateRangeFilter(StringBuilder queryFilter, Map= " : " <= "); - queryFilter.append("TO_TIMESTAMP('").append(filter).append(fromValue ? " 00:00:00" : " 23:59:59").append("', 'YYYY-MM-DD HH24:MI:SS')"); + queryFilter.append("TO_TIMESTAMP(:").append(paramName).append(", 'YYYY-MM-DD HH24:MI:SS')"); } else { queryFilter.append(column).append(fromValue ? " >= :" : " <= :").append(paramName); - String value = filter; - value += (fromValue ? " 00:00:00" : " 23:59:59"); - params.put(paramName, value); } + params.put(paramName, filter + (fromValue ? " 00:00:00" : " 23:59:59")); if (!isMin) { queryFilter.append(")"); } From 465d1b645decc3b3d8777fda88025a46fc6c77f2 Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 8 Aug 2023 20:17:44 +0200 Subject: [PATCH 09/16] Adjust tests to new ACL logic Signed-off-by: RBickert --- .../persistence/FindingsSearchQueryManager.java | 4 ++-- .../resources/v1/FindingResourceTest.java | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index e470ac79a..17b67739c 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -114,7 +114,7 @@ public PaginatedResult getAllFindings(final Map filters, final b params.put("showSuppressed", false); } processFilters(filters, queryFilter, params, false); - final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter + " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().toLowerCase().equals("descending") ? " DESC" : "ASC")); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter + (this.orderBy != null ? " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().equalsIgnoreCase("descending") ? " DESC" : "ASC") : "")); PaginatedResult result = new PaginatedResult(); query.setNamedParameters(params); final List totalList = query.executeList(); @@ -163,7 +163,7 @@ public PaginatedResult getAllFindingsGroupedByVulnerability(final Map query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, GroupedFinding.QUERY + queryFilter + " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().toLowerCase().equals("descending") ? " DESC" : "ASC")); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter + (this.orderBy != null ? " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().equalsIgnoreCase("descending") ? " DESC" : "ASC") : "")); PaginatedResult result = new PaginatedResult(); query.setNamedParameters(params); final List totalList = query.executeList(); diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index f84013877..63796e931 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -418,10 +418,10 @@ public void getAllFindingsWithAclEnabled() { .header(X_API_KEY, team.getApiKeys().get(0).getKey()) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(4), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(3), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(4, json.size()); + Assert.assertEquals(3, json.size()); Assert.assertEquals(date.getTime() ,json.getJsonObject(0).getJsonObject("vulnerability").getJsonNumber("published").longValue()); Assert.assertEquals(p1.getName() ,json.getJsonObject(0).getJsonObject("component").getString("projectName")); Assert.assertEquals(p1.getVersion() ,json.getJsonObject(0).getJsonObject("component").getString("projectVersion")); @@ -431,13 +431,9 @@ public void getAllFindingsWithAclEnabled() { Assert.assertEquals(p1.getVersion() ,json.getJsonObject(1).getJsonObject("component").getString("projectVersion")); Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(1).getJsonObject("component").getString("project")); Assert.assertEquals(date.getTime() ,json.getJsonObject(2).getJsonObject("vulnerability").getJsonNumber("published").longValue()); - Assert.assertEquals(p1_child.getName() ,json.getJsonObject(2).getJsonObject("component").getString("projectName")); - Assert.assertEquals(p1_child.getVersion() ,json.getJsonObject(2).getJsonObject("component").getString("projectVersion")); - Assert.assertEquals(p1_child.getUuid().toString(), json.getJsonObject(2).getJsonObject("component").getString("project")); - Assert.assertEquals(date.getTime() ,json.getJsonObject(3).getJsonObject("vulnerability").getJsonNumber("published").longValue()); - Assert.assertEquals(p1.getName() ,json.getJsonObject(3).getJsonObject("component").getString("projectName")); - Assert.assertEquals(p1.getVersion() ,json.getJsonObject(3).getJsonObject("component").getString("projectVersion")); - Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(3).getJsonObject("component").getString("project")); + Assert.assertEquals(p1.getName() ,json.getJsonObject(2).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(2).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(2).getJsonObject("component").getString("project")); } @Test @@ -578,7 +574,7 @@ public void getAllFindingsGroupedByVulnerabilityWithAclEnabled() { Assert.assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").size()); Assert.assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); Assert.assertEquals(666, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); - Assert.assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getInt("affectedProjectCount")); + Assert.assertEquals(1, json.getJsonObject(1).getJsonObject("vulnerability").getInt("affectedProjectCount")); Assert.assertEquals("INTERNAL", json.getJsonObject(2).getJsonObject("vulnerability").getString("source")); Assert.assertEquals("Vuln-3", json.getJsonObject(2).getJsonObject("vulnerability").getString("vulnId")); From d0aae467655467d42874c2f4a0aafbb3f137d0b1 Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 8 Aug 2023 20:29:11 +0200 Subject: [PATCH 10/16] Fix wrong query when getting grouped findings Signed-off-by: RBickert --- .../dependencytrack/persistence/FindingsSearchQueryManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index 17b67739c..e873ada83 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -163,7 +163,7 @@ public PaginatedResult getAllFindingsGroupedByVulnerability(final Map query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, Finding.QUERY_ALL_FINDINGS + queryFilter + (this.orderBy != null ? " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().equalsIgnoreCase("descending") ? " DESC" : "ASC") : "")); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, GroupedFinding.QUERY + queryFilter + (this.orderBy != null ? " ORDER BY " + sortingAttributes.get(this.orderBy) + " " + (this.orderDirection.name().toLowerCase().equals("descending") ? " DESC" : "ASC") : "")); PaginatedResult result = new PaginatedResult(); query.setNamedParameters(params); final List totalList = query.executeList(); From 00ca26b25947604dc762e0f9613c79841ecabdc5 Mon Sep 17 00:00:00 2001 From: RBickert Date: Wed, 23 Aug 2023 15:31:43 +0200 Subject: [PATCH 11/16] Remove first and last occurrence from grouped finding Signed-off-by: RBickert --- .../org/dependencytrack/model/GroupedFinding.java | 5 ----- .../persistence/FindingsSearchQueryManager.java | 12 +----------- .../resources/v1/FindingResource.java | 8 +------- .../dependencytrack/model/GroupedFindingTest.java | 8 +------- 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/GroupedFinding.java b/src/main/java/org/dependencytrack/model/GroupedFinding.java index 57c539fb9..c3cf067d5 100644 --- a/src/main/java/org/dependencytrack/model/GroupedFinding.java +++ b/src/main/java/org/dependencytrack/model/GroupedFinding.java @@ -53,15 +53,12 @@ public class GroupedFinding implements Serializable { "VULNERABILITY"."PUBLISHED", "VULNERABILITY"."CWES", COUNT(DISTINCT "PROJECT"."ID") AS "AFFECTED_PROJECT_COUNT", - MIN("AFFECTEDVERSIONATTRIBUTION"."FIRST_SEEN") AS "FIRST_OCCURRENCE", - MAX("AFFECTEDVERSIONATTRIBUTION"."LAST_SEEN") AS "LAST_OCCURRENCE" FROM "COMPONENT" INNER JOIN "COMPONENTS_VULNERABILITIES" ON ("COMPONENT"."ID" = "COMPONENTS_VULNERABILITIES"."COMPONENT_ID") INNER JOIN "VULNERABILITY" ON ("COMPONENTS_VULNERABILITIES"."VULNERABILITY_ID" = "VULNERABILITY"."ID") INNER JOIN "FINDINGATTRIBUTION" ON ("COMPONENT"."ID" = "FINDINGATTRIBUTION"."COMPONENT_ID") AND ("VULNERABILITY"."ID" = "FINDINGATTRIBUTION"."VULNERABILITY_ID") LEFT JOIN "ANALYSIS" ON ("COMPONENT"."ID" = "ANALYSIS"."COMPONENT_ID") AND ("VULNERABILITY"."ID" = "ANALYSIS"."VULNERABILITY_ID") AND ("COMPONENT"."PROJECT_ID" = "ANALYSIS"."PROJECT_ID") INNER JOIN "PROJECT" ON ("COMPONENT"."PROJECT_ID" = "PROJECT"."ID") - LEFT JOIN "AFFECTEDVERSIONATTRIBUTION" ON ("VULNERABILITY"."ID" = "AFFECTEDVERSIONATTRIBUTION"."VULNERABILITY") LEFT JOIN "PROJECT_ACCESS_TEAMS" ON ("PROJECT"."ID" = "PROJECT_ACCESS_TEAMS"."PROJECT_ID") """; @@ -78,8 +75,6 @@ public GroupedFinding(Object ...o) { optValue(vulnerability, "published", o[10]); optValue(vulnerability, "cwes", Finding.getCwes(o[11])); optValue(vulnerability, "affectedProjectCount", o[12]); - optValue(attribution, "firstOccurrence", o[13]); - optValue(attribution, "lastOccurrence", o[14]); } public Map getVulnerability() { diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index e873ada83..151e1e129 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -68,9 +68,7 @@ public class FindingsSearchQueryManager extends QueryManager implements IQueryMa Map.entry("analysis.state", "\"ANALYSIS\".\"STATE\""), Map.entry("analysis.isSuppressed", "\"ANALYSIS\".\"SUPPRESSED\""), Map.entry("attribution.attributedOn", "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\""), - Map.entry("vulnerability.affectedProjectCount", "COUNT(DISTINCT \"PROJECT\".\"ID\")"), - Map.entry("attribution.firstOccurrence", "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")"), - Map.entry("attribution.lastOccurrence", "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")") + Map.entry("vulnerability.affectedProjectCount", "COUNT(DISTINCT \"PROJECT\".\"ID\")") ); /** @@ -221,14 +219,6 @@ private void processAggregateFilters(Map filters, StringBuilder switch (filter) { case "occurrencesFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", true, false, true); case "occurrencesTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "COUNT(DISTINCT \"PROJECT\".\"ID\")", false, false, true); - case "aggregatedAttributedOnDateFrom" -> { - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", true, true); - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", true, false); - } - case "aggregatedAttributedOnDateTo" -> { - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MIN(\"AFFECTEDVERSIONATTRIBUTION\".\"FIRST_SEEN\")", false, true); - processAggregatedDateRangeFilter(queryFilter, params, filter, filters.get(filter), "MAX(\"AFFECTEDVERSIONATTRIBUTION\".\"LAST_SEEN\")", false, false); - } } } } diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 73b92eb86..4f76d4277 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -269,11 +269,7 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") @ApiParam(value = "Filter occurrences in projects from this value") @QueryParam("occurrencesFrom") String occurrencesFrom, @ApiParam(value = "Filter occurrences in projects to this value") - @QueryParam("occurrencesTo") String occurrencesTo, - @ApiParam(value = "Filter first attributed on and last attributed on from this date") - @QueryParam("aggregatedAttributedOnDateFrom") String aggregatedAttributedOnDateFrom, - @ApiParam(value = "Filter first attributed on and last attributed on to this date") - @QueryParam("aggregatedAttributedOnDateTo") String aggregatedAttributedOnDateTo) { + @QueryParam("occurrencesTo") String occurrencesTo) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Map filters = new HashMap<>(); filters.put("severity", severity); @@ -285,8 +281,6 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") filters.put("cvssTo", cvssTo); filters.put("occurrencesFrom", occurrencesFrom); filters.put("occurrencesTo", occurrencesTo); - filters.put("aggregatedAttributedOnDateFrom", aggregatedAttributedOnDateFrom); - filters.put("aggregatedAttributedOnDateTo", aggregatedAttributedOnDateTo); final PaginatedResult result = qm.getAllFindingsGroupedByVulnerability(filters, showInactive); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } diff --git a/src/test/java/org/dependencytrack/model/GroupedFindingTest.java b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java index 722c182eb..e32fe0e0c 100644 --- a/src/test/java/org/dependencytrack/model/GroupedFindingTest.java +++ b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java @@ -30,12 +30,8 @@ public class GroupedFindingTest extends PersistenceCapableTest { private Date published = new Date(); - private Date firstOccurrence = new Date(); - - private Date lastOccurrence = new Date(); - private GroupedFinding groupedFinding = new GroupedFinding("vuln-source", "vuln-vulnId", "vuln-title", - Severity.HIGH, null, BigDecimal.valueOf(8.4), null, null, null, AnalyzerIdentity.INTERNAL_ANALYZER, published, null, 3, firstOccurrence, lastOccurrence); + Severity.HIGH, null, BigDecimal.valueOf(8.4), null, null, null, AnalyzerIdentity.INTERNAL_ANALYZER, published, null, 3); @Test @@ -54,7 +50,5 @@ public void testVulnerability() { public void testAttribution() { Map map = groupedFinding.getAttribution(); Assert.assertEquals(AnalyzerIdentity.INTERNAL_ANALYZER, map.get("analyzerIdentity")); - Assert.assertEquals(firstOccurrence, map.get("firstOccurrence")); - Assert.assertEquals(lastOccurrence, map.get("lastOccurrence")); } } From 7d6b59e17ab3e93ffba314b2e92ef13a0e08d5a3 Mon Sep 17 00:00:00 2001 From: RBickert Date: Wed, 23 Aug 2023 16:00:27 +0200 Subject: [PATCH 12/16] Rename "CVSS" to "CVSSv3" Signed-off-by: RBickert --- .../FindingsSearchQueryManager.java | 4 ++-- .../resources/v1/FindingResource.java | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index 151e1e129..4eba2351a 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -187,8 +187,8 @@ private void processFilters(Map filters, StringBuilder queryFilt case "attributedOnDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", true, true, false); case "attributedOnDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", false, true, false); case "textSearchField" -> processInputFilter(queryFilter, params, filter, filters.get(filter), filters.get("textSearchInput")); - case "cvssFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", true, false, false); - case "cvssTo" -> processRangeFilter(queryFilter, params,filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", false, false, false); + case "cvssv3From" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", true, false, false); + case "cvssv3To" -> processRangeFilter(queryFilter, params,filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", false, false, false); } } preprocessACLs(queryFilter, params); diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 4f76d4277..1670bb935 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -215,10 +215,10 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") @QueryParam("textSearchField") String textSearchField, @ApiParam(value = "Filter by this text input") @QueryParam("textSearchInput") String textSearchInput, - @ApiParam(value = "Filter CVSS from this value") - @QueryParam("cvssFrom") String cvssFrom, - @ApiParam(value = "Filter CVSS from this Value") - @QueryParam("cvssTo") String cvssTo) { + @ApiParam(value = "Filter CVSSv3 from this value") + @QueryParam("cvssv3From") String cvssv3From, + @ApiParam(value = "Filter CVSSv3 from this Value") + @QueryParam("cvssv3To") String cvssv3To) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Map filters = new HashMap<>(); filters.put("severity", severity); @@ -230,8 +230,8 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") filters.put("attributedOnDateTo", attributedOnDateTo); filters.put("textSearchField", textSearchField); filters.put("textSearchInput", textSearchInput); - filters.put("cvssFrom", cvssFrom); - filters.put("cvssTo", cvssTo); + filters.put("cvssv3From", cvssv3From); + filters.put("cvssv3To", cvssv3To); final PaginatedResult result = qm.getAllFindings(filters, showSuppressed, showInactive); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } @@ -262,10 +262,10 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") @QueryParam("textSearchField") String textSearchField, @ApiParam(value = "Filter by this text input") @QueryParam("textSearchInput") String textSearchInput, - @ApiParam(value = "Filter CVSS from this value") - @QueryParam("cvssFrom") String cvssFrom, - @ApiParam(value = "Filter CVSS to this value") - @QueryParam("cvssTo") String cvssTo, + @ApiParam(value = "Filter CVSSv3 from this value") + @QueryParam("cvssv3From") String cvssv3From, + @ApiParam(value = "Filter CVSSv3 to this value") + @QueryParam("cvssv3To") String cvssv3To, @ApiParam(value = "Filter occurrences in projects from this value") @QueryParam("occurrencesFrom") String occurrencesFrom, @ApiParam(value = "Filter occurrences in projects to this value") @@ -277,8 +277,8 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") filters.put("publishDateTo", publishDateTo); filters.put("textSearchField", textSearchField); filters.put("textSearchInput", textSearchInput); - filters.put("cvssFrom", cvssFrom); - filters.put("cvssTo", cvssTo); + filters.put("cvssv3From", cvssv3From); + filters.put("cvssv3To", cvssv3To); filters.put("occurrencesFrom", occurrencesFrom); filters.put("occurrencesTo", occurrencesTo); final PaginatedResult result = qm.getAllFindingsGroupedByVulnerability(filters, showInactive); From 1eb039943e44498f0cd9140564fab31fe600ed71 Mon Sep 17 00:00:00 2001 From: RBickert Date: Mon, 28 Aug 2023 15:41:01 +0200 Subject: [PATCH 13/16] Add CVSSv2 to FindingsSearchQueryManager Adds filters and sorting for CVSSv2 to the FindingsSearchQueryManager to use it in the Vulnerability Audit in the Frontend Signed-off-by: RBickert --- .../org/dependencytrack/model/GroupedFinding.java | 1 + .../persistence/FindingsSearchQueryManager.java | 3 +++ .../resources/v1/FindingResource.java | 12 ++++++++++++ .../dependencytrack/model/GroupedFindingTest.java | 3 ++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/GroupedFinding.java b/src/main/java/org/dependencytrack/model/GroupedFinding.java index c3cf067d5..00b406dc7 100644 --- a/src/main/java/org/dependencytrack/model/GroupedFinding.java +++ b/src/main/java/org/dependencytrack/model/GroupedFinding.java @@ -70,6 +70,7 @@ public GroupedFinding(Object ...o) { optValue(vulnerability, "vulnId", o[1]); optValue(vulnerability, "title", o[2]); optValue(vulnerability, "severity", VulnerabilityUtil.getSeverity(o[3], (BigDecimal) o[4], (BigDecimal) o[5], (BigDecimal) o[6], (BigDecimal) o[7], (BigDecimal) o[8])); + optValue(vulnerability, "cvssV2BaseScore", o[4]); optValue(vulnerability, "cvssV3BaseScore", o[5]); optValue(attribution, "analyzerIdentity", o[9]); optValue(vulnerability, "published", o[10]); diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index 4eba2351a..8a2786f95 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -61,6 +61,7 @@ public class FindingsSearchQueryManager extends QueryManager implements IQueryMa """), Map.entry("attribution.analyzerIdentity", "\"FINDINGATTRIBUTION\".\"ANALYZERIDENTITY\""), Map.entry("vulnerability.published", "\"VULNERABILITY\".\"PUBLISHED\""), + Map.entry("vulnerability.cvssV2BaseScore", "\"VULNERABILITY\".\"CVSSV2BASESCORE\""), Map.entry("vulnerability.cvssV3BaseScore", "\"VULNERABILITY\".\"CVSSV3BASESCORE\""), Map.entry("component.projectName", "concat(\"PROJECT\".\"NAME\", ' ', \"PROJECT\".\"VERSION\")"), Map.entry("component.name", "\"COMPONENT\".\"NAME\""), @@ -187,6 +188,8 @@ private void processFilters(Map filters, StringBuilder queryFilt case "attributedOnDateFrom" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", true, true, false); case "attributedOnDateTo" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"FINDINGATTRIBUTION\".\"ATTRIBUTED_ON\"", false, true, false); case "textSearchField" -> processInputFilter(queryFilter, params, filter, filters.get(filter), filters.get("textSearchInput")); + case "cvssv2From" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV2BASESCORE\"", true, false, false); + case "cvssv2To" -> processRangeFilter(queryFilter, params,filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV2BASESCORE\"", false, false, false); case "cvssv3From" -> processRangeFilter(queryFilter, params, filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", true, false, false); case "cvssv3To" -> processRangeFilter(queryFilter, params,filter, filters.get(filter), "\"VULNERABILITY\".\"CVSSV3BASESCORE\"", false, false, false); } diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 1670bb935..cec9c4cec 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -215,6 +215,10 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") @QueryParam("textSearchField") String textSearchField, @ApiParam(value = "Filter by this text input") @QueryParam("textSearchInput") String textSearchInput, + @ApiParam(value = "Filter CVSSv2 from this value") + @QueryParam("cvssv2From") String cvssv2From, + @ApiParam(value = "Filter CVSSv2 from this Value") + @QueryParam("cvssv2To") String cvssv2To, @ApiParam(value = "Filter CVSSv3 from this value") @QueryParam("cvssv3From") String cvssv3From, @ApiParam(value = "Filter CVSSv3 from this Value") @@ -230,6 +234,8 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") filters.put("attributedOnDateTo", attributedOnDateTo); filters.put("textSearchField", textSearchField); filters.put("textSearchInput", textSearchInput); + filters.put("cvssv2From", cvssv2From); + filters.put("cvssv2To", cvssv2To); filters.put("cvssv3From", cvssv3From); filters.put("cvssv3To", cvssv3To); final PaginatedResult result = qm.getAllFindings(filters, showSuppressed, showInactive); @@ -262,6 +268,10 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") @QueryParam("textSearchField") String textSearchField, @ApiParam(value = "Filter by this text input") @QueryParam("textSearchInput") String textSearchInput, + @ApiParam(value = "Filter CVSSv2 from this value") + @QueryParam("cvssv2From") String cvssv2From, + @ApiParam(value = "Filter CVSSv2 to this value") + @QueryParam("cvssv2To") String cvssv2To, @ApiParam(value = "Filter CVSSv3 from this value") @QueryParam("cvssv3From") String cvssv3From, @ApiParam(value = "Filter CVSSv3 to this value") @@ -277,6 +287,8 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") filters.put("publishDateTo", publishDateTo); filters.put("textSearchField", textSearchField); filters.put("textSearchInput", textSearchInput); + filters.put("cvssv2From", cvssv2From); + filters.put("cvssv2To", cvssv2To); filters.put("cvssv3From", cvssv3From); filters.put("cvssv3To", cvssv3To); filters.put("occurrencesFrom", occurrencesFrom); diff --git a/src/test/java/org/dependencytrack/model/GroupedFindingTest.java b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java index e32fe0e0c..a15bb0c87 100644 --- a/src/test/java/org/dependencytrack/model/GroupedFindingTest.java +++ b/src/test/java/org/dependencytrack/model/GroupedFindingTest.java @@ -31,7 +31,7 @@ public class GroupedFindingTest extends PersistenceCapableTest { private Date published = new Date(); private GroupedFinding groupedFinding = new GroupedFinding("vuln-source", "vuln-vulnId", "vuln-title", - Severity.HIGH, null, BigDecimal.valueOf(8.4), null, null, null, AnalyzerIdentity.INTERNAL_ANALYZER, published, null, 3); + Severity.HIGH, BigDecimal.valueOf(8.5), BigDecimal.valueOf(8.4), null, null, null, AnalyzerIdentity.INTERNAL_ANALYZER, published, null, 3); @Test @@ -42,6 +42,7 @@ public void testVulnerability() { Assert.assertEquals("vuln-title", map.get("title")); Assert.assertEquals(Severity.HIGH, map.get("severity")); Assert.assertEquals(published, map.get("published")); + Assert.assertEquals(BigDecimal.valueOf(8.5), map.get("cvssV2BaseScore")); Assert.assertEquals(BigDecimal.valueOf(8.4), map.get("cvssV3BaseScore")); Assert.assertEquals(3, map.get("affectedProjectCount")); } From fa3cc93ed14248506e721999da0199c1b5b76cd7 Mon Sep 17 00:00:00 2001 From: RBickert Date: Mon, 28 Aug 2023 15:53:32 +0200 Subject: [PATCH 14/16] Fix duplicate entries in Vulnerability Audit Fixes duplicate entries of the same finding appearing for every team membership of the user Signed-off-by: RBickert --- src/main/java/org/dependencytrack/model/Finding.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index 11081567d..ec8f7c685 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -89,7 +89,7 @@ public class Finding implements Serializable { "LEFT JOIN \"ANALYSIS\" ON (\"COMPONENT\".\"ID\" = \"ANALYSIS\".\"COMPONENT_ID\") AND (\"VULNERABILITY\".\"ID\" = \"ANALYSIS\".\"VULNERABILITY_ID\") AND (\"COMPONENT\".\"PROJECT_ID\" = \"ANALYSIS\".\"PROJECT_ID\") " + "WHERE \"COMPONENT\".\"PROJECT_ID\" = ?"; - public static final String QUERY_ALL_FINDINGS = "SELECT " + + public static final String QUERY_ALL_FINDINGS = "SELECT DISTINCT " + "\"COMPONENT\".\"UUID\"," + "\"COMPONENT\".\"NAME\"," + "\"COMPONENT\".\"GROUP\"," + From 506b0dc02a9df0c985d451567ff72bc88d06144b Mon Sep 17 00:00:00 2001 From: RBickert Date: Tue, 29 Aug 2023 10:49:09 +0200 Subject: [PATCH 15/16] Make "getAllFindings" test consistent by ordering result Signed-off-by: RBickert --- .../resources/v1/FindingResourceTest.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 63796e931..de3350915 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -327,9 +327,9 @@ public void getFindingsByProjectWithComponentLatestVersionWithoutRepositoryMetaC @Test public void getAllFindings() { - Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); - Project p1_child = qm.createProject("Acme Example", null, "1.0", null, p1, null, true, false); - Project p2 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Project p1 = qm.createProject("Acme Example 1", null, "1.0", null, null, null, true, false); + Project p1_child = qm.createProject("Acme Example 2", null, "1.0", null, p1, null, true, false); + Project p2 = qm.createProject("Acme Example 3", null, "1.0", null, null, null, true, false); Component c1 = createComponent(p1, "Component A", "1.0"); Component c2 = createComponent(p1, "Component B", "1.0"); Component c3 = createComponent(p1_child, "Component C", "1.0"); @@ -350,7 +350,10 @@ public void getAllFindings() { qm.addVulnerability(v2, c3, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING).request() + Response response = target(V1_FINDING) + .queryParam("sortName", "component.projectName") + .queryParam("sortOrder", "asc") + .request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -367,13 +370,13 @@ public void getAllFindings() { Assert.assertEquals(p1.getVersion() ,json.getJsonObject(1).getJsonObject("component").getString("projectVersion")); Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(1).getJsonObject("component").getString("project")); Assert.assertEquals(date.getTime() ,json.getJsonObject(2).getJsonObject("vulnerability").getJsonNumber("published").longValue()); - Assert.assertEquals(p1_child.getName() ,json.getJsonObject(2).getJsonObject("component").getString("projectName")); - Assert.assertEquals(p1_child.getVersion() ,json.getJsonObject(2).getJsonObject("component").getString("projectVersion")); - Assert.assertEquals(p1_child.getUuid().toString(), json.getJsonObject(2).getJsonObject("component").getString("project")); + Assert.assertEquals(p1.getName() ,json.getJsonObject(2).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1.getVersion() ,json.getJsonObject(2).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(2).getJsonObject("component").getString("project")); Assert.assertEquals(date.getTime() ,json.getJsonObject(3).getJsonObject("vulnerability").getJsonNumber("published").longValue()); - Assert.assertEquals(p1.getName() ,json.getJsonObject(3).getJsonObject("component").getString("projectName")); - Assert.assertEquals(p1.getVersion() ,json.getJsonObject(3).getJsonObject("component").getString("projectVersion")); - Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(3).getJsonObject("component").getString("project")); + Assert.assertEquals(p1_child.getName() ,json.getJsonObject(3).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1_child.getVersion() ,json.getJsonObject(3).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1_child.getUuid().toString(), json.getJsonObject(3).getJsonObject("component").getString("project")); Assert.assertEquals(date.getTime() ,json.getJsonObject(4).getJsonObject("vulnerability").getJsonNumber("published").longValue()); Assert.assertEquals(p2.getName() ,json.getJsonObject(4).getJsonObject("component").getString("projectName")); Assert.assertEquals(p2.getVersion() ,json.getJsonObject(4).getJsonObject("component").getString("projectVersion")); From 33ce964e853451fbc1e39b13944884b4a8e3a155 Mon Sep 17 00:00:00 2001 From: RBickert Date: Thu, 8 Feb 2024 13:55:21 +0100 Subject: [PATCH 16/16] Remove CweImporter from FindingResourceTest Signed-off-by: RBickert --- .../org/dependencytrack/resources/v1/FindingResourceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index de3350915..278a19026 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -31,7 +31,6 @@ import org.dependencytrack.model.RepositoryType; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; -import org.dependencytrack.persistence.CweImporter; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer;