]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13566 Add security standards filters to hotspot search WS
authorMichal Duda <michal.duda@sonarsource.com>
Thu, 1 Oct 2020 15:59:54 +0000 (17:59 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 7 Oct 2020 20:07:44 +0000 (20:07 +0000)
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java

index 9ce8c318f2da0c5594104ae89b4866ceb5ea4d88..ad748b320cadd386a878e648467be03d5c0b99eb 100644 (file)
@@ -460,7 +460,7 @@ public class IssueIndex {
         facet.getFilterScope(),
         boolQuery()
           .must(securityCategoryFilter)
-          .must(termQuery(FIELD_ISSUE_TYPE, VULNERABILITY.name())));
+          .must(termsQuery(FIELD_ISSUE_TYPE, VULNERABILITY.name(), SECURITY_HOTSPOT.name())));
     }
   }
 
index 214184cea9d14cba63e8ab3c0917b7da2a6255e1..41a6b9c46aa02ffbc834dd8a5da955607f859672 100644 (file)
@@ -47,7 +47,6 @@ import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.Paging;
 import org.sonar.api.utils.System2;
 import org.sonar.api.web.UserRole;
-import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
@@ -81,6 +80,9 @@ import static org.sonar.api.utils.DateUtils.longToDate;
 import static org.sonar.api.utils.Paging.forPageIndex;
 import static org.sonar.core.util.stream.MoreCollectors.toList;
 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION;
+import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES;
+import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE;
 import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
@@ -98,6 +100,10 @@ public class SearchAction implements HotspotsWsAction {
   private static final String PARAM_PULL_REQUEST = "pullRequest";
   private static final String PARAM_SINCE_LEAK_PERIOD = "sinceLeakPeriod";
   private static final String PARAM_ONLY_MINE = "onlyMine";
+  private static final String PARAM_OWASP_TOP_10 = "owaspTop10";
+  private static final String PARAM_SANS_TOP_25 = "sansTop25";
+  private static final String PARAM_SONARSOURCE_SECURITY = "sonarsourceSecurity";
+
   private static final List<String> STATUSES = ImmutableList.of(STATUS_TO_REVIEW, STATUS_REVIEWED);
 
   private final DbClient dbClient;
@@ -105,7 +111,7 @@ public class SearchAction implements HotspotsWsAction {
   private final IssueIndex issueIndex;
   private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker;
   private final HotspotWsResponseFormatter responseFormatter;
-  private System2 system2;
+  private final System2 system2;
 
   public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex,
     IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
@@ -124,7 +130,7 @@ public class SearchAction implements HotspotsWsAction {
       .createAction("search")
       .setHandler(this)
       .setDescription("Search for Security Hotpots."
-          + "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
+        + "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
       .setSince("8.1")
       .setInternal(true);
 
@@ -163,6 +169,19 @@ public class SearchAction implements HotspotsWsAction {
       .setDescription("If 'projectKey' is provided, returns only Security Hotspots assigned to the current user")
       .setBooleanPossibleValues()
       .setRequired(false);
+    action.createParam(PARAM_OWASP_TOP_10)
+      .setDescription("Comma-separated list of OWASP Top 10 lowercase categories.")
+      .setSince("8.6")
+      .setPossibleValues("a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10");
+    action.createParam(PARAM_SANS_TOP_25)
+      .setDescription("Comma-separated list of SANS Top 25 categories.")
+      .setSince("8.6")
+      .setPossibleValues(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES);
+    action.createParam(PARAM_SONARSOURCE_SECURITY)
+      .setDescription("Comma-separated list of SonarSource security categories. Use '" + SecurityStandards.SQCategory.OTHERS.getKey() +
+        "' to select issues not associated with any category")
+      .setSince("8.6")
+      .setPossibleValues(Arrays.stream(SecurityStandards.SQCategory.values()).map(SecurityStandards.SQCategory::getKey).collect(Collectors.toList()));
 
     action.setResponseExample(getClass().getResource("search-example.json"));
   }
@@ -174,7 +193,7 @@ public class SearchAction implements HotspotsWsAction {
     try (DbSession dbSession = dbClient.openSession(false)) {
       checkIfNeedIssueSync(dbSession, wsRequest);
       Optional<ComponentDto> project = getAndValidateProjectOrApplication(dbSession, wsRequest);
-      SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project, wsRequest.getHotspotKeys());
+      SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project.orElse(null));
       loadComponents(dbSession, searchResponseData);
       loadRules(dbSession, searchResponseData);
       writeProtobuf(formatResponse(searchResponseData), request, response);
@@ -192,15 +211,19 @@ public class SearchAction implements HotspotsWsAction {
   }
 
   private static WsRequest toWsRequest(Request request) {
-    List<String> hotspotKeysList = request.paramAsStrings(PARAM_HOTSPOTS);
-    Set<String> hotspotKeys = hotspotKeysList == null ? ImmutableSet.of() : hotspotKeysList.stream().collect(MoreCollectors.toSet(hotspotKeysList.size()));
+    List<String> hotspotList = request.paramAsStrings(PARAM_HOTSPOTS);
+    Set<String> hotspotKeys = hotspotList != null ? ImmutableSet.copyOf(hotspotList) : ImmutableSet.of();
+    List<String> owaspTop10List = request.paramAsStrings(PARAM_OWASP_TOP_10);
+    Set<String> owaspTop10 = owaspTop10List != null ? ImmutableSet.copyOf(owaspTop10List) : ImmutableSet.of();
+    List<String> sansTop25List = request.paramAsStrings(PARAM_SANS_TOP_25);
+    Set<String> sansTop25 = sansTop25List != null ? ImmutableSet.copyOf(sansTop25List) : ImmutableSet.of();
+    List<String> sonarsourceSecurityList = request.paramAsStrings(PARAM_SONARSOURCE_SECURITY);
+    Set<String> sonarsourceSecurity = sonarsourceSecurityList != null ? ImmutableSet.copyOf(sonarsourceSecurityList) : ImmutableSet.of();
+
     return new WsRequest(
-      request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE),
-      request.param(PARAM_PROJECT_KEY), request.param(PARAM_BRANCH), request.param(PARAM_PULL_REQUEST),
-      hotspotKeys,
-      request.param(PARAM_STATUS), request.param(PARAM_RESOLUTION),
-      request.paramAsBoolean(PARAM_SINCE_LEAK_PERIOD),
-      request.paramAsBoolean(PARAM_ONLY_MINE));
+      request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE), request.param(PARAM_PROJECT_KEY), request.param(PARAM_BRANCH),
+      request.param(PARAM_PULL_REQUEST), hotspotKeys, request.param(PARAM_STATUS), request.param(PARAM_RESOLUTION),
+      request.paramAsBoolean(PARAM_SINCE_LEAK_PERIOD), request.paramAsBoolean(PARAM_ONLY_MINE), owaspTop10, sansTop25, sonarsourceSecurity);
   }
 
   private void validateParameters(WsRequest wsRequest) {
@@ -244,7 +267,7 @@ public class SearchAction implements HotspotsWsAction {
 
   private Optional<ComponentDto> getAndValidateProjectOrApplication(DbSession dbSession, WsRequest wsRequest) {
     return wsRequest.getProjectKey().map(projectKey -> {
-      ComponentDto project = getProject(dbSession, projectKey, wsRequest.getBranch(), wsRequest.getPullRequest())
+      ComponentDto project = getProject(dbSession, projectKey, wsRequest.getBranch().orElse(null), wsRequest.getPullRequest().orElse(null))
         .filter(t -> Scopes.PROJECT.equals(t.scope()) && SUPPORTED_QUALIFIERS.contains(t.qualifier()))
         .filter(ComponentDto::isEnabled)
         .orElseThrow(() -> new NotFoundException(format("Project '%s' not found", projectKey)));
@@ -253,17 +276,17 @@ public class SearchAction implements HotspotsWsAction {
     });
   }
 
-  private Optional<ComponentDto> getProject(DbSession dbSession, String projectKey, Optional<String> branch, Optional<String> pullRequest) {
-    if (branch.isPresent()) {
-      return dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branch.get());
-    } else if (pullRequest.isPresent()) {
-      return dbClient.componentDao().selectByKeyAndPullRequest(dbSession, projectKey, pullRequest.get());
+  private Optional<ComponentDto> getProject(DbSession dbSession, String projectKey, @Nullable String branch, @Nullable String pullRequest) {
+    if (branch != null) {
+      return dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branch);
+    } else if (pullRequest != null) {
+      return dbClient.componentDao().selectByKeyAndPullRequest(dbSession, projectKey, pullRequest);
     }
     return dbClient.componentDao().selectByKey(dbSession, projectKey);
   }
 
-  private SearchResponseData searchHotspots(WsRequest wsRequest, DbSession dbSession, Optional<ComponentDto> project, Set<String> hotspotKeys) {
-    SearchResponse result = doIndexSearch(wsRequest, dbSession, project, hotspotKeys);
+  private SearchResponseData searchHotspots(WsRequest wsRequest, DbSession dbSession, @Nullable ComponentDto project) {
+    SearchResponse result = doIndexSearch(wsRequest, dbSession, project);
     List<String> issueKeys = Arrays.stream(result.getHits().getHits())
       .map(SearchHit::getId)
       .collect(toList(result.getHits().getHits().length));
@@ -286,38 +309,39 @@ public class SearchAction implements HotspotsWsAction {
       .collect(Collectors.toList());
   }
 
-  private SearchResponse doIndexSearch(WsRequest wsRequest, DbSession dbSession, Optional<ComponentDto> project, Set<String> hotspotKeys) {
+  private SearchResponse doIndexSearch(WsRequest wsRequest, DbSession dbSession, @Nullable ComponentDto project) {
     IssueQuery.Builder builder = IssueQuery.builder()
       .types(singleton(RuleType.SECURITY_HOTSPOT.name()))
       .sort(IssueQuery.SORT_HOTSPOTS)
       .asc(true)
       .statuses(wsRequest.getStatus().map(Collections::singletonList).orElse(STATUSES));
-    project.ifPresent(p -> {
-      builder.organizationUuid(p.getOrganizationUuid());
 
-      String projectUuid = firstNonNull(p.getMainBranchProjectUuid(), p.uuid());
-      if (Qualifiers.APP.equals(p.qualifier())) {
+    if (project != null) {
+      builder.organizationUuid(project.getOrganizationUuid());
+      String projectUuid = firstNonNull(project.getMainBranchProjectUuid(), project.uuid());
+      if (Qualifiers.APP.equals(project.qualifier())) {
         builder.viewUuids(singletonList(projectUuid));
       } else {
         builder.projectUuids(singletonList(projectUuid));
       }
 
-      if (p.getMainBranchProjectUuid() == null) {
+      if (project.getMainBranchProjectUuid() == null) {
         builder.mainBranch(true);
       } else {
-        builder.branchUuid(p.uuid());
+        builder.branchUuid(project.uuid());
         builder.mainBranch(false);
       }
 
       if (wsRequest.isSinceLeakPeriod() && !wsRequest.getPullRequest().isPresent()) {
-        Date sinceDate = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, p.uuid())
+        Date sinceDate = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, project.uuid())
           .map(s -> longToDate(s.getPeriodDate()))
           .orElseGet(() -> new Date(system2.now()));
         builder.createdAfter(sinceDate, false);
       }
-    });
-    if (!hotspotKeys.isEmpty()) {
-      builder.issueKeys(hotspotKeys);
+    }
+
+    if (!wsRequest.getHotspotKeys().isEmpty()) {
+      builder.issueKeys(wsRequest.getHotspotKeys());
     }
 
     if (wsRequest.isOnlyMine()) {
@@ -327,6 +351,15 @@ public class SearchAction implements HotspotsWsAction {
 
     wsRequest.getStatus().ifPresent(status -> builder.resolved(STATUS_REVIEWED.equals(status)));
     wsRequest.getResolution().ifPresent(resolution -> builder.resolutions(singleton(resolution)));
+    if (!wsRequest.getOwaspTop10().isEmpty()) {
+      builder.owaspTop10(wsRequest.getOwaspTop10());
+    }
+    if (!wsRequest.getSansTop25().isEmpty()) {
+      builder.sansTop25(wsRequest.getSansTop25());
+    }
+    if (!wsRequest.getSonarsourceSecurity().isEmpty()) {
+      builder.sonarsourceSecurity(wsRequest.getSonarsourceSecurity());
+    }
 
     IssueQuery query = builder.build();
     SearchOptions searchOptions = new SearchOptions()
@@ -430,12 +463,15 @@ public class SearchAction implements HotspotsWsAction {
     private final String resolution;
     private final boolean sinceLeakPeriod;
     private final boolean onlyMine;
+    private final Set<String> owaspTop10;
+    private final Set<String> sansTop25;
+    private final Set<String> sonarsourceSecurity;
 
     private WsRequest(int page, int index,
       @Nullable String projectKey, @Nullable String branch, @Nullable String pullRequest,
       Set<String> hotspotKeys,
       @Nullable String status, @Nullable String resolution, @Nullable Boolean sinceLeakPeriod,
-      @Nullable Boolean onlyMine) {
+      @Nullable Boolean onlyMine, Set<String> owaspTop10, Set<String> sansTop25, Set<String> sonarsourceSecurity) {
       this.page = page;
       this.index = index;
       this.projectKey = projectKey;
@@ -446,6 +482,9 @@ public class SearchAction implements HotspotsWsAction {
       this.resolution = resolution;
       this.sinceLeakPeriod = sinceLeakPeriod != null && sinceLeakPeriod;
       this.onlyMine = onlyMine != null && onlyMine;
+      this.owaspTop10 = owaspTop10;
+      this.sansTop25 = sansTop25;
+      this.sonarsourceSecurity = sonarsourceSecurity;
     }
 
     int getPage() {
@@ -487,6 +526,18 @@ public class SearchAction implements HotspotsWsAction {
     boolean isOnlyMine() {
       return onlyMine;
     }
+
+    public Set<String> getOwaspTop10() {
+      return owaspTop10;
+    }
+
+    public Set<String> getSansTop25() {
+      return sansTop25;
+    }
+
+    public Set<String> getSonarsourceSecurity() {
+      return sonarsourceSecurity;
+    }
   }
 
   private static final class SearchResponseData {
index fb8d5e05b671ac2e74f35a3adc665865ba9a49e8..0ac14278d18e17c98136b15b3fdb95e1177fe518 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.hotspot.ws;
 
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.tngtech.java.junit.dataprovider.DataProvider;
@@ -43,6 +42,7 @@ import org.junit.runner.RunWith;
 import org.sonar.api.impl.utils.TestSystem2;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.System2;
 import org.sonar.api.web.UserRole;
 import org.sonar.db.DbClient;
@@ -56,6 +56,7 @@ import org.sonar.db.rule.RuleTesting;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.index.AsyncIssueIndexing;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
 import org.sonar.server.issue.index.IssueIndexer;
@@ -73,6 +74,7 @@ import org.sonarqube.ws.Hotspots;
 import org.sonarqube.ws.Hotspots.Component;
 import org.sonarqube.ws.Hotspots.SearchWsResponse;
 
+import static com.google.common.collect.ImmutableSet.of;
 import static java.util.Collections.singleton;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
@@ -81,9 +83,7 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -112,26 +112,38 @@ public class SearchActionTest {
   @Rule
   public UserSessionRule userSessionRule = UserSessionRule.standalone();
 
-  private TestSystem2 system2 = new TestSystem2();
-  private DbClient dbClient = dbTester.getDbClient();
-  private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester);
-
-  private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule));
-  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), null);
-  private ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client());
-  private PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
-  private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
-  private IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
-  private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex,
+  private final TestSystem2 system2 = new TestSystem2();
+  private final DbClient dbClient = dbTester.getDbClient();
+  private final TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester);
+  private final IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule));
+  private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), mock(AsyncIssueIndexing.class));
+  private final ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client());
+  private final PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
+  private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
+  private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
+  private final SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex,
     issueIndexSyncProgressChecker, responseFormatter, system2);
-  private WsActionTester actionTester = new WsActionTester(underTest);
+  private final WsActionTester actionTester = new WsActionTester(underTest);
 
   @Test
   public void verify_ws_def() {
+    WebService.Param onlyMineParam = actionTester.getDef().param("onlyMine");
+    WebService.Param owaspTop10Param = actionTester.getDef().param("owaspTop10");
+    WebService.Param sansTop25Param = actionTester.getDef().param("sansTop25");
+    WebService.Param sonarsourceSecurityParam = actionTester.getDef().param("sonarsourceSecurity");
+
     assertThat(actionTester.getDef().isInternal()).isTrue();
-    assertThat(actionTester.getDef().param("onlyMine").isRequired()).isFalse();
+    assertThat(onlyMineParam).isNotNull();
+    assertThat(onlyMineParam.isRequired()).isFalse();
     assertThat(actionTester.getDef().param("onlyMine").possibleValues())
       .containsExactlyInAnyOrder("yes", "no", "true", "false");
+
+    assertThat(owaspTop10Param).isNotNull();
+    assertThat(owaspTop10Param.isRequired()).isFalse();
+    assertThat(sansTop25Param).isNotNull();
+    assertThat(sansTop25Param.isRequired()).isFalse();
+    assertThat(sonarsourceSecurityParam).isNotNull();
+    assertThat(sonarsourceSecurityParam.isRequired()).isFalse();
   }
 
   @Test
@@ -732,12 +744,12 @@ public class SearchActionTest {
     userSessionRule.registerComponents(project);
     userSessionRule.logIn().addProjectPermission(UserRole.USER, project);
 
-    assertThatThrownBy(() -> actionTester.newRequest()
+    TestRequest request = actionTester.newRequest()
       .setParam("hotspots", IntStream.range(2, 10).mapToObj(String::valueOf).collect(joining(",")))
-      .setParam("onlyMine", "true")
-      .execute())
-        .isInstanceOf(IllegalArgumentException.class)
-        .hasMessage("Parameter 'onlyMine' can be used with parameter 'projectKey' only");
+      .setParam("onlyMine", "true");
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Parameter 'onlyMine' can be used with parameter 'projectKey' only");
   }
 
   @Test
@@ -746,12 +758,12 @@ public class SearchActionTest {
 
     userSessionRule.anonymous();
 
-    assertThatThrownBy(() -> actionTester.newRequest()
+    TestRequest request = actionTester.newRequest()
       .setParam("projectKey", project.getKey())
-      .setParam("onlyMine", "true")
-      .execute())
-        .isInstanceOf(IllegalArgumentException.class)
-        .hasMessage("Parameter 'onlyMine' requires user to be logged in");
+      .setParam("onlyMine", "true");
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Parameter 'onlyMine' requires user to be logged in");
   }
 
   @Test
@@ -949,7 +961,7 @@ public class SearchActionTest {
       });
     Stream<Object[]> sqCategoryOTHERS = Stream.of(
       new Object[] {Collections.emptySet(), SQCategory.OTHERS},
-      new Object[] {ImmutableSet.of("foo", "donut", "acme"), SQCategory.OTHERS});
+      new Object[] {of("foo", "donut", "acme"), SQCategory.OTHERS});
     return Stream.concat(allCategoriesButOTHERS, sqCategoryOTHERS).toArray(Object[][]::new);
   }
 
@@ -1308,6 +1320,72 @@ public class SearchActionTest {
       .containsExactlyInAnyOrder(selectedHotspots.stream().map(IssueDto::getKey).toArray(String[]::new));
   }
 
+  @Test
+  public void returns_hotspots_with_specified_sonarsourceSecurity_category() {
+    ComponentDto project = dbTester.components().insertPublicProject();
+    userSessionRule.registerComponents(project);
+    indexPermissions();
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule1 = newRule(SECURITY_HOTSPOT);
+    RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("cwe:117", "cwe:190")));
+    RuleDefinitionDto rule3 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("owaspTop10:a1", "cwe:601")));
+    insertHotspot(project, file, rule1);
+    IssueDto hotspot2 = insertHotspot(project, file, rule2);
+    insertHotspot(project, file, rule3);
+    indexIssues();
+
+    SearchWsResponse response = newRequest(project).setParam("sonarsourceSecurity", "log-injection")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(SearchWsResponse.Hotspot::getKey)
+      .containsExactly(hotspot2.getKey());
+  }
+
+  @Test
+  public void returns_hotspots_with_specified_owaspTop10_category() {
+    ComponentDto project = dbTester.components().insertPublicProject();
+    userSessionRule.registerComponents(project);
+    indexPermissions();
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule1 = newRule(SECURITY_HOTSPOT);
+    RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("cwe:117", "cwe:190")));
+    RuleDefinitionDto rule3 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("owaspTop10:a1", "cwe:601")));
+    insertHotspot(project, file, rule1);
+    insertHotspot(project, file, rule2);
+    IssueDto hotspot3 = insertHotspot(project, file, rule3);
+    indexIssues();
+
+    SearchWsResponse response = newRequest(project).setParam("owaspTop10", "a1")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(SearchWsResponse.Hotspot::getKey)
+      .containsExactly(hotspot3.getKey());
+  }
+
+  @Test
+  public void returns_hotspots_with_specified_sansTop25_category() {
+    ComponentDto project = dbTester.components().insertPublicProject();
+    userSessionRule.registerComponents(project);
+    indexPermissions();
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule1 = newRule(SECURITY_HOTSPOT);
+    RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("cwe:117", "cwe:190")));
+    RuleDefinitionDto rule3 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("owaspTop10:a1", "cwe:601")));
+    insertHotspot(project, file, rule1);
+    insertHotspot(project, file, rule2);
+    IssueDto hotspot3 = insertHotspot(project, file, rule3);
+    indexIssues();
+
+    SearchWsResponse response = newRequest(project).setParam("sansTop25", "insecure-interaction")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getHotspotsList())
+      .extracting(SearchWsResponse.Hotspot::getKey)
+      .containsExactly(hotspot3.getKey());
+  }
+
   @Test
   public void returns_hotspots_on_the_leak_period_when_sinceLeakPeriod_is_true() {
     ComponentDto project = dbTester.components().insertPublicProject();
@@ -1395,7 +1473,7 @@ public class SearchActionTest {
   }
 
   @Test
-  public void returnsall_issues_when_sinceLeakPeriod_is_true_and_is_pr() {
+  public void returns_all_issues_when_sinceLeakPeriod_is_true_and_is_pr() {
     long referenceDate = 800_996_999_332L;
 
     system2.setNow(referenceDate + 10_000);