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 31KB

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