]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19339 Expose api/hotspots/pull
authorJacek <jacek.poreda@sonarsource.com>
Wed, 24 May 2023 14:47:00 +0000 (16:47 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 30 May 2023 20:02:52 +0000 (20:02 +0000)
23 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullTaintActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetriever.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionResponseWriterTest.java
sonar-ws/src/main/protobuf/ws-hotspots.proto

index 7090b1f46e5745417c7d2b7e4ac2b0ee0d53f473..decbe73a6fa76ab0da41f1e7a5f524c399806061 100644 (file)
  */
 package org.sonar.db.issue;
 
-import java.util.ArrayList;
 import java.util.List;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
+import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNullElse;
 
 public class IssueQueryParams {
@@ -38,9 +38,9 @@ public class IssueQueryParams {
   public IssueQueryParams(String branchUuid, @Nullable List<String> languages, @Nullable List<String> ruleRepositories,
     @Nullable List<String> excludingRuleRepositories, boolean resolvedOnly, @Nullable Long changedSince) {
     this.branchUuid = branchUuid;
-    this.languages = requireNonNullElse(languages, new ArrayList<>());
-    this.ruleRepositories = requireNonNullElse(ruleRepositories, new ArrayList<>());
-    this.excludingRuleRepositories = requireNonNullElse(excludingRuleRepositories, new ArrayList<>());
+    this.languages = requireNonNullElse(languages, emptyList());
+    this.ruleRepositories = requireNonNullElse(ruleRepositories, emptyList());
+    this.excludingRuleRepositories = requireNonNullElse(excludingRuleRepositories, emptyList());
     this.resolvedOnly = resolvedOnly;
     this.changedSince = changedSince;
   }
index 96b7ac7cc1b955cc0a963e13822f8f60284dc454..e93840411901728bdda5437a0dc6d09778887617 100644 (file)
     i.locations as locations,
     i.component_uuid as component_uuid,
     i.assignee as assigneeUuid,
+    u.login as assigneeLogin,
     i.rule_description_context_key as ruleDescriptionContextKey
   </sql>
 
     from issues i
         inner join rules r on r.uuid = i.rule_uuid
         inner join components p on p.uuid=i.component_uuid
+        left join users u on i.assignee = u.uuid
     where
       <if test="keys.size() > 0">
         i.kee IN
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java
new file mode 100644 (file)
index 0000000..e845bfe
--- /dev/null
@@ -0,0 +1,434 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.hotspot.ws;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.db.issue.IssueDbTester;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Hotspots;
+import org.sonarqube.ws.Issues;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.BranchDto.DEFAULT_MAIN_BRANCH_NAME;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+public class PullActionIT {
+
+  private static final long NOW = 10_000_000_000L;
+  private static final long PAST = 1_000_000_000L;
+
+  private static final String DEFAULT_BRANCH = DEFAULT_MAIN_BRANCH_NAME;
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+
+  private final System2 system2 = mock(System2.class);
+  private final PullHotspotsActionProtobufObjectGenerator pullActionProtobufObjectGenerator = new PullHotspotsActionProtobufObjectGenerator();
+
+  private final ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT);
+  private final ComponentFinder componentFinder = new ComponentFinder(db.getDbClient(), resourceTypes);
+
+  private final IssueDbTester issueDbTester = new IssueDbTester(db);
+  private final ComponentDbTester componentDbTester = new ComponentDbTester(db);
+
+  private final PullAction underTest = new PullAction(system2, componentFinder, db.getDbClient(), userSession,
+    pullActionProtobufObjectGenerator);
+  private final WsActionTester tester = new WsActionTester(underTest);
+
+  private ComponentDto correctProject, incorrectProject;
+  private ComponentDto correctFile, incorrectFile;
+
+  @Before
+  public void before() {
+    when(system2.now()).thenReturn(NOW);
+    correctProject = db.components().insertPrivateProject().getMainBranchComponent();
+    correctFile = db.components().insertComponent(newFileDto(correctProject));
+
+    incorrectProject = db.components().insertPrivateProject().getMainBranchComponent();
+    incorrectFile = db.components().insertComponent(newFileDto(incorrectProject));
+  }
+
+  @Test
+  public void wsExecution_whenMissingParams_shouldThrowIllegalArgumentException() {
+    TestRequest request = tester.newRequest();
+
+    assertThatThrownBy(() -> request.executeProtobuf(Issues.IssuesPullQueryTimestamp.class))
+      .isInstanceOf(IllegalArgumentException.class);
+  }
+
+  @Test
+  public void wsExecution_whenNotExistingProjectKey_shouldThrowException() {
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", "projectKey")
+      .setParam("branchName", DEFAULT_BRANCH);
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("Project 'projectKey' not found");
+  }
+
+  @Test
+  public void wsExecution_whenValidProjectKeyWithoutPermissionsTo_shouldThrowException() {
+    userSession.logIn();
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", correctProject.getKey())
+      .setParam("branchName", DEFAULT_BRANCH);
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(ForbiddenException.class)
+      .hasMessage("Insufficient privileges");
+  }
+
+  @Test
+  public void wsExecution_whenNotExistingBranchKey_shouldThrowException() {
+    DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
+      .setStartLine(1)
+      .setEndLine(2)
+      .setStartOffset(3)
+      .setEndOffset(4)
+      .build();
+    DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder()
+      .setChecksum("hash")
+      .setTextRange(textRange);
+
+    RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("java").setRuleKey("S1000"));
+    IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR")
+      .setManualSeverity(true)
+      .setMessage("message")
+      .setCreatedAt(NOW)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setLocations(mainLocation.build())
+      .setType(Common.RuleType.BUG.getNumber()));
+    loginWithBrowsePermission(issueDto);
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", issueDto.getProjectKey())
+      .setParam("branchName", "non-existent-branch");
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage(format("Branch 'non-existent-branch' in project '%s' not found", issueDto.getProjectKey()));
+  }
+
+  @Test
+  public void wsExecution_whenValidProjectKeyAndOneHotspotOnBranch_shouldReturnOneHotspot() throws IOException {
+    DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
+      .setStartLine(1)
+      .setEndLine(2)
+      .setStartOffset(3)
+      .setEndOffset(4)
+      .build();
+    DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder()
+      .setChecksum("hash")
+      .setTextRange(textRange);
+
+    UserDto assignee = db.users().insertUser();
+    IssueDto issueDto = issueDbTester.insertHotspot(p -> p.setSeverity("MINOR")
+      .setMessage("message")
+      .setAssigneeUuid(assignee.getUuid())
+      .setCreatedAt(NOW)
+      .setStatus(Issue.STATUS_TO_REVIEW)
+      .setLocations(mainLocation.build()));
+
+    loginWithBrowsePermission(issueDto);
+
+    TestResponse response = tester.newRequest()
+      .setParam("projectKey", issueDto.getProjectKey())
+      .setParam("branchName", DEFAULT_BRANCH)
+      .execute();
+
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues).hasSize(1);
+
+    Hotspots.TextRange expectedTextRange = Hotspots.TextRange.newBuilder()
+      .setStartLine(1)
+      .setEndLine(2)
+      .setStartLineOffset(3)
+      .setEndLineOffset(4)
+      .setHash("hash")
+      .build();
+    Hotspots.HotspotLite expectedHotspotLite = Hotspots.HotspotLite.newBuilder()
+      .setKey(issueDto.getKey())
+      .setFilePath(issueDto.getFilePath())
+      .setVulnerabilityProbability("LOW")
+      .setStatus(Issue.STATUS_TO_REVIEW)
+      .setMessage("message")
+      .setCreationDate(NOW)
+      .setTextRange(expectedTextRange)
+      .setRuleKey(issueDto.getRuleKey().toString())
+      .setAssignee(assignee.getLogin())
+      .build();
+    Hotspots.HotspotLite issueLite = issues.get(0);
+    assertThat(issueLite).isEqualTo(expectedHotspotLite);
+  }
+
+  @Test
+  public void wsExecution_whenHotspotOnAnotherBranchThanMain_shouldReturnOneIssue() throws IOException {
+    ComponentDto developBranch = componentDbTester.insertPrivateProjectWithCustomBranch("develop").getMainBranchComponent();
+    ComponentDto developFile = db.components().insertComponent(newFileDto(developBranch));
+    List<String> hotspotKeys = generateHotspots(developBranch, developFile, 1);
+    loginWithBrowsePermission(developBranch.uuid(), developFile.uuid());
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", developBranch.getKey())
+      .setParam("branchName", "develop");
+
+    TestResponse response = request.execute();
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues).hasSize(1)
+      .extracting(Hotspots.HotspotLite::getKey)
+      .containsExactlyInAnyOrderElementsOf(hotspotKeys);
+  }
+
+  @Test
+  public void wsExecution_whenIncrementalModeThen_shouldReturnClosedIssues() throws IOException {
+    IssueDto toReviewHotspot = issueDbTester.insertHotspot(p -> p.setSeverity("MINOR")
+      .setMessage("toReviewHotspot")
+      .setCreatedAt(NOW)
+      .setStatus(Issue.STATUS_TO_REVIEW));
+
+    issueDbTester.insertHotspot(p -> p.setSeverity("MINOR")
+      .setMessage("closedIssue")
+      .setCreatedAt(NOW)
+      .setStatus(Issue.STATUS_CLOSED)
+      .setComponentUuid(toReviewHotspot.getComponentUuid())
+      .setProjectUuid(toReviewHotspot.getProjectUuid())
+      .setIssueUpdateTime(PAST)
+      .setIssueCreationTime(PAST));
+
+    loginWithBrowsePermission(toReviewHotspot);
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", toReviewHotspot.getProjectKey())
+      .setParam("branchName", DEFAULT_BRANCH)
+      .setParam("changedSince", PAST + "");
+
+    TestResponse response = request.execute();
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues).hasSize(2);
+  }
+
+  @Test
+  public void wsExecution_whenDifferentHotspotsInTheTable_shouldReturnOnlyThatBelongToSelectedProject() throws IOException {
+    loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+    List<String> correctIssueKeys = generateHotspots(correctProject, correctFile, 10);
+    List<String> incorrectIssueKeys = generateHotspots(incorrectProject, incorrectFile, 5);
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", correctProject.getKey())
+      .setParam("branchName", DEFAULT_BRANCH);
+
+    TestResponse response = request.execute();
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues)
+      .hasSize(10)
+      .extracting(Hotspots.HotspotLite::getKey)
+      .containsExactlyInAnyOrderElementsOf(correctIssueKeys)
+      .doesNotContainAnyElementsOf(incorrectIssueKeys);
+  }
+
+  @Test
+  public void wsExecution_whenNoIssuesBelongToTheProject_shouldReturnZeroIssues() throws IOException {
+    loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+    generateHotspots(incorrectProject, incorrectFile, 5);
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", correctProject.getKey())
+      .setParam("branchName", DEFAULT_BRANCH);
+
+    TestResponse response = request.execute();
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues).isEmpty();
+  }
+
+  @Test
+  public void wsExecution_whenLanguagesParam_shouldReturnOneIssue() throws IOException {
+    loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+    RuleDto javaRule = db.rules().insert(r -> r.setLanguage("java"));
+
+    IssueDto javaIssue = issueDbTester.insertHotspot(p -> p.setSeverity("MINOR")
+      .setMessage("openIssue")
+      .setCreatedAt(NOW)
+      .setRule(javaRule)
+      .setRuleUuid(javaRule.getUuid())
+      .setStatus(Issue.STATUS_TO_REVIEW)
+      .setLanguage("java")
+      .setProject(correctProject)
+      .setComponent(correctFile));
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", correctProject.getKey())
+      .setParam("branchName", DEFAULT_BRANCH)
+      .setParam("languages", "java");
+
+    TestResponse response = request.execute();
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues).hasSize(1)
+      .extracting(Hotspots.HotspotLite::getKey)
+      .containsExactly(javaIssue.getKey());
+  }
+
+  @Test
+  public void wsExecution_whenChangedSinceParam_shouldReturnMatchingIssue() throws IOException {
+    loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+    RuleDto javaRule = db.rules().insert(r -> r.setLanguage("java"));
+
+    IssueDto issueBefore = issueDbTester.insertHotspot(p -> p.setSeverity("MINOR")
+      .setMessage("openIssue")
+      .setCreatedAt(NOW)
+      .setRule(javaRule)
+      .setRuleUuid(javaRule.getUuid())
+      .setStatus(Issue.STATUS_TO_REVIEW)
+      .setLanguage("java")
+      .setProject(correctProject)
+      .setComponent(correctFile));
+
+    IssueDto issueAfter = issueDbTester.insertHotspot(p -> p.setSeverity("MINOR")
+      .setMessage("openIssue")
+      .setCreatedAt(NOW)
+      .setRule(javaRule)
+      .setRuleUuid(javaRule.getUuid())
+      .setStatus(Issue.STATUS_TO_REVIEW)
+      .setLanguage("java")
+      .setProject(correctProject)
+      .setUpdatedAt(NOW)
+      .setComponent(correctFile));
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", correctProject.getKey())
+      .setParam("branchName", DEFAULT_BRANCH)
+      .setParam("languages", "java")
+      .setParam("changedSince", String.valueOf(issueBefore.getIssueUpdateTime() + 1L));
+
+    TestResponse response = request.execute();
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues).extracting(Hotspots.HotspotLite::getKey)
+      .doesNotContain(issueBefore.getKey())
+      .containsExactly(issueAfter.getKey());
+  }
+
+  @Test
+  public void wsExecution_whenWrongLanguageSet_shouldReturnZeroIssues() throws IOException {
+    loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+    RuleDto javascriptRule = db.rules().insert(r -> r.setLanguage("javascript"));
+
+    issueDbTester.insertHotspot(p -> p.setSeverity("MINOR")
+      .setMessage("openIssue")
+      .setCreatedAt(NOW)
+      .setRule(javascriptRule)
+      .setRuleUuid(javascriptRule.getUuid())
+      .setStatus(Issue.STATUS_TO_REVIEW)
+      .setProject(correctProject)
+      .setComponent(correctFile));
+
+    TestRequest request = tester.newRequest()
+      .setParam("projectKey", correctProject.getKey())
+      .setParam("branchName", DEFAULT_BRANCH)
+      .setParam("languages", "java");
+
+    TestResponse response = request.execute();
+    List<Hotspots.HotspotLite> issues = readAllIssues(response);
+
+    assertThat(issues).isEmpty();
+  }
+
+  private List<String> generateHotspots(ComponentDto project, ComponentDto file, int numberOfIssues) {
+    Consumer<IssueDto> consumer = i -> i.setProject(project)
+      .setComponentUuid(file.uuid())
+      .setStatus(Issue.STATUS_TO_REVIEW);
+    return Stream.generate(() -> issueDbTester.insertHotspot(consumer))
+      .limit(numberOfIssues)
+      .map(IssueDto::getKey)
+      .collect(Collectors.toList());
+  }
+
+  private List<Hotspots.HotspotLite> readAllIssues(TestResponse response) throws IOException {
+    List<Hotspots.HotspotLite> issues = new ArrayList<>();
+    InputStream inputStream = response.getInputStream();
+    Hotspots.HotspotPullQueryTimestamp hotspotPullQueryTimestamp = Hotspots.HotspotPullQueryTimestamp.parseDelimitedFrom(inputStream);
+    assertThat(hotspotPullQueryTimestamp).isNotNull();
+    assertThat(hotspotPullQueryTimestamp.getQueryTimestamp()).isEqualTo(NOW);
+    while (inputStream.available() > 0) {
+      issues.add(Hotspots.HotspotLite.parseDelimitedFrom(inputStream));
+    }
+
+    return issues;
+  }
+
+  private void loginWithBrowsePermission(IssueDto issueDto) {
+    loginWithBrowsePermission(issueDto.getProjectUuid(), issueDto.getComponentUuid());
+  }
+
+  private void loginWithBrowsePermission(String projectUuid, String componentUuid) {
+    UserDto user = db.users().insertUser("john");
+    userSession.logIn(user).addProjectPermission(USER, getComponentOrFail(projectUuid, "project not found"), getComponentOrFail(componentUuid, "component not found"));
+  }
+
+  private ComponentDto getComponentOrFail(String componentUuid, String failMessage) {
+    return db.getDbClient().componentDao()
+      .selectByUuid(db.getSession(), componentUuid)
+      .orElseGet(() -> fail(failMessage));
+  }
+}
index eb5302f3d6a02ac2f597fc220fbef31ef3b8c4cc..fc334308a785adf8c08c85f461f394f42355d057 100644 (file)
@@ -67,9 +67,6 @@ public class PullActionIT {
 
   private static final String DEFAULT_BRANCH = DEFAULT_MAIN_BRANCH_NAME;
 
-  @Rule
-  public DbTester dbTester = DbTester.create();
-
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
@@ -468,11 +465,11 @@ public class PullActionIT {
   }
 
   private void loginWithBrowsePermission(String projectUuid, String componentUuid) {
-    UserDto user = dbTester.users().insertUser("john");
+    UserDto user = db.users().insertUser("john");
     userSession.logIn(user)
       .addProjectPermission(USER,
-        db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), projectUuid).get(),
-        db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), componentUuid).get());
+        db.getDbClient().componentDao().selectByUuid(db.getSession(), projectUuid).get(),
+        db.getDbClient().componentDao().selectByUuid(db.getSession(), componentUuid).get());
   }
 
 }
index a3d623e0a1e899c1bce41e74ba42803442cc80bd..5b9f9acc3375e71bad3bbb4216bb237fd4bbe864 100644 (file)
@@ -71,9 +71,6 @@ public class PullTaintActionIT {
   private static final String DEFAULT_BRANCH = DEFAULT_MAIN_BRANCH_NAME;
   public static final DbIssues.MessageFormatting MESSAGE_FORMATTING = DbIssues.MessageFormatting.newBuilder().setStart(0).setEnd(4).setType(CODE).build();
 
-  @Rule
-  public DbTester dbTester = DbTester.create();
-
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
@@ -400,7 +397,7 @@ public class PullTaintActionIT {
       .setType(2)
       .setRuleDescriptionContextKey(ruledescriptionContextKey));
 
-    //this one should not be returned - it is a normal issue, no taint
+    // this one should not be returned - it is a normal issue, no taint
     issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
       .setManualSeverity(true)
       .setMessage("openIssue")
@@ -496,11 +493,11 @@ public class PullTaintActionIT {
   }
 
   private void loginWithBrowsePermission(String projectUuid, String componentUuid) {
-    UserDto user = dbTester.users().insertUser("john");
+    UserDto user = db.users().insertUser("john");
     userSession.logIn(user)
       .addProjectPermission(USER,
-        db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), projectUuid).get(),
-        db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), componentUuid).get());
+        db.getDbClient().componentDao().selectByUuid(db.getSession(), projectUuid).get(),
+        db.getDbClient().componentDao().selectByUuid(db.getSession(), componentUuid).get());
   }
 
 }
index d080694a2ea7645c68f102d7c7f0b1182aad34f2..a2e3c3eb873c108ba5b08270620447ce6866c98e 100644 (file)
@@ -33,6 +33,7 @@ import org.sonar.db.protobuf.DbCommons;
 import org.sonar.db.protobuf.DbIssues;
 import org.sonar.server.tester.UserSessionRule;
 
+import static java.util.Collections.emptyMap;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.atLeastOnce;
@@ -74,7 +75,7 @@ public class PullTaintActionResponseWriterIT {
 
     issueDto.setLocations(locations);
 
-    underTest.appendIssuesToResponse(List.of(issueDto), outputStream);
+    underTest.appendIssuesToResponse(List.of(issueDto), emptyMap(), outputStream);
 
     verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
   }
index bd35aa56eaba828d400d0b86ad2e6bdc268dc14c..888410bb4cf9fb9f45d22e55cd621c8d404c81c0 100644 (file)
@@ -34,6 +34,8 @@ public class HotspotsWsModule extends Module {
       AddCommentAction.class,
       DeleteCommentAction.class,
       EditCommentAction.class,
+      PullAction.class,
+      PullHotspotsActionProtobufObjectGenerator.class,
       HotspotsWs.class);
   }
 }
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java
new file mode 100644 (file)
index 0000000..a15adce
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.hotspot.ws;
+
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueQueryParams;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.ws.BasePullAction;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Common;
+
+import static java.util.Collections.emptyList;
+
+public class PullAction extends BasePullAction implements HotspotsWsAction {
+  private static final String ISSUE_TYPE = "hotspots";
+  private static final String ACTION_NAME = "pull";
+  private static final String RESOURCE_EXAMPLE = "pull-hotspot-example.proto";
+  private static final String SINCE_VERSION = "10.1";
+
+  private final DbClient dbClient;
+
+  public PullAction(System2 system2, ComponentFinder componentFinder, DbClient dbClient, UserSession userSession,
+    PullHotspotsActionProtobufObjectGenerator protobufObjectGenerator) {
+    super(system2, componentFinder, dbClient, userSession, protobufObjectGenerator, ACTION_NAME,
+      ISSUE_TYPE, "", SINCE_VERSION, RESOURCE_EXAMPLE);
+    this.dbClient = dbClient;
+  }
+
+  @Override
+  protected Set<String> getIssueKeysSnapshot(IssueQueryParams issueQueryParams, int page) {
+    Long changedSinceDate = issueQueryParams.getChangedSince();
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      if (changedSinceDate != null) {
+        return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, issueQueryParams.getBranchUuid(),
+          changedSinceDate, issueQueryParams.getRuleRepositories(), emptyList(),
+          issueQueryParams.getLanguages(), page);
+      }
+
+      return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, issueQueryParams.getBranchUuid(),
+        issueQueryParams.getRuleRepositories(),
+        emptyList(), issueQueryParams.getLanguages(), page);
+
+    }
+  }
+
+  @Override
+  protected IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages,
+    @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) {
+    return new IssueQueryParams(branchDto.getUuid(), languages, emptyList(), emptyList(), false, changedSince);
+  }
+
+  @Override
+  protected boolean filterNonClosedIssues(IssueDto issueDto, IssueQueryParams queryParams) {
+    return issueDto.getType() == Common.RuleType.SECURITY_HOTSPOT_VALUE;
+  }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java
new file mode 100644 (file)
index 0000000..083b1f2
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.hotspot.ws;
+
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.server.issue.ws.pull.ProtobufObjectGenerator;
+import org.sonar.server.security.SecurityStandards;
+import org.sonarqube.ws.Hotspots;
+
+import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
+
+@ServerSide
+public class PullHotspotsActionProtobufObjectGenerator implements ProtobufObjectGenerator {
+
+  @Override
+  public Hotspots.HotspotPullQueryTimestamp generateTimestampMessage(long timestamp) {
+    Hotspots.HotspotPullQueryTimestamp.Builder responseBuilder = Hotspots.HotspotPullQueryTimestamp.newBuilder();
+    responseBuilder.setQueryTimestamp(timestamp);
+    return responseBuilder.build();
+  }
+
+  @Override
+  public Hotspots.HotspotLite generateIssueMessage(IssueDto hotspotDto, RuleDto ruleDto) {
+    Hotspots.HotspotLite.Builder builder = Hotspots.HotspotLite.newBuilder()
+      .setKey(hotspotDto.getKey())
+      .setFilePath(hotspotDto.getFilePath())
+      .setCreationDate(hotspotDto.getCreatedAt())
+      .setStatus(hotspotDto.getStatus())
+      .setRuleKey(hotspotDto.getRuleKey().toString())
+      .setStatus(hotspotDto.getStatus())
+      .setVulnerabilityProbability(getVulnerabilityProbability(ruleDto));
+
+    String resolution = hotspotDto.getResolution();
+    if (resolution != null) {
+      builder.setResolution(resolution);
+    }
+
+    String assigneeLogin = hotspotDto.getAssigneeLogin();
+    if (assigneeLogin != null) {
+      builder.setAssignee(assigneeLogin);
+    }
+
+    String message = hotspotDto.getMessage();
+    if (message != null) {
+      builder.setMessage(message);
+    }
+
+    DbIssues.Locations mainLocation = hotspotDto.parseLocations();
+    if (mainLocation != null) {
+      Hotspots.TextRange textRange = getTextRange(mainLocation);
+      builder.setTextRange(textRange);
+    }
+    return builder.build();
+  }
+
+  private static String getVulnerabilityProbability(RuleDto ruleDto) {
+    SecurityStandards.SQCategory sqCategory = fromSecurityStandards(ruleDto.getSecurityStandards()).getSqCategory();
+    return sqCategory.getVulnerability().name();
+  }
+
+  private static Hotspots.TextRange getTextRange(DbIssues.Locations mainLocation) {
+    int startLine = mainLocation.getTextRange().getStartLine();
+    int endLine = mainLocation.getTextRange().getEndLine();
+    int startOffset = mainLocation.getTextRange().getStartOffset();
+    int endOffset = mainLocation.getTextRange().getEndOffset();
+
+    return Hotspots.TextRange.newBuilder()
+      .setHash(mainLocation.getChecksum())
+      .setStartLine(startLine)
+      .setEndLine(endLine)
+      .setStartLineOffset(startOffset)
+      .setEndLineOffset(endOffset)
+      .build();
+  }
+
+  @Override
+  public Hotspots.HotspotLite generateClosedIssueMessage(String uuid) {
+    return Hotspots.HotspotLite.newBuilder()
+      .setKey(uuid)
+      .setClosed(true)
+      .build();
+  }
+}
index 920366f0d9d17f7435b680a5cbac1da1d395f50f..cef735db211e27a6cd8825923d766bf3048878fc 100644 (file)
@@ -21,10 +21,13 @@ package org.sonar.server.issue.ws;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -36,17 +39,19 @@ import org.sonar.db.component.BranchDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.issue.IssueQueryParams;
 import org.sonar.db.project.ProjectDto;
+import org.sonar.db.rule.RuleDto;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.issue.ws.pull.ProtobufObjectGenerator;
 import org.sonar.server.issue.ws.pull.PullActionIssuesRetriever;
 import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
 import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.WsAction;
 
 import static java.lang.String.format;
 import static java.util.Collections.emptyList;
 import static org.sonar.api.web.UserRole.USER;
 
-public abstract class BasePullAction implements IssuesWsAction {
+public abstract class BasePullAction implements WsAction {
   protected static final String PROJECT_KEY_PARAM = "projectKey";
   protected static final String BRANCH_NAME_PARAM = "branchName";
   protected static final String LANGUAGES_PARAM = "languages";
@@ -108,16 +113,11 @@ public abstract class BasePullAction implements IssuesWsAction {
         "If not present all non-closed %s are returned.", issueType, issueType))
       .setExampleValue(1_654_032_306_000L);
 
-    if (issueType.equals("issues")) {
-      action.createParam(RULE_REPOSITORIES_PARAM)
-        .setDescription(format("Comma separated list of rule repositories. If not present all %s regardless of" +
-          " their rule repository are returned.", issueType))
-        .setExampleValue(repositoryExample);
+    additionalParams(action);
+  }
 
-      action.createParam(RESOLVED_ONLY_PARAM)
-        .setDescription(format("If true only %s with resolved status are returned", issueType))
-        .setExampleValue("true");
-    }
+  protected void additionalParams(WebService.NewAction action) {
+    // define additional parameters if needed
   }
 
   @Override
@@ -128,36 +128,37 @@ public abstract class BasePullAction implements IssuesWsAction {
     String changedSince = request.param(CHANGED_SINCE_PARAM);
     Long changedSinceTimestamp = changedSince != null ? Long.parseLong(changedSince) : null;
 
-    if (issueType.equals("issues")) {
-      boolean resolvedOnly = Boolean.parseBoolean(request.param(RESOLVED_ONLY_PARAM));
+    BasePullRequest wsRequest = new BasePullRequest(projectKey, branchName)
+      .languages(languages)
+      .changedSinceTimestamp(changedSinceTimestamp);
 
-      List<String> ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM);
-      if (ruleRepositories != null && !ruleRepositories.isEmpty()) {
-        validateRuleRepositories(ruleRepositories);
-      }
+    processAdditionalParams(request, wsRequest);
 
-      streamResponse(projectKey, branchName, languages, ruleRepositories, resolvedOnly, changedSinceTimestamp, response.stream().output());
-    } else {
-      streamResponse(projectKey, branchName, languages, emptyList(), false, changedSinceTimestamp, response.stream().output());
-    }
+    doHandle(wsRequest, response.stream().output());
   }
 
-  private void streamResponse(String projectKey, String branchName, @Nullable List<String> languages,
-    @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince, OutputStream outputStream)
-    throws IOException {
+  protected void processAdditionalParams(Request request, BasePullRequest wsRequest) {
+    // process additional parameters if needed
+  }
+
+  private void doHandle(BasePullRequest wsRequest, OutputStream outputStream) throws IOException {
 
     try (DbSession dbSession = dbClient.openSession(false)) {
-      ProjectDto projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
+      ProjectDto projectDto = componentFinder.getProjectByKey(dbSession, wsRequest.projectKey);
       userSession.checkProjectPermission(USER, projectDto);
-      BranchDto branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchName, null);
-      pullActionResponseWriter.appendTimestampToResponse(outputStream);
-      IssueQueryParams issueQueryParams = initializeQueryParams(branchDto, languages, ruleRepositories, resolvedOnly, changedSince);
+      BranchDto branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, wsRequest.branchName, null);
+      IssueQueryParams issueQueryParams = initializeQueryParams(branchDto, wsRequest.languages, wsRequest.repositories,
+        wsRequest.resolvedOnly, wsRequest.changedSinceTimestamp);
       retrieveAndSendIssues(dbSession, issueQueryParams, outputStream);
     }
   }
 
+  protected abstract IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages,
+    @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince);
+
   private void retrieveAndSendIssues(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream)
     throws IOException {
+    pullActionResponseWriter.appendTimestampToResponse(outputStream);
 
     var issuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
 
@@ -170,20 +171,18 @@ public abstract class BasePullAction implements IssuesWsAction {
     }
   }
 
-  protected abstract void validateRuleRepositories(List<String> ruleRepositories);
-
-  protected abstract IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages,
-    @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince);
-
-  protected abstract Set<String> getIssueKeysSnapshot(IssueQueryParams queryParams, int page);
-
   private void processNonClosedIssuesInBatches(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream,
     PullActionIssuesRetriever issuesRetriever) {
     int nextPage = 1;
+    Map<String, RuleDto> ruleCache = new HashMap<>();
     do {
-      Set<String> issueKeysSnapshot = new HashSet<>(getIssueKeysSnapshot(queryParams, nextPage));
-      Consumer<List<IssueDto>> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream);
-      issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer);
+      Set<String> issueKeysSnapshot = getIssueKeysSnapshot(queryParams, nextPage);
+      Consumer<List<IssueDto>> listConsumer = issueDtos -> {
+        populateRuleCache(dbSession, ruleCache, issueDtos);
+        pullActionResponseWriter.appendIssuesToResponse(issueDtos, ruleCache, outputStream);
+      };
+      Predicate<IssueDto> filter = issueDto -> filterNonClosedIssues(issueDto, queryParams);
+      issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer, filter);
 
       if (issueKeysSnapshot.isEmpty()) {
         nextPage = -1;
@@ -192,4 +191,52 @@ public abstract class BasePullAction implements IssuesWsAction {
       }
     } while (nextPage > 0);
   }
+
+  private void populateRuleCache(DbSession dbSession, Map<String, RuleDto> ruleCache, List<IssueDto> issueDtos) {
+    Set<String> rulesToQueryFor = issueDtos.stream()
+      .map(IssueDto::getRuleUuid)
+      .filter(ruleUuid -> !ruleCache.containsKey(ruleUuid))
+      .collect(Collectors.toSet());
+    dbClient.ruleDao().selectByUuids(dbSession, rulesToQueryFor)
+      .forEach(ruleDto -> ruleCache.putIfAbsent(ruleDto.getUuid(), ruleDto));
+  }
+
+  protected abstract boolean filterNonClosedIssues(IssueDto issueDto, IssueQueryParams queryParams);
+
+  protected abstract Set<String> getIssueKeysSnapshot(IssueQueryParams queryParams, int page);
+
+  protected static class BasePullRequest {
+    private final String projectKey;
+    private final String branchName;
+    private List<String> languages = null;
+    private List<String> repositories = null;
+    private boolean resolvedOnly = false;
+    private Long changedSinceTimestamp = null;
+
+    public BasePullRequest(String projectKey, String branchName) {
+      this.projectKey = projectKey;
+      this.branchName = branchName;
+    }
+
+    public BasePullRequest languages(@Nullable List<String> languages) {
+      this.languages = languages;
+      return this;
+    }
+
+    public BasePullRequest repositories(@Nullable List<String> repositories) {
+      this.repositories = repositories == null ? emptyList() : repositories;
+      return this;
+    }
+
+    public BasePullRequest resolvedOnly(boolean resolvedOnly) {
+      this.resolvedOnly = resolvedOnly;
+      return this;
+    }
+
+    public BasePullRequest changedSinceTimestamp(@Nullable Long changedSinceTimestamp) {
+      this.changedSinceTimestamp = changedSinceTimestamp;
+      return this;
+    }
+  }
+
 }
index 3b5143d6a51354b8f780e9c57a10c9e3d670d027..f1cb59a934c906aa56d12f6a68945d6384bf4c37 100644 (file)
@@ -23,21 +23,26 @@ import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import javax.annotation.Nullable;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.BranchDto;
+import org.sonar.db.issue.IssueDto;
 import org.sonar.db.issue.IssueQueryParams;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.issue.TaintChecker;
 import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator;
 import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Common.RuleType;
 
+import static java.lang.Boolean.parseBoolean;
 import static java.util.Optional.ofNullable;
 import static org.sonarqube.ws.WsUtils.checkArgument;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL;
 
-public class PullAction extends BasePullAction {
+public class PullAction extends BasePullAction implements IssuesWsAction {
   private static final String ISSUE_TYPE = "issues";
   private static final String REPOSITORY_EXAMPLE = "java";
   private static final String RESOURCE_EXAMPLE = "pull-example.proto";
@@ -54,6 +59,31 @@ public class PullAction extends BasePullAction {
     this.taintChecker = taintChecker;
   }
 
+  @Override
+  protected void additionalParams(WebService.NewAction action) {
+    action.createParam(RULE_REPOSITORIES_PARAM)
+      .setDescription("Comma separated list of rule repositories. If not present all issues regardless of" +
+        " their rule repository are returned.")
+      .setExampleValue(repositoryExample);
+
+    action.createParam(RESOLVED_ONLY_PARAM)
+      .setDescription("If true only issues with resolved status are returned")
+      .setExampleValue("true");
+  }
+
+  @Override
+  protected void processAdditionalParams(Request request, BasePullRequest wsRequest) {
+    boolean resolvedOnly = parseBoolean(request.param(RESOLVED_ONLY_PARAM));
+
+    List<String> ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM);
+    if (ruleRepositories != null && !ruleRepositories.isEmpty()) {
+      validateRuleRepositories(ruleRepositories);
+    }
+    wsRequest
+      .repositories(ruleRepositories)
+      .resolvedOnly(resolvedOnly);
+  }
+
   @Override
   protected Set<String> getIssueKeysSnapshot(IssueQueryParams issueQueryParams, int page) {
     try (DbSession dbSession = dbClient.openSession(false)) {
@@ -78,10 +108,15 @@ public class PullAction extends BasePullAction {
   }
 
   @Override
-  protected void validateRuleRepositories(List<String> ruleRepositories) {
+  protected boolean filterNonClosedIssues(IssueDto issueDto, IssueQueryParams queryParams) {
+    return issueDto.getType() != RuleType.SECURITY_HOTSPOT_VALUE &&
+      (!queryParams.isResolvedOnly() || issueDto.getStatus().equals("RESOLVED"));
+  }
+
+  private void validateRuleRepositories(List<String> ruleRepositories) {
     checkArgument(ruleRepositories
       .stream()
-      .filter(taintChecker.getTaintRepositories()::contains)
-      .count() == 0, "Incorrect rule repositories list: it should only include repositories that define Issues, and no Taint Vulnerabilities");
+      .noneMatch(taintChecker.getTaintRepositories()::contains),
+      "Incorrect rule repositories list: it should only include repositories that define Issues, and no Taint Vulnerabilities");
   }
 }
index 0d79646b4da497a656709fce73843d976e7d4750..7d09194275867f590315bac75e8e207f71999baf 100644 (file)
@@ -27,17 +27,19 @@ import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.BranchDto;
+import org.sonar.db.issue.IssueDto;
 import org.sonar.db.issue.IssueQueryParams;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.issue.TaintChecker;
 import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator;
 import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Common.RuleType;
 
 import static java.util.Collections.emptyList;
 import static java.util.Optional.ofNullable;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL_TAINT;
 
-public class PullTaintAction extends BasePullAction {
+public class PullTaintAction extends BasePullAction implements IssuesWsAction {
   private static final String ISSUE_TYPE = "taint vulnerabilities";
   private static final String RESOURCE_EXAMPLE = "pull-taint-example.proto";
   private static final String SINCE_VERSION = "9.6";
@@ -66,7 +68,7 @@ public class PullTaintAction extends BasePullAction {
 
       return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, issueQueryParams.getBranchUuid(),
         issueQueryParams.getRuleRepositories(),
-        emptyList(), issueQueryParams.getLanguages(),page);
+        emptyList(), issueQueryParams.getLanguages(), page);
 
     }
   }
@@ -78,6 +80,7 @@ public class PullTaintAction extends BasePullAction {
   }
 
   @Override
-  protected void validateRuleRepositories(List<String> ruleRepositories) {
+  protected boolean filterNonClosedIssues(IssueDto issueDto, IssueQueryParams queryParams) {
+    return issueDto.getType() != RuleType.SECURITY_HOTSPOT_VALUE;
   }
 }
index ff43742445b00a6f0a0052d90d8b36be7cd4f5c5..95433100d02d3b55918b0a75d4b012c1e46ff138 100644 (file)
@@ -22,12 +22,13 @@ package org.sonar.server.issue.ws.pull;
 import com.google.protobuf.AbstractMessageLite;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
 import org.sonarqube.ws.Issues;
 
 public interface ProtobufObjectGenerator {
   AbstractMessageLite generateTimestampMessage(long timestamp);
 
-  AbstractMessageLite generateIssueMessage(IssueDto issueDto);
+  AbstractMessageLite generateIssueMessage(IssueDto issueDto, RuleDto ruleDto);
 
   AbstractMessageLite generateClosedIssueMessage(String uuid);
 
index 646934a4ede895ca7c479e03cc7a504f313b32c7..e19a57bd5cf60cac066053f44908afdb977222b9 100644 (file)
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -41,7 +42,7 @@ public class PullActionIssuesRetriever {
     this.issueQueryParams = queryParams;
   }
 
-  public void processIssuesByBatch(DbSession dbSession, Set<String> issueKeysSnapshot, Consumer<List<IssueDto>> listConsumer) {
+  public void processIssuesByBatch(DbSession dbSession, Set<String> issueKeysSnapshot, Consumer<List<IssueDto>> listConsumer, Predicate<? super IssueDto> filter) {
     boolean hasMoreIssues = !issueKeysSnapshot.isEmpty();
     long offset = 0;
 
@@ -49,7 +50,11 @@ public class PullActionIssuesRetriever {
 
     while (hasMoreIssues) {
       Set<String> page = paginate(issueKeysSnapshot, offset);
-      issueDtos.addAll(filterIssues(nextOpenIssues(dbSession, page)));
+      List<IssueDto> nextOpenIssues = nextOpenIssues(dbSession, page)
+        .stream()
+        .filter(filter)
+        .toList();
+      issueDtos.addAll(nextOpenIssues);
       offset += page.size();
       hasMoreIssues = offset < issueKeysSnapshot.size();
     }
@@ -57,18 +62,6 @@ public class PullActionIssuesRetriever {
     listConsumer.accept(issueDtos);
   }
 
-  private List<IssueDto> filterIssues(List<IssueDto> issues) {
-    return issues
-      .stream()
-      .filter(i -> hasCorrectTypeAndStatus(i, issueQueryParams))
-      .toList();
-  }
-
-  private static boolean hasCorrectTypeAndStatus(IssueDto issueDto, IssueQueryParams queryParams) {
-    return issueDto.getType() != 4 &&
-      (queryParams.isResolvedOnly() ? issueDto.getStatus().equals("RESOLVED") : true);
-  }
-
   public List<String> retrieveClosedIssues(DbSession dbSession) {
     return dbClient.issueDao().selectRecentlyClosedIssues(dbSession, issueQueryParams);
   }
index 47e0085f92457cb0b61290ee63741ee7534a173f..0eb3629f5b036e62bf2889c7b8cf4bc10e945cbf 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.issue.ws.pull;
 import org.sonar.api.server.ServerSide;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
 import org.sonarqube.ws.Common;
 
 import static org.sonarqube.ws.Issues.IssueLite;
@@ -40,7 +41,7 @@ public class PullActionProtobufObjectGenerator implements ProtobufObjectGenerato
   }
 
   @Override
-  public IssueLite generateIssueMessage(IssueDto issueDto) {
+  public IssueLite generateIssueMessage(IssueDto issueDto, RuleDto ruleDto) {
     IssueLite.Builder issueBuilder = IssueLite.newBuilder();
     DbIssues.Locations mainLocation = issueDto.parseLocations();
 
index 82b91ee89451717f6eddb12b9d959439b96b2852..b193aecd0e564f927d3fabaa6ab6d857b910e566 100644 (file)
@@ -23,9 +23,11 @@ import com.google.protobuf.AbstractMessageLite;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
+import java.util.Map;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.utils.System2;
 import org.sonar.db.issue.IssueDto;
+import org.sonar.db.rule.RuleDto;
 
 @ServerSide
 public class PullActionResponseWriter {
@@ -43,10 +45,11 @@ public class PullActionResponseWriter {
     messageLite.writeDelimitedTo(outputStream);
   }
 
-  public void appendIssuesToResponse(List<IssueDto> issueDtos, OutputStream outputStream) {
+  public void appendIssuesToResponse(List<IssueDto> issueDtos, Map<String, RuleDto> ruleCache, OutputStream outputStream) {
     try {
       for (IssueDto issueDto : issueDtos) {
-        AbstractMessageLite messageLite = protobufObjectGenerator.generateIssueMessage(issueDto);
+        RuleDto ruleDto = ruleCache.get(issueDto.getRuleUuid());
+        AbstractMessageLite messageLite = protobufObjectGenerator.generateIssueMessage(issueDto, ruleDto);
         messageLite.writeDelimitedTo(outputStream);
       }
       outputStream.flush();
index beb7d2cf21b8874de2f872b9cb9b199ee7f5641a..4bf5de02faaf916c18044bcf0ca41a16ba63f5bc 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.ws.MessageFormattingUtils;
 import org.sonarqube.ws.Common;
@@ -61,7 +62,7 @@ public class PullTaintActionProtobufObjectGenerator implements ProtobufObjectGen
   }
 
   @Override
-  public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto) {
+  public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto, RuleDto ruleDto) {
     TaintVulnerabilityLite.Builder taintBuilder = TaintVulnerabilityLite.newBuilder();
     Locations locations = issueDto.parseLocations();
 
diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto
new file mode 100644 (file)
index 0000000..b53fda7
--- /dev/null
@@ -0,0 +1,26 @@
+# The response contains a single protocol buffer message: HotspotPullQueryTimestamp followed by 0..n number of HotspotLite protocol buffer messages.
+message HotspotPullQueryTimestamp {
+  required int64 queryTimestamp = 1;
+}
+
+message HotspotLite {
+  optional string key = 1;
+  optional string filePath = 2;
+  optional string vulnerabilityProbability = 3;
+  optional string status = 4;
+  optional string resolution = 5;
+  optional string message = 6;
+  optional int64 creationDate = 7;
+  optional TextRange textRange = 8;
+  optional string ruleKey = 9;
+  optional bool closed = 10;
+  optional string assignee = 11;
+}
+
+message TextRange {
+  optional int32 startLine = 1;
+  optional int32 startLineOffset = 2;
+  optional int32 endLine = 3;
+  optional int32 endLineOffset = 4;
+  optional string hash = 5;
+}
index 3c6882dfd68981429e8046de2fe75ecf81c1ebec..2d5c3593e06f11c0361012aed7392087b3c9e8b0 100644 (file)
@@ -29,6 +29,6 @@ public class HotspotsWsModuleTest {
   public void verify_count_of_added_components() {
     ListContainer container = new ListContainer();
     new HotspotsWsModule().configure(container);
-    assertThat(container.getAddedObjects()).hasSize(10);
+    assertThat(container.getAddedObjects()).hasSize(12);
   }
 }
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java
new file mode 100644 (file)
index 0000000..772420b
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.hotspot.ws;
+
+import java.util.Date;
+import java.util.Set;
+import org.junit.Test;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
+import org.sonarqube.ws.Hotspots.HotspotLite;
+import org.sonarqube.ws.Hotspots.HotspotPullQueryTimestamp;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PullHotspotsActionProtobufObjectGeneratorTest {
+
+  private final PullHotspotsActionProtobufObjectGenerator underTest = new PullHotspotsActionProtobufObjectGenerator();
+
+  @Test
+  public void generateTimestampMessage_shouldMapTimestamp() {
+    long timestamp = System.currentTimeMillis();
+    HotspotPullQueryTimestamp result = underTest.generateTimestampMessage(timestamp);
+    assertThat(result.getQueryTimestamp()).isEqualTo(timestamp);
+  }
+
+  @Test
+  public void generateIssueMessage_shouldMapDtoFields() {
+    IssueDto issueDto = new IssueDto()
+      .setKee("key")
+      .setFilePath("/home/src/Class.java")
+      .setProjectKey("my-project-key")
+      .setStatus("REVIEWED")
+      .setResolution("FIXED")
+      .setRuleKey("repo", "rule")
+      .setRuleUuid("rule-uuid-1")
+      .setMessage("Look at me, I'm the issue now!")
+      .setAssigneeLogin("assignee-login")
+      .setIssueCreationDate(new Date());
+
+    DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+      .setTextRange(range(2, 3))
+      .build();
+    issueDto.setLocations(locations);
+
+    RuleDto ruleDto = new RuleDto()
+      .setSecurityStandards(Set.of("cwe:489,cwe:570,cwe:571"));
+
+    HotspotLite result = underTest.generateIssueMessage(issueDto, ruleDto);
+    assertThat(result).extracting(
+      HotspotLite::getKey,
+      HotspotLite::getFilePath,
+      HotspotLite::getVulnerabilityProbability,
+      HotspotLite::getStatus,
+      HotspotLite::getResolution,
+      HotspotLite::getRuleKey,
+      HotspotLite::getAssignee)
+      .containsExactly("key", "/home/src/Class.java", "LOW", "REVIEWED", "FIXED", "repo:rule", "assignee-login");
+  }
+
+  @Test
+  public void generateClosedIssueMessage_shouldMapClosedHotspotFields() {
+    HotspotLite result = underTest.generateClosedIssueMessage("uuid");
+    assertThat(result).extracting(HotspotLite::getKey, HotspotLite::getClosed)
+      .containsExactly("uuid", true);
+  }
+
+  private static org.sonar.db.protobuf.DbCommons.TextRange range(int startLine, int endLine) {
+    return DbCommons.TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine).build();
+  }
+
+}
index d3f1b4737a29ed29d995de660b5c26cc5db4e25f..a8b9d57f141650768d0214d12b8cb293e4d4d3e7 100644 (file)
@@ -65,7 +65,7 @@ public class PullActionIssuesRetrieverTest {
     List<IssueDto> returnedDtos = new ArrayList<>();
     Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
 
-    pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), Set.of(), listConsumer);
+    pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), Set.of(), listConsumer, issueDto -> true);
 
     assertThat(returnedDtos).isEmpty();
   }
@@ -83,7 +83,7 @@ public class PullActionIssuesRetrieverTest {
 
     Set<String> thousandIssueUuidsSnapshot = thousandIssues.stream().map(IssueDto::getKee).collect(Collectors.toSet());
     thousandIssueUuidsSnapshot.add(singleIssue.getKee());
-    pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), thousandIssueUuidsSnapshot, listConsumer);
+    pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), thousandIssueUuidsSnapshot, listConsumer, issueDto -> true);
 
     ArgumentCaptor<Set<String>> uuidsCaptor = ArgumentCaptor.forClass(Set.class);
     verify(issueDao, times(2)).selectByBranch(any(), uuidsCaptor.capture(), any());
@@ -105,7 +105,7 @@ public class PullActionIssuesRetrieverTest {
     Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
 
     Set<String> issueKeysSnapshot = issuesWithSameCreationTimestamp.stream().map(IssueDto::getKee).collect(Collectors.toSet());
-    pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), issueKeysSnapshot, listConsumer);
+    pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), issueKeysSnapshot, listConsumer, issueDto -> true);
 
     assertThat(returnedDtos).hasSize(100);
   }
index d9418459d9f3f8d888250a31fec64540aebcca95..4ace2b8cc5cbbc251ab05e0b102ed21820385e62 100644 (file)
@@ -27,6 +27,7 @@ import org.junit.Test;
 import org.sonar.api.utils.System2;
 import org.sonar.db.issue.IssueDto;
 
+import static java.util.Collections.emptyMap;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.atLeastOnce;
@@ -55,7 +56,7 @@ public class PullActionResponseWriterTest {
     issueDto.setStatus("OPEN");
     issueDto.setRuleKey("repo", "rule");
 
-    underTest.appendIssuesToResponse(List.of(issueDto), outputStream);
+    underTest.appendIssuesToResponse(List.of(issueDto), emptyMap(), outputStream);
 
     verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
   }
index 0e0599e42de39e3a77f59751f2e9e55f863dd1e4..58fcfc367f8f0de34828527ee3974535dc169737 100644 (file)
@@ -89,12 +89,40 @@ message Component {
   optional string pullRequest = 8;
 }
 
+
 message Rule {
   optional string key = 1;
   optional string name = 2;
   optional string securityCategory = 3;
   optional string vulnerabilityProbability = 4;
-  optional string riskDescription = 5 [deprecated=true];
-  optional string vulnerabilityDescription = 6 [deprecated=true];
-  optional string fixRecommendations = 7 [deprecated=true];
+  optional string riskDescription = 5 [deprecated = true];
+  optional string vulnerabilityDescription = 6 [deprecated = true];
+  optional string fixRecommendations = 7 [deprecated = true];
+}
+
+// Response of GET api/hotspots/pull
+message HotspotPullQueryTimestamp {
+  required int64 queryTimestamp = 1;
+}
+
+message HotspotLite {
+  optional string key = 1;
+  optional string filePath = 2;
+  optional string vulnerabilityProbability = 3;
+  optional string status = 4;
+  optional string resolution = 5;
+  optional string message = 6;
+  optional int64 creationDate = 7;
+  optional TextRange textRange = 8;
+  optional string ruleKey = 9;
+  optional bool closed = 10;
+  optional string assignee = 11;
+}
+
+message TextRange {
+  optional int32 startLine = 1;
+  optional int32 startLineOffset = 2;
+  optional int32 endLine = 3;
+  optional int32 endLineOffset = 4;
+  optional string hash = 5;
 }