@@ -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; | |||
} |
@@ -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 |
@@ -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)); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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()); | |||
} |
@@ -34,6 +34,8 @@ public class HotspotsWsModule extends Module { | |||
AddCommentAction.class, | |||
DeleteCommentAction.class, | |||
EditCommentAction.class, | |||
PullAction.class, | |||
PullHotspotsActionProtobufObjectGenerator.class, | |||
HotspotsWs.class); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
@@ -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); | |||
} |
@@ -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(); | |||
@@ -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(); |
@@ -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(); | |||
@@ -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; | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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()); | |||
} |
@@ -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; | |||
} |