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.

utils.ts 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. import { isArray } from 'lodash';
  21. import { searchUsers } from '../../api/users';
  22. import { formatMeasure } from '../../helpers/measures';
  23. import {
  24. cleanQuery,
  25. parseAsArray,
  26. parseAsBoolean,
  27. parseAsDate,
  28. parseAsString,
  29. queriesEqual,
  30. serializeDateShort,
  31. serializeString,
  32. serializeStringArray
  33. } from '../../helpers/query';
  34. import { scrollToElement } from '../../helpers/scrolling';
  35. import { get, save } from '../../helpers/storage';
  36. import { isDefined } from '../../helpers/types';
  37. import { Facet, RawFacet } from '../../types/issues';
  38. import { SecurityStandard, StandardType } from '../../types/security';
  39. export interface Query {
  40. assigned: boolean;
  41. assignees: string[];
  42. author: string[];
  43. createdAfter: Date | undefined;
  44. createdAt: string;
  45. createdBefore: Date | undefined;
  46. createdInLast: string;
  47. cwe: string[];
  48. directories: string[];
  49. files: string[];
  50. issues: string[];
  51. languages: string[];
  52. owaspTop10: string[];
  53. projects: string[];
  54. resolutions: string[];
  55. resolved: boolean;
  56. rules: string[];
  57. sansTop25: string[];
  58. scopes: string[];
  59. severities: string[];
  60. sinceLeakPeriod: boolean;
  61. sonarsourceSecurity: string[];
  62. sort: string;
  63. statuses: string[];
  64. tags: string[];
  65. types: string[];
  66. }
  67. export const STANDARDS = 'standards';
  68. export const STANDARD_TYPES: StandardType[] = [
  69. SecurityStandard.OWASP_TOP10,
  70. SecurityStandard.SANS_TOP25,
  71. SecurityStandard.CWE,
  72. SecurityStandard.SONARSOURCE
  73. ];
  74. // allow sorting by CREATION_DATE only
  75. const parseAsSort = (sort: string) => (sort === 'CREATION_DATE' ? 'CREATION_DATE' : '');
  76. const ISSUES_DEFAULT = 'sonarqube.issues.default';
  77. export function parseQuery(query: T.RawQuery): Query {
  78. return {
  79. assigned: parseAsBoolean(query.assigned),
  80. assignees: parseAsArray(query.assignees, parseAsString),
  81. author: isArray(query.author) ? query.author : [query.author].filter(isDefined),
  82. createdAfter: parseAsDate(query.createdAfter),
  83. createdAt: parseAsString(query.createdAt),
  84. createdBefore: parseAsDate(query.createdBefore),
  85. createdInLast: parseAsString(query.createdInLast),
  86. cwe: parseAsArray(query.cwe, parseAsString),
  87. directories: parseAsArray(query.directories, parseAsString),
  88. files: parseAsArray(query.files, parseAsString),
  89. issues: parseAsArray(query.issues, parseAsString),
  90. languages: parseAsArray(query.languages, parseAsString),
  91. owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
  92. projects: parseAsArray(query.projects, parseAsString),
  93. resolutions: parseAsArray(query.resolutions, parseAsString),
  94. resolved: parseAsBoolean(query.resolved),
  95. rules: parseAsArray(query.rules, parseAsString),
  96. sansTop25: parseAsArray(query.sansTop25, parseAsString),
  97. scopes: parseAsArray(query.scopes, parseAsString),
  98. severities: parseAsArray(query.severities, parseAsString),
  99. sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false),
  100. sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString),
  101. sort: parseAsSort(query.s),
  102. statuses: parseAsArray(query.statuses, parseAsString),
  103. tags: parseAsArray(query.tags, parseAsString),
  104. types: parseAsArray(query.types, parseAsString)
  105. };
  106. }
  107. export function getOpen(query: T.RawQuery): string | undefined {
  108. return query.open;
  109. }
  110. export const areMyIssuesSelected = (query: T.RawQuery) => query.myIssues === 'true';
  111. export function serializeQuery(query: Query): T.RawQuery {
  112. const filter = {
  113. assigned: query.assigned ? undefined : 'false',
  114. assignees: serializeStringArray(query.assignees),
  115. author: query.author,
  116. createdAfter: serializeDateShort(query.createdAfter),
  117. createdAt: serializeString(query.createdAt),
  118. createdBefore: serializeDateShort(query.createdBefore),
  119. createdInLast: serializeString(query.createdInLast),
  120. cwe: serializeStringArray(query.cwe),
  121. directories: serializeStringArray(query.directories),
  122. files: serializeStringArray(query.files),
  123. issues: serializeStringArray(query.issues),
  124. languages: serializeStringArray(query.languages),
  125. owaspTop10: serializeStringArray(query.owaspTop10),
  126. projects: serializeStringArray(query.projects),
  127. resolutions: serializeStringArray(query.resolutions),
  128. resolved: query.resolved ? undefined : 'false',
  129. rules: serializeStringArray(query.rules),
  130. s: serializeString(query.sort),
  131. sansTop25: serializeStringArray(query.sansTop25),
  132. scopes: serializeStringArray(query.scopes),
  133. severities: serializeStringArray(query.severities),
  134. sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined,
  135. sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),
  136. statuses: serializeStringArray(query.statuses),
  137. tags: serializeStringArray(query.tags),
  138. types: serializeStringArray(query.types)
  139. };
  140. return cleanQuery(filter);
  141. }
  142. export const areQueriesEqual = (a: T.RawQuery, b: T.RawQuery) =>
  143. queriesEqual(parseQuery(a), parseQuery(b));
  144. export function parseFacets(facets: RawFacet[]): T.Dict<Facet> {
  145. if (!facets) {
  146. return {};
  147. }
  148. const result: T.Dict<Facet> = {};
  149. facets.forEach(facet => {
  150. const values: Facet = {};
  151. facet.values.forEach(value => {
  152. values[value.val] = value.count;
  153. });
  154. result[facet.property] = values;
  155. });
  156. return result;
  157. }
  158. export function formatFacetStat(stat: number | undefined) {
  159. return stat && formatMeasure(stat, 'SHORT_INT');
  160. }
  161. export const searchAssignees = (
  162. query: string,
  163. page = 1
  164. ): Promise<{ paging: T.Paging; results: T.UserBase[] }> => {
  165. return searchUsers({ p: page, q: query }).then(({ paging, users }) => ({
  166. paging,
  167. results: users
  168. }));
  169. };
  170. const LOCALSTORAGE_MY = 'my';
  171. const LOCALSTORAGE_ALL = 'all';
  172. export const isMySet = () => {
  173. return get(ISSUES_DEFAULT) === LOCALSTORAGE_MY;
  174. };
  175. export const saveMyIssues = (myIssues: boolean) =>
  176. save(ISSUES_DEFAULT, myIssues ? LOCALSTORAGE_MY : LOCALSTORAGE_ALL);
  177. export function getLocations(
  178. { flows, secondaryLocations }: Pick<T.Issue, 'flows' | 'secondaryLocations'>,
  179. selectedFlowIndex: number | undefined
  180. ) {
  181. if (selectedFlowIndex !== undefined) {
  182. return flows[selectedFlowIndex] || [];
  183. } else {
  184. return flows.length > 0 ? flows[0] : secondaryLocations;
  185. }
  186. }
  187. export function getSelectedLocation(
  188. issue: Pick<T.Issue, 'flows' | 'secondaryLocations'>,
  189. selectedFlowIndex: number | undefined,
  190. selectedLocationIndex: number | undefined
  191. ) {
  192. const locations = getLocations(issue, selectedFlowIndex);
  193. if (
  194. selectedLocationIndex !== undefined &&
  195. selectedLocationIndex >= 0 &&
  196. locations.length >= selectedLocationIndex
  197. ) {
  198. return locations[selectedLocationIndex];
  199. } else {
  200. return undefined;
  201. }
  202. }
  203. export function allLocationsEmpty(
  204. issue: Pick<T.Issue, 'flows' | 'secondaryLocations'>,
  205. selectedFlowIndex: number | undefined
  206. ) {
  207. return getLocations(issue, selectedFlowIndex).every(location => !location.msg);
  208. }
  209. export function scrollToIssue(issue: string, smooth = true) {
  210. const element = document.querySelector(`[data-issue="${issue}"]`);
  211. if (element) {
  212. scrollToElement(element, { topOffset: 250, bottomOffset: 100, smooth });
  213. }
  214. }
  215. export function shouldOpenStandardsFacet(
  216. openFacets: T.Dict<boolean>,
  217. query: Partial<Query>
  218. ): boolean {
  219. return (
  220. openFacets[STANDARDS] ||
  221. isFilteredBySecurityIssueTypes(query) ||
  222. isOneStandardChildFacetOpen(openFacets, query)
  223. );
  224. }
  225. export function shouldOpenStandardsChildFacet(
  226. openFacets: T.Dict<boolean>,
  227. query: Partial<Query>,
  228. standardType: SecurityStandard
  229. ): boolean {
  230. const filter = query[standardType];
  231. return (
  232. openFacets[STANDARDS] !== false &&
  233. (openFacets[standardType] ||
  234. (standardType !== SecurityStandard.CWE && filter !== undefined && filter.length > 0))
  235. );
  236. }
  237. export function shouldOpenSonarSourceSecurityFacet(
  238. openFacets: T.Dict<boolean>,
  239. query: Partial<Query>
  240. ): boolean {
  241. // Open it by default if the parent is open, and no other standard is open.
  242. return (
  243. shouldOpenStandardsChildFacet(openFacets, query, SecurityStandard.SONARSOURCE) ||
  244. (shouldOpenStandardsFacet(openFacets, query) && !isOneStandardChildFacetOpen(openFacets, query))
  245. );
  246. }
  247. function isFilteredBySecurityIssueTypes(query: Partial<Query>): boolean {
  248. return query.types !== undefined && query.types.includes('VULNERABILITY');
  249. }
  250. function isOneStandardChildFacetOpen(openFacets: T.Dict<boolean>, query: Partial<Query>): boolean {
  251. return STANDARD_TYPES.some(standardType =>
  252. shouldOpenStandardsChildFacet(openFacets, query, standardType)
  253. );
  254. }