From: Jacek Date: Wed, 24 May 2023 14:47:00 +0000 (+0200) Subject: SONAR-19339 Expose api/hotspots/pull X-Git-Tag: 10.1.0.73491~192 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=1998f37bb4c45c5f3160705a9b70ad73d9cc04cc;p=sonarqube.git SONAR-19339 Expose api/hotspots/pull --- diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java index 7090b1f46e5..decbe73a6fa 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java @@ -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 languages, @Nullable List ruleRepositories, @Nullable List 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; } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index 96b7ac7cc1b..e9384041190 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -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 @@ -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 i.kee IN diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java new file mode 100644 index 00000000000..e845bfe0a6d --- /dev/null +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/PullActionIT.java @@ -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 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 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 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 issues = readAllIssues(response); + + assertThat(issues).hasSize(2); + } + + @Test + public void wsExecution_whenDifferentHotspotsInTheTable_shouldReturnOnlyThatBelongToSelectedProject() throws IOException { + loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid()); + List correctIssueKeys = generateHotspots(correctProject, correctFile, 10); + List incorrectIssueKeys = generateHotspots(incorrectProject, incorrectFile, 5); + + TestRequest request = tester.newRequest() + .setParam("projectKey", correctProject.getKey()) + .setParam("branchName", DEFAULT_BRANCH); + + TestResponse response = request.execute(); + List 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 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 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 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 issues = readAllIssues(response); + + assertThat(issues).isEmpty(); + } + + private List generateHotspots(ComponentDto project, ComponentDto file, int numberOfIssues) { + Consumer 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 readAllIssues(TestResponse response) throws IOException { + List 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)); + } +} diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullActionIT.java index eb5302f3d6a..fc334308a78 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullActionIT.java @@ -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()); } } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullTaintActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullTaintActionIT.java index a3d623e0a1e..5b9f9acc337 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullTaintActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/PullTaintActionIT.java @@ -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()); } } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterIT.java index d080694a2ea..a2e3c3eb873 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterIT.java @@ -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()); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java index bd35aa56eab..888410bb4cf 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java @@ -34,6 +34,8 @@ public class HotspotsWsModule extends Module { AddCommentAction.class, DeleteCommentAction.class, EditCommentAction.class, + PullAction.class, + PullHotspotsActionProtobufObjectGenerator.class, HotspotsWs.class); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java new file mode 100644 index 00000000000..a15adce755c --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullAction.java @@ -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 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 languages, + @Nullable List ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) { + return new IssueQueryParams(branchDto.getUuid(), languages, emptyList(), emptyList(), false, changedSince); + } + + @Override + protected boolean filterNonClosedIssues(IssueDto issueDto, IssueQueryParams queryParams) { + return issueDto.getType() == Common.RuleType.SECURITY_HOTSPOT_VALUE; + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java new file mode 100644 index 00000000000..083b1f2f03e --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGenerator.java @@ -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(); + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java index 920366f0d9d..cef735db211 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java @@ -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 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 languages, - @Nullable List 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 languages, + @Nullable List 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 ruleRepositories); - - protected abstract IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List languages, - @Nullable List ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince); - - protected abstract Set getIssueKeysSnapshot(IssueQueryParams queryParams, int page); - private void processNonClosedIssuesInBatches(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream, PullActionIssuesRetriever issuesRetriever) { int nextPage = 1; + Map ruleCache = new HashMap<>(); do { - Set issueKeysSnapshot = new HashSet<>(getIssueKeysSnapshot(queryParams, nextPage)); - Consumer> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream); - issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer); + Set issueKeysSnapshot = getIssueKeysSnapshot(queryParams, nextPage); + Consumer> listConsumer = issueDtos -> { + populateRuleCache(dbSession, ruleCache, issueDtos); + pullActionResponseWriter.appendIssuesToResponse(issueDtos, ruleCache, outputStream); + }; + Predicate 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 ruleCache, List issueDtos) { + Set 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 getIssueKeysSnapshot(IssueQueryParams queryParams, int page); + + protected static class BasePullRequest { + private final String projectKey; + private final String branchName; + private List languages = null; + private List 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 languages) { + this.languages = languages; + return this; + } + + public BasePullRequest repositories(@Nullable List 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; + } + } + } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java index 3b5143d6a51..f1cb59a934c 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java @@ -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 ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM); + if (ruleRepositories != null && !ruleRepositories.isEmpty()) { + validateRuleRepositories(ruleRepositories); + } + wsRequest + .repositories(ruleRepositories) + .resolvedOnly(resolvedOnly); + } + @Override protected Set 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 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 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"); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java index 0d79646b4da..7d091942758 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java @@ -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 ruleRepositories) { + protected boolean filterNonClosedIssues(IssueDto issueDto, IssueQueryParams queryParams) { + return issueDto.getType() != RuleType.SECURITY_HOTSPOT_VALUE; } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java index ff43742445b..95433100d02 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java @@ -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); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetriever.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetriever.java index 646934a4ede..e19a57bd5cf 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetriever.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetriever.java @@ -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 issueKeysSnapshot, Consumer> listConsumer) { + public void processIssuesByBatch(DbSession dbSession, Set issueKeysSnapshot, Consumer> listConsumer, Predicate filter) { boolean hasMoreIssues = !issueKeysSnapshot.isEmpty(); long offset = 0; @@ -49,7 +50,11 @@ public class PullActionIssuesRetriever { while (hasMoreIssues) { Set page = paginate(issueKeysSnapshot, offset); - issueDtos.addAll(filterIssues(nextOpenIssues(dbSession, page))); + List 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 filterIssues(List 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 retrieveClosedIssues(DbSession dbSession) { return dbClient.issueDao().selectRecentlyClosedIssues(dbSession, issueQueryParams); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java index 47e0085f924..0eb3629f5b0 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java @@ -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(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java index 82b91ee8945..b193aecd0e5 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java @@ -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 issueDtos, OutputStream outputStream) { + public void appendIssuesToResponse(List issueDtos, Map 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(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java index beb7d2cf21b..4bf5de02faa 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java @@ -32,6 +32,7 @@ import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDto; import org.sonar.db.protobuf.DbIssues; +import org.sonar.db.rule.RuleDto; import org.sonar.server.user.UserSession; import org.sonar.server.ws.MessageFormattingUtils; import org.sonarqube.ws.Common; @@ -61,7 +62,7 @@ public class PullTaintActionProtobufObjectGenerator implements ProtobufObjectGen } @Override - public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto) { + public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto, RuleDto ruleDto) { TaintVulnerabilityLite.Builder taintBuilder = TaintVulnerabilityLite.newBuilder(); Locations locations = issueDto.parseLocations(); diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto new file mode 100644 index 00000000000..b53fda769ed --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/pull-hotspot-example.proto @@ -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; +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java index 3c6882dfd68..2d5c3593e06 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java @@ -29,6 +29,6 @@ public class HotspotsWsModuleTest { public void verify_count_of_added_components() { ListContainer container = new ListContainer(); new HotspotsWsModule().configure(container); - assertThat(container.getAddedObjects()).hasSize(10); + assertThat(container.getAddedObjects()).hasSize(12); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java new file mode 100644 index 00000000000..772420b8bf2 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/PullHotspotsActionProtobufObjectGeneratorTest.java @@ -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(); + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java index d3f1b4737a2..a8b9d57f141 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java @@ -65,7 +65,7 @@ public class PullActionIssuesRetrieverTest { List returnedDtos = new ArrayList<>(); Consumer> 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 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> uuidsCaptor = ArgumentCaptor.forClass(Set.class); verify(issueDao, times(2)).selectByBranch(any(), uuidsCaptor.capture(), any()); @@ -105,7 +105,7 @@ public class PullActionIssuesRetrieverTest { Consumer> listConsumer = returnedDtos::addAll; Set 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); } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionResponseWriterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionResponseWriterTest.java index d9418459d9f..4ace2b8cc5c 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionResponseWriterTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionResponseWriterTest.java @@ -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()); } diff --git a/sonar-ws/src/main/protobuf/ws-hotspots.proto b/sonar-ws/src/main/protobuf/ws-hotspots.proto index 0e0599e42de..58fcfc367f8 100644 --- a/sonar-ws/src/main/protobuf/ws-hotspots.proto +++ b/sonar-ws/src/main/protobuf/ws-hotspots.proto @@ -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; }