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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { intersection, isArray, uniq } from 'lodash';
  21. import { getUsers } from '../../api/users';
  22. import { DEFAULT_ISSUES_QUERY } from '../../components/shared/utils';
  23. import { formatMeasure } from '../../helpers/measures';
  24. import {
  25. cleanQuery,
  26. parseAsArray,
  27. parseAsBoolean,
  28. parseAsDate,
  29. parseAsString,
  30. queriesEqual,
  31. serializeDateShort,
  32. serializeString,
  33. serializeStringArray,
  34. } from '../../helpers/query';
  35. import { get, save } from '../../helpers/storage';
  36. import { isDefined } from '../../helpers/types';
  37. import {
  38. CleanCodeAttributeCategory,
  39. SoftwareImpactSeverity,
  40. SoftwareQuality,
  41. } from '../../types/clean-code-taxonomy';
  42. import {
  43. Facet,
  44. IssueDeprecatedStatus,
  45. IssueResolution,
  46. IssueStatus,
  47. RawFacet,
  48. } from '../../types/issues';
  49. import { MetricType } from '../../types/metrics';
  50. import { SecurityStandard } from '../../types/security';
  51. import { Dict, Issue, Paging, RawQuery } from '../../types/types';
  52. import { RestUser } from '../../types/users';
  53. const OWASP_ASVS_4_0 = 'owaspAsvs-4.0';
  54. export interface Query {
  55. assigned: boolean;
  56. assignees: string[];
  57. author: string[];
  58. cleanCodeAttributeCategories: CleanCodeAttributeCategory[];
  59. codeVariants: string[];
  60. createdAfter: Date | undefined;
  61. createdAt: string;
  62. createdBefore: Date | undefined;
  63. createdInLast: string;
  64. cwe: string[];
  65. directories: string[];
  66. files: string[];
  67. impactSeverities: SoftwareImpactSeverity[];
  68. impactSoftwareQualities: SoftwareQuality[];
  69. issues: string[];
  70. languages: string[];
  71. owaspTop10: string[];
  72. 'owaspTop10-2021': string[];
  73. 'pciDss-3.2': string[];
  74. 'pciDss-4.0': string[];
  75. [OWASP_ASVS_4_0]: string[];
  76. owaspAsvsLevel: string;
  77. projects: string[];
  78. rules: string[];
  79. scopes: string[];
  80. severities: string[];
  81. inNewCodePeriod: boolean;
  82. sonarsourceSecurity: string[];
  83. sort: string;
  84. issueStatuses: IssueStatus[];
  85. tags: string[];
  86. types: string[];
  87. }
  88. export const STANDARDS = 'standards';
  89. // allow sorting by CREATION_DATE only
  90. const parseAsSort = (sort: string) => (sort === 'CREATION_DATE' ? 'CREATION_DATE' : '');
  91. const ISSUES_DEFAULT = 'sonarqube.issues.default';
  92. export function parseQuery(query: RawQuery): Query {
  93. return {
  94. assigned: parseAsBoolean(query.assigned),
  95. assignees: parseAsArray(query.assignees, parseAsString),
  96. author: isArray(query.author) ? query.author : [query.author].filter(isDefined),
  97. cleanCodeAttributeCategories: parseAsArray<CleanCodeAttributeCategory>(
  98. query.cleanCodeAttributeCategories,
  99. parseAsString,
  100. ),
  101. createdAfter: parseAsDate(query.createdAfter),
  102. createdAt: parseAsString(query.createdAt),
  103. createdBefore: parseAsDate(query.createdBefore),
  104. createdInLast: parseAsString(query.createdInLast),
  105. cwe: parseAsArray(query.cwe, parseAsString),
  106. directories: parseAsArray(query.directories, parseAsString),
  107. files: parseAsArray(query.files, parseAsString),
  108. impactSeverities: parseAsArray<SoftwareImpactSeverity>(query.impactSeverities, parseAsString),
  109. impactSoftwareQualities: parseAsArray<SoftwareQuality>(
  110. query.impactSoftwareQualities,
  111. parseAsString,
  112. ),
  113. inNewCodePeriod: parseAsBoolean(query.inNewCodePeriod, false),
  114. issues: parseAsArray(query.issues, parseAsString),
  115. languages: parseAsArray(query.languages, parseAsString),
  116. owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
  117. 'owaspTop10-2021': parseAsArray(query['owaspTop10-2021'], parseAsString),
  118. 'pciDss-3.2': parseAsArray(query['pciDss-3.2'], parseAsString),
  119. 'pciDss-4.0': parseAsArray(query['pciDss-4.0'], parseAsString),
  120. [OWASP_ASVS_4_0]: parseAsArray(query[OWASP_ASVS_4_0], parseAsString),
  121. owaspAsvsLevel: parseAsString(query['owaspAsvsLevel']),
  122. projects: parseAsArray(query.projects, parseAsString),
  123. rules: parseAsArray(query.rules, parseAsString),
  124. scopes: parseAsArray(query.scopes, parseAsString),
  125. severities: parseAsArray(query.severities, parseAsString),
  126. sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString),
  127. sort: parseAsSort(query.s),
  128. issueStatuses: parseIssueStatuses(query),
  129. tags: parseAsArray(query.tags, parseAsString),
  130. types: parseAsArray(query.types, parseAsString),
  131. codeVariants: parseAsArray(query.codeVariants, parseAsString),
  132. };
  133. }
  134. function parseIssueStatuses(query: RawQuery) {
  135. let result: Array<IssueStatus> = [];
  136. if (query.issueStatuses) {
  137. return parseAsArray<IssueStatus>(query.issueStatuses, parseAsString);
  138. }
  139. const deprecatedStatusesMap = {
  140. [IssueDeprecatedStatus.Open]: [IssueStatus.Open],
  141. [IssueDeprecatedStatus.Confirmed]: [IssueStatus.Confirmed],
  142. [IssueDeprecatedStatus.Reopened]: [IssueStatus.Open],
  143. [IssueDeprecatedStatus.Resolved]: [
  144. IssueStatus.Fixed,
  145. IssueStatus.Accepted,
  146. IssueStatus.FalsePositive,
  147. ],
  148. [IssueDeprecatedStatus.Closed]: [IssueStatus.Fixed],
  149. };
  150. const deprecatedResolutionsMap = {
  151. [IssueResolution.FalsePositive]: [IssueStatus.FalsePositive],
  152. [IssueResolution.WontFix]: [IssueStatus.Accepted],
  153. [IssueResolution.Fixed]: [IssueStatus.Fixed],
  154. [IssueResolution.Removed]: [IssueStatus.Fixed],
  155. [IssueResolution.Unresolved]: [IssueStatus.Open, IssueStatus.Confirmed],
  156. };
  157. const issuesStatusesFromDeprecatedStatuses = parseAsArray<IssueDeprecatedStatus>(
  158. query.statuses,
  159. parseAsString,
  160. )
  161. .map((status) => deprecatedStatusesMap[status])
  162. .filter(Boolean)
  163. .flat();
  164. const issueStatusesFromResolutions = parseAsArray<IssueResolution>(
  165. query.resolutions,
  166. parseAsString,
  167. )
  168. .map((status) => deprecatedResolutionsMap[status])
  169. .filter(Boolean)
  170. .flat();
  171. const intesectedIssueStatuses = intersection(
  172. issuesStatusesFromDeprecatedStatuses,
  173. issueStatusesFromResolutions,
  174. );
  175. result = intesectedIssueStatuses.length
  176. ? intesectedIssueStatuses
  177. : issueStatusesFromResolutions.concat(issuesStatusesFromDeprecatedStatuses);
  178. if (
  179. query.resolved === 'false' &&
  180. [IssueStatus.Open, IssueStatus.Confirmed].every((status) => !result.includes(status))
  181. ) {
  182. result = result.concat(
  183. parseAsArray<IssueStatus>(DEFAULT_ISSUES_QUERY.issueStatuses, parseAsString),
  184. );
  185. }
  186. return uniq(result);
  187. }
  188. export function getOpen(query: RawQuery): string | undefined {
  189. return query.open;
  190. }
  191. export function getOpenIssue(props: { location: { query: RawQuery } }, issues: Issue[]) {
  192. const open = getOpen(props.location.query);
  193. return open ? issues.find((issue) => issue.key === open) : undefined;
  194. }
  195. export const areMyIssuesSelected = (query: RawQuery) => query.myIssues === 'true';
  196. export function serializeQuery(query: Query): RawQuery {
  197. const filter = {
  198. assigned: query.assigned ? undefined : 'false',
  199. assignees: serializeStringArray(query.assignees),
  200. author: query.author,
  201. cleanCodeAttributeCategories: serializeStringArray(query.cleanCodeAttributeCategories),
  202. createdAfter: serializeDateShort(query.createdAfter),
  203. createdAt: serializeString(query.createdAt),
  204. createdBefore: serializeDateShort(query.createdBefore),
  205. createdInLast: serializeString(query.createdInLast),
  206. cwe: serializeStringArray(query.cwe),
  207. directories: serializeStringArray(query.directories),
  208. files: serializeStringArray(query.files),
  209. issues: serializeStringArray(query.issues),
  210. languages: serializeStringArray(query.languages),
  211. owaspTop10: serializeStringArray(query.owaspTop10),
  212. 'owaspTop10-2021': serializeStringArray(query['owaspTop10-2021']),
  213. 'pciDss-3.2': serializeStringArray(query['pciDss-3.2']),
  214. 'pciDss-4.0': serializeStringArray(query['pciDss-4.0']),
  215. [OWASP_ASVS_4_0]: serializeStringArray(query[OWASP_ASVS_4_0]),
  216. owaspAsvsLevel: serializeString(query['owaspAsvsLevel']),
  217. projects: serializeStringArray(query.projects),
  218. rules: serializeStringArray(query.rules),
  219. s: serializeString(query.sort),
  220. scopes: serializeStringArray(query.scopes),
  221. severities: serializeStringArray(query.severities),
  222. impactSeverities: serializeStringArray(query.impactSeverities),
  223. impactSoftwareQualities: serializeStringArray(query.impactSoftwareQualities),
  224. inNewCodePeriod: query.inNewCodePeriod ? 'true' : undefined,
  225. sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),
  226. issueStatuses: serializeStringArray(query.issueStatuses),
  227. tags: serializeStringArray(query.tags),
  228. types: serializeStringArray(query.types),
  229. codeVariants: serializeStringArray(query.codeVariants),
  230. };
  231. return cleanQuery(filter);
  232. }
  233. export const areQueriesEqual = (a: RawQuery, b: RawQuery) =>
  234. queriesEqual(parseQuery(a), parseQuery(b));
  235. export function parseFacets(facets?: RawFacet[]): Dict<Facet> {
  236. if (!facets) {
  237. return {};
  238. }
  239. const result: Dict<Facet> = {};
  240. facets.forEach((facet) => {
  241. const values: Facet = {};
  242. facet.values.forEach((value) => {
  243. values[value.val] = value.count;
  244. });
  245. result[facet.property] = values;
  246. });
  247. return result;
  248. }
  249. export function formatFacetStat(stat: number | undefined) {
  250. return stat && formatMeasure(stat, MetricType.ShortInteger);
  251. }
  252. export const searchAssignees = (
  253. query: string,
  254. page = 1,
  255. ): Promise<{ paging: Paging; results: RestUser[] }> => {
  256. return getUsers<RestUser>({ pageIndex: page, q: query }).then(({ page, users }) => ({
  257. paging: page,
  258. results: users,
  259. }));
  260. };
  261. const LOCALSTORAGE_MY = 'my';
  262. const LOCALSTORAGE_ALL = 'all';
  263. export const isMySet = () => {
  264. return get(ISSUES_DEFAULT) === LOCALSTORAGE_MY;
  265. };
  266. export const saveMyIssues = (myIssues: boolean) =>
  267. save(ISSUES_DEFAULT, myIssues ? LOCALSTORAGE_MY : LOCALSTORAGE_ALL);
  268. export function getLocations(
  269. {
  270. flows,
  271. secondaryLocations,
  272. flowsWithType,
  273. }: Pick<Issue, 'flows' | 'secondaryLocations' | 'flowsWithType'>,
  274. selectedFlowIndex: number | undefined,
  275. ) {
  276. if (secondaryLocations.length > 0) {
  277. return secondaryLocations;
  278. } else if (selectedFlowIndex !== undefined) {
  279. return flows[selectedFlowIndex] || flowsWithType[selectedFlowIndex]?.locations || [];
  280. }
  281. return [];
  282. }
  283. export function getSelectedLocation(
  284. issue: Pick<Issue, 'flows' | 'secondaryLocations' | 'flowsWithType'>,
  285. selectedFlowIndex: number | undefined,
  286. selectedLocationIndex: number | undefined,
  287. ) {
  288. const locations = getLocations(issue, selectedFlowIndex);
  289. if (
  290. selectedLocationIndex !== undefined &&
  291. selectedLocationIndex >= 0 &&
  292. locations.length >= selectedLocationIndex
  293. ) {
  294. return locations[selectedLocationIndex];
  295. }
  296. return undefined;
  297. }
  298. export function allLocationsEmpty(
  299. issue: Pick<Issue, 'flows' | 'secondaryLocations' | 'flowsWithType'>,
  300. selectedFlowIndex: number | undefined,
  301. ) {
  302. return getLocations(issue, selectedFlowIndex).every((location) => !location.msg);
  303. }
  304. export function shouldOpenStandardsFacet(
  305. openFacets: Dict<boolean>,
  306. query: Partial<Query>,
  307. ): boolean {
  308. return (
  309. openFacets[STANDARDS] ||
  310. isFilteredBySecurityIssueTypes(query) ||
  311. isOneStandardChildFacetOpen(openFacets, query)
  312. );
  313. }
  314. export function shouldOpenStandardsChildFacet(
  315. openFacets: Dict<boolean>,
  316. query: Partial<Query>,
  317. standardType:
  318. | SecurityStandard.CWE
  319. | SecurityStandard.OWASP_TOP10
  320. | SecurityStandard.OWASP_TOP10_2021
  321. | SecurityStandard.SONARSOURCE,
  322. ): boolean {
  323. const filter = query[standardType];
  324. return (
  325. openFacets[STANDARDS] !== false &&
  326. (openFacets[standardType] ||
  327. (standardType !== SecurityStandard.CWE && filter !== undefined && filter.length > 0))
  328. );
  329. }
  330. export function shouldOpenSonarSourceSecurityFacet(
  331. openFacets: Dict<boolean>,
  332. query: Partial<Query>,
  333. ): boolean {
  334. // Open it by default if the parent is open, and no other standard is open.
  335. return (
  336. shouldOpenStandardsChildFacet(openFacets, query, SecurityStandard.SONARSOURCE) ||
  337. (shouldOpenStandardsFacet(openFacets, query) && !isOneStandardChildFacetOpen(openFacets, query))
  338. );
  339. }
  340. function isFilteredBySecurityIssueTypes(query: Partial<Query>): boolean {
  341. return query.types !== undefined && query.types.includes('VULNERABILITY');
  342. }
  343. function isOneStandardChildFacetOpen(openFacets: Dict<boolean>, query: Partial<Query>): boolean {
  344. return [SecurityStandard.OWASP_TOP10, SecurityStandard.CWE, SecurityStandard.SONARSOURCE].some(
  345. (
  346. standardType:
  347. | SecurityStandard.CWE
  348. | SecurityStandard.OWASP_TOP10
  349. | SecurityStandard.OWASP_TOP10_2021
  350. | SecurityStandard.SONARSOURCE,
  351. ) => shouldOpenStandardsChildFacet(openFacets, query, standardType),
  352. );
  353. }