Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

ListAction.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.hotspot.ws;
  21. import com.google.common.base.Preconditions;
  22. import java.util.Collection;
  23. import java.util.HashSet;
  24. import java.util.List;
  25. import java.util.Set;
  26. import java.util.stream.Collectors;
  27. import java.util.stream.Stream;
  28. import javax.annotation.Nullable;
  29. import org.sonar.api.rules.RuleType;
  30. import org.sonar.api.server.ws.Request;
  31. import org.sonar.api.server.ws.Response;
  32. import org.sonar.api.server.ws.WebService;
  33. import org.sonar.api.utils.Paging;
  34. import org.sonar.db.DbClient;
  35. import org.sonar.db.DbSession;
  36. import org.sonar.db.Pagination;
  37. import org.sonar.db.component.BranchDto;
  38. import org.sonar.db.component.ComponentDto;
  39. import org.sonar.db.issue.IssueDto;
  40. import org.sonar.db.issue.IssueListQuery;
  41. import org.sonar.db.newcodeperiod.NewCodePeriodType;
  42. import org.sonar.db.protobuf.DbIssues;
  43. import org.sonar.server.component.ComponentFinder;
  44. import org.sonar.server.component.ComponentFinder.ProjectAndBranch;
  45. import org.sonar.server.hotspot.ws.HotspotWsResponseFormatter.SearchResponseData;
  46. import org.sonar.server.issue.NewCodePeriodResolver;
  47. import org.sonar.server.user.UserSession;
  48. import org.sonarqube.ws.Common;
  49. import org.sonarqube.ws.Hotspots;
  50. import static com.google.common.base.Strings.isNullOrEmpty;
  51. import static java.lang.String.format;
  52. import static java.util.Collections.emptyList;
  53. import static java.util.Collections.singletonList;
  54. import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
  55. import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
  56. import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
  57. import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
  58. import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
  59. import static org.sonar.api.server.ws.WebService.Param.PAGE;
  60. import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
  61. import static org.sonar.api.utils.Paging.forPageIndex;
  62. import static org.sonar.api.web.UserRole.USER;
  63. import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE;
  64. import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
  65. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  66. import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
  67. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  68. public class ListAction implements HotspotsWsAction {
  69. private static final String PARAM_PROJECT = "project";
  70. private static final String PARAM_BRANCH = "branch";
  71. private static final String PARAM_PULL_REQUEST = "pullRequest";
  72. private static final String PARAM_STATUS = "status";
  73. private static final String PARAM_RESOLUTION = "resolution";
  74. private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod";
  75. private static final List<String> STATUSES = List.of(STATUS_TO_REVIEW, STATUS_REVIEWED);
  76. private final DbClient dbClient;
  77. private final UserSession userSession;
  78. private final HotspotWsResponseFormatter responseFormatter;
  79. private final NewCodePeriodResolver newCodePeriodResolver;
  80. private final ComponentFinder componentFinder;
  81. public ListAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter,
  82. NewCodePeriodResolver newCodePeriodResolver, ComponentFinder componentFinder) {
  83. this.dbClient = dbClient;
  84. this.userSession = userSession;
  85. this.responseFormatter = responseFormatter;
  86. this.newCodePeriodResolver = newCodePeriodResolver;
  87. this.componentFinder = componentFinder;
  88. }
  89. @Override
  90. public void define(WebService.NewController controller) {
  91. WebService.NewAction action = controller
  92. .createAction("list")
  93. .setHandler(this)
  94. .setInternal(true)
  95. .setDescription("List Security Hotpots. This endpoint is used in degraded mode, when issue indexation is running." +
  96. "<br>Total number of Security Hotspots will be always equal to a page size, as counting all issues is not supported. " +
  97. "<br>Requires the 'Browse' permission on the specified project. ")
  98. .setSince("10.2");
  99. action.addPagingParams(100, MAX_PAGE_SIZE);
  100. action.createParam(PARAM_PROJECT)
  101. .setDescription("Key of the project")
  102. .setRequired(true)
  103. .setExampleValue(KEY_PROJECT_EXAMPLE_001);
  104. action.createParam(PARAM_BRANCH)
  105. .setDescription("Branch key. Not available in the community edition.")
  106. .setExampleValue(KEY_BRANCH_EXAMPLE_001);
  107. action.createParam(PARAM_PULL_REQUEST)
  108. .setDescription("Pull request id. Not available in the community edition.")
  109. .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
  110. action.createParam(PARAM_STATUS)
  111. .setDescription("If '%s' is provided, only Security Hotspots with the specified status are returned.", PARAM_PROJECT)
  112. .setPossibleValues(STATUSES)
  113. .setRequired(false);
  114. action.createParam(PARAM_RESOLUTION)
  115. .setDescription(format(
  116. "If '%s' is provided and if status is '%s', only Security Hotspots with the specified resolution are returned.",
  117. PARAM_PROJECT, STATUS_REVIEWED))
  118. .setPossibleValues(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED)
  119. .setRequired(false);
  120. action.createParam(PARAM_IN_NEW_CODE_PERIOD)
  121. .setDescription("If '%s' is provided, only Security Hotspots created in the new code period are returned.", PARAM_IN_NEW_CODE_PERIOD)
  122. .setBooleanPossibleValues()
  123. .setDefaultValue("false");
  124. action.setResponseExample(getClass().getResource("search-example.json"));
  125. }
  126. @Override
  127. public void handle(Request request, Response response) throws Exception {
  128. WsRequest wsRequest = toWsRequest(request);
  129. try (DbSession dbSession = dbClient.openSession(false)) {
  130. ProjectAndBranch projectAndBranch = validate(dbSession, wsRequest);
  131. SearchResponseData searchResponseData = searchHotspots(dbSession, wsRequest, projectAndBranch);
  132. loadComponents(dbSession, searchResponseData);
  133. writeProtobuf(formatResponse(searchResponseData), request, response);
  134. }
  135. }
  136. private static WsRequest toWsRequest(Request request) {
  137. return new WsRequest(
  138. request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE))
  139. .project(request.param(PARAM_PROJECT))
  140. .branch(request.param(PARAM_BRANCH))
  141. .pullRequest(request.param(PARAM_PULL_REQUEST))
  142. .status(request.param(PARAM_STATUS))
  143. .resolution(request.param(PARAM_RESOLUTION))
  144. .inNewCodePeriod(request.paramAsBoolean(PARAM_IN_NEW_CODE_PERIOD));
  145. }
  146. private ProjectAndBranch validate(DbSession dbSession, WsRequest wsRequest) {
  147. Preconditions.checkArgument(isNullOrEmpty(wsRequest.branch) || isNullOrEmpty(wsRequest.pullRequest),
  148. "Only one of parameters '%s' and '%s' can be provided", PARAM_BRANCH, PARAM_PULL_REQUEST);
  149. ProjectAndBranch projectAndBranch = componentFinder.getProjectAndBranch(dbSession, wsRequest.project,
  150. wsRequest.branch, wsRequest.pullRequest);
  151. userSession.checkEntityPermission(USER, projectAndBranch.getProject());
  152. return projectAndBranch;
  153. }
  154. private SearchResponseData searchHotspots(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) {
  155. List<IssueDto> hotspots = getHotspotKeys(dbSession, wsRequest, projectAndBranch);
  156. Paging paging = forPageIndex(wsRequest.page).withPageSize(wsRequest.pageSize).andTotal(hotspots.size());
  157. return new SearchResponseData(paging, hotspots);
  158. }
  159. private List<IssueDto> getHotspotKeys(DbSession dbSession, WsRequest wsRequest, ProjectAndBranch projectAndBranch) {
  160. BranchDto branch = projectAndBranch.getBranch();
  161. IssueListQuery.IssueListQueryBuilder queryBuilder = IssueListQuery.IssueListQueryBuilder.newIssueListQueryBuilder()
  162. .project(wsRequest.project)
  163. .branch(branch.getBranchKey())
  164. .pullRequest(branch.getPullRequestKey())
  165. .statuses(wsRequest.status != null ? singletonList(wsRequest.status) : emptyList())
  166. .resolutions(wsRequest.resolution != null ? singletonList(wsRequest.resolution) : emptyList())
  167. .types(singletonList(RuleType.SECURITY_HOTSPOT.getDbConstant()));
  168. String branchKey = branch.getBranchKey();
  169. if (wsRequest.inNewCodePeriod && wsRequest.pullRequest == null && branchKey != null) {
  170. NewCodePeriodResolver.ResolvedNewCodePeriod newCodePeriod = newCodePeriodResolver.resolveForProjectAndBranch(dbSession, wsRequest.project, branchKey);
  171. if (NewCodePeriodType.REFERENCE_BRANCH == newCodePeriod.type()) {
  172. queryBuilder.newCodeOnReference(true);
  173. } else {
  174. queryBuilder.createdAfter(newCodePeriod.periodDate());
  175. }
  176. }
  177. Pagination pagination = Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize);
  178. return dbClient.issueDao().selectByQuery(dbSession, queryBuilder.build(), pagination);
  179. }
  180. private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
  181. Set<String> componentUuids = searchResponseData.getHotspots().stream()
  182. .flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid()))
  183. .collect(Collectors.toSet());
  184. Set<String> locationComponentUuids = searchResponseData.getHotspots()
  185. .stream()
  186. .flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream())
  187. .collect(Collectors.toSet());
  188. Set<String> aggregatedComponentUuids = Stream.of(componentUuids, locationComponentUuids)
  189. .flatMap(Collection::stream)
  190. .collect(Collectors.toSet());
  191. if (!aggregatedComponentUuids.isEmpty()) {
  192. List<ComponentDto> componentDtos = dbClient.componentDao().selectByUuids(dbSession, aggregatedComponentUuids);
  193. searchResponseData.addComponents(componentDtos);
  194. }
  195. }
  196. private static Set<String> getHotspotLocationComponentUuids(IssueDto hotspot) {
  197. Set<String> locationComponentUuids = new HashSet<>();
  198. DbIssues.Locations locations = hotspot.parseLocations();
  199. if (locations == null) {
  200. return locationComponentUuids;
  201. }
  202. List<DbIssues.Flow> flows = locations.getFlowList();
  203. for (DbIssues.Flow flow : flows) {
  204. List<DbIssues.Location> flowLocations = flow.getLocationList();
  205. for (DbIssues.Location location : flowLocations) {
  206. if (location.hasComponentId()) {
  207. locationComponentUuids.add(location.getComponentId());
  208. }
  209. }
  210. }
  211. return locationComponentUuids;
  212. }
  213. private Hotspots.ListWsResponse formatResponse(SearchResponseData searchResponseData) {
  214. Hotspots.ListWsResponse.Builder responseBuilder = Hotspots.ListWsResponse.newBuilder();
  215. formatPaging(searchResponseData, responseBuilder);
  216. if (searchResponseData.isPresent()) {
  217. responseFormatter.formatHotspots(searchResponseData, responseBuilder);
  218. }
  219. return responseBuilder.build();
  220. }
  221. private static void formatPaging(SearchResponseData searchResponseData, Hotspots.ListWsResponse.Builder responseBuilder) {
  222. Paging paging = searchResponseData.getPaging();
  223. Common.Paging.Builder pagingBuilder = Common.Paging.newBuilder()
  224. .setPageIndex(paging.pageIndex())
  225. .setPageSize(searchResponseData.getHotspots().size());
  226. responseBuilder.setPaging(pagingBuilder.build());
  227. }
  228. private static final class WsRequest {
  229. private final int page;
  230. private final int pageSize;
  231. private String project;
  232. private String branch;
  233. private String pullRequest;
  234. private String status;
  235. private String resolution;
  236. private boolean inNewCodePeriod;
  237. private WsRequest(int page, int pageSize) {
  238. this.page = page;
  239. this.pageSize = pageSize;
  240. }
  241. public WsRequest project(@Nullable String project) {
  242. this.project = project;
  243. return this;
  244. }
  245. public WsRequest branch(@Nullable String branch) {
  246. this.branch = branch;
  247. return this;
  248. }
  249. public WsRequest pullRequest(@Nullable String pullRequest) {
  250. this.pullRequest = pullRequest;
  251. return this;
  252. }
  253. public WsRequest status(@Nullable String status) {
  254. this.status = status;
  255. return this;
  256. }
  257. public WsRequest resolution(@Nullable String resolution) {
  258. this.resolution = resolution;
  259. return this;
  260. }
  261. public WsRequest inNewCodePeriod(@Nullable Boolean inNewCodePeriod) {
  262. this.inNewCodePeriod = inNewCodePeriod != null && inNewCodePeriod;
  263. return this;
  264. }
  265. }
  266. }