*/
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 {
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;
}
i.locations as locations,
i.component_uuid as component_uuid,
i.assignee as assigneeUuid,
+ u.login as assigneeLogin,
i.rule_description_context_key as ruleDescriptionContextKey
</sql>
from issues i
inner join rules r on r.uuid = i.rule_uuid
inner join components p on p.uuid=i.component_uuid
+ left join users u on i.assignee = u.uuid
where
<if test="keys.size() > 0">
i.kee IN
--- /dev/null
+/*
+ * 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));
+ }
+}
private static final String DEFAULT_BRANCH = DEFAULT_MAIN_BRANCH_NAME;
- @Rule
- public DbTester dbTester = DbTester.create();
-
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();
}
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());
}
}
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();
.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")
}
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());
}
}
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;
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());
}
AddCommentAction.class,
DeleteCommentAction.class,
EditCommentAction.class,
+ PullAction.class,
+ PullHotspotsActionProtobufObjectGenerator.class,
HotspotsWs.class);
}
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
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;
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";
"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
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);
}
}
- 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;
}
} 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;
+ }
+ }
+
}
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";
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)) {
}
@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");
}
}
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";
return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, issueQueryParams.getBranchUuid(),
issueQueryParams.getRuleRepositories(),
- emptyList(), issueQueryParams.getLanguages(),page);
+ emptyList(), issueQueryParams.getLanguages(), page);
}
}
}
@Override
- protected void validateRuleRepositories(List<String> ruleRepositories) {
+ protected boolean filterNonClosedIssues(IssueDto issueDto, IssueQueryParams queryParams) {
+ return issueDto.getType() != RuleType.SECURITY_HOTSPOT_VALUE;
}
}
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);
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;
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;
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();
}
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);
}
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;
}
@Override
- public IssueLite generateIssueMessage(IssueDto issueDto) {
+ public IssueLite generateIssueMessage(IssueDto issueDto, RuleDto ruleDto) {
IssueLite.Builder issueBuilder = IssueLite.newBuilder();
DbIssues.Locations mainLocation = issueDto.parseLocations();
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 {
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();
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;
}
@Override
- public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto) {
+ public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto, RuleDto ruleDto) {
TaintVulnerabilityLite.Builder taintBuilder = TaintVulnerabilityLite.newBuilder();
Locations locations = issueDto.parseLocations();
--- /dev/null
+# 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;
+}
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);
}
}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
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();
}
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());
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);
}
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;
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());
}
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;
}