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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171
  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. esRequest.aggregation(aggregation);
  671. }
  672. private static void addAssigneesFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  673. if (!options.getFacets().contains(PARAM_ASSIGNEES)) {
  674. return;
  675. }
  676. Consumer<FilterAggregationBuilder> assigneeAggregations = t -> {
  677. // optional second aggregation to return the issue count for selected assignees (if any)
  678. Object[] assignees = query.assignees().toArray();
  679. aggregationHelper.getSubAggregationHelper().buildSelectedItemsAggregation(ASSIGNEES.getName(), ASSIGNEES.getTopAggregationDef(), assignees)
  680. .ifPresent(t::subAggregation);
  681. // third aggregation to always return the count of unassigned in the assignee facet
  682. t.subAggregation(addEffortAggregationIfNeeded(query, AggregationBuilders
  683. .missing(ASSIGNEES.getName() + FACET_SUFFIX_MISSING)
  684. .field(ASSIGNEES.getFieldName())));
  685. };
  686. AggregationBuilder aggregation = aggregationHelper.buildTermTopAggregation(
  687. ASSIGNEES.getName(), ASSIGNEES.getTopAggregationDef(), ASSIGNEES.getNumberOfTerms(),
  688. NO_EXTRA_FILTER, assigneeAggregations);
  689. esRequest.aggregation(aggregation);
  690. }
  691. private void addCreatedAtFacetIfNeeded(SearchOptions options, IssueQuery query, TopAggregationHelper aggregationHelper, AllFilters allFilters,
  692. SearchSourceBuilder esRequest) {
  693. if (options.getFacets().contains(PARAM_CREATED_AT)) {
  694. getCreatedAtFacet(query, aggregationHelper, allFilters).ifPresent(esRequest::aggregation);
  695. }
  696. }
  697. private Optional<AggregationBuilder> getCreatedAtFacet(IssueQuery query, TopAggregationHelper aggregationHelper, AllFilters allFilters) {
  698. long startTime;
  699. boolean startInclusive;
  700. PeriodStart createdAfter = query.createdAfter();
  701. if (createdAfter == null) {
  702. OptionalLong minDate = getMinCreatedAt(allFilters);
  703. if (!minDate.isPresent()) {
  704. return Optional.empty();
  705. }
  706. startTime = minDate.getAsLong();
  707. startInclusive = true;
  708. } else {
  709. startTime = createdAfter.date().getTime();
  710. startInclusive = createdAfter.inclusive();
  711. }
  712. Date createdBefore = query.createdBefore();
  713. long endTime = createdBefore == null ? system.now() : createdBefore.getTime();
  714. Duration timeSpan = new Duration(startTime, endTime);
  715. DateHistogramInterval bucketSize = computeDateHistogramBucketSize(timeSpan);
  716. FilterAggregationBuilder topAggregation = aggregationHelper.buildTopAggregation(
  717. CREATED_AT.getName(),
  718. CREATED_AT.getTopAggregationDef(),
  719. NO_EXTRA_FILTER,
  720. t -> {
  721. AggregationBuilder dateHistogram = AggregationBuilders.dateHistogram(CREATED_AT.getName())
  722. .field(CREATED_AT.getFieldName())
  723. .dateHistogramInterval(bucketSize)
  724. .minDocCount(0L)
  725. .format(DateUtils.DATETIME_FORMAT)
  726. .timeZone(Optional.ofNullable(query.timeZone()).orElse(system.getDefaultTimeZone().toZoneId()))
  727. // ES dateHistogram bounds are inclusive while createdBefore parameter is exclusive
  728. .extendedBounds(new LongBounds(startInclusive ? startTime : (startTime + 1), endTime - 1L));
  729. addEffortAggregationIfNeeded(query, dateHistogram);
  730. t.subAggregation(dateHistogram);
  731. });
  732. return Optional.of(topAggregation);
  733. }
  734. private static DateHistogramInterval computeDateHistogramBucketSize(Duration timeSpan) {
  735. if (timeSpan.isShorterThan(TWENTY_DAYS)) {
  736. return DateHistogramInterval.DAY;
  737. }
  738. if (timeSpan.isShorterThan(TWENTY_WEEKS)) {
  739. return DateHistogramInterval.WEEK;
  740. }
  741. if (timeSpan.isShorterThan(TWENTY_MONTHS)) {
  742. return DateHistogramInterval.MONTH;
  743. }
  744. return DateHistogramInterval.YEAR;
  745. }
  746. private OptionalLong getMinCreatedAt(AllFilters filters) {
  747. String facetNameAndField = CREATED_AT.getFieldName();
  748. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  749. .size(0);
  750. BoolQueryBuilder esFilter = boolQuery();
  751. filters.stream().filter(Objects::nonNull).forEach(esFilter::must);
  752. if (esFilter.hasClauses()) {
  753. sourceBuilder.query(QueryBuilders.boolQuery().filter(esFilter));
  754. }
  755. sourceBuilder.aggregation(AggregationBuilders.min(facetNameAndField).field(facetNameAndField));
  756. SearchRequest request = EsClient.prepareSearch(TYPE_ISSUE.getMainType())
  757. .source(sourceBuilder);
  758. Min minValue = client.search(request).getAggregations().get(facetNameAndField);
  759. double actualValue = minValue.getValue();
  760. if (Double.isInfinite(actualValue)) {
  761. return OptionalLong.empty();
  762. }
  763. return OptionalLong.of((long) actualValue);
  764. }
  765. private void addAssignedToMeFacetIfNeeded(SearchOptions options, TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  766. String uuid = userSession.getUuid();
  767. if (options.getFacets().contains(ASSIGNED_TO_ME.getName()) && !StringUtils.isEmpty(uuid)) {
  768. AggregationBuilder aggregation = aggregationHelper.buildTermTopAggregation(
  769. ASSIGNED_TO_ME.getName(),
  770. ASSIGNED_TO_ME.getTopAggregationDef(),
  771. ASSIGNED_TO_ME.getNumberOfTerms(),
  772. NO_EXTRA_FILTER,
  773. t ->
  774. // add sub-aggregation to return issue count for current user
  775. aggregationHelper.getSubAggregationHelper()
  776. .buildSelectedItemsAggregation(ASSIGNED_TO_ME.getName(), ASSIGNED_TO_ME.getTopAggregationDef(), new String[] {uuid})
  777. .ifPresent(t::subAggregation));
  778. esRequest.aggregation(aggregation);
  779. }
  780. }
  781. private static void addEffortTopAggregation(TopAggregationHelper aggregationHelper, SearchSourceBuilder esRequest) {
  782. AggregationBuilder topAggregation = aggregationHelper.buildTopAggregation(
  783. FACET_MODE_EFFORT,
  784. EFFORT_TOP_AGGREGATION,
  785. NO_EXTRA_FILTER,
  786. t -> t.subAggregation(EFFORT_AGGREGATION));
  787. esRequest.aggregation(topAggregation);
  788. }
  789. public List<String> searchTags(IssueQuery query, @Nullable String textQuery, int size) {
  790. Terms terms = listTermsMatching(FIELD_ISSUE_TAGS, query, textQuery, BucketOrder.key(true), size);
  791. return EsUtils.termsKeys(terms);
  792. }
  793. public Map<String, Long> countTags(IssueQuery query, int maxNumberOfTags) {
  794. Terms terms = listTermsMatching(FIELD_ISSUE_TAGS, query, null, BucketOrder.count(false), maxNumberOfTags);
  795. return EsUtils.termsToMap(terms);
  796. }
  797. public List<String> searchAuthors(IssueQuery query, @Nullable String textQuery, int maxNumberOfAuthors) {
  798. Terms terms = listTermsMatching(FIELD_ISSUE_AUTHOR_LOGIN, query, textQuery, BucketOrder.key(true), maxNumberOfAuthors);
  799. return EsUtils.termsKeys(terms);
  800. }
  801. private Terms listTermsMatching(String fieldName, IssueQuery query, @Nullable String textQuery, BucketOrder termsOrder, int size) {
  802. SearchRequest requestBuilder = EsClient.prepareSearch(TYPE_ISSUE.getMainType());
  803. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  804. // Avoids returning search hits
  805. .size(0);
  806. requestBuilder.source(sourceBuilder);
  807. sourceBuilder.query(boolQuery().must(QueryBuilders.matchAllQuery()).filter(createBoolFilter(query)));
  808. TermsAggregationBuilder aggreg = AggregationBuilders.terms("_ref")
  809. .field(fieldName)
  810. .size(size)
  811. .order(termsOrder)
  812. .minDocCount(1L);
  813. if (textQuery != null) {
  814. aggreg.includeExclude(new IncludeExclude(format(SUBSTRING_MATCH_REGEXP, escapeSpecialRegexChars(textQuery)), null));
  815. }
  816. sourceBuilder.aggregation(aggreg);
  817. SearchResponse searchResponse = client.search(requestBuilder);
  818. return searchResponse.getAggregations().get("_ref");
  819. }
  820. private BoolQueryBuilder createBoolFilter(IssueQuery query) {
  821. BoolQueryBuilder boolQuery = boolQuery();
  822. createAllFilters(query).stream()
  823. .filter(Objects::nonNull)
  824. .forEach(boolQuery::must);
  825. return boolQuery;
  826. }
  827. public List<ProjectStatistics> searchProjectStatistics(List<String> projectUuids, List<Long> froms, @Nullable String assigneeUuid) {
  828. checkState(projectUuids.size() == froms.size(),
  829. "Expected same size for projectUuids (had size %s) and froms (had size %s)", projectUuids.size(), froms.size());
  830. if (projectUuids.isEmpty()) {
  831. return Collections.emptyList();
  832. }
  833. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  834. .query(
  835. boolQuery()
  836. .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION))
  837. .filter(termQuery(FIELD_ISSUE_ASSIGNEE_UUID, assigneeUuid))
  838. .mustNot(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())))
  839. .size(0);
  840. IntStream.range(0, projectUuids.size()).forEach(i -> {
  841. String projectUuid = projectUuids.get(i);
  842. long from = froms.get(i);
  843. sourceBuilder
  844. .aggregation(AggregationBuilders
  845. .filter(projectUuid, boolQuery()
  846. .filter(termQuery(FIELD_ISSUE_PROJECT_UUID, projectUuid))
  847. .filter(rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT).gte(epochMillisToEpochSeconds(from))))
  848. .subAggregation(
  849. AggregationBuilders.terms("branchUuid").field(FIELD_ISSUE_BRANCH_UUID)
  850. .subAggregation(
  851. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY))
  852. .subAggregation(
  853. AggregationBuilders.max("maxFuncCreatedAt").field(FIELD_ISSUE_FUNC_CREATED_AT))));
  854. });
  855. SearchRequest requestBuilder = EsClient.prepareSearch(TYPE_ISSUE.getMainType());
  856. requestBuilder.source(sourceBuilder);
  857. SearchResponse response = client.search(requestBuilder);
  858. return response.getAggregations().asList().stream()
  859. .map(x -> (ParsedFilter) x)
  860. .flatMap(projectBucket -> ((ParsedStringTerms) projectBucket.getAggregations().get("branchUuid")).getBuckets().stream()
  861. .flatMap(branchBucket -> {
  862. long count = ((ParsedValueCount) branchBucket.getAggregations().get(AGG_COUNT)).getValue();
  863. if (count < 1L) {
  864. return Stream.empty();
  865. }
  866. long lastIssueDate = (long) ((ParsedMax) branchBucket.getAggregations().get("maxFuncCreatedAt")).getValue();
  867. return Stream.of(new ProjectStatistics(branchBucket.getKeyAsString(), count, lastIssueDate));
  868. }))
  869. .collect(MoreCollectors.toList(projectUuids.size()));
  870. }
  871. public List<PrStatistics> searchBranchStatistics(String projectUuid, List<String> branchUuids) {
  872. if (branchUuids.isEmpty()) {
  873. return Collections.emptyList();
  874. }
  875. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
  876. .query(
  877. boolQuery()
  878. .must(termsQuery(FIELD_ISSUE_BRANCH_UUID, branchUuids))
  879. .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION))
  880. .must(termQuery(FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(false))))
  881. .size(0)
  882. .aggregation(AggregationBuilders.terms("branchUuids")
  883. .field(FIELD_ISSUE_BRANCH_UUID)
  884. .size(branchUuids.size())
  885. .subAggregation(AggregationBuilders.terms("types")
  886. .field(FIELD_ISSUE_TYPE)));
  887. SearchRequest requestBuilder = EsClient.prepareSearch(TYPE_ISSUE.getMainType())
  888. .routing(AuthorizationDoc.idOf(projectUuid));
  889. requestBuilder.source(sourceBuilder);
  890. SearchResponse response = client.search(requestBuilder);
  891. return ((ParsedStringTerms) response.getAggregations().get("branchUuids")).getBuckets().stream()
  892. .map(bucket -> new PrStatistics(bucket.getKeyAsString(),
  893. ((ParsedStringTerms) bucket.getAggregations().get("types")).getBuckets()
  894. .stream()
  895. .collect(uniqueIndex(MultiBucketsAggregation.Bucket::getKeyAsString, MultiBucketsAggregation.Bucket::getDocCount))))
  896. .collect(MoreCollectors.toList(branchUuids.size()));
  897. }
  898. /**
  899. * @deprecated SansTop25 report is outdated and will be removed in future versions
  900. */
  901. @Deprecated
  902. public List<SecurityStandardCategoryStatistics> getSansTop25Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
  903. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  904. Stream.of(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)
  905. .forEach(sansCategory -> request.aggregation(newSecurityReportSubAggregations(
  906. AggregationBuilders.filter(sansCategory, boolQuery().filter(termQuery(FIELD_ISSUE_SANS_TOP_25, sansCategory))),
  907. includeCwe,
  908. SecurityStandards.CWES_BY_SANS_TOP_25.get(sansCategory))));
  909. return processSecurityReportSearchResults(request, includeCwe);
  910. }
  911. public List<SecurityStandardCategoryStatistics> getCweTop25Reports(String projectUuid, boolean isViewOrApp) {
  912. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  913. CWES_BY_CWE_TOP_25.keySet()
  914. .forEach(cweYear -> request.aggregation(
  915. newSecurityReportSubAggregations(
  916. AggregationBuilders.filter(cweYear, boolQuery().filter(existsQuery(FIELD_ISSUE_CWE))),
  917. true,
  918. CWES_BY_CWE_TOP_25.get(cweYear))));
  919. List<SecurityStandardCategoryStatistics> result = processSecurityReportSearchResults(request, true);
  920. for (SecurityStandardCategoryStatistics cweReport : result) {
  921. Set<String> foundRules = cweReport.getChildren().stream()
  922. .map(SecurityStandardCategoryStatistics::getCategory)
  923. .collect(Collectors.toSet());
  924. CWES_BY_CWE_TOP_25.get(cweReport.getCategory()).stream()
  925. .filter(rule -> !foundRules.contains(rule))
  926. .forEach(rule -> cweReport.getChildren().add(emptyCweStatistics(rule)));
  927. }
  928. return result;
  929. }
  930. private static SecurityStandardCategoryStatistics emptyCweStatistics(String rule) {
  931. return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null);
  932. }
  933. public List<SecurityStandardCategoryStatistics> getSonarSourceReport(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
  934. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  935. Arrays.stream(SQCategory.values())
  936. .forEach(sonarsourceCategory -> request.aggregation(
  937. newSecurityReportSubAggregations(
  938. AggregationBuilders.filter(sonarsourceCategory.getKey(), boolQuery().filter(termQuery(FIELD_ISSUE_SQ_SECURITY_CATEGORY, sonarsourceCategory.getKey()))),
  939. includeCwe,
  940. SecurityStandards.CWES_BY_SQ_CATEGORY.get(sonarsourceCategory))));
  941. return processSecurityReportSearchResults(request, includeCwe);
  942. }
  943. public List<SecurityStandardCategoryStatistics> getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
  944. SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
  945. IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i)
  946. .forEach(owaspCategory -> request.aggregation(
  947. newSecurityReportSubAggregations(
  948. AggregationBuilders.filter(owaspCategory, boolQuery().filter(termQuery(FIELD_ISSUE_OWASP_TOP_10, owaspCategory))),
  949. includeCwe,
  950. null)));
  951. return processSecurityReportSearchResults(request, includeCwe);
  952. }
  953. private List<SecurityStandardCategoryStatistics> processSecurityReportSearchResults(SearchSourceBuilder sourceBuilder, boolean includeCwe) {
  954. SearchRequest request = EsClient.prepareSearch(TYPE_ISSUE.getMainType())
  955. .source(sourceBuilder);
  956. SearchResponse response = client.search(request);
  957. return response.getAggregations().asList().stream()
  958. .map(c -> processSecurityReportIssueSearchResults((ParsedFilter) c, includeCwe))
  959. .collect(MoreCollectors.toList());
  960. }
  961. private static SecurityStandardCategoryStatistics processSecurityReportIssueSearchResults(ParsedFilter categoryBucket, boolean includeCwe) {
  962. List<SecurityStandardCategoryStatistics> children = new ArrayList<>();
  963. if (includeCwe) {
  964. Stream<? extends Terms.Bucket> stream = ((ParsedStringTerms) categoryBucket.getAggregations().get(AGG_CWES)).getBuckets().stream();
  965. children = stream.map(cweBucket -> processSecurityReportCategorySearchResults(cweBucket, cweBucket.getKeyAsString(), null)).collect(toList());
  966. }
  967. return processSecurityReportCategorySearchResults(categoryBucket, categoryBucket.getName(), children);
  968. }
  969. private static SecurityStandardCategoryStatistics processSecurityReportCategorySearchResults(HasAggregations categoryBucket, String categoryName,
  970. @Nullable List<SecurityStandardCategoryStatistics> children) {
  971. List<? extends Terms.Bucket> severityBuckets = ((ParsedStringTerms) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_VULNERABILITIES)).getAggregations()
  972. .get(AGG_SEVERITIES)).getBuckets();
  973. long vulnerabilities = severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum();
  974. // Worst severity having at least one issue
  975. OptionalInt severityRating = severityBuckets.stream()
  976. .filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0)
  977. .mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1)
  978. .max();
  979. long toReviewSecurityHotspots = ((ParsedValueCount) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_TO_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT))
  980. .getValue();
  981. long reviewedSecurityHotspots = ((ParsedValueCount) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_REVIEWED_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT))
  982. .getValue();
  983. Optional<Double> percent = computePercent(toReviewSecurityHotspots, reviewedSecurityHotspots);
  984. Integer securityReviewRating = computeRating(percent.orElse(null)).getIndex();
  985. return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots,
  986. reviewedSecurityHotspots, securityReviewRating, children);
  987. }
  988. private static AggregationBuilder newSecurityReportSubAggregations(AggregationBuilder categoriesAggs, boolean includeCwe, @Nullable Collection<String> cwesInCategory) {
  989. AggregationBuilder aggregationBuilder = addSecurityReportIssueCountAggregations(categoriesAggs);
  990. if (includeCwe) {
  991. final TermsAggregationBuilder cwesAgg = AggregationBuilders.terms(AGG_CWES)
  992. .field(FIELD_ISSUE_CWE)
  993. // 100 should be enough to display all CWEs. If not, the UI will be broken anyway
  994. .size(MAX_FACET_SIZE);
  995. if (cwesInCategory != null) {
  996. cwesAgg.includeExclude(new IncludeExclude(cwesInCategory.toArray(new String[0]), new String[0]));
  997. }
  998. categoriesAggs.subAggregation(addSecurityReportIssueCountAggregations(cwesAgg));
  999. }
  1000. return aggregationBuilder;
  1001. }
  1002. private static AggregationBuilder addSecurityReportIssueCountAggregations(AggregationBuilder categoryAggs) {
  1003. return categoryAggs
  1004. .subAggregation(
  1005. AggregationBuilders.filter(AGG_VULNERABILITIES, NON_RESOLVED_VULNERABILITIES_FILTER)
  1006. .subAggregation(
  1007. AggregationBuilders.terms(AGG_SEVERITIES).field(FIELD_ISSUE_SEVERITY)
  1008. .subAggregation(
  1009. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY))))
  1010. .subAggregation(AggregationBuilders.filter(AGG_TO_REVIEW_SECURITY_HOTSPOTS, TO_REVIEW_HOTSPOTS_FILTER)
  1011. .subAggregation(
  1012. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
  1013. .subAggregation(AggregationBuilders.filter(AGG_IN_REVIEW_SECURITY_HOTSPOTS, IN_REVIEW_HOTSPOTS_FILTER)
  1014. .subAggregation(
  1015. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
  1016. .subAggregation(AggregationBuilders.filter(AGG_REVIEWED_SECURITY_HOTSPOTS, REVIEWED_HOTSPOTS_FILTER)
  1017. .subAggregation(
  1018. AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)));
  1019. }
  1020. private static SearchSourceBuilder prepareNonClosedVulnerabilitiesAndHotspotSearch(String projectUuid, boolean isViewOrApp) {
  1021. BoolQueryBuilder componentFilter = boolQuery();
  1022. if (isViewOrApp) {
  1023. IndexType.IndexMainType mainType = TYPE_VIEW;
  1024. componentFilter.filter(QueryBuilders.termsLookupQuery(FIELD_ISSUE_BRANCH_UUID,
  1025. new TermsLookup(
  1026. mainType.getIndex().getName(),
  1027. mainType.getType(),
  1028. projectUuid,
  1029. ViewIndexDefinition.FIELD_PROJECTS)));
  1030. } else {
  1031. componentFilter.filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectUuid));
  1032. }
  1033. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  1034. return sourceBuilder
  1035. .query(
  1036. componentFilter
  1037. .should(NON_RESOLVED_VULNERABILITIES_FILTER)
  1038. .should(TO_REVIEW_HOTSPOTS_FILTER)
  1039. .should(IN_REVIEW_HOTSPOTS_FILTER)
  1040. .should(REVIEWED_HOTSPOTS_FILTER)
  1041. .minimumShouldMatch(1))
  1042. .size(0);
  1043. }
  1044. }