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.

IssueIndex.java 60KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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.issue.index;
  21. import com.google.common.base.Preconditions;
  22. import java.util.ArrayList;
  23. import java.util.Arrays;
  24. import java.util.Collection;
  25. import java.util.Collections;
  26. import java.util.Date;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Objects;
  30. import java.util.Optional;
  31. import java.util.OptionalInt;
  32. import java.util.OptionalLong;
  33. import java.util.Set;
  34. import java.util.function.Consumer;
  35. import java.util.stream.Collectors;
  36. import java.util.stream.IntStream;
  37. import java.util.stream.Stream;
  38. import javax.annotation.CheckForNull;
  39. import javax.annotation.Nullable;
  40. import org.apache.commons.lang.StringUtils;
  41. import org.elasticsearch.action.search.SearchRequest;
  42. import org.elasticsearch.action.search.SearchResponse;
  43. import org.elasticsearch.index.query.BoolQueryBuilder;
  44. import org.elasticsearch.index.query.QueryBuilder;
  45. import org.elasticsearch.index.query.QueryBuilders;
  46. import org.elasticsearch.indices.TermsLookup;
  47. import org.elasticsearch.search.aggregations.AggregationBuilder;
  48. import org.elasticsearch.search.aggregations.AggregationBuilders;
  49. import org.elasticsearch.search.aggregations.BucketOrder;
  50. import org.elasticsearch.search.aggregations.HasAggregations;
  51. import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
  52. import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
  53. import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter;
  54. import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
  55. import org.elasticsearch.search.aggregations.bucket.histogram.LongBounds;
  56. import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude;
  57. import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
  58. import org.elasticsearch.search.aggregations.bucket.terms.Terms;
  59. import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
  60. import org.elasticsearch.search.aggregations.metrics.Min;
  61. import org.elasticsearch.search.aggregations.metrics.ParsedMax;
  62. import org.elasticsearch.search.aggregations.metrics.ParsedValueCount;
  63. import org.elasticsearch.search.aggregations.metrics.SumAggregationBuilder;
  64. import org.elasticsearch.search.builder.SearchSourceBuilder;
  65. import org.elasticsearch.search.sort.FieldSortBuilder;
  66. import org.joda.time.Duration;
  67. import org.sonar.api.issue.Issue;
  68. import org.sonar.api.rule.Severity;
  69. import org.sonar.api.rules.RuleType;
  70. import org.sonar.api.utils.DateUtils;
  71. import org.sonar.api.utils.System2;
  72. import org.sonar.core.util.stream.MoreCollectors;
  73. import org.sonar.server.es.BaseDoc;
  74. import org.sonar.server.es.EsClient;
  75. import org.sonar.server.es.EsUtils;
  76. import org.sonar.server.es.IndexType;
  77. import org.sonar.server.es.SearchOptions;
  78. import org.sonar.server.es.Sorting;
  79. import org.sonar.server.es.searchrequest.RequestFiltersComputer;
  80. import org.sonar.server.es.searchrequest.RequestFiltersComputer.AllFilters;
  81. import org.sonar.server.es.searchrequest.SimpleFieldTopAggregationDefinition;
  82. import org.sonar.server.es.searchrequest.SubAggregationHelper;
  83. import org.sonar.server.es.searchrequest.TopAggregationDefinition;
  84. import org.sonar.server.es.searchrequest.TopAggregationDefinition.SimpleFieldFilterScope;
  85. import org.sonar.server.es.searchrequest.TopAggregationHelper;
  86. import org.sonar.server.issue.index.IssueQuery.PeriodStart;
  87. import org.sonar.server.permission.index.AuthorizationDoc;
  88. import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
  89. import org.sonar.server.security.SecurityStandards;
  90. import org.sonar.server.security.SecurityStandards.SQCategory;
  91. import org.sonar.server.user.UserSession;
  92. import org.sonar.server.view.index.ViewIndexDefinition;
  93. import static com.google.common.base.Preconditions.checkState;
  94. import static java.lang.String.format;
  95. import static java.util.Collections.singletonList;
  96. import static java.util.stream.Collectors.toList;
  97. import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
  98. import static org.elasticsearch.index.query.QueryBuilders.existsQuery;
  99. import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
  100. import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
  101. import static org.elasticsearch.index.query.QueryBuilders.termQuery;
  102. import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
  103. import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
  104. import static org.sonar.api.rules.RuleType.VULNERABILITY;
  105. import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
  106. import static org.sonar.server.es.BaseDoc.epochMillisToEpochSeconds;
  107. import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars;
  108. import static org.sonar.server.es.IndexType.FIELD_INDEX_TYPE;
  109. import static org.sonar.server.es.searchrequest.TopAggregationDefinition.NON_STICKY;
  110. import static org.sonar.server.es.searchrequest.TopAggregationDefinition.STICKY;
  111. import static org.sonar.server.es.searchrequest.TopAggregationHelper.NO_EXTRA_FILTER;
  112. import static org.sonar.server.es.searchrequest.TopAggregationHelper.NO_OTHER_SUBAGGREGATION;
  113. import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNED_TO_ME;
  114. import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNEES;
  115. import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHOR;
  116. import static org.sonar.server.issue.index.IssueIndex.Facet.CREATED_AT;
  117. import static org.sonar.server.issue.index.IssueIndex.Facet.CWE;
  118. import static org.sonar.server.issue.index.IssueIndex.Facet.DIRECTORIES;
  119. import static org.sonar.server.issue.index.IssueIndex.Facet.FILES;
  120. import static org.sonar.server.issue.index.IssueIndex.Facet.LANGUAGES;
  121. import static org.sonar.server.issue.index.IssueIndex.Facet.MODULE_UUIDS;
  122. import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_TOP_10;
  123. import static org.sonar.server.issue.index.IssueIndex.Facet.PROJECT_UUIDS;
  124. import static org.sonar.server.issue.index.IssueIndex.Facet.RESOLUTIONS;
  125. import static org.sonar.server.issue.index.IssueIndex.Facet.RULES;
  126. import static org.sonar.server.issue.index.IssueIndex.Facet.SANS_TOP_25;
  127. import static org.sonar.server.issue.index.IssueIndex.Facet.SCOPES;
  128. import static org.sonar.server.issue.index.IssueIndex.Facet.SEVERITIES;
  129. import static org.sonar.server.issue.index.IssueIndex.Facet.SONARSOURCE_SECURITY;
  130. import static org.sonar.server.issue.index.IssueIndex.Facet.STATUSES;
  131. import static org.sonar.server.issue.index.IssueIndex.Facet.TAGS;
  132. import static org.sonar.server.issue.index.IssueIndex.Facet.TYPES;
  133. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID;
  134. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN;
  135. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID;
  136. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID;
  137. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_CWE;
  138. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH;
  139. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_EFFORT;
  140. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_FILE_PATH;
  141. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT;
  142. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT;
  143. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT;
  144. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_IS_MAIN_BRANCH;
  145. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_KEY;
  146. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_LANGUAGE;
  147. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_LINE;
  148. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_MODULE_PATH;
  149. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID;
  150. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10;
  151. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID;
  152. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_RESOLUTION;
  153. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_RULE_UUID;
  154. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25;
  155. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SCOPE;
  156. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SEVERITY;
  157. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE;
  158. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_SQ_SECURITY_CATEGORY;
  159. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_STATUS;
  160. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_TAGS;
  161. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_TYPE;
  162. import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_VULNERABILITY_PROBABILITY;
  163. import static org.sonar.server.issue.index.IssueIndexDefinition.TYPE_ISSUE;
  164. import static org.sonar.server.security.SecurityReviewRating.computePercent;
  165. import static org.sonar.server.security.SecurityReviewRating.computeRating;
  166. import static org.sonar.server.security.SecurityStandards.CWES_BY_CWE_TOP_25;
  167. import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION;
  168. import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES;
  169. import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE;
  170. import static org.sonar.server.view.index.ViewIndexDefinition.TYPE_VIEW;
  171. import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT;
  172. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
  173. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR;
  174. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AT;
  175. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE;
  176. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES;
  177. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
  178. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
  179. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
  180. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLUTIONS;
  181. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;
  182. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25;
  183. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SCOPES;
  184. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SEVERITIES;
  185. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SONARSOURCE_SECURITY;
  186. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_STATUSES;
  187. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TAGS;
  188. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TYPES;
  189. /**
  190. * The unique entry-point to interact with Elasticsearch index "issues".
  191. * All the requests are listed here.
  192. */
  193. public class IssueIndex {
  194. public static final String FACET_PROJECTS = "projects";
  195. public static final String FACET_ASSIGNED_TO_ME = "assigned_to_me";
  196. public static final String FACET_MODULES = "moduleUuids";
  197. private static final int DEFAULT_FACET_SIZE = 15;
  198. private static final int MAX_FACET_SIZE = 100;
  199. private static final String AGG_VULNERABILITIES = "vulnerabilities";
  200. private static final String AGG_SEVERITIES = "severities";
  201. private static final String AGG_TO_REVIEW_SECURITY_HOTSPOTS = "toReviewSecurityHotspots";
  202. private static final String AGG_IN_REVIEW_SECURITY_HOTSPOTS = "inReviewSecurityHotspots";
  203. private static final String AGG_REVIEWED_SECURITY_HOTSPOTS = "reviewedSecurityHotspots";
  204. private static final String AGG_CWES = "cwes";
  205. private static final BoolQueryBuilder NON_RESOLVED_VULNERABILITIES_FILTER = boolQuery()
  206. .filter(termQuery(FIELD_ISSUE_TYPE, VULNERABILITY.name()))
  207. .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION));
  208. private static final BoolQueryBuilder IN_REVIEW_HOTSPOTS_FILTER = boolQuery()
  209. .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))
  210. .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_IN_REVIEW))
  211. .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION));
  212. private static final BoolQueryBuilder TO_REVIEW_HOTSPOTS_FILTER = boolQuery()
  213. .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))
  214. .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_TO_REVIEW))
  215. .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION));
  216. private static final BoolQueryBuilder REVIEWED_HOTSPOTS_FILTER = boolQuery()
  217. .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))
  218. .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_REVIEWED))
  219. .filter(termQuery(FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_FIXED));
  220. private static final Object[] NO_SELECTED_VALUES = {0};
  221. private static final SimpleFieldTopAggregationDefinition EFFORT_TOP_AGGREGATION = new SimpleFieldTopAggregationDefinition(FIELD_ISSUE_EFFORT, NON_STICKY);
  222. public enum Facet {
  223. SEVERITIES(PARAM_SEVERITIES, FIELD_ISSUE_SEVERITY, STICKY, Severity.ALL.size()),
  224. STATUSES(PARAM_STATUSES, FIELD_ISSUE_STATUS, STICKY, Issue.STATUSES.size()),
  225. // Resolutions facet returns one more element than the number of resolutions to take into account unresolved issues
  226. RESOLUTIONS(PARAM_RESOLUTIONS, FIELD_ISSUE_RESOLUTION, STICKY, Issue.RESOLUTIONS.size() + 1),
  227. TYPES(PARAM_TYPES, FIELD_ISSUE_TYPE, STICKY, RuleType.values().length),
  228. SCOPES(PARAM_SCOPES, FIELD_ISSUE_SCOPE, STICKY, MAX_FACET_SIZE),
  229. LANGUAGES(PARAM_LANGUAGES, FIELD_ISSUE_LANGUAGE, STICKY, MAX_FACET_SIZE),
  230. RULES(PARAM_RULES, FIELD_ISSUE_RULE_UUID, STICKY, MAX_FACET_SIZE),
  231. TAGS(PARAM_TAGS, FIELD_ISSUE_TAGS, STICKY, MAX_FACET_SIZE),
  232. AUTHOR(PARAM_AUTHOR, FIELD_ISSUE_AUTHOR_LOGIN, STICKY, MAX_FACET_SIZE),
  233. PROJECT_UUIDS(FACET_PROJECTS, FIELD_ISSUE_PROJECT_UUID, STICKY, MAX_FACET_SIZE),
  234. MODULE_UUIDS(FACET_MODULES, FIELD_ISSUE_MODULE_UUID, STICKY, MAX_FACET_SIZE),
  235. FILES(PARAM_FILES, FIELD_ISSUE_FILE_PATH, STICKY, MAX_FACET_SIZE),
  236. DIRECTORIES(PARAM_DIRECTORIES, FIELD_ISSUE_DIRECTORY_PATH, STICKY, MAX_FACET_SIZE),
  237. ASSIGNEES(PARAM_ASSIGNEES, FIELD_ISSUE_ASSIGNEE_UUID, STICKY, MAX_FACET_SIZE),
  238. ASSIGNED_TO_ME(FACET_ASSIGNED_TO_ME, FIELD_ISSUE_ASSIGNEE_UUID, STICKY, 1),
  239. OWASP_TOP_10(PARAM_OWASP_TOP_10, FIELD_ISSUE_OWASP_TOP_10, STICKY, DEFAULT_FACET_SIZE),
  240. SANS_TOP_25(PARAM_SANS_TOP_25, FIELD_ISSUE_SANS_TOP_25, STICKY, DEFAULT_FACET_SIZE),
  241. CWE(PARAM_CWE, FIELD_ISSUE_CWE, STICKY, DEFAULT_FACET_SIZE),
  242. CREATED_AT(PARAM_CREATED_AT, FIELD_ISSUE_FUNC_CREATED_AT, NON_STICKY),
  243. SONARSOURCE_SECURITY(PARAM_SONARSOURCE_SECURITY, FIELD_ISSUE_SQ_SECURITY_CATEGORY, STICKY, DEFAULT_FACET_SIZE);
  244. private final String name;
  245. private final SimpleFieldTopAggregationDefinition topAggregation;
  246. private final Integer numberOfTerms;
  247. Facet(String name, String fieldName, boolean sticky, int numberOfTerms) {
  248. this.name = name;
  249. this.topAggregation = new SimpleFieldTopAggregationDefinition(fieldName, sticky);
  250. this.numberOfTerms = numberOfTerms;
  251. }
  252. Facet(String name, String fieldName, boolean sticky) {
  253. this.name = name;
  254. this.topAggregation = new SimpleFieldTopAggregationDefinition(fieldName, sticky);
  255. this.numberOfTerms = null;
  256. }
  257. public String getName() {
  258. return name;
  259. }
  260. public String getFieldName() {
  261. return topAggregation.getFilterScope().getFieldName();
  262. }
  263. public TopAggregationDefinition.FilterScope getFilterScope() {
  264. return topAggregation.getFilterScope();
  265. }
  266. public SimpleFieldTopAggregationDefinition getTopAggregationDef() {
  267. return topAggregation;
  268. }
  269. public int getNumberOfTerms() {
  270. checkState(numberOfTerms != null, "numberOfTerms should have been provided in constructor");
  271. return numberOfTerms;
  272. }
  273. }
  274. private static final Map<String, Facet> FACETS_BY_NAME = Arrays.stream(Facet.values())
  275. .collect(uniqueIndex(Facet::getName));
  276. private static final String SUBSTRING_MATCH_REGEXP = ".*%s.*";
  277. // TODO to be documented
  278. // TODO move to Facets ?
  279. private static final String FACET_SUFFIX_MISSING = "_missing";
  280. private static final String IS_ASSIGNED_FILTER = "__isAssigned";
  281. private static final SumAggregationBuilder EFFORT_AGGREGATION = AggregationBuilders.sum(FACET_MODE_EFFORT).field(FIELD_ISSUE_EFFORT);
  282. private static final BucketOrder EFFORT_AGGREGATION_ORDER = BucketOrder.aggregation(FACET_MODE_EFFORT, false);
  283. private static final Duration TWENTY_DAYS = Duration.standardDays(20L);
  284. private static final Duration TWENTY_WEEKS = Duration.standardDays(20L * 7L);
  285. private static final Duration TWENTY_MONTHS = Duration.standardDays(20L * 30L);
  286. private static final String AGG_COUNT = "count";
  287. private final Sorting sorting;
  288. private final EsClient client;
  289. private final System2 system;
  290. private final UserSession userSession;
  291. private final WebAuthorizationTypeSupport authorizationTypeSupport;
  292. public IssueIndex(EsClient client, System2 system, UserSession userSession, WebAuthorizationTypeSupport authorizationTypeSupport) {
  293. this.client = client;
  294. this.system = system;
  295. this.userSession = userSession;
  296. this.authorizationTypeSupport = authorizationTypeSupport;
  297. this.sorting = new Sorting();
  298. this.sorting.add(IssueQuery.SORT_BY_STATUS, FIELD_ISSUE_STATUS);
  299. this.sorting.add(IssueQuery.SORT_BY_STATUS, FIELD_ISSUE_KEY);
  300. this.sorting.add(IssueQuery.SORT_BY_SEVERITY, FIELD_ISSUE_SEVERITY_VALUE);
  301. this.sorting.add(IssueQuery.SORT_BY_SEVERITY, FIELD_ISSUE_KEY);
  302. this.sorting.add(IssueQuery.SORT_BY_CREATION_DATE, FIELD_ISSUE_FUNC_CREATED_AT);
  303. this.sorting.add(IssueQuery.SORT_BY_CREATION_DATE, FIELD_ISSUE_KEY);
  304. this.sorting.add(IssueQuery.SORT_BY_UPDATE_DATE, FIELD_ISSUE_FUNC_UPDATED_AT);
  305. this.sorting.add(IssueQuery.SORT_BY_UPDATE_DATE, FIELD_ISSUE_KEY);
  306. this.sorting.add(IssueQuery.SORT_BY_CLOSE_DATE, FIELD_ISSUE_FUNC_CLOSED_AT);
  307. this.sorting.add(IssueQuery.SORT_BY_CLOSE_DATE, FIELD_ISSUE_KEY);
  308. this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, FIELD_ISSUE_PROJECT_UUID);
  309. this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, FIELD_ISSUE_FILE_PATH);
  310. this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, FIELD_ISSUE_LINE);
  311. this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, FIELD_ISSUE_SEVERITY_VALUE).reverse();
  312. this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, FIELD_ISSUE_KEY);
  313. this.sorting.add(IssueQuery.SORT_HOTSPOTS, FIELD_ISSUE_VULNERABILITY_PROBABILITY).reverse();
  314. this.sorting.add(IssueQuery.SORT_HOTSPOTS, FIELD_ISSUE_SQ_SECURITY_CATEGORY);
  315. this.sorting.add(IssueQuery.SORT_HOTSPOTS, FIELD_ISSUE_RULE_UUID);
  316. this.sorting.add(IssueQuery.SORT_HOTSPOTS, FIELD_ISSUE_PROJECT_UUID);
  317. this.sorting.add(IssueQuery.SORT_HOTSPOTS, FIELD_ISSUE_FILE_PATH);
  318. this.sorting.add(IssueQuery.SORT_HOTSPOTS, FIELD_ISSUE_LINE);
  319. this.sorting.add(IssueQuery.SORT_HOTSPOTS, FIELD_ISSUE_KEY);
  320. // by default order by created date, project, file, line and issue key (in order to be deterministic when same ms)
  321. this.sorting.addDefault(FIELD_ISSUE_FUNC_CREATED_AT).reverse();
  322. this.sorting.addDefault(FIELD_ISSUE_PROJECT_UUID);
  323. this.sorting.addDefault(FIELD_ISSUE_FILE_PATH);
  324. this.sorting.addDefault(FIELD_ISSUE_LINE);
  325. this.sorting.addDefault(FIELD_ISSUE_KEY);
  326. }
  327. public SearchResponse search(IssueQuery query, SearchOptions options) {
  328. SearchRequest requestBuilder = EsClient.prepareSearch(TYPE_ISSUE.getMainType());
  329. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  330. requestBuilder.source(sourceBuilder);
  331. configureSorting(query, sourceBuilder);
  332. configurePagination(options, sourceBuilder);
  333. configureRouting(query, options, requestBuilder);
  334. AllFilters allFilters = createAllFilters(query);
  335. RequestFiltersComputer filterComputer = newFilterComputer(options, allFilters);
  336. configureTopAggregations(query, options, sourceBuilder, allFilters, filterComputer);
  337. configureQuery(sourceBuilder, filterComputer);
  338. configureTopFilters(sourceBuilder, filterComputer);
  339. sourceBuilder.fetchSource(false)
  340. .trackTotalHits(true);
  341. return client.search(requestBuilder);
  342. }
  343. private void configureTopAggregations(IssueQuery query, SearchOptions options, SearchSourceBuilder esRequest, AllFilters allFilters, RequestFiltersComputer filterComputer) {
  344. TopAggregationHelper aggregationHelper = newAggregationHelper(filterComputer, query);
  345. configureTopAggregations(aggregationHelper, query, options, allFilters, esRequest);
  346. }
  347. private static void configureQuery(SearchSourceBuilder esRequest, RequestFiltersComputer filterComputer) {
  348. QueryBuilder esQuery = filterComputer.getQueryFilters()
  349. .map(t -> (QueryBuilder) boolQuery().must(matchAllQuery()).filter(t))
  350. .orElse(matchAllQuery());
  351. esRequest.query(esQuery);
  352. }
  353. private static void configureTopFilters(SearchSourceBuilder esRequest, RequestFiltersComputer filterComputer) {
  354. filterComputer.getPostFilters().ifPresent(esRequest::postFilter);
  355. }
  356. /**
  357. * Optimization - do not send ES request to all shards when scope is restricted
  358. * to a set of projects. Because project UUID is used for routing, the request
  359. * can be sent to only the shards containing the specified projects.
  360. * Note that sticky facets may involve all projects, so this optimization must be
  361. * disabled when facets are enabled.
  362. */
  363. private static void configureRouting(IssueQuery query, SearchOptions options, SearchRequest searchRequest) {
  364. Collection<String> uuids = query.projectUuids();
  365. if (!uuids.isEmpty() && options.getFacets().isEmpty()) {
  366. searchRequest.routing(uuids.stream().map(AuthorizationDoc::idOf).toArray(String[]::new));
  367. }
  368. }
  369. private static void configurePagination(SearchOptions options, SearchSourceBuilder esSearch) {
  370. esSearch.from(options.getOffset()).size(options.getLimit());
  371. }
  372. private AllFilters createAllFilters(IssueQuery query) {
  373. AllFilters filters = RequestFiltersComputer.newAllFilters();
  374. filters.addFilter("__indexType", new SimpleFieldFilterScope(FIELD_INDEX_TYPE), termQuery(FIELD_INDEX_TYPE, TYPE_ISSUE.getName()));
  375. filters.addFilter("__authorization", new SimpleFieldFilterScope("parent"), createAuthorizationFilter());
  376. // Issue is assigned Filter
  377. if (Boolean.TRUE.equals(query.assigned())) {
  378. filters.addFilter(IS_ASSIGNED_FILTER, Facet.ASSIGNEES.getFilterScope(), existsQuery(FIELD_ISSUE_ASSIGNEE_UUID));
  379. } else if (Boolean.FALSE.equals(query.assigned())) {
  380. filters.addFilter(IS_ASSIGNED_FILTER, ASSIGNEES.getFilterScope(), boolQuery().mustNot(existsQuery(FIELD_ISSUE_ASSIGNEE_UUID)));
  381. }
  382. // Issue is Resolved Filter
  383. if (Boolean.TRUE.equals(query.resolved())) {
  384. filters.addFilter("__isResolved", Facet.RESOLUTIONS.getFilterScope(), existsQuery(FIELD_ISSUE_RESOLUTION));
  385. } else if (Boolean.FALSE.equals(query.resolved())) {
  386. filters.addFilter("__isResolved", Facet.RESOLUTIONS.getFilterScope(), boolQuery().mustNot(existsQuery(FIELD_ISSUE_RESOLUTION)));
  387. }
  388. // Field Filters
  389. filters.addFilter(FIELD_ISSUE_KEY, new SimpleFieldFilterScope(FIELD_ISSUE_KEY), createTermsFilter(FIELD_ISSUE_KEY, query.issueKeys()));
  390. filters.addFilter(FIELD_ISSUE_ASSIGNEE_UUID, ASSIGNEES.getFilterScope(), createTermsFilter(FIELD_ISSUE_ASSIGNEE_UUID, query.assignees()));
  391. filters.addFilter(FIELD_ISSUE_SCOPE, SCOPES.getFilterScope(), createTermsFilter(FIELD_ISSUE_SCOPE, query.scopes()));
  392. filters.addFilter(FIELD_ISSUE_LANGUAGE, LANGUAGES.getFilterScope(), createTermsFilter(FIELD_ISSUE_LANGUAGE, query.languages()));
  393. filters.addFilter(FIELD_ISSUE_TAGS, TAGS.getFilterScope(), createTermsFilter(FIELD_ISSUE_TAGS, query.tags()));
  394. filters.addFilter(FIELD_ISSUE_TYPE, TYPES.getFilterScope(), createTermsFilter(FIELD_ISSUE_TYPE, query.types()));
  395. filters.addFilter(
  396. FIELD_ISSUE_RESOLUTION, RESOLUTIONS.getFilterScope(),
  397. createTermsFilter(FIELD_ISSUE_RESOLUTION, query.resolutions()));
  398. filters.addFilter(
  399. FIELD_ISSUE_AUTHOR_LOGIN, AUTHOR.getFilterScope(),
  400. createTermsFilter(FIELD_ISSUE_AUTHOR_LOGIN, query.authors()));
  401. filters.addFilter(
  402. FIELD_ISSUE_RULE_UUID, RULES.getFilterScope(), createTermsFilter(
  403. FIELD_ISSUE_RULE_UUID,
  404. query.ruleUuids()));
  405. filters.addFilter(FIELD_ISSUE_STATUS, STATUSES.getFilterScope(), createTermsFilter(FIELD_ISSUE_STATUS, query.statuses()));
  406. // security category
  407. addSecurityCategoryFilter(FIELD_ISSUE_OWASP_TOP_10, OWASP_TOP_10, query.owaspTop10(), filters);
  408. addSecurityCategoryFilter(FIELD_ISSUE_SANS_TOP_25, SANS_TOP_25, query.sansTop25(), filters);
  409. addSecurityCategoryFilter(FIELD_ISSUE_CWE, CWE, query.cwe(), filters);
  410. addSecurityCategoryFilter(FIELD_ISSUE_SQ_SECURITY_CATEGORY, SONARSOURCE_SECURITY, query.sonarsourceSecurity(), filters);
  411. addSeverityFilter(query, filters);
  412. addComponentRelatedFilters(query, filters);
  413. addDatesFilter(filters, query);
  414. addCreatedAfterByProjectsFilter(filters, query);
  415. return filters;
  416. }
  417. private static void addSecurityCategoryFilter(String fieldName, Facet facet, Collection<String> values, AllFilters allFilters) {
  418. QueryBuilder securityCategoryFilter = createTermsFilter(fieldName, values);
  419. if (securityCategoryFilter != null) {
  420. allFilters.addFilter(
  421. fieldName,
  422. facet.getFilterScope(),
  423. boolQuery()
  424. .must(securityCategoryFilter)
  425. .must(termsQuery(FIELD_ISSUE_TYPE, VULNERABILITY.name(), SECURITY_HOTSPOT.name())));
  426. }
  427. }
  428. private static void addSeverityFilter(IssueQuery query, AllFilters allFilters) {
  429. QueryBuilder severityFieldFilter = createTermsFilter(FIELD_ISSUE_SEVERITY, query.severities());
  430. if (severityFieldFilter != null) {
  431. allFilters.addFilter(
  432. FIELD_ISSUE_SEVERITY,
  433. SEVERITIES.getFilterScope(),
  434. boolQuery()
  435. .must(severityFieldFilter)
  436. // Ignore severity of Security HotSpots
  437. .mustNot(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())));
  438. }
  439. }
  440. private static void addComponentRelatedFilters(IssueQuery query, AllFilters filters) {
  441. addCommonComponentRelatedFilters(query, filters);
  442. if (query.viewUuids().isEmpty()) {
  443. addBranchComponentRelatedFilters(query, filters);
  444. } else {
  445. addViewRelatedFilters(query, filters);
  446. }
  447. }
  448. private static void addCommonComponentRelatedFilters(IssueQuery query, AllFilters filters) {
  449. filters.addFilter(FIELD_ISSUE_COMPONENT_UUID, new SimpleFieldFilterScope(FIELD_ISSUE_COMPONENT_UUID),
  450. createTermsFilter(FIELD_ISSUE_COMPONENT_UUID, query.componentUuids()));
  451. if (!Boolean.TRUE.equals(query.onComponentOnly())) {
  452. filters.addFilter(
  453. FIELD_ISSUE_PROJECT_UUID, new SimpleFieldFilterScope(FIELD_ISSUE_PROJECT_UUID),
  454. createTermsFilter(FIELD_ISSUE_PROJECT_UUID, query.projectUuids()));
  455. filters.addFilter(
  456. "__module", new SimpleFieldFilterScope(FIELD_ISSUE_MODULE_PATH),
  457. createTermsFilter(FIELD_ISSUE_MODULE_PATH, query.moduleRootUuids()));
  458. filters.addFilter(
  459. FIELD_ISSUE_MODULE_UUID, new SimpleFieldFilterScope(FIELD_ISSUE_MODULE_UUID),
  460. createTermsFilter(FIELD_ISSUE_MODULE_UUID, query.moduleUuids()));
  461. filters.addFilter(
  462. FIELD_ISSUE_DIRECTORY_PATH, new SimpleFieldFilterScope(FIELD_ISSUE_DIRECTORY_PATH),
  463. createTermsFilter(FIELD_ISSUE_DIRECTORY_PATH, query.directories()));
  464. filters.addFilter(
  465. FIELD_ISSUE_FILE_PATH, new SimpleFieldFilterScope(FIELD_ISSUE_FILE_PATH),
  466. createTermsFilter(FIELD_ISSUE_FILE_PATH, query.files()));
  467. }
  468. }
  469. private static void addBranchComponentRelatedFilters(IssueQuery query, AllFilters allFilters) {
  470. if (Boolean.TRUE.equals(query.onComponentOnly())) {
  471. return;
  472. }
  473. allFilters.addFilter(
  474. "__is_main_branch", new SimpleFieldFilterScope(FIELD_ISSUE_IS_MAIN_BRANCH),
  475. createTermFilter(FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(query.isMainBranch())));
  476. allFilters.addFilter(
  477. FIELD_ISSUE_BRANCH_UUID, new SimpleFieldFilterScope(FIELD_ISSUE_BRANCH_UUID),
  478. createTermFilter(FIELD_ISSUE_BRANCH_UUID, query.branchUuid()));
  479. }
  480. private static void addViewRelatedFilters(IssueQuery query, AllFilters allFilters) {
  481. if (Boolean.TRUE.equals(query.onComponentOnly())) {
  482. return;
  483. }
  484. Collection<String> viewUuids = query.viewUuids();
  485. String branchUuid = query.branchUuid();
  486. boolean onApplicationBranch = branchUuid != null && !viewUuids.isEmpty();
  487. if (onApplicationBranch) {
  488. allFilters.addFilter("__view", new SimpleFieldFilterScope("view"), createViewFilter(singletonList(query.branchUuid())));
  489. } else {
  490. allFilters.addFilter("__is_main_branch", new SimpleFieldFilterScope(FIELD_ISSUE_IS_MAIN_BRANCH), createTermFilter(FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(true)));
  491. allFilters.addFilter("__view", new SimpleFieldFilterScope("view"), createViewFilter(viewUuids));
  492. }
  493. }
  494. @CheckForNull
  495. private static QueryBuilder createViewFilter(Collection<String> viewUuids) {
  496. if (viewUuids.isEmpty()) {
  497. return null;
  498. }
  499. BoolQueryBuilder viewsFilter = boolQuery();
  500. for (String viewUuid : viewUuids) {
  501. IndexType.IndexMainType mainType = TYPE_VIEW;
  502. viewsFilter.should(QueryBuilders.termsLookupQuery(FIELD_ISSUE_BRANCH_UUID,
  503. new TermsLookup(
  504. mainType.getIndex().getName(),
  505. mainType.getType(),
  506. viewUuid,
  507. ViewIndexDefinition.FIELD_PROJECTS)));
  508. }
  509. return viewsFilter;
  510. }
  511. private static RequestFiltersComputer newFilterComputer(SearchOptions options, AllFilters allFilters) {
  512. Collection<String> facetNames = options.getFacets();
  513. Set<TopAggregationDefinition<?>> facets = Stream.concat(
  514. Stream.of(EFFORT_TOP_AGGREGATION),
  515. facetNames.stream()
  516. .map(FACETS_BY_NAME::get)
  517. .filter(Objects::nonNull)
  518. .map(Facet::getTopAggregationDef))
  519. .collect(MoreCollectors.toSet(facetNames.size()));
  520. return new RequestFiltersComputer(allFilters, facets);
  521. }
  522. private static TopAggregationHelper newAggregationHelper(RequestFiltersComputer filterComputer, IssueQuery query) {
  523. if (hasQueryEffortFacet(query)) {
  524. return new TopAggregationHelper(filterComputer, new SubAggregationHelper(EFFORT_AGGREGATION, EFFORT_AGGREGATION_ORDER));
  525. }
  526. return new TopAggregationHelper(filterComputer, new SubAggregationHelper());
  527. }
  528. private static AggregationBuilder addEffortAggregationIfNeeded(IssueQuery query, AggregationBuilder aggregation) {
  529. if (hasQueryEffortFacet(query)) {
  530. aggregation.subAggregation(EFFORT_AGGREGATION);
  531. }
  532. return aggregation;
  533. }
  534. private static boolean hasQueryEffortFacet(IssueQuery query) {
  535. return FACET_MODE_EFFORT.equals(query.facetMode());
  536. }
  537. @CheckForNull
  538. private static QueryBuilder createTermsFilter(String field, Collection<?> values) {
  539. return values.isEmpty() ? null : termsQuery(field, values);
  540. }
  541. @CheckForNull
  542. private static QueryBuilder createTermFilter(String field, @Nullable String value) {
  543. return value == null ? null : termQuery(field, value);
  544. }
  545. private void configureSorting(IssueQuery query, SearchSourceBuilder esRequest) {
  546. createSortBuilders(query).forEach(esRequest::sort);
  547. }
  548. private List<FieldSortBuilder> createSortBuilders(IssueQuery query) {
  549. String sortField = query.sort();
  550. if (sortField != null) {
  551. boolean asc = Boolean.TRUE.equals(query.asc());
  552. return sorting.fill(sortField, asc);
  553. }
  554. return sorting.fillDefault();
  555. }
  556. private QueryBuilder createAuthorizationFilter() {
  557. return authorizationTypeSupport.createQueryFilter();
  558. }
  559. private void addDatesFilter(AllFilters filters, IssueQuery query) {
  560. PeriodStart createdAfter = query.createdAfter();
  561. Date createdBefore = query.createdBefore();
  562. validateCreationDateBounds(createdBefore, createdAfter != null ? createdAfter.date() : null);
  563. if (createdAfter != null) {
  564. filters.addFilter(
  565. "__createdAfter", CREATED_AT.getFilterScope(),
  566. QueryBuilders
  567. .rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT)
  568. .from(BaseDoc.dateToEpochSeconds(createdAfter.date()), createdAfter.inclusive()));
  569. }
  570. if (createdBefore != null) {
  571. filters.addFilter(
  572. "__createdBefore", CREATED_AT.getFilterScope(),
  573. QueryBuilders
  574. .rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT)
  575. .lt(BaseDoc.dateToEpochSeconds(createdBefore)));
  576. }
  577. Date createdAt = query.createdAt();
  578. if (createdAt != null) {
  579. filters.addFilter(
  580. "__createdAt", CREATED_AT.getFilterScope(),
  581. termQuery(FIELD_ISSUE_FUNC_CREATED_AT, BaseDoc.dateToEpochSeconds(createdAt)));
  582. }
  583. }
  584. private static void addCreatedAfterByProjectsFilter(AllFilters allFilters, IssueQuery query) {
  585. Map<String, PeriodStart> createdAfterByProjectUuids = query.createdAfterByProjectUuids();
  586. BoolQueryBuilder boolQueryBuilder = boolQuery();
  587. createdAfterByProjectUuids.forEach((projectOrProjectBranchUuid, createdAfterDate) -> boolQueryBuilder.should(boolQuery()
  588. .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid))
  589. .filter(rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT).from(BaseDoc.dateToEpochSeconds(createdAfterDate.date()), createdAfterDate.inclusive()))));
  590. allFilters.addFilter("__created_after_by_project_uuids", new SimpleFieldFilterScope("createdAfterByProjectUuids"), boolQueryBuilder);
  591. }
  592. private void validateCreationDateBounds(@Nullable Date createdBefore, @Nullable Date createdAfter) {
  593. Preconditions.checkArgument(createdAfter == null || createdAfter.compareTo(new Date(system.now())) <= 0,
  594. "Start bound cannot be in the future");
  595. Preconditions.checkArgument(createdAfter == null || createdBefore == null || createdAfter.before(createdBefore),
  596. "Start bound cannot be larger or equal to end bound");
  597. }
  598. private void configureTopAggregations(TopAggregationHelper aggregationHelper, IssueQuery query, SearchOptions options,
  599. AllFilters queryFilters, SearchSourceBuilder esRequest) {
  600. addFacetIfNeeded(options, aggregationHelper, esRequest, STATUSES, NO_SELECTED_VALUES);
  601. addFacetIfNeeded(options, aggregationHelper, esRequest, PROJECT_UUIDS, query.projectUuids().toArray());
  602. addFacetIfNeeded(options, aggregationHelper, esRequest, MODULE_UUIDS, query.moduleUuids().toArray());
  603. addFacetIfNeeded(options, aggregationHelper, esRequest, DIRECTORIES, query.directories().toArray());
  604. addFacetIfNeeded(options, aggregationHelper, esRequest, FILES, query.files().toArray());
  605. addFacetIfNeeded(options, aggregationHelper, esRequest, SCOPES, query.scopes().toArray());
  606. addFacetIfNeeded(options, aggregationHelper, esRequest, LANGUAGES, query.languages().toArray());
  607. addFacetIfNeeded(options, aggregationHelper, esRequest, RULES, query.ruleUuids().toArray());
  608. addFacetIfNeeded(options, aggregationHelper, esRequest, AUTHOR, query.authors().toArray());
  609. addFacetIfNeeded(options, aggregationHelper, esRequest, TAGS, query.tags().toArray());
  610. addFacetIfNeeded(options, aggregationHelper, esRequest, TYPES, query.types().toArray());
  611. addSecurityCategoryFacetIfNeeded(PARAM_OWASP_TOP_10, OWASP_TOP_10, options, aggregationHelper, esRequest, query.owaspTop10().toArray());
  612. addSecurityCategoryFacetIfNeeded(PARAM_SANS_TOP_25, SANS_TOP_25, options, aggregationHelper, esRequest, query.sansTop25().toArray());
  613. addSecurityCategoryFacetIfNeeded(PARAM_CWE, CWE, options, aggregationHelper, esRequest, query.cwe().toArray());
  614. addSecurityCategoryFacetIfNeeded(PARAM_SONARSOURCE_SECURITY, SONARSOURCE_SECURITY, options, aggregationHelper, esRequest, query.sonarsourceSecurity().toArray());
  615. addSeverityFacetIfNeeded(options, aggregationHelper, esRequest);
  616. addResolutionFacetIfNeeded(options, query, aggregationHelper, esRequest);
  617. addAssigneesFacetIfNeeded(options, query, aggregationHelper, esRequest);
  618. addCreatedAtFacetIfNeeded(options, query, aggregationHelper, queryFilters, esRequest);
  619. addAssignedToMeFacetIfNeeded(options, aggregationHelper, esRequest);
  620. addEffortTopAggregation(aggregationHelper, esRequest);
  621. }
  622. private static void addFacetIfNeeded(SearchOptions options, TopAggregationHelper aggregationHelper,
  623. SearchSourceBuilder esRequest, Facet facet, Object[] selectedValues) {
  624. if (!options.getFacets().contains(facet.getName())) {
  625. return;
  626. }
  627. FilterAggregationBuilder topAggregation = aggregationHelper.buildTermTopAggregation(
  628. facet.getName(), facet.getTopAggregationDef(), facet.getNumberOfTerms(),
  629. NO_EXTRA_FILTER,
  630. t -> aggregationHelper.getSubAggregationHelper().buildSelectedItemsAggregation(facet.getName(), facet.getTopAggregationDef(), selectedValues)
  631. .ifPresent(t::subAggregation));
  632. esRequest.aggregation(topAggregation);
  633. }
  634. private static void addSecurityCategoryFacetIfNeeded(String param, Facet facet, SearchOptions options, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest,
  635. Object[] selectedValues) {
  636. if (!options.getFacets().contains(param)) {
  637. return;
  638. }
  639. AggregationBuilder aggregation = aggregationHelper.buildTermTopAggregation(
  640. facet.getName(), facet.getTopAggregationDef(), facet.getNumberOfTerms(),
  641. filter -> filter.must(termQuery(FIELD_ISSUE_TYPE, VULNERABILITY.name())),
  642. t -> aggregationHelper.getSubAggregationHelper().buildSelectedItemsAggregation(facet.getName(), facet.getTopAggregationDef(), selectedValues)
  643. .ifPresent(t::subAggregation));
  644. esRequest.aggregation(aggregation);
  645. }
  646. private static void addSeverityFacetIfNeeded(SearchOptions options, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  647. if (!options.getFacets().contains(PARAM_SEVERITIES)) {
  648. return;
  649. }
  650. AggregationBuilder aggregation = aggregationHelper.buildTermTopAggregation(
  651. SEVERITIES.getName(), SEVERITIES.getTopAggregationDef(), SEVERITIES.getNumberOfTerms(),
  652. // Ignore severity of Security HotSpots
  653. filter -> filter.mustNot(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())),
  654. NO_OTHER_SUBAGGREGATION);
  655. esRequest.aggregation(aggregation);
  656. }
  657. private static void addResolutionFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  658. if (!options.getFacets().contains(PARAM_RESOLUTIONS)) {
  659. return;
  660. }
  661. AggregationBuilder aggregation = aggregationHelper.buildTermTopAggregation(
  662. RESOLUTIONS.getName(), RESOLUTIONS.getTopAggregationDef(), RESOLUTIONS.getNumberOfTerms(),
  663. NO_EXTRA_FILTER,
  664. t -> {
  665. // add aggregation of type "missing" to return count of unresolved issues in the facet
  666. t.subAggregation(
  667. addEffortAggregationIfNeeded(query, AggregationBuilders
  668. .missing(RESOLUTIONS.getName() + FACET_SUFFIX_MISSING)
  669. .field(RESOLUTIONS.getFieldName())));
  670. });
  671. esRequest.aggregation(aggregation);
  672. }
  673. private static void addAssigneesFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  674. if (!options.getFacets().contains(PARAM_ASSIGNEES)) {
  675. return;
  676. }
  677. Consumer<FilterAggregationBuilder> assigneeAggregations = t -> {
  678. // optional second aggregation to return the issue count for selected assignees (if any)
  679. Object[] assignees = query.assignees().toArray();
  680. aggregationHelper.getSubAggregationHelper().buildSelectedItemsAggregation(ASSIGNEES.getName(), ASSIGNEES.getTopAggregationDef(), assignees)
  681. .ifPresent(t::subAggregation);
  682. // third aggregation to always return the count of unassigned in the assignee facet
  683. t.subAggregation(addEffortAggregationIfNeeded(query, AggregationBuilders
  684. .missing(ASSIGNEES.getName() + FACET_SUFFIX_MISSING)
  685. .field(ASSIGNEES.getFieldName())));
  686. };
  687. AggregationBuilder aggregation = aggregationHelper.buildTermTopAggregation(
  688. ASSIGNEES.getName(), ASSIGNEES.getTopAggregationDef(), ASSIGNEES.getNumberOfTerms(),
  689. NO_EXTRA_FILTER, assigneeAggregations);
  690. esRequest.aggregation(aggregation);
  691. }
  692. private void addCreatedAtFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, AllFilters allFilters,
  693. SearchSourceBuilder esRequest) {
  694. if (options.getFacets().contains(PARAM_CREATED_AT)) {
  695. getCreatedAtFacet(query, aggregationHelper, allFilters).ifPresent(esRequest::aggregation);
  696. }
  697. }
  698. private Optional<AggregationBuilder> getCreatedAtFacet(IssueQuery query, TopAggregationHelper aggregationHelper, AllFilters allFilters) {
  699. long startTime;
  700. boolean startInclusive;
  701. PeriodStart createdAfter = query.createdAfter();
  702. if (createdAfter == null) {
  703. OptionalLong minDate = getMinCreatedAt(allFilters);
  704. if (!minDate.isPresent()) {
  705. return Optional.empty();
  706. }
  707. startTime = minDate.getAsLong();
  708. startInclusive = true;
  709. } else {
  710. startTime = createdAfter.date().getTime();
  711. startInclusive = createdAfter.inclusive();
  712. }
  713. Date createdBefore = query.createdBefore();
  714. long endTime = createdBefore == null ? system.now() : createdBefore.getTime();
  715. Duration timeSpan = new Duration(startTime, endTime);
  716. DateHistogramInterval bucketSize = computeDateHistogramBucketSize(timeSpan);
  717. FilterAggregationBuilder topAggregation = aggregationHelper.buildTopAggregation(
  718. CREATED_AT.getName(),
  719. CREATED_AT.getTopAggregationDef(),
  720. NO_EXTRA_FILTER,
  721. t -> {
  722. AggregationBuilder dateHistogram = AggregationBuilders.dateHistogram(CREATED_AT.getName())
  723. .field(CREATED_AT.getFieldName())
  724. .dateHistogramInterval(bucketSize)
  725. .minDocCount(0L)
  726. .format(DateUtils.DATETIME_FORMAT)
  727. .timeZone(Optional.ofNullable(query.timeZone()).orElse(system.getDefaultTimeZone().toZoneId()))
  728. // ES dateHistogram bounds are inclusive while createdBefore parameter is exclusive
  729. .extendedBounds(new LongBounds(startInclusive ? startTime : (startTime + 1), endTime - 1L));
  730. addEffortAggregationIfNeeded(query, dateHistogram);
  731. t.subAggregation(dateHistogram);
  732. });
  733. return Optional.of(topAggregation);
  734. }
  735. private static DateHistogramInterval computeDateHistogramBucketSize(Duration timeSpan) {
  736. if (timeSpan.isShorterThan(TWENTY_DAYS)) {
  737. return DateHistogramInterval.DAY;
  738. }
  739. if (timeSpan.isShorterThan(TWENTY_WEEKS)) {
  740. return DateHistogramInterval.WEEK;
  741. }
  742. if (timeSpan.isShorterThan(TWENTY_MONTHS)) {
  743. return DateHistogramInterval.MONTH;
  744. }
  745. return DateHistogramInterval.YEAR;
  746. }
  747. private OptionalLong getMinCreatedAt(AllFilters filters) {
  748. String facetNameAndField = CREATED_AT.getFieldName();
  749. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  750. .size(0);
  751. BoolQueryBuilder esFilter = boolQuery();
  752. filters.stream().filter(Objects::nonNull).forEach(esFilter::must);
  753. if (esFilter.hasClauses()) {
  754. sourceBuilder.query(QueryBuilders.boolQuery().filter(esFilter));
  755. }
  756. sourceBuilder.aggregation(AggregationBuilders.min(facetNameAndField).field(facetNameAndField));
  757. SearchRequest request = EsClient.prepareSearch(TYPE_ISSUE.getMainType())
  758. .source(sourceBuilder);
  759. Min minValue = client.search(request).getAggregations().get(facetNameAndField);
  760. double actualValue = minValue.getValue();
  761. if (Double.isInfinite(actualValue)) {
  762. return OptionalLong.empty();
  763. }
  764. return OptionalLong.of((long) actualValue);
  765. }
  766. private void addAssignedToMeFacetIfNeeded(SearchOptions options, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  767. String uuid = userSession.getUuid();
  768. if (options.getFacets().contains(ASSIGNED_TO_ME.getName()) && !StringUtils.isEmpty(uuid)) {
  769. AggregationBuilder aggregation = aggregationHelper.buildTermTopAggregation(
  770. ASSIGNED_TO_ME.getName(),
  771. ASSIGNED_TO_ME.getTopAggregationDef(),
  772. ASSIGNED_TO_ME.getNumberOfTerms(),
  773. NO_EXTRA_FILTER,
  774. t -> {
  775. // add sub-aggregation to return issue count for current user
  776. aggregationHelper.getSubAggregationHelper()
  777. .buildSelectedItemsAggregation(ASSIGNED_TO_ME.getName(), ASSIGNED_TO_ME.getTopAggregationDef(), new String[] {uuid})
  778. .ifPresent(t::subAggregation);
  779. });
  780. esRequest.aggregation(aggregation);
  781. }
  782. }
  783. private static void addEffortTopAggregation(TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  784. AggregationBuilder topAggregation = aggregationHelper.buildTopAggregation(
  785. FACET_MODE_EFFORT,
  786. EFFORT_TOP_AGGREGATION,
  787. NO_EXTRA_FILTER,
  788. t -> t.subAggregation(EFFORT_AGGREGATION));
  789. esRequest.aggregation(topAggregation);
  790. }
  791. public List<String> searchTags(IssueQuery query, @Nullable String textQuery, int size) {
  792. Terms terms = listTermsMatching(FIELD_ISSUE_TAGS, query, textQuery, BucketOrder.key(true), size);
  793. return EsUtils.termsKeys(terms);
  794. }
  795. public Map<String, Long> countTags(IssueQuery query, int maxNumberOfTags) {
  796. Terms terms = listTermsMatching(FIELD_ISSUE_TAGS, query, null, BucketOrder.count(false), maxNumberOfTags);
  797. return EsUtils.termsToMap(terms);
  798. }
  799. public List<String> searchAuthors(IssueQuery query, @Nullable String textQuery, int maxNumberOfAuthors) {
  800. Terms terms = listTermsMatching(FIELD_ISSUE_AUTHOR_LOGIN, query, textQuery, BucketOrder.key(true), maxNumberOfAuthors);
  801. return EsUtils.termsKeys(terms);
  802. }
  803. private Terms listTermsMatching(String fieldName, IssueQuery query, @Nullable String textQuery, BucketOrder termsOrder, int size) {
  804. SearchRequest requestBuilder = EsClient.prepareSearch(TYPE_ISSUE.getMainType());
  805. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  806. // Avoids returning search hits
  807. .size(0);
  808. requestBuilder.source(sourceBuilder);
  809. sourceBuilder.query(boolQuery().must(QueryBuilders.matchAllQuery()).filter(createBoolFilter(query)));
  810. TermsAggregationBuilder aggreg = AggregationBuilders.terms("_ref")
  811. .field(fieldName)
  812. .size(size)
  813. .order(termsOrder)
  814. .minDocCount(1L);
  815. if (textQuery != null) {
  816. aggreg.includeExclude(new IncludeExclude(format(SUBSTRING_MATCH_REGEXP, escapeSpecialRegexChars(textQuery)), null));
  817. }
  818. sourceBuilder.aggregation(aggreg);
  819. SearchResponse searchResponse = client.search(requestBuilder);
  820. return searchResponse.getAggregations().get("_ref");
  821. }
  822. private BoolQueryBuilder createBoolFilter(IssueQuery query) {
  823. BoolQueryBuilder boolQuery = boolQuery();
  824. createAllFilters(query).stream()
  825. .filter(Objects::nonNull)
  826. .forEach(boolQuery::must);
  827. return boolQuery;
  828. }
  829. public List<ProjectStatistics> searchProjectStatistics(List<String> projectUuids, List<Long> froms, @Nullable String assigneeUuid) {
  830. checkState(projectUuids.size() == froms.size(),
  831. "Expected same size for projectUuids (had size %s) and froms (had size %s)", projectUuids.size(), froms.size());
  832. if (projectUuids.isEmpty()) {
  833. return Collections.emptyList();
  834. }
  835. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  836. .query(
  837. boolQuery()
  838. .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION))
  839. .filter(termQuery(FIELD_ISSUE_ASSIGNEE_UUID, assigneeUuid))
  840. .mustNot(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())))
  841. .size(0);
  842. IntStream.range(0, projectUuids.size()).forEach(i -> {
  843. String projectUuid = projectUuids.get(i);
  844. long from = froms.get(i);
  845. sourceBuilder
  846. .aggregation(AggregationBuilders
  847. .filter(projectUuid, boolQuery()
  848. .filter(termQuery(FIELD_ISSUE_PROJECT_UUID, projectUuid))
  849. .filter(rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT).gte(epochMillisToEpochSeconds(from))))
  850. .subAggregation(
  851. AggregationBuilders.terms("branchUuid").field(FIELD_ISSUE_BRANCH_UUID)
  852. .subAggregation(
  853. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY))
  854. .subAggregation(
  855. AggregationBuilders.max("maxFuncCreatedAt").field(FIELD_ISSUE_FUNC_CREATED_AT))));
  856. });
  857. SearchRequest requestBuilder = EsClient.prepareSearch(TYPE_ISSUE.getMainType());
  858. requestBuilder.source(sourceBuilder);
  859. SearchResponse response = client.search(requestBuilder);
  860. return response.getAggregations().asList().stream()
  861. .map(x -> (ParsedFilter) x)
  862. .flatMap(projectBucket -> ((ParsedStringTerms) projectBucket.getAggregations().get("branchUuid")).getBuckets().stream()
  863. .flatMap(branchBucket -> {
  864. long count = ((ParsedValueCount) branchBucket.getAggregations().get(AGG_COUNT)).getValue();
  865. if (count < 1L) {
  866. return Stream.empty();
  867. }
  868. long lastIssueDate = (long) ((ParsedMax) branchBucket.getAggregations().get("maxFuncCreatedAt")).getValue();
  869. return Stream.of(new ProjectStatistics(branchBucket.getKeyAsString(), count, lastIssueDate));
  870. }))
  871. .collect(MoreCollectors.toList(projectUuids.size()));
  872. }
  873. public List<PrStatistics> searchBranchStatistics(String projectUuid, List<String> branchUuids) {
  874. if (branchUuids.isEmpty()) {
  875. return Collections.emptyList();
  876. }
  877. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  878. .query(
  879. boolQuery()
  880. .must(termsQuery(FIELD_ISSUE_BRANCH_UUID, branchUuids))
  881. .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION))
  882. .must(termQuery(FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(false))))
  883. .size(0)
  884. .aggregation(AggregationBuilders.terms("branchUuids")
  885. .field(FIELD_ISSUE_BRANCH_UUID)
  886. .size(branchUuids.size())
  887. .subAggregation(AggregationBuilders.terms("types")
  888. .field(FIELD_ISSUE_TYPE)));
  889. SearchRequest requestBuilder = EsClient.prepareSearch(TYPE_ISSUE.getMainType())
  890. .routing(AuthorizationDoc.idOf(projectUuid));
  891. requestBuilder.source(sourceBuilder);
  892. SearchResponse response = client.search(requestBuilder);
  893. return ((ParsedStringTerms) response.getAggregations().get("branchUuids")).getBuckets().stream()
  894. .map(bucket -> new PrStatistics(bucket.getKeyAsString(),
  895. ((ParsedStringTerms) bucket.getAggregations().get("types")).getBuckets()
  896. .stream()
  897. .collect(uniqueIndex(MultiBucketsAggregation.Bucket::getKeyAsString, MultiBucketsAggregation.Bucket::getDocCount))))
  898. .collect(MoreCollectors.toList(branchUuids.size()));
  899. }
  900. /**
  901. * @deprecated SansTop25 report is outdated and will be removed in future versions
  902. */
  903. @Deprecated
  904. public List<SecurityStandardCategoryStatistics> getSansTop25Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
  905. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  906. Stream.of(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)
  907. .forEach(sansCategory -> request.aggregation(newSecurityReportSubAggregations(
  908. AggregationBuilders.filter(sansCategory, boolQuery().filter(termQuery(FIELD_ISSUE_SANS_TOP_25, sansCategory))),
  909. includeCwe,
  910. SecurityStandards.CWES_BY_SANS_TOP_25.get(sansCategory))));
  911. return processSecurityReportSearchResults(request, includeCwe);
  912. }
  913. public List<SecurityStandardCategoryStatistics> getCweTop25Reports(String projectUuid, boolean isViewOrApp) {
  914. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  915. CWES_BY_CWE_TOP_25.keySet()
  916. .forEach(cweYear -> request.aggregation(
  917. newSecurityReportSubAggregations(
  918. AggregationBuilders.filter(cweYear, boolQuery().filter(existsQuery(FIELD_ISSUE_CWE))),
  919. true,
  920. CWES_BY_CWE_TOP_25.get(cweYear))));
  921. List<SecurityStandardCategoryStatistics> result = processSecurityReportSearchResults(request, true);
  922. for (SecurityStandardCategoryStatistics cweReport : result) {
  923. Set<String> foundRules = cweReport.getChildren().stream()
  924. .map(SecurityStandardCategoryStatistics::getCategory)
  925. .collect(Collectors.toSet());
  926. CWES_BY_CWE_TOP_25.get(cweReport.getCategory()).stream()
  927. .filter(rule -> !foundRules.contains(rule))
  928. .forEach(rule -> cweReport.getChildren().add(emptyCweStatistics(rule)));
  929. }
  930. return result;
  931. }
  932. private static SecurityStandardCategoryStatistics emptyCweStatistics(String rule) {
  933. return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null);
  934. }
  935. public List<SecurityStandardCategoryStatistics> getSonarSourceReport(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
  936. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  937. Arrays.stream(SQCategory.values())
  938. .forEach(sonarsourceCategory -> request.aggregation(
  939. newSecurityReportSubAggregations(
  940. AggregationBuilders.filter(sonarsourceCategory.getKey(), boolQuery().filter(termQuery(FIELD_ISSUE_SQ_SECURITY_CATEGORY, sonarsourceCategory.getKey()))),
  941. includeCwe,
  942. SecurityStandards.CWES_BY_SQ_CATEGORY.get(sonarsourceCategory))));
  943. return processSecurityReportSearchResults(request, includeCwe);
  944. }
  945. public List<SecurityStandardCategoryStatistics> getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
  946. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  947. IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i)
  948. .forEach(owaspCategory -> request.aggregation(
  949. newSecurityReportSubAggregations(
  950. AggregationBuilders.filter(owaspCategory, boolQuery().filter(termQuery(FIELD_ISSUE_OWASP_TOP_10, owaspCategory))),
  951. includeCwe,
  952. null)));
  953. return processSecurityReportSearchResults(request, includeCwe);
  954. }
  955. private List<SecurityStandardCategoryStatistics> processSecurityReportSearchResults(SearchSourceBuilder sourceBuilder, boolean includeCwe) {
  956. SearchRequest request = EsClient.prepareSearch(TYPE_ISSUE.getMainType())
  957. .source(sourceBuilder);
  958. SearchResponse response = client.search(request);
  959. return response.getAggregations().asList().stream()
  960. .map(c -> processSecurityReportIssueSearchResults((ParsedFilter) c, includeCwe))
  961. .collect(MoreCollectors.toList());
  962. }
  963. private static SecurityStandardCategoryStatistics processSecurityReportIssueSearchResults(ParsedFilter categoryBucket, boolean includeCwe) {
  964. List<SecurityStandardCategoryStatistics> children = new ArrayList<>();
  965. if (includeCwe) {
  966. Stream<? extends Terms.Bucket> stream = ((ParsedStringTerms) categoryBucket.getAggregations().get(AGG_CWES)).getBuckets().stream();
  967. children = stream.map(cweBucket -> processSecurityReportCategorySearchResults(cweBucket, cweBucket.getKeyAsString(), null)).collect(toList());
  968. }
  969. return processSecurityReportCategorySearchResults(categoryBucket, categoryBucket.getName(), children);
  970. }
  971. private static SecurityStandardCategoryStatistics processSecurityReportCategorySearchResults(HasAggregations categoryBucket, String categoryName,
  972. @Nullable List<SecurityStandardCategoryStatistics> children) {
  973. List<? extends Terms.Bucket> severityBuckets = ((ParsedStringTerms) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_VULNERABILITIES)).getAggregations()
  974. .get(AGG_SEVERITIES)).getBuckets();
  975. long vulnerabilities = severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum();
  976. // Worst severity having at least one issue
  977. OptionalInt severityRating = severityBuckets.stream()
  978. .filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0)
  979. .mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1)
  980. .max();
  981. long toReviewSecurityHotspots = ((ParsedValueCount) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_TO_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT))
  982. .getValue();
  983. long reviewedSecurityHotspots = ((ParsedValueCount) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_REVIEWED_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT))
  984. .getValue();
  985. Optional<Double> percent = computePercent(toReviewSecurityHotspots, reviewedSecurityHotspots);
  986. Integer securityReviewRating = computeRating(percent.orElse(null)).getIndex();
  987. return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots,
  988. reviewedSecurityHotspots, securityReviewRating, children);
  989. }
  990. private static AggregationBuilder newSecurityReportSubAggregations(AggregationBuilder categoriesAggs, boolean includeCwe, @Nullable Collection<String> cwesInCategory) {
  991. AggregationBuilder aggregationBuilder = addSecurityReportIssueCountAggregations(categoriesAggs);
  992. if (includeCwe) {
  993. final TermsAggregationBuilder cwesAgg = AggregationBuilders.terms(AGG_CWES)
  994. .field(FIELD_ISSUE_CWE)
  995. // 100 should be enough to display all CWEs. If not, the UI will be broken anyway
  996. .size(MAX_FACET_SIZE);
  997. if (cwesInCategory != null) {
  998. cwesAgg.includeExclude(new IncludeExclude(cwesInCategory.toArray(new String[0]), new String[0]));
  999. }
  1000. categoriesAggs.subAggregation(addSecurityReportIssueCountAggregations(cwesAgg));
  1001. }
  1002. return aggregationBuilder;
  1003. }
  1004. private static AggregationBuilder addSecurityReportIssueCountAggregations(AggregationBuilder categoryAggs) {
  1005. return categoryAggs
  1006. .subAggregation(
  1007. AggregationBuilders.filter(AGG_VULNERABILITIES, NON_RESOLVED_VULNERABILITIES_FILTER)
  1008. .subAggregation(
  1009. AggregationBuilders.terms(AGG_SEVERITIES).field(FIELD_ISSUE_SEVERITY)
  1010. .subAggregation(
  1011. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY))))
  1012. .subAggregation(AggregationBuilders.filter(AGG_TO_REVIEW_SECURITY_HOTSPOTS, TO_REVIEW_HOTSPOTS_FILTER)
  1013. .subAggregation(
  1014. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
  1015. .subAggregation(AggregationBuilders.filter(AGG_IN_REVIEW_SECURITY_HOTSPOTS, IN_REVIEW_HOTSPOTS_FILTER)
  1016. .subAggregation(
  1017. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
  1018. .subAggregation(AggregationBuilders.filter(AGG_REVIEWED_SECURITY_HOTSPOTS, REVIEWED_HOTSPOTS_FILTER)
  1019. .subAggregation(
  1020. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)));
  1021. }
  1022. private static SearchSourceBuilder prepareNonClosedVulnerabilitiesAndHotspotSearch(String projectUuid, boolean isViewOrApp) {
  1023. BoolQueryBuilder componentFilter = boolQuery();
  1024. if (isViewOrApp) {
  1025. IndexType.IndexMainType mainType = TYPE_VIEW;
  1026. componentFilter.filter(QueryBuilders.termsLookupQuery(FIELD_ISSUE_BRANCH_UUID,
  1027. new TermsLookup(
  1028. mainType.getIndex().getName(),
  1029. mainType.getType(),
  1030. projectUuid,
  1031. ViewIndexDefinition.FIELD_PROJECTS)));
  1032. } else {
  1033. componentFilter.filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectUuid));
  1034. }
  1035. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  1036. return sourceBuilder
  1037. .query(
  1038. componentFilter
  1039. .should(NON_RESOLVED_VULNERABILITIES_FILTER)
  1040. .should(TO_REVIEW_HOTSPOTS_FILTER)
  1041. .should(IN_REVIEW_HOTSPOTS_FILTER)
  1042. .should(REVIEWED_HOTSPOTS_FILTER)
  1043. .minimumShouldMatch(1))
  1044. .size(0);
  1045. }
  1046. }