Browse Source

SONAR-19339 Expose api/hotspots/pull

tags/10.1.0.73491
Jacek 1 year ago
parent
commit
1998f37bb4
23 changed files with 937 additions and 90 deletions
  1. 4
    4
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java
  2. 2
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
  3. 434
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java
  4. 3
    6
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullActionIT.java
  5. 4
    7
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullTaintActionIT.java
  6. 2
    1
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterIT.java
  7. 2
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
  8. 82
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java
  9. 103
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java
  10. 85
    38
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java
  11. 39
    4
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java
  12. 6
    3
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java
  13. 2
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java
  14. 7
    14
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetriever.java
  15. 2
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java
  16. 5
    2
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java
  17. 2
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java
  18. 26
    0
      server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto
  19. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
  20. 90
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java
  21. 3
    3
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java
  22. 2
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionResponseWriterTest.java
  23. 31
    3
      sonar-ws/src/main/protobuf/ws-hotspots.proto

+ 4
- 4
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java View File

@@ -19,11 +19,11 @@
*/
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;
}

+ 2
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml View File

@@ -663,6 +663,7 @@
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>

@@ -673,6 +674,7 @@
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

+ 434
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java View File

@@ -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));
}
}

+ 3
- 6
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullActionIT.java View 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());
}

}

+ 4
- 7
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullTaintActionIT.java View 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());
}

}

+ 2
- 1
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterIT.java View 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());
}

+ 2
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java View File

@@ -34,6 +34,8 @@ public class HotspotsWsModule extends Module {
AddCommentAction.class,
DeleteCommentAction.class,
EditCommentAction.class,
PullAction.class,
PullHotspotsActionProtobufObjectGenerator.class,
HotspotsWs.class);
}
}

+ 82
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java View File

@@ -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;
}

}

+ 103
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java View File

@@ -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();
}
}

+ 85
- 38
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java View 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;
}
}

}

+ 39
- 4
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java View 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");
}
}

+ 6
- 3
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java View 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;
}
}

+ 2
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java View 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);


+ 7
- 14
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetriever.java View 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);
}

+ 2
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java View 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();


+ 5
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java View 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();

+ 2
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java View 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();


+ 26
- 0
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto View File

@@ -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;
}

+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java View 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);
}
}

+ 90
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java View File

@@ -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();
}

}

+ 3
- 3
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java View 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);
}

+ 2
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionResponseWriterTest.java View 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());
}

+ 31
- 3
sonar-ws/src/main/protobuf/ws-hotspots.proto View 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;
}

Loading…
Cancel
Save