From ff6994bef060d563218a94010ca8ca09741f50fb Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Thu, 4 Apr 2019 16:41:19 +0200 Subject: [PATCH] SONAR-11886 Ignore Hotspots in Issues facets / filters * SONAR-11886 Ignore Hotspots in Issues facets / filters * SONAR-11886 Do not return severity on Security Hotspots --- .../sonar/server/issue/index/IssueIndex.java | 62 +++++-- .../server/issue/ws/SearchResponseFormat.java | 7 +- .../issue/index/IssueIndexFiltersTest.java | 25 --- .../index/IssueIndexSecurityHotspotsTest.java | 166 ++++++++++++++++++ .../server/issue/index/IssueIndexTest.java | 2 - .../server/issue/ws/SearchActionTest.java | 46 +++++ 6 files changed, 261 insertions(+), 47 deletions(-) create mode 100644 server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java index 28b2bacd4d3..e4dbb57dcd6 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java @@ -53,11 +53,11 @@ import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuil import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.bucket.terms.InternalTerms; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.metrics.max.InternalMax; import org.elasticsearch.search.aggregations.metrics.min.Min; import org.elasticsearch.search.aggregations.metrics.sum.SumAggregationBuilder; @@ -95,6 +95,8 @@ import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.index.query.QueryBuilders.termsQuery; +import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; +import static org.sonar.api.rules.RuleType.VULNERABILITY; import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; import static org.sonar.server.es.BaseDoc.epochMillisToEpochSeconds; import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; @@ -356,20 +358,29 @@ public class IssueIndex { filters.put(FIELD_ISSUE_RULE_ID, createTermsFilter( FIELD_ISSUE_RULE_ID, query.rules().stream().map(RuleDefinitionDto::getId).collect(Collectors.toList()))); - filters.put(FIELD_ISSUE_SEVERITY, createTermsFilter(FIELD_ISSUE_SEVERITY, query.severities())); filters.put(FIELD_ISSUE_STATUS, createTermsFilter(FIELD_ISSUE_STATUS, query.statuses())); filters.put(FIELD_ISSUE_ORGANIZATION_UUID, createTermFilter(FIELD_ISSUE_ORGANIZATION_UUID, query.organizationUuid())); filters.put(FIELD_ISSUE_OWASP_TOP_10, createTermsFilter(FIELD_ISSUE_OWASP_TOP_10, query.owaspTop10())); filters.put(FIELD_ISSUE_SANS_TOP_25, createTermsFilter(FIELD_ISSUE_SANS_TOP_25, query.sansTop25())); filters.put(FIELD_ISSUE_CWE, createTermsFilter(FIELD_ISSUE_CWE, query.cwe())); + addSeverityFilter(query, filters); addComponentRelatedFilters(query, filters); - addDatesFilter(filters, query); addCreatedAfterByProjectsFilter(filters, query); return filters; } + private static void addSeverityFilter(IssueQuery query, Map filters) { + QueryBuilder severityFieldFilter = createTermsFilter(FIELD_ISSUE_SEVERITY, query.severities()); + if (severityFieldFilter != null) { + filters.put(FIELD_ISSUE_SEVERITY, boolQuery() + .must(severityFieldFilter) + // Ignore severity of Security HotSpots + .mustNot(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))); + } + } + private static void addComponentRelatedFilters(IssueQuery query, Map filters) { addCommonComponentRelatedFilters(query, filters); if (query.viewUuids().isEmpty()) { @@ -452,6 +463,19 @@ public class IssueIndex { return FACET_MODE_EFFORT.equals(query.facetMode()); } + private static AggregationBuilder createSeverityFacet(IssueQuery query, Map filters, QueryBuilder queryBuilder) { + String fieldName = SEVERITIES.getFieldName(); + String facetName = SEVERITIES.getName(); + StickyFacetBuilder stickyFacetBuilder = newStickyFacetBuilder(query, filters, queryBuilder); + BoolQueryBuilder facetFilter = stickyFacetBuilder.getStickyFacetFilter(fieldName) + // Ignore severity of Security HotSpots + .mustNot(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())); + FilterAggregationBuilder facetTopAggregation = stickyFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, SEVERITIES.getSize()); + return AggregationBuilders + .global(facetName) + .subAggregation(facetTopAggregation); + } + private static AggregationBuilder createAssigneesFacet(IssueQuery query, Map filters, QueryBuilder queryBuilder) { String fieldName = ASSIGNEES.getFieldName(); String facetName = ASSIGNEES.getName(); @@ -460,11 +484,11 @@ public class IssueIndex { Map assigneeFilters = Maps.newHashMap(filters); assigneeFilters.remove(IS_ASSIGNED_FILTER); assigneeFilters.remove(fieldName); - StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, assigneeFilters, queryBuilder); - BoolQueryBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName); - FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, ASSIGNEES.getSize()); + StickyFacetBuilder stickyFacetBuilder = newStickyFacetBuilder(query, assigneeFilters, queryBuilder); + BoolQueryBuilder facetFilter = stickyFacetBuilder.getStickyFacetFilter(fieldName); + FilterAggregationBuilder facetTopAggregation = stickyFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, ASSIGNEES.getSize()); if (!query.assignees().isEmpty()) { - facetTopAggregation = assigneeFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t, query.assignees().toArray()); + facetTopAggregation = stickyFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t, query.assignees().toArray()); } // Add missing facet for unassigned issues @@ -486,10 +510,10 @@ public class IssueIndex { Map resolutionFilters = Maps.newHashMap(filters); resolutionFilters.remove("__isResolved"); resolutionFilters.remove(fieldName); - StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, resolutionFilters, esQuery); - BoolQueryBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName); - FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, RESOLUTIONS.getSize()); - facetTopAggregation = assigneeFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t); + StickyFacetBuilder stickyFacetBuilder = newStickyFacetBuilder(query, resolutionFilters, esQuery); + BoolQueryBuilder facetFilter = stickyFacetBuilder.getStickyFacetFilter(fieldName); + FilterAggregationBuilder facetTopAggregation = stickyFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, RESOLUTIONS.getSize()); + facetTopAggregation = stickyFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t); // Add missing facet for unresolved issues facetTopAggregation.subAggregation( @@ -570,7 +594,6 @@ public class IssueIndex { private void configureStickyFacets(IssueQuery query, SearchOptions options, Map filters, QueryBuilder esQuery, SearchRequestBuilder esSearch) { if (!options.getFacets().isEmpty()) { StickyFacetBuilder stickyFacetBuilder = newStickyFacetBuilder(query, filters, esQuery); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, SEVERITIES); addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, STATUSES); addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, PROJECT_UUIDS, query.projectUuids().toArray()); addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, MODULE_UUIDS, query.moduleUuids().toArray()); @@ -585,6 +608,9 @@ public class IssueIndex { addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, OWASP_TOP_10, query.owaspTop10().toArray()); addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, SANS_TOP_25, query.sansTop25().toArray()); addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, CWE, query.cwe().toArray()); + if (options.getFacets().contains(PARAM_SEVERITIES)) { + esSearch.addAggregation(createSeverityFacet(query, filters, esQuery)); + } if (options.getFacets().contains(PARAM_RESOLUTIONS)) { esSearch.addAggregation(createResolutionFacet(query, filters, esQuery)); } @@ -756,7 +782,7 @@ public class IssueIndex { boolQuery() .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION)) .filter(termQuery(FIELD_ISSUE_ASSIGNEE_UUID, assigneeUuid)) - .mustNot(termQuery(FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name()))) + .mustNot(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))) .setSize(0); IntStream.range(0, projectUuids.size()).forEach(i -> { String projectUuid = projectUuids.get(i); @@ -890,25 +916,25 @@ public class IssueIndex { return categoryAggs .subAggregation( AggregationBuilders.filter("vulnerabilities", boolQuery() - .filter(termQuery(FIELD_ISSUE_TYPE, RuleType.VULNERABILITY.name())) + .filter(termQuery(FIELD_ISSUE_TYPE, VULNERABILITY.name())) .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION))) .subAggregation( AggregationBuilders.terms("severity").field(FIELD_ISSUE_SEVERITY) .subAggregation( AggregationBuilders.count(COUNT).field(FIELD_ISSUE_KEY)))) .subAggregation(AggregationBuilders.filter("openSecurityHotspots", boolQuery() - .filter(termQuery(FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) + .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())) .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION))) .subAggregation( AggregationBuilders.count(COUNT).field(FIELD_ISSUE_KEY))) .subAggregation(AggregationBuilders.filter("toReviewSecurityHotspots", boolQuery() - .filter(termQuery(FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) + .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())) .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED)) .filter(termQuery(FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_FIXED))) .subAggregation( AggregationBuilders.count(COUNT).field(FIELD_ISSUE_KEY))) .subAggregation(AggregationBuilders.filter("wontFixSecurityHotspots", boolQuery() - .filter(termQuery(FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) + .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())) .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED)) .filter(termQuery(FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_WONT_FIX))) .subAggregation( @@ -931,7 +957,7 @@ public class IssueIndex { return client.prepareSearch(TYPE_ISSUE.getMainType()) .setQuery( componentFilter - .filter(termsQuery(FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name(), RuleType.VULNERABILITY.name())) + .filter(termsQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name(), VULNERABILITY.name())) .mustNot(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_CLOSED))) .setSize(0); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java index 9ea6aaf92d1..9a92a1db6fb 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java @@ -29,6 +29,7 @@ import java.util.Set; import org.sonar.api.resources.Language; import org.sonar.api.resources.Languages; import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.Duration; import org.sonar.api.utils.Durations; @@ -162,7 +163,7 @@ public class SearchResponseFormat { private void formatIssue(Issue.Builder issueBuilder, IssueDto dto, SearchResponseData data) { issueBuilder.setKey(dto.getKey()); - ofNullable(dto.getType()).map(Common.RuleType::forNumber).ifPresent(issueBuilder::setType); + issueBuilder.setType(Common.RuleType.forNumber(dto.getType())); ComponentDto component = data.getComponentByUuid(dto.getComponentUuid()); issueBuilder.setOrganization(data.getOrganizationKey(component.getOrganizationUuid())); @@ -182,7 +183,9 @@ public class SearchResponseFormat { issueBuilder.setExternalRuleEngine(engineNameFrom(dto.getRuleKey())); } issueBuilder.setFromHotspot(dto.isFromHotspot()); - issueBuilder.setSeverity(Common.Severity.valueOf(dto.getSeverity())); + if (dto.getType() != RuleType.SECURITY_HOTSPOT.getDbConstant()) { + issueBuilder.setSeverity(Common.Severity.valueOf(dto.getSeverity())); + } ofNullable(data.getUserByUuid(dto.getAssigneeUuid())).ifPresent(assignee -> issueBuilder.setAssignee(assignee.getLogin())); ofNullable(emptyToNull(dto.getResolution())).ifPresent(issueBuilder::setResolution); issueBuilder.setStatus(dto.getStatus()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java index 20ba5cb1867..94e91c80676 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java @@ -23,10 +23,8 @@ import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import org.assertj.core.api.Fail; -import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.search.SearchHit; import org.junit.Rule; import org.junit.Test; @@ -40,7 +38,6 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.rule.RuleDefinitionDto; import org.sonar.server.es.EsTester; -import org.sonar.server.es.Facets; import org.sonar.server.es.SearchOptions; import org.sonar.server.permission.index.IndexPermissions; import org.sonar.server.permission.index.PermissionIndexerTester; @@ -56,7 +53,6 @@ import static java.util.Collections.singletonList; import static java.util.TimeZone.getTimeZone; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; import static org.junit.rules.ExpectedException.none; import static org.sonar.api.resources.Qualifiers.APP; import static org.sonar.api.resources.Qualifiers.PROJECT; @@ -471,19 +467,6 @@ public class IssueIndexFiltersTest { assertThatSearchReturnsEmpty(IssueQuery.builder().severities(singletonList(Severity.BLOCKER))); } - @Test - public void facets_on_severities() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setSeverity(Severity.INFO), - newDoc("I2", file).setSeverity(Severity.INFO), - newDoc("I3", file).setSeverity(Severity.MAJOR)); - - assertThatFacetHasOnly(IssueQuery.builder(), "severities", entry("INFO", 2L), entry("MAJOR", 1L)); - } - @Test public void filter_by_statuses() { ComponentDto project = newPrivateProjectDto(newOrganizationDto()); @@ -789,12 +772,4 @@ public class IssueIndexFiltersTest { List keys = searchAndReturnKeys(query); assertThat(keys).isEmpty(); } - - @SafeVarargs - private final void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry... expectedEntries) { - SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet))); - Facets facets = new Facets(result, system2.getDefaultTimeZone()); - assertThat(facets.getNames()).containsOnly(facet, "effort"); - assertThat(facets.get(facet)).containsOnly(expectedEntries); - } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java new file mode 100644 index 00000000000..426243cd7d6 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java @@ -0,0 +1,166 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.issue.index; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.search.SearchHit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.internal.TestSystem2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Collections.singletonList; +import static java.util.TimeZone.getTimeZone; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.junit.rules.ExpectedException.none; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.api.rule.Severity.INFO; +import static org.sonar.api.rule.Severity.MAJOR; +import static org.sonar.api.rules.RuleType.BUG; +import static org.sonar.api.rules.RuleType.CODE_SMELL; +import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; +import static org.sonar.api.rules.RuleType.VULNERABILITY; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; +import static org.sonar.server.issue.IssueDocTesting.newDoc; + +public class IssueIndexSecurityHotspotsTest { + + @Rule + public EsTester es = EsTester.create(); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = none(); + private System2 system2 = new TestSystem2().setNow(1_500_000_000_000L).setDefaultTimeZone(getTimeZone("GMT-01:00")); + @Rule + public DbTester db = DbTester.create(system2); + + private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); + private PermissionIndexerTester authorizationIndexer = new PermissionIndexerTester(es, issueIndexer); + + private IssueIndex underTest = new IssueIndex(es.client(), system2, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule)); + + @Test + public void filter_by_security_hotspots_type() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setType(BUG), + newDoc("I2", file).setType(CODE_SMELL), + newDoc("I3", file).setType(VULNERABILITY), + newDoc("I4", file).setType(VULNERABILITY), + newDoc("I5", file).setType(SECURITY_HOTSPOT), + newDoc("I6", file).setType(SECURITY_HOTSPOT)); + + assertThatSearchReturnsOnly(IssueQuery.builder().types(singletonList(SECURITY_HOTSPOT.name())), "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().types(asList(SECURITY_HOTSPOT.name(), VULNERABILITY.name())), "I3", "I4", "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder(), "I1", "I2", "I3", "I4", "I5", "I6"); + } + + @Test + public void filter_by_severities_ignore_hotspots() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setSeverity(Severity.INFO).setType(BUG), + newDoc("I2", file).setSeverity(Severity.MAJOR).setType(CODE_SMELL), + newDoc("I3", file).setSeverity(Severity.MAJOR).setType(VULNERABILITY), + newDoc("I4", file).setSeverity(Severity.CRITICAL).setType(VULNERABILITY), + // This entry should be ignored + newDoc("I5", file).setSeverity(Severity.MAJOR).setType(SECURITY_HOTSPOT)); + + assertThatSearchReturnsOnly(IssueQuery.builder().severities(asList(Severity.INFO, Severity.MAJOR)), "I1", "I2", "I3"); + assertThatSearchReturnsOnly(IssueQuery.builder().severities(asList(Severity.INFO, Severity.MAJOR)).types(singletonList(VULNERABILITY.name())), "I3"); + assertThatSearchReturnsEmpty(IssueQuery.builder().severities(singletonList(Severity.MAJOR)).types(singletonList(SECURITY_HOTSPOT.name()))); + } + + @Test + public void facet_on_severities_ignore_hotspots() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setSeverity(INFO).setType(BUG), + newDoc("I2", file).setSeverity(INFO).setType(CODE_SMELL), + newDoc("I3", file).setSeverity(INFO).setType(VULNERABILITY), + newDoc("I4", file).setSeverity(MAJOR).setType(VULNERABILITY), + // These 2 entries should be ignored + newDoc("I5", file).setSeverity(INFO).setType(SECURITY_HOTSPOT), + newDoc("I6", file).setSeverity(MAJOR).setType(SECURITY_HOTSPOT)); + + assertThatFacetHasOnly(IssueQuery.builder(), "severities", entry("INFO", 3L), entry("MAJOR", 1L)); + assertThatFacetHasOnly(IssueQuery.builder().types(singletonList(VULNERABILITY.name())), "severities", entry("INFO", 1L), entry("MAJOR", 1L)); + assertThatFacetHasOnly(IssueQuery.builder().types(asList(BUG.name(), CODE_SMELL.name(), VULNERABILITY.name())), "severities", entry("INFO", 3L), entry("MAJOR", 1L)); + assertThatFacetHasOnly(IssueQuery.builder().types(singletonList(SECURITY_HOTSPOT.name())), "severities"); + } + + private void indexIssues(IssueDoc... issues) { + issueIndexer.index(asList(issues).iterator()); + authorizationIndexer.allow(stream(issues).map(issue -> new IndexPermissions(issue.projectUuid(), PROJECT).allowAnyone()).collect(toList())); + } + + @SafeVarargs + private final void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry... expectedEntries) { + SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet))); + Facets facets = new Facets(result, system2.getDefaultTimeZone()); + assertThat(facets.getNames()).containsOnly(facet, "effort"); + assertThat(facets.get(facet)).containsOnly(expectedEntries); + } + + private void assertThatSearchReturnsOnly(IssueQuery.Builder query, String... expectedIssueKeys) { + List keys = searchAndReturnKeys(query); + assertThat(keys).containsExactlyInAnyOrder(expectedIssueKeys); + } + + private void assertThatSearchReturnsEmpty(IssueQuery.Builder query) { + List keys = searchAndReturnKeys(query); + assertThat(keys).isEmpty(); + } + + private List searchAndReturnKeys(IssueQuery.Builder query) { + return Arrays.stream(underTest.search(query.build(), new SearchOptions()).getHits().getHits()) + .map(SearchHit::getId) + .collect(Collectors.toList()); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java index c390ad4e31b..336d049ba05 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java @@ -48,7 +48,6 @@ import org.sonar.server.permission.index.PermissionIndexerTester; import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.rule.index.RuleIndexer; import org.sonar.server.tester.UserSessionRule; -import org.sonar.server.view.index.ViewIndexer; import static com.google.common.collect.ImmutableSortedSet.of; import static java.util.Arrays.asList; @@ -86,7 +85,6 @@ public class IssueIndexTest { public DbTester db = DbTester.create(system2); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); - private ViewIndexer viewIndexer = new ViewIndexer(db.getDbClient(), es.client()); private RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient()); private PermissionIndexerTester authorizationIndexer = new PermissionIndexerTester(es, issueIndexer); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java index 1f44c178677..ee64596c9da 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java @@ -720,6 +720,52 @@ public class SearchActionTest { .containsExactlyInAnyOrder(Common.RuleType.BUG, Common.RuleType.SECURITY_HOTSPOT); } + @Test + public void security_hotspot_are_ignored_when_filtering_by_severities() { + ComponentDto project = db.components().insertPublicProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + RuleDefinitionDto rule = db.rules().insert(); + db.issues().insert(rule, project, file, i -> i.setType(RuleType.BUG).setSeverity(Severity.MAJOR.name())); + db.issues().insert(rule, project, file, i -> i.setType(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR.name())); + db.issues().insert(rule, project, file, i -> i.setType(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR.name())); + db.issues().insert(rule, project, file, i -> i.setType(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.MAJOR.name())); + indexPermissions(); + indexIssues(); + + SearchWsResponse result = ws.newRequest() + .setParam("severities", Severity.MAJOR.name()) + .setParam(FACETS, "severities") + .executeProtobuf(SearchWsResponse.class); + + assertThat(result.getIssuesList()) + .extracting(Issue::getType) + .containsExactlyInAnyOrder(Common.RuleType.BUG, Common.RuleType.VULNERABILITY, Common.RuleType.CODE_SMELL); + assertThat(result.getFacets().getFacets(0).getValuesList()) + .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount) + .containsExactlyInAnyOrder(tuple("MAJOR", 3L), tuple("INFO", 0L), tuple("MINOR", 0L), tuple("CRITICAL", 0L), tuple("BLOCKER", 0L)); + } + + @Test + public void do_not_return_severity_on_security_hotspots() { + ComponentDto project = db.components().insertPublicProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + RuleDefinitionDto rule = db.rules().insert(); + db.issues().insert(rule, project, file, i -> i.setType(RuleType.BUG).setSeverity(Severity.MAJOR.name())); + db.issues().insert(rule, project, file, i -> i.setType(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.MAJOR.name())); + indexPermissions(); + indexIssues(); + + SearchWsResponse result = ws.newRequest() + .setParam("types", String.format("%s,%s", RuleType.BUG, RuleType.SECURITY_HOTSPOT)) + .executeProtobuf(SearchWsResponse.class); + + assertThat(result.getIssuesList()) + .extracting(Issue::getType, Issue::hasSeverity) + .containsExactlyInAnyOrder( + tuple(Common.RuleType.BUG, true), + tuple(Common.RuleType.SECURITY_HOTSPOT, false)); + } + @Test public void return_total_effort() { UserDto john = db.users().insertUser(); -- 2.39.5