You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SearchAction.java 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 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 java.util.Arrays;
  22. import java.util.Collection;
  23. import java.util.Collections;
  24. import java.util.Date;
  25. import java.util.HashMap;
  26. import java.util.HashSet;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Objects;
  30. import java.util.Optional;
  31. import java.util.Set;
  32. import java.util.stream.Collectors;
  33. import java.util.stream.Stream;
  34. import javax.annotation.Nullable;
  35. import org.apache.lucene.search.TotalHits;
  36. import org.elasticsearch.action.search.SearchResponse;
  37. import org.elasticsearch.search.SearchHit;
  38. import org.jetbrains.annotations.NotNull;
  39. import org.sonar.api.resources.Qualifiers;
  40. import org.sonar.api.resources.Scopes;
  41. import org.sonar.api.rule.RuleKey;
  42. import org.sonar.api.rules.RuleType;
  43. import org.sonar.api.server.ws.Change;
  44. import org.sonar.api.server.ws.Request;
  45. import org.sonar.api.server.ws.Response;
  46. import org.sonar.api.server.ws.WebService;
  47. import org.sonar.api.utils.Paging;
  48. import org.sonar.api.utils.System2;
  49. import org.sonar.db.DbClient;
  50. import org.sonar.db.DbSession;
  51. import org.sonar.db.component.BranchDto;
  52. import org.sonar.db.component.ComponentDto;
  53. import org.sonar.db.component.SnapshotDto;
  54. import org.sonar.db.issue.IssueDto;
  55. import org.sonar.db.project.ProjectDto;
  56. import org.sonar.db.protobuf.DbIssues;
  57. import org.sonar.db.rule.RuleDto;
  58. import org.sonar.server.es.SearchOptions;
  59. import org.sonar.server.exceptions.NotFoundException;
  60. import org.sonar.server.issue.TextRangeResponseFormatter;
  61. import org.sonar.server.issue.index.IssueIndex;
  62. import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
  63. import org.sonar.server.issue.index.IssueQuery;
  64. import org.sonar.server.security.SecurityStandards;
  65. import org.sonar.server.user.UserSession;
  66. import org.sonarqube.ws.Common;
  67. import org.sonarqube.ws.Hotspots;
  68. import org.sonarqube.ws.Hotspots.SearchWsResponse;
  69. import static com.google.common.base.MoreObjects.firstNonNull;
  70. import static com.google.common.base.Preconditions.checkArgument;
  71. import static com.google.common.base.Strings.isNullOrEmpty;
  72. import static java.lang.String.format;
  73. import static java.util.Collections.singleton;
  74. import static java.util.Collections.singletonList;
  75. import static java.util.Optional.ofNullable;
  76. import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
  77. import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
  78. import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
  79. import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
  80. import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
  81. import static org.sonar.api.server.ws.WebService.Param.PAGE;
  82. import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
  83. import static org.sonar.api.utils.DateUtils.formatDateTime;
  84. import static org.sonar.api.utils.DateUtils.longToDate;
  85. import static org.sonar.api.utils.Paging.forPageIndex;
  86. import static org.sonar.api.web.UserRole.USER;
  87. import static org.sonar.core.util.stream.MoreCollectors.toList;
  88. import static org.sonar.core.util.stream.MoreCollectors.toSet;
  89. import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
  90. import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
  91. import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION;
  92. import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES;
  93. import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE;
  94. import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
  95. import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
  96. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  97. import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
  98. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  99. import static org.sonarqube.ws.WsUtils.nullToEmpty;
  100. public class SearchAction implements HotspotsWsAction {
  101. private static final Set<String> SUPPORTED_QUALIFIERS = Set.of(Qualifiers.PROJECT, Qualifiers.APP);
  102. private static final String PARAM_PROJECT_KEY = "projectKey";
  103. private static final String PARAM_STATUS = "status";
  104. private static final String PARAM_RESOLUTION = "resolution";
  105. private static final String PARAM_HOTSPOTS = "hotspots";
  106. private static final String PARAM_BRANCH = "branch";
  107. private static final String PARAM_PULL_REQUEST = "pullRequest";
  108. private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod";
  109. private static final String PARAM_ONLY_MINE = "onlyMine";
  110. private static final String PARAM_PCI_DSS_32 = "pciDss-3.2";
  111. private static final String PARAM_PCI_DSS_40 = "pciDss-4.0";
  112. private static final String PARAM_OWASP_TOP_10_2017 = "owaspTop10";
  113. private static final String PARAM_OWASP_TOP_10_2021 = "owaspTop10-2021";
  114. private static final String PARAM_SANS_TOP_25 = "sansTop25";
  115. private static final String PARAM_SONARSOURCE_SECURITY = "sonarsourceSecurity";
  116. private static final String PARAM_CWE = "cwe";
  117. private static final String PARAM_FILES = "files";
  118. private static final List<String> STATUSES = List.of(STATUS_TO_REVIEW, STATUS_REVIEWED);
  119. private final DbClient dbClient;
  120. private final UserSession userSession;
  121. private final IssueIndex issueIndex;
  122. private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker;
  123. private final HotspotWsResponseFormatter responseFormatter;
  124. private final TextRangeResponseFormatter textRangeFormatter;
  125. private final System2 system2;
  126. public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex,
  127. IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
  128. HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter, System2 system2) {
  129. this.dbClient = dbClient;
  130. this.userSession = userSession;
  131. this.issueIndex = issueIndex;
  132. this.issueIndexSyncProgressChecker = issueIndexSyncProgressChecker;
  133. this.responseFormatter = responseFormatter;
  134. this.textRangeFormatter = textRangeFormatter;
  135. this.system2 = system2;
  136. }
  137. private static Set<String> setFromList(@Nullable List<String> list) {
  138. return list != null ? Set.copyOf(list) : Set.of();
  139. }
  140. private static WsRequest toWsRequest(Request request) {
  141. Set<String> hotspotKeys = setFromList(request.paramAsStrings(PARAM_HOTSPOTS));
  142. Set<String> pciDss32 = setFromList(request.paramAsStrings(PARAM_PCI_DSS_32));
  143. Set<String> pciDss40 = setFromList(request.paramAsStrings(PARAM_PCI_DSS_40));
  144. Set<String> owasp2017Top10 = setFromList(request.paramAsStrings(PARAM_OWASP_TOP_10_2017));
  145. Set<String> owasp2021Top10 = setFromList(request.paramAsStrings(PARAM_OWASP_TOP_10_2021));
  146. Set<String> sansTop25 = setFromList(request.paramAsStrings(PARAM_SANS_TOP_25));
  147. Set<String> sonarsourceSecurity = setFromList(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY));
  148. Set<String> cwes = setFromList(request.paramAsStrings(PARAM_CWE));
  149. Set<String> files = setFromList(request.paramAsStrings(PARAM_FILES));
  150. return new WsRequest(
  151. request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE), request.param(PARAM_PROJECT_KEY), request.param(PARAM_BRANCH),
  152. request.param(PARAM_PULL_REQUEST), hotspotKeys, request.param(PARAM_STATUS), request.param(PARAM_RESOLUTION),
  153. request.paramAsBoolean(PARAM_IN_NEW_CODE_PERIOD), request.paramAsBoolean(PARAM_ONLY_MINE), pciDss32, pciDss40, owasp2017Top10, owasp2021Top10, sansTop25,
  154. sonarsourceSecurity, cwes, files);
  155. }
  156. @Override
  157. public void handle(Request request, Response response) throws Exception {
  158. WsRequest wsRequest = toWsRequest(request);
  159. validateParameters(wsRequest);
  160. try (DbSession dbSession = dbClient.openSession(false)) {
  161. checkIfNeedIssueSync(dbSession, wsRequest);
  162. Optional<ComponentDto> project = getAndValidateProjectOrApplication(dbSession, wsRequest);
  163. SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project.orElse(null));
  164. loadComponents(dbSession, searchResponseData);
  165. loadRules(dbSession, searchResponseData);
  166. writeProtobuf(formatResponse(searchResponseData), request, response);
  167. }
  168. }
  169. private void checkIfNeedIssueSync(DbSession dbSession, WsRequest wsRequest) {
  170. Optional<String> projectKey = wsRequest.getProjectKey();
  171. if (projectKey.isPresent()) {
  172. issueIndexSyncProgressChecker.checkIfComponentNeedIssueSync(dbSession, projectKey.get());
  173. } else {
  174. // component keys not provided - asking for global
  175. issueIndexSyncProgressChecker.checkIfIssueSyncInProgress(dbSession);
  176. }
  177. }
  178. @Override
  179. public void define(WebService.NewController controller) {
  180. WebService.NewAction action = controller
  181. .createAction("search")
  182. .setHandler(this)
  183. .setDescription("Search for Security Hotpots. <br>"
  184. + "Requires the 'Browse' permission on the specified project(s). <br>"
  185. + "For applications, it also requires 'Browse' permission on its child projects. <br>"
  186. + "When issue indexation is in progress returns 503 service unavailable HTTP code.")
  187. .setSince("8.1")
  188. .setInternal(true)
  189. .setChangelog(
  190. new Change("9.6", "Added parameters 'pciDss-3.2' and 'pciDss-4.0"),
  191. new Change("9.7", "Hotspot flows in the response may contain a description and a type"),
  192. new Change("9.7", "Hotspot in the response contain the corresponding ruleKey"));
  193. action.addPagingParams(100);
  194. action.createParam(PARAM_PROJECT_KEY)
  195. .setDescription(format(
  196. "Key of the project or application. This parameter is required unless %s is provided.",
  197. PARAM_HOTSPOTS))
  198. .setExampleValue(KEY_PROJECT_EXAMPLE_001);
  199. action.createParam(PARAM_BRANCH)
  200. .setDescription("Branch key. Not available in the community edition.")
  201. .setExampleValue(KEY_BRANCH_EXAMPLE_001);
  202. action.createParam(PARAM_PULL_REQUEST)
  203. .setDescription("Pull request id. Not available in the community edition.")
  204. .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
  205. action.createParam(PARAM_HOTSPOTS)
  206. .setDescription(format(
  207. "Comma-separated list of Security Hotspot keys. This parameter is required unless %s is provided.",
  208. PARAM_PROJECT_KEY))
  209. .setExampleValue("AWhXpLoInp4On-Y3xc8x");
  210. action.createParam(PARAM_STATUS)
  211. .setDescription("If '%s' is provided, only Security Hotspots with the specified status are returned.", PARAM_PROJECT_KEY)
  212. .setPossibleValues(STATUSES)
  213. .setRequired(false);
  214. action.createParam(PARAM_RESOLUTION)
  215. .setDescription(format(
  216. "If '%s' is provided and if status is '%s', only Security Hotspots with the specified resolution are returned.",
  217. PARAM_PROJECT_KEY, STATUS_REVIEWED))
  218. .setPossibleValues(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED)
  219. .setRequired(false);
  220. action.createParam(PARAM_IN_NEW_CODE_PERIOD)
  221. .setDescription("If '%s' is provided, only Security Hotspots created in the new code period are returned.", PARAM_IN_NEW_CODE_PERIOD)
  222. .setBooleanPossibleValues()
  223. .setDefaultValue("false")
  224. .setSince("9.5");
  225. action.createParam(PARAM_PCI_DSS_32)
  226. .setDescription("Comma-separated list of PCI DSS v3.2 categories.")
  227. .setSince("9.6")
  228. .setExampleValue("4,6.5.8,10.1");
  229. action.createParam(PARAM_PCI_DSS_40)
  230. .setDescription("Comma-separated list of PCI DSS v4.0 categories.")
  231. .setSince("9.6")
  232. .setExampleValue("4,6.5.8,10.1");
  233. action.createParam(PARAM_ONLY_MINE)
  234. .setDescription("If 'projectKey' is provided, returns only Security Hotspots assigned to the current user")
  235. .setBooleanPossibleValues()
  236. .setRequired(false);
  237. action.createParam(PARAM_OWASP_TOP_10_2017)
  238. .setDescription("Comma-separated list of OWASP 2017 Top 10 lowercase categories.")
  239. .setSince("8.6")
  240. .setPossibleValues("a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10");
  241. action.createParam(PARAM_OWASP_TOP_10_2021)
  242. .setDescription("Comma-separated list of OWASP 2021 Top 10 lowercase categories.")
  243. .setSince("9.4")
  244. .setPossibleValues("a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10");
  245. action.createParam(PARAM_SANS_TOP_25)
  246. .setDescription("Comma-separated list of SANS Top 25 categories.")
  247. .setSince("8.6")
  248. .setPossibleValues(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES);
  249. action.createParam(PARAM_SONARSOURCE_SECURITY)
  250. .setDescription("Comma-separated list of SonarSource security categories. Use '" + SecurityStandards.SQCategory.OTHERS.getKey() +
  251. "' to select issues not associated with any category")
  252. .setSince("8.6")
  253. .setPossibleValues(Arrays.stream(SecurityStandards.SQCategory.values()).map(SecurityStandards.SQCategory::getKey).collect(Collectors.toList()));
  254. action.createParam(PARAM_CWE)
  255. .setDescription("Comma-separated list of CWE numbers")
  256. .setExampleValue("89,434,352")
  257. .setSince("8.8");
  258. action.createParam(PARAM_FILES)
  259. .setDescription("Comma-separated list of files. Returns only hotspots found in those files")
  260. .setExampleValue("src/main/java/org/sonar/server/Test.java")
  261. .setSince("9.0");
  262. action.setResponseExample(getClass().getResource("search-example.json"));
  263. }
  264. private void validateParameters(WsRequest wsRequest) {
  265. Optional<String> projectKey = wsRequest.getProjectKey();
  266. Optional<String> branch = wsRequest.getBranch();
  267. Optional<String> pullRequest = wsRequest.getPullRequest();
  268. Set<String> hotspotKeys = wsRequest.getHotspotKeys();
  269. checkArgument(
  270. projectKey.isPresent() || !hotspotKeys.isEmpty(),
  271. "A value must be provided for either parameter '%s' or parameter '%s'", PARAM_PROJECT_KEY, PARAM_HOTSPOTS);
  272. checkArgument(
  273. branch.isEmpty() || projectKey.isPresent(),
  274. "Parameter '%s' must be used with parameter '%s'", PARAM_BRANCH, PARAM_PROJECT_KEY);
  275. checkArgument(
  276. pullRequest.isEmpty() || projectKey.isPresent(),
  277. "Parameter '%s' must be used with parameter '%s'", PARAM_PULL_REQUEST, PARAM_PROJECT_KEY);
  278. checkArgument(
  279. !(branch.isPresent() && pullRequest.isPresent()),
  280. "Only one of parameters '%s' and '%s' can be provided", PARAM_BRANCH, PARAM_PULL_REQUEST);
  281. Optional<String> status = wsRequest.getStatus();
  282. Optional<String> resolution = wsRequest.getResolution();
  283. checkArgument(status.isEmpty() || hotspotKeys.isEmpty(),
  284. "Parameter '%s' can't be used with parameter '%s'", PARAM_STATUS, PARAM_HOTSPOTS);
  285. checkArgument(resolution.isEmpty() || hotspotKeys.isEmpty(),
  286. "Parameter '%s' can't be used with parameter '%s'", PARAM_RESOLUTION, PARAM_HOTSPOTS);
  287. resolution.ifPresent(
  288. r -> checkArgument(status.filter(STATUS_REVIEWED::equals).isPresent(),
  289. "Value '%s' of parameter '%s' can only be provided if value of parameter '%s' is '%s'",
  290. r, PARAM_RESOLUTION, PARAM_STATUS, STATUS_REVIEWED));
  291. if (wsRequest.isOnlyMine()) {
  292. checkArgument(userSession.isLoggedIn(),
  293. "Parameter '%s' requires user to be logged in", PARAM_ONLY_MINE);
  294. checkArgument(wsRequest.getProjectKey().isPresent(),
  295. "Parameter '%s' can be used with parameter '%s' only", PARAM_ONLY_MINE, PARAM_PROJECT_KEY);
  296. }
  297. }
  298. private Optional<ComponentDto> getAndValidateProjectOrApplication(DbSession dbSession, WsRequest wsRequest) {
  299. return wsRequest.getProjectKey().map(projectKey -> {
  300. ComponentDto project = getProject(dbSession, projectKey, wsRequest.getBranch().orElse(null), wsRequest.getPullRequest().orElse(null))
  301. .filter(t -> Scopes.PROJECT.equals(t.scope()) && SUPPORTED_QUALIFIERS.contains(t.qualifier()))
  302. .filter(ComponentDto::isEnabled)
  303. .orElseThrow(() -> new NotFoundException(format("Project '%s' not found", projectKey)));
  304. userSession.checkComponentPermission(USER, project);
  305. userSession.checkChildProjectsPermission(USER, project);
  306. return project;
  307. });
  308. }
  309. private Optional<ComponentDto> getProject(DbSession dbSession, String projectKey, @Nullable String branch, @Nullable String pullRequest) {
  310. if (branch != null) {
  311. return dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branch);
  312. } else if (pullRequest != null) {
  313. return dbClient.componentDao().selectByKeyAndPullRequest(dbSession, projectKey, pullRequest);
  314. }
  315. return dbClient.componentDao().selectByKey(dbSession, projectKey);
  316. }
  317. private SearchResponseData searchHotspots(WsRequest wsRequest, DbSession dbSession, @Nullable ComponentDto project) {
  318. SearchResponse result = doIndexSearch(wsRequest, dbSession, project);
  319. List<String> issueKeys = Arrays.stream(result.getHits().getHits())
  320. .map(SearchHit::getId)
  321. .collect(toList(result.getHits().getHits().length));
  322. List<IssueDto> hotspots = toIssueDtos(dbSession, issueKeys);
  323. Paging paging = forPageIndex(wsRequest.getPage()).withPageSize(wsRequest.getIndex()).andTotal((int) getTotalHits(result).value);
  324. return new SearchResponseData(paging, hotspots);
  325. }
  326. private static TotalHits getTotalHits(SearchResponse response) {
  327. return ofNullable(response.getHits().getTotalHits()).orElseThrow(() -> new IllegalStateException("Could not get total hits of search results"));
  328. }
  329. private List<IssueDto> toIssueDtos(DbSession dbSession, List<String> issueKeys) {
  330. List<IssueDto> unorderedHotspots = dbClient.issueDao().selectByKeys(dbSession, issueKeys);
  331. Map<String, IssueDto> hotspotsByKey = unorderedHotspots
  332. .stream()
  333. .collect(uniqueIndex(IssueDto::getKey, unorderedHotspots.size()));
  334. return issueKeys.stream()
  335. .map(hotspotsByKey::get)
  336. .filter(Objects::nonNull)
  337. .collect(Collectors.toList());
  338. }
  339. private SearchResponse doIndexSearch(WsRequest wsRequest, DbSession dbSession, @Nullable ComponentDto project) {
  340. var builder = IssueQuery.builder()
  341. .types(singleton(RuleType.SECURITY_HOTSPOT.name()))
  342. .sort(IssueQuery.SORT_HOTSPOTS)
  343. .asc(true)
  344. .statuses(wsRequest.getStatus().map(Collections::singletonList).orElse(STATUSES));
  345. if (project != null) {
  346. String projectUuid = firstNonNull(project.getMainBranchProjectUuid(), project.uuid());
  347. if (Qualifiers.APP.equals(project.qualifier())) {
  348. builder.viewUuids(singletonList(projectUuid));
  349. if (wsRequest.isInNewCodePeriod() && wsRequest.getPullRequest().isEmpty()) {
  350. addInNewCodePeriodFilterByProjects(builder, dbSession, project);
  351. }
  352. } else {
  353. builder.projectUuids(singletonList(projectUuid));
  354. if (wsRequest.isInNewCodePeriod() && wsRequest.getPullRequest().isEmpty()) {
  355. addInNewCodePeriodFilter(dbSession, project, builder);
  356. }
  357. }
  358. addMainBranchFilter(project, builder);
  359. }
  360. if (!wsRequest.getHotspotKeys().isEmpty()) {
  361. builder.issueKeys(wsRequest.getHotspotKeys());
  362. }
  363. if (!wsRequest.getFiles().isEmpty()) {
  364. builder.files(wsRequest.getFiles());
  365. }
  366. if (wsRequest.isOnlyMine()) {
  367. userSession.checkLoggedIn();
  368. builder.assigneeUuids(Collections.singletonList(userSession.getUuid()));
  369. }
  370. wsRequest.getStatus().ifPresent(status -> builder.resolved(STATUS_REVIEWED.equals(status)));
  371. wsRequest.getResolution().ifPresent(resolution -> builder.resolutions(singleton(resolution)));
  372. addSecurityStandardFilters(wsRequest, builder);
  373. IssueQuery query = builder.build();
  374. SearchOptions searchOptions = new SearchOptions()
  375. .setPage(wsRequest.page, wsRequest.index);
  376. return issueIndex.search(query, searchOptions);
  377. }
  378. private static void addSecurityStandardFilters(WsRequest wsRequest, IssueQuery.Builder builder) {
  379. if (!wsRequest.getPciDss32().isEmpty()) {
  380. builder.pciDss32(wsRequest.getPciDss32());
  381. }
  382. if (!wsRequest.getPciDss40().isEmpty()) {
  383. builder.pciDss40(wsRequest.getPciDss40());
  384. }
  385. if (!wsRequest.getOwaspTop10For2017().isEmpty()) {
  386. builder.owaspTop10(wsRequest.getOwaspTop10For2017());
  387. }
  388. if (!wsRequest.getOwaspTop10For2021().isEmpty()) {
  389. builder.owaspTop10For2021(wsRequest.getOwaspTop10For2021());
  390. }
  391. if (!wsRequest.getSansTop25().isEmpty()) {
  392. builder.sansTop25(wsRequest.getSansTop25());
  393. }
  394. if (!wsRequest.getSonarsourceSecurity().isEmpty()) {
  395. builder.sonarsourceSecurity(wsRequest.getSonarsourceSecurity());
  396. }
  397. if (!wsRequest.getCwe().isEmpty()) {
  398. builder.cwe(wsRequest.getCwe());
  399. }
  400. }
  401. private static void addMainBranchFilter(@NotNull ComponentDto project, IssueQuery.Builder builder) {
  402. if (project.getMainBranchProjectUuid() == null) {
  403. builder.mainBranch(true);
  404. } else {
  405. builder.branchUuid(project.uuid());
  406. builder.mainBranch(false);
  407. }
  408. }
  409. private void addInNewCodePeriodFilter(DbSession dbSession, @NotNull ComponentDto project, IssueQuery.Builder builder) {
  410. Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, project.uuid());
  411. boolean isLastAnalysisUsingReferenceBranch = snapshot.map(SnapshotDto::getPeriodMode)
  412. .orElse("").equals(REFERENCE_BRANCH.name());
  413. if (isLastAnalysisUsingReferenceBranch) {
  414. builder.newCodeOnReference(true);
  415. } else {
  416. var sinceDate = snapshot
  417. .map(s -> longToDate(s.getPeriodDate()))
  418. .orElseGet(() -> new Date(system2.now()));
  419. builder.createdAfter(sinceDate, false);
  420. }
  421. }
  422. private void addInNewCodePeriodFilterByProjects(IssueQuery.Builder builder, DbSession dbSession, ComponentDto application) {
  423. Set<String> projectUuids;
  424. if (application.getMainBranchProjectUuid() == null) {
  425. projectUuids = dbClient.applicationProjectsDao().selectProjects(dbSession, application.uuid()).stream()
  426. .map(ProjectDto::getUuid)
  427. .collect(Collectors.toSet());
  428. } else {
  429. projectUuids = dbClient.applicationProjectsDao().selectProjectBranchesFromAppBranchUuid(dbSession, application.uuid()).stream()
  430. .map(BranchDto::getUuid)
  431. .collect(Collectors.toSet());
  432. }
  433. long now = system2.now();
  434. List<SnapshotDto> snapshots = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids);
  435. Set<String> newCodeReferenceByProjects = snapshots
  436. .stream()
  437. .filter(s -> !isNullOrEmpty(s.getPeriodMode()) && s.getPeriodMode().equals(REFERENCE_BRANCH.name()))
  438. .map(SnapshotDto::getComponentUuid)
  439. .collect(toSet());
  440. Map<String, IssueQuery.PeriodStart> leakByProjects = snapshots
  441. .stream()
  442. .filter(s -> isNullOrEmpty(s.getPeriodMode()) || !s.getPeriodMode().equals(REFERENCE_BRANCH.name()))
  443. .collect(uniqueIndex(SnapshotDto::getComponentUuid, s ->
  444. new IssueQuery.PeriodStart(longToDate(s.getPeriodDate() == null ? now : s.getPeriodDate()), false)));
  445. builder.createdAfterByProjectUuids(leakByProjects);
  446. builder.newCodeOnReferenceByProjectUuids(newCodeReferenceByProjects);
  447. }
  448. private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
  449. Set<String> componentUuids = searchResponseData.getOrderedHotspots().stream()
  450. .flatMap(hotspot -> Stream.of(hotspot.getComponentUuid(), hotspot.getProjectUuid()))
  451. .collect(Collectors.toSet());
  452. Set<String> locationComponentUuids = searchResponseData.getOrderedHotspots()
  453. .stream()
  454. .flatMap(hotspot -> getHotspotLocationComponentUuids(hotspot).stream())
  455. .collect(Collectors.toSet());
  456. Set<String> aggregatedComponentUuids = Stream.of(componentUuids, locationComponentUuids)
  457. .flatMap(Collection::stream)
  458. .collect(Collectors.toSet());
  459. if (!aggregatedComponentUuids.isEmpty()) {
  460. searchResponseData.addComponents(dbClient.componentDao().selectByUuids(dbSession, aggregatedComponentUuids));
  461. }
  462. }
  463. private static Set<String> getHotspotLocationComponentUuids(IssueDto hotspot) {
  464. Set<String> locationComponentUuids = new HashSet<>();
  465. DbIssues.Locations locations = hotspot.parseLocations();
  466. if (locations == null) {
  467. return locationComponentUuids;
  468. }
  469. List<DbIssues.Flow> flows = locations.getFlowList();
  470. for (DbIssues.Flow flow : flows) {
  471. List<DbIssues.Location> flowLocations = flow.getLocationList();
  472. for (DbIssues.Location location : flowLocations) {
  473. if (location.hasComponentId()) {
  474. locationComponentUuids.add(location.getComponentId());
  475. }
  476. }
  477. }
  478. return locationComponentUuids;
  479. }
  480. private void loadRules(DbSession dbSession, SearchResponseData searchResponseData) {
  481. Set<RuleKey> ruleKeys = searchResponseData.getOrderedHotspots()
  482. .stream()
  483. .map(IssueDto::getRuleKey)
  484. .collect(Collectors.toSet());
  485. if (!ruleKeys.isEmpty()) {
  486. searchResponseData.addRules(dbClient.ruleDao().selectByKeys(dbSession, ruleKeys));
  487. }
  488. }
  489. private SearchWsResponse formatResponse(SearchResponseData searchResponseData) {
  490. SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder();
  491. formatPaging(searchResponseData, responseBuilder);
  492. if (!searchResponseData.isEmpty()) {
  493. formatHotspots(searchResponseData, responseBuilder);
  494. formatComponents(searchResponseData, responseBuilder);
  495. }
  496. return responseBuilder.build();
  497. }
  498. private static void formatPaging(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
  499. Paging paging = searchResponseData.getPaging();
  500. Common.Paging.Builder pagingBuilder = Common.Paging.newBuilder()
  501. .setPageIndex(paging.pageIndex())
  502. .setPageSize(paging.pageSize())
  503. .setTotal(paging.total());
  504. responseBuilder.setPaging(pagingBuilder.build());
  505. }
  506. private void formatHotspots(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
  507. List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots();
  508. if (orderedHotspots.isEmpty()) {
  509. return;
  510. }
  511. SearchWsResponse.Hotspot.Builder builder = SearchWsResponse.Hotspot.newBuilder();
  512. for (IssueDto hotspot : orderedHotspots) {
  513. RuleDto rule = searchResponseData.getRule(hotspot.getRuleKey())
  514. // due to join with table Rule when retrieving data from Issues, this can't happen
  515. .orElseThrow(() -> new IllegalStateException(format(
  516. "Rule with key '%s' not found for Hotspot '%s'", hotspot.getRuleKey(), hotspot.getKey())));
  517. SecurityStandards.SQCategory sqCategory = fromSecurityStandards(rule.getSecurityStandards()).getSqCategory();
  518. builder
  519. .clear()
  520. .setKey(hotspot.getKey())
  521. .setComponent(hotspot.getComponentKey())
  522. .setProject(hotspot.getProjectKey())
  523. .setSecurityCategory(sqCategory.getKey())
  524. .setVulnerabilityProbability(sqCategory.getVulnerability().name())
  525. .setRuleKey(hotspot.getRuleKey().toString());
  526. ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
  527. ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
  528. ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
  529. builder.setMessage(nullToEmpty(hotspot.getMessage()));
  530. ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
  531. builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
  532. builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
  533. builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
  534. completeHotspotLocations(hotspot, builder, searchResponseData);
  535. responseBuilder.addHotspots(builder.build());
  536. }
  537. }
  538. private void completeHotspotLocations(IssueDto hotspot, SearchWsResponse.Hotspot.Builder hotspotBuilder, SearchResponseData data) {
  539. DbIssues.Locations locations = hotspot.parseLocations();
  540. if (locations == null) {
  541. return;
  542. }
  543. textRangeFormatter.formatTextRange(locations, hotspotBuilder::setTextRange);
  544. hotspotBuilder.addAllFlows(textRangeFormatter.formatFlows(locations, hotspotBuilder.getComponent(), data.getComponentsByUuid()));
  545. }
  546. private void formatComponents(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
  547. Collection<ComponentDto> components = searchResponseData.getComponents();
  548. if (components.isEmpty()) {
  549. return;
  550. }
  551. Hotspots.Component.Builder builder = Hotspots.Component.newBuilder();
  552. for (ComponentDto component : components) {
  553. responseBuilder.addComponents(responseFormatter.formatComponent(builder, component));
  554. }
  555. }
  556. private static final class WsRequest {
  557. private final int page;
  558. private final int index;
  559. private final String projectKey;
  560. private final String branch;
  561. private final String pullRequest;
  562. private final Set<String> hotspotKeys;
  563. private final String status;
  564. private final String resolution;
  565. private final boolean inNewCodePeriod;
  566. private final boolean onlyMine;
  567. private final Set<String> pciDss32;
  568. private final Set<String> pciDss40;
  569. private final Set<String> owaspTop10For2017;
  570. private final Set<String> owaspTop10For2021;
  571. private final Set<String> sansTop25;
  572. private final Set<String> sonarsourceSecurity;
  573. private final Set<String> cwe;
  574. private final Set<String> files;
  575. private WsRequest(int page, int index,
  576. @Nullable String projectKey, @Nullable String branch, @Nullable String pullRequest,
  577. Set<String> hotspotKeys,
  578. @Nullable String status, @Nullable String resolution, @Nullable Boolean inNewCodePeriod,
  579. @Nullable Boolean onlyMine, Set<String> pciDss32, Set<String> pciDss40, Set<String> owaspTop10For2017, Set<String> owaspTop10For2021, Set<String> sansTop25,
  580. Set<String> sonarsourceSecurity,
  581. Set<String> cwe, @Nullable Set<String> files) {
  582. this.page = page;
  583. this.index = index;
  584. this.projectKey = projectKey;
  585. this.branch = branch;
  586. this.pullRequest = pullRequest;
  587. this.hotspotKeys = hotspotKeys;
  588. this.status = status;
  589. this.resolution = resolution;
  590. this.inNewCodePeriod = inNewCodePeriod != null && inNewCodePeriod;
  591. this.onlyMine = onlyMine != null && onlyMine;
  592. this.pciDss32 = pciDss32;
  593. this.pciDss40 = pciDss40;
  594. this.owaspTop10For2017 = owaspTop10For2017;
  595. this.owaspTop10For2021 = owaspTop10For2021;
  596. this.sansTop25 = sansTop25;
  597. this.sonarsourceSecurity = sonarsourceSecurity;
  598. this.cwe = cwe;
  599. this.files = files;
  600. }
  601. int getPage() {
  602. return page;
  603. }
  604. int getIndex() {
  605. return index;
  606. }
  607. Optional<String> getProjectKey() {
  608. return ofNullable(projectKey);
  609. }
  610. Optional<String> getBranch() {
  611. return ofNullable(branch);
  612. }
  613. Optional<String> getPullRequest() {
  614. return ofNullable(pullRequest);
  615. }
  616. Set<String> getHotspotKeys() {
  617. return hotspotKeys;
  618. }
  619. Optional<String> getStatus() {
  620. return ofNullable(status);
  621. }
  622. Optional<String> getResolution() {
  623. return ofNullable(resolution);
  624. }
  625. boolean isInNewCodePeriod() {
  626. return inNewCodePeriod;
  627. }
  628. boolean isOnlyMine() {
  629. return onlyMine;
  630. }
  631. public Set<String> getPciDss32() {
  632. return pciDss32;
  633. }
  634. public Set<String> getPciDss40() {
  635. return pciDss40;
  636. }
  637. public Set<String> getOwaspTop10For2017() {
  638. return owaspTop10For2017;
  639. }
  640. public Set<String> getOwaspTop10For2021() {
  641. return owaspTop10For2021;
  642. }
  643. public Set<String> getSansTop25() {
  644. return sansTop25;
  645. }
  646. public Set<String> getSonarsourceSecurity() {
  647. return sonarsourceSecurity;
  648. }
  649. public Set<String> getCwe() {
  650. return cwe;
  651. }
  652. public Set<String> getFiles() {
  653. return files;
  654. }
  655. }
  656. private static final class SearchResponseData {
  657. private final Paging paging;
  658. private final List<IssueDto> orderedHotspots;
  659. private final Map<String, ComponentDto> componentsByUuid = new HashMap<>();
  660. private final Map<RuleKey, RuleDto> rulesByRuleKey = new HashMap<>();
  661. private SearchResponseData(Paging paging, List<IssueDto> orderedHotspots) {
  662. this.paging = paging;
  663. this.orderedHotspots = orderedHotspots;
  664. }
  665. boolean isEmpty() {
  666. return orderedHotspots.isEmpty();
  667. }
  668. public Paging getPaging() {
  669. return paging;
  670. }
  671. List<IssueDto> getOrderedHotspots() {
  672. return orderedHotspots;
  673. }
  674. void addComponents(Collection<ComponentDto> components) {
  675. for (ComponentDto component : components) {
  676. componentsByUuid.put(component.uuid(), component);
  677. }
  678. }
  679. Collection<ComponentDto> getComponents() {
  680. return componentsByUuid.values();
  681. }
  682. public Map<String, ComponentDto> getComponentsByUuid() {
  683. return componentsByUuid;
  684. }
  685. void addRules(Collection<RuleDto> rules) {
  686. rules.forEach(t -> rulesByRuleKey.put(t.getKey(), t));
  687. }
  688. Optional<RuleDto> getRule(RuleKey ruleKey) {
  689. return ofNullable(rulesByRuleKey.get(ruleKey));
  690. }
  691. }
  692. }