@cccsaurora/howler-ui 2.18.0-dev.736 → 2.18.0-dev.739

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/api/index.d.ts +0 -2
  2. package/api/index.js +2 -4
  3. package/api/search/facet/hit.d.ts +3 -1
  4. package/api/search/facet/index.d.ts +1 -3
  5. package/api/search/index.d.ts +1 -2
  6. package/api/search/index.js +1 -2
  7. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  8. package/components/app/App.js +7 -39
  9. package/components/app/hooks/useMatchers.d.ts +1 -1
  10. package/components/app/hooks/useMatchers.js +11 -23
  11. package/components/app/hooks/useMatchers.test.js +22 -22
  12. package/components/app/hooks/useTitle.js +3 -3
  13. package/components/app/providers/FavouritesProvider.js +2 -2
  14. package/components/app/providers/HitProvider.d.ts +22 -0
  15. package/components/app/providers/{RecordProvider.js → HitProvider.js} +41 -41
  16. package/components/app/providers/{RecordSearchProvider.d.ts → HitSearchProvider.d.ts} +6 -6
  17. package/components/app/providers/{RecordSearchProvider.js → HitSearchProvider.js} +17 -12
  18. package/components/app/providers/{RecordSearchProvider.test.js → HitSearchProvider.test.js} +70 -51
  19. package/components/app/providers/ModalProvider.d.ts +0 -1
  20. package/components/app/providers/ParameterProvider.d.ts +2 -9
  21. package/components/app/providers/ParameterProvider.js +240 -165
  22. package/components/app/providers/ParameterProvider.test.js +94 -346
  23. package/components/app/providers/UserListProvider.js +8 -28
  24. package/components/elements/PluginTypography.d.ts +1 -2
  25. package/components/elements/PluginTypography.js +2 -3
  26. package/components/elements/UserList.d.ts +2 -5
  27. package/components/elements/UserList.js +8 -18
  28. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  29. package/components/elements/display/ChipPopper.d.ts +1 -1
  30. package/components/elements/display/HowlerCard.js +1 -1
  31. package/components/elements/display/Modal.js +0 -2
  32. package/components/elements/display/icons/BundleButton.d.ts +6 -0
  33. package/components/elements/display/icons/BundleButton.js +32 -0
  34. package/components/elements/hit/HitActions.js +4 -4
  35. package/components/elements/hit/HitBanner.d.ts +0 -1
  36. package/components/elements/hit/HitBanner.js +49 -29
  37. package/components/elements/hit/HitCard.d.ts +0 -2
  38. package/components/elements/hit/HitCard.js +7 -7
  39. package/components/elements/{record/RecordComments.d.ts → hit/HitComments.d.ts} +4 -5
  40. package/components/elements/{record/RecordComments.js → hit/HitComments.js} +28 -29
  41. package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
  42. package/components/elements/hit/HitLabels.js +2 -2
  43. package/components/elements/hit/HitOutline.d.ts +0 -1
  44. package/components/elements/hit/HitOutline.js +3 -3
  45. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  46. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  47. package/components/elements/hit/HitRelated.d.ts +6 -0
  48. package/components/elements/hit/HitRelated.js +7 -0
  49. package/components/elements/hit/HitSummary.d.ts +1 -2
  50. package/components/elements/hit/HitSummary.js +5 -6
  51. package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
  52. package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
  53. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  54. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  55. package/components/elements/view/ViewTitle.d.ts +0 -1
  56. package/components/elements/view/ViewTitle.js +2 -9
  57. package/components/hooks/useHitActions.d.ts +1 -1
  58. package/components/hooks/useHitActions.js +4 -4
  59. package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
  60. package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
  61. package/components/hooks/useMyPreferences.js +1 -10
  62. package/components/hooks/useMySearch.js +2 -2
  63. package/components/hooks/useMySitemap.js +1 -4
  64. package/components/hooks/useMyTheme.js +2 -9
  65. package/components/hooks/useParamState.test.js +4 -3
  66. package/components/routes/action/edit/ActionEditor.js +2 -2
  67. package/components/routes/action/view/ActionSearch.js +1 -1
  68. package/components/routes/advanced/QueryBuilder.js +1 -1
  69. package/components/routes/advanced/QueryEditor.js +3 -3
  70. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  71. package/components/routes/analytics/AnalyticDetails.js +2 -2
  72. package/components/routes/analytics/AnalyticSearch.js +1 -1
  73. package/components/routes/dossiers/DossierEditor.js +2 -2
  74. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  75. package/components/routes/help/ApiDocumentation.js +1 -1
  76. package/components/routes/help/BundleDocumentation.d.ts +3 -0
  77. package/components/routes/help/BundleDocumentation.js +12 -0
  78. package/components/routes/help/HitBannerDocumentation.js +0 -1
  79. package/components/routes/help/HitDocumentation.js +3 -1
  80. package/components/routes/help/markdown/en/bundles.md.js +1 -0
  81. package/components/routes/help/markdown/fr/bundles.md.js +1 -0
  82. package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
  83. package/components/routes/hits/search/BundleParentMenu.js +32 -0
  84. package/components/routes/hits/search/BundleScroller.d.ts +2 -0
  85. package/components/routes/hits/search/BundleScroller.js +6 -0
  86. package/components/routes/hits/search/{RecordBrowser.js → HitBrowser.js} +9 -9
  87. package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts} +3 -3
  88. package/components/routes/hits/search/HitContextMenu.js +227 -0
  89. package/components/{elements/record/RecordContextMenu.test.js → routes/hits/search/HitContextMenu.test.js} +39 -94
  90. package/components/routes/hits/search/{RecordQuery.d.ts → HitQuery.d.ts} +2 -2
  91. package/components/routes/hits/search/{RecordQuery.js → HitQuery.js} +6 -6
  92. package/components/routes/hits/search/InformationPane.d.ts +0 -1
  93. package/components/routes/hits/search/InformationPane.js +60 -47
  94. package/components/routes/hits/search/LayoutSettings.js +3 -3
  95. package/components/routes/hits/search/QuerySettings.js +1 -2
  96. package/components/routes/hits/search/QuerySettings.test.js +9 -14
  97. package/components/routes/hits/search/SearchPane.js +49 -26
  98. package/components/routes/hits/search/ViewLink.js +3 -3
  99. package/components/routes/hits/search/ViewLink.test.js +8 -8
  100. package/components/routes/hits/search/grid/AddColumnModal.js +4 -5
  101. package/components/routes/hits/search/grid/EnhancedCell.d.ts +1 -2
  102. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  103. package/components/routes/hits/search/grid/HitGrid.js +18 -20
  104. package/components/routes/hits/search/grid/{RecordRow.d.ts → HitRow.d.ts} +2 -3
  105. package/components/routes/hits/search/grid/{RecordRow.js → HitRow.js} +8 -10
  106. package/components/routes/hits/view/HitViewer.js +13 -12
  107. package/components/routes/home/ViewCard.js +41 -47
  108. package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
  109. package/components/routes/overviews/OverviewViewer.js +2 -2
  110. package/components/routes/views/ViewComposer.js +19 -46
  111. package/locales/en/translation.json +3 -89
  112. package/locales/fr/translation.json +3 -87
  113. package/models/WithMetadata.d.ts +1 -2
  114. package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
  115. package/models/entities/generated/Hit.d.ts +0 -1
  116. package/models/entities/generated/Howler.d.ts +4 -0
  117. package/models/entities/generated/Rule.d.ts +10 -2
  118. package/models/entities/generated/Threat.d.ts +2 -2
  119. package/models/entities/generated/View.d.ts +0 -1
  120. package/package.json +103 -120
  121. package/plugins/clue/components/ClueTypography.js +2 -2
  122. package/plugins/clue/utils.d.ts +1 -2
  123. package/tests/mocks.d.ts +1 -11
  124. package/tests/mocks.js +7 -12
  125. package/tests/server-handlers.js +1 -6
  126. package/tests/server.d.ts +1 -1
  127. package/tests/utils.d.ts +0 -4
  128. package/tests/utils.js +0 -20
  129. package/utils/constants.d.ts +3 -3
  130. package/utils/hitFunctions.d.ts +1 -2
  131. package/utils/hitFunctions.js +4 -4
  132. package/utils/viewUtils.js +0 -3
  133. package/api/search/case.d.ts +0 -4
  134. package/api/search/case.js +0 -8
  135. package/api/v2/case/index.d.ts +0 -8
  136. package/api/v2/case/index.js +0 -20
  137. package/api/v2/case/items.d.ts +0 -6
  138. package/api/v2/case/items.js +0 -18
  139. package/api/v2/index.d.ts +0 -4
  140. package/api/v2/index.js +0 -6
  141. package/api/v2/search/facet.d.ts +0 -3
  142. package/api/v2/search/facet.js +0 -12
  143. package/api/v2/search/index.d.ts +0 -5
  144. package/api/v2/search/index.js +0 -24
  145. package/components/app/providers/RecordProvider.d.ts +0 -23
  146. package/components/elements/ContextMenu.d.ts +0 -56
  147. package/components/elements/ContextMenu.js +0 -109
  148. package/components/elements/ContextMenu.test.js +0 -215
  149. package/components/elements/ObjectDetails.d.ts +0 -6
  150. package/components/elements/case/CaseCard.d.ts +0 -12
  151. package/components/elements/case/CaseCard.js +0 -42
  152. package/components/elements/case/CasePreview.d.ts +0 -6
  153. package/components/elements/case/CasePreview.js +0 -17
  154. package/components/elements/case/StatusIcon.d.ts +0 -5
  155. package/components/elements/case/StatusIcon.js +0 -13
  156. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -9
  157. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  158. package/components/elements/hit/related/RelatedRecords.js +0 -63
  159. package/components/elements/observable/ObservableCard.d.ts +0 -6
  160. package/components/elements/observable/ObservableCard.js +0 -22
  161. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  162. package/components/elements/observable/ObservablePreview.js +0 -12
  163. package/components/elements/record/RecordContextMenu.js +0 -247
  164. package/components/elements/record/RecordContextMenu.test.d.ts +0 -1
  165. package/components/elements/record/RecordRelated.d.ts +0 -7
  166. package/components/elements/record/RecordRelated.js +0 -34
  167. package/components/hooks/useRelatedRecords.d.ts +0 -13
  168. package/components/hooks/useRelatedRecords.js +0 -32
  169. package/components/routes/cases/CaseViewer.d.ts +0 -2
  170. package/components/routes/cases/CaseViewer.js +0 -22
  171. package/components/routes/cases/Cases.d.ts +0 -2
  172. package/components/routes/cases/Cases.js +0 -101
  173. package/components/routes/cases/constants.d.ts +0 -5
  174. package/components/routes/cases/constants.js +0 -5
  175. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  176. package/components/routes/cases/detail/AlertPanel.js +0 -33
  177. package/components/routes/cases/detail/CaseAssets.d.ts +0 -11
  178. package/components/routes/cases/detail/CaseAssets.js +0 -104
  179. package/components/routes/cases/detail/CaseAssets.test.d.ts +0 -1
  180. package/components/routes/cases/detail/CaseAssets.test.js +0 -167
  181. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  182. package/components/routes/cases/detail/CaseDashboard.js +0 -66
  183. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  184. package/components/routes/cases/detail/CaseDetails.js +0 -61
  185. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  186. package/components/routes/cases/detail/CaseOverview.js +0 -43
  187. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
  188. package/components/routes/cases/detail/CaseSidebar.js +0 -107
  189. package/components/routes/cases/detail/CaseSidebar.test.d.ts +0 -1
  190. package/components/routes/cases/detail/CaseSidebar.test.js +0 -246
  191. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  192. package/components/routes/cases/detail/CaseTask.js +0 -57
  193. package/components/routes/cases/detail/CaseTimeline.d.ts +0 -12
  194. package/components/routes/cases/detail/CaseTimeline.js +0 -106
  195. package/components/routes/cases/detail/CaseTimeline.test.d.ts +0 -1
  196. package/components/routes/cases/detail/CaseTimeline.test.js +0 -227
  197. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  198. package/components/routes/cases/detail/ItemPage.js +0 -99
  199. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  200. package/components/routes/cases/detail/RelatedCasePanel.js +0 -34
  201. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  202. package/components/routes/cases/detail/TaskPanel.js +0 -52
  203. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -11
  204. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -24
  205. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  206. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -26
  207. package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
  208. package/components/routes/cases/detail/assets/Asset.js +0 -12
  209. package/components/routes/cases/detail/assets/Asset.test.d.ts +0 -1
  210. package/components/routes/cases/detail/assets/Asset.test.js +0 -72
  211. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -20
  212. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -83
  213. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +0 -1
  214. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +0 -295
  215. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
  216. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -103
  217. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +0 -1
  218. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -363
  219. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +0 -25
  220. package/components/routes/cases/detail/sidebar/FolderEntry.js +0 -88
  221. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +0 -1
  222. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +0 -206
  223. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +0 -5
  224. package/components/routes/cases/detail/sidebar/RootDropZone.js +0 -33
  225. package/components/routes/cases/detail/sidebar/types.d.ts +0 -9
  226. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  227. package/components/routes/cases/detail/sidebar/utils.js +0 -29
  228. package/components/routes/cases/detail/sidebar/utils.test.d.ts +0 -1
  229. package/components/routes/cases/detail/sidebar/utils.test.js +0 -82
  230. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  231. package/components/routes/cases/hooks/useCase.js +0 -51
  232. package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
  233. package/components/routes/cases/modals/AddToCaseModal.js +0 -62
  234. package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
  235. package/components/routes/cases/modals/RenameItemModal.js +0 -48
  236. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  237. package/components/routes/cases/modals/ResolveModal.js +0 -115
  238. package/components/routes/cases/modals/ResolveModal.test.d.ts +0 -1
  239. package/components/routes/cases/modals/ResolveModal.test.js +0 -384
  240. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  241. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  242. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  243. package/components/routes/observables/ObservableViewer.js +0 -27
  244. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  245. package/models/entities/generated/Case.d.ts +0 -28
  246. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  247. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  248. package/models/entities/generated/EmailParent.d.ts +0 -19
  249. package/models/entities/generated/Enrichments.d.ts +0 -7
  250. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  251. package/models/entities/generated/HttpResponse.d.ts +0 -11
  252. package/models/entities/generated/Item.d.ts +0 -9
  253. package/models/entities/generated/Observable.d.ts +0 -85
  254. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  255. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  256. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  257. package/models/entities/generated/ObservableFile.d.ts +0 -36
  258. package/models/entities/generated/ObservableHowler.d.ts +0 -43
  259. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  260. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  261. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  262. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  263. package/models/entities/generated/ObservableSource.d.ts +0 -23
  264. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  265. package/models/entities/generated/ObservableTls.d.ts +0 -12
  266. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  267. package/models/entities/generated/Task.d.ts +0 -10
  268. package/utils/typeUtils.d.ts +0 -7
  269. package/utils/typeUtils.js +0 -27
  270. /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
  271. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  272. /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
  273. /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
  274. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -1,227 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /* eslint-disable react/no-children-prop */
3
- /// <reference types="vitest" />
4
- import { act, render, screen } from '@testing-library/react';
5
- import userEvent from '@testing-library/user-event';
6
- import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
7
- import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
8
- import { createElement } from 'react';
9
- import { MemoryRouter } from 'react-router-dom';
10
- import { createMockHit, createMockObservable } from '@cccsaurora/howler-ui/tests/utils';
11
- import { beforeEach, describe, expect, it, vi } from 'vitest';
12
- import { buildFilters } from './CaseTimeline';
13
- // ---------------------------------------------------------------------------
14
- // Pure logic tests
15
- // ---------------------------------------------------------------------------
16
- describe('buildFilters', () => {
17
- it('returns an empty array when both lists are empty', () => {
18
- expect(buildFilters([], [])).toEqual([]);
19
- });
20
- it('builds a tactic filter', () => {
21
- const result = buildFilters([{ id: 'TA0001', name: 'Initial Access', kind: 'tactic' }], []);
22
- expect(result).toEqual(['threat.tactic.id:(TA0001)']);
23
- });
24
- it('builds a technique filter', () => {
25
- const result = buildFilters([{ id: 'T1059', name: 'Command Scripting', kind: 'technique' }], []);
26
- expect(result).toEqual(['threat.technique.id:(T1059)']);
27
- });
28
- it('OR-combines multiple tactics in one clause', () => {
29
- const result = buildFilters([
30
- { id: 'TA0001', name: 'Initial Access', kind: 'tactic' },
31
- { id: 'TA0002', name: 'Execution', kind: 'tactic' }
32
- ], []);
33
- expect(result).toEqual(['threat.tactic.id:(TA0001 OR TA0002)']);
34
- });
35
- it('emits separate clauses for tactics and techniques', () => {
36
- const result = buildFilters([
37
- { id: 'TA0001', name: 'Initial Access', kind: 'tactic' },
38
- { id: 'T1059', name: 'Command Scripting', kind: 'technique' }
39
- ], []);
40
- expect(result).toContain('threat.tactic.id:(TA0001)');
41
- expect(result).toContain('threat.technique.id:(T1059)');
42
- expect(result).toHaveLength(2);
43
- });
44
- it('builds an escalation filter', () => {
45
- const result = buildFilters([], ['evidence', 'hit']);
46
- expect(result).toEqual(['howler.escalation:(evidence OR hit)']);
47
- });
48
- it('combines mitre and escalation filters', () => {
49
- const result = buildFilters([{ id: 'TA0001', name: 'Initial Access', kind: 'tactic' }], ['evidence']);
50
- expect(result).toHaveLength(2);
51
- expect(result).toContain('threat.tactic.id:(TA0001)');
52
- expect(result).toContain('howler.escalation:(evidence)');
53
- });
54
- });
55
- // ---------------------------------------------------------------------------
56
- // Component rendering tests
57
- // ---------------------------------------------------------------------------
58
- const mockDispatchApi = vi.fn();
59
- vi.mock('components/hooks/useMyApi', () => ({
60
- default: () => ({ dispatchApi: mockDispatchApi })
61
- }));
62
- vi.mock('../hooks/useCase', () => ({
63
- default: ({ case: c }) => ({ case: c, update: vi.fn(), loading: false, missing: false })
64
- }));
65
- vi.mock('react-router-dom', async () => {
66
- const actual = await vi.importActual('react-router-dom');
67
- return { ...actual, useOutletContext: () => undefined };
68
- });
69
- // Stub card components — their internals are irrelevant to timeline tests
70
- vi.mock('components/elements/hit/HitCard', () => ({
71
- default: ({ id }) => _jsx("div", { children: `HitCard:${id}` })
72
- }));
73
- vi.mock('components/elements/observable/ObservableCard', () => ({
74
- default: ({ id }) => _jsx("div", { children: `ObservableCard:${id}` })
75
- }));
76
- // Return the request object rather than a Promise so dispatchApi's first
77
- // argument is JSON-serialisable (JSON.stringify(Promise) === '{}').
78
- vi.mock('api', () => ({
79
- default: {
80
- v2: {
81
- search: {
82
- post: (_indexes, request) => request ?? {},
83
- facet: {
84
- post: (_indexes, request) => request ?? {}
85
- }
86
- }
87
- }
88
- }
89
- }));
90
- const mockLoadRecords = vi.fn();
91
- const mockConfig = {
92
- lookups: {
93
- tactics: { TA0001: { key: 'TA0001', name: 'Initial Access', url: '' } },
94
- techniques: { T1059: { key: 'T1059', name: 'Command Scripting', url: '' } }
95
- }
96
- };
97
- const mockCase = {
98
- case_id: 'case-001',
99
- items: [
100
- { type: 'hit', value: 'hit-1', path: 'hits/hit-1' },
101
- { type: 'observable', value: 'obs-1', path: 'observables/obs-1' }
102
- ]
103
- };
104
- const Wrapper = ({ children }) => (_jsx(ApiConfigContext.Provider, { value: { config: mockConfig, setConfig: vi.fn() }, children: _jsxs(RecordContext.Provider, { value: { records: {}, loadRecords: mockLoadRecords }, children: [_jsx(MemoryRouter, { initialEntries: ['/cases/case-001/timeline'], children: children }), ' '] }) }));
105
- const CaseTimeline = (await import('./CaseTimeline')).default;
106
- // Reusable mock response factories
107
- const mockFacetResponse = {
108
- 'threat.tactic.id': { TA0001: 1 },
109
- 'threat.technique.id': { T1059: 1 },
110
- 'howler.escalation': { evidence: 2, hit: 1 }
111
- };
112
- const mockSearchResponse = (items = [
113
- createMockHit({ howler: { id: 'hit-1' }, event: { created: '2024-01-01T00:00:00Z' } }),
114
- createMockObservable({ howler: { id: 'obs-1' } })
115
- ]) => ({ items, total: items.length, rows: items.length, offset: 0 });
116
- describe('CaseTimeline component', () => {
117
- beforeEach(() => {
118
- mockDispatchApi.mockClear();
119
- mockLoadRecords.mockClear();
120
- });
121
- it('renders loading skeletons while the search is pending', () => {
122
- mockDispatchApi.mockReturnValue(new Promise(() => { })); // never resolves
123
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
124
- expect(document.querySelectorAll('.MuiSkeleton-root').length).toBeGreaterThan(0);
125
- });
126
- it('shows the empty state when no entries are returned', async () => {
127
- mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse([]));
128
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
129
- await screen.findByText('page.cases.timeline.empty');
130
- });
131
- it('renders HitCard for hit entries and ObservableCard for observable entries', async () => {
132
- mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse());
133
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
134
- expect(await screen.findByText('HitCard:hit-1')).toBeTruthy();
135
- expect(screen.getByText('ObservableCard:obs-1')).toBeTruthy();
136
- });
137
- it('renders the formatted event.created timestamp for hits', async () => {
138
- mockDispatchApi
139
- .mockResolvedValueOnce(mockFacetResponse)
140
- .mockResolvedValueOnce(mockSearchResponse([createMockHit({ howler: { id: 'hit-1' }, event: { created: '2024-06-15T12:34:56Z' } })]));
141
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
142
- await screen.findByText(/2024-06-15/);
143
- });
144
- it('falls back to entry.timestamp when event.created is absent', async () => {
145
- const entry = createMockHit({ howler: { id: 'hit-1' } });
146
- entry.timestamp = '2024-03-10T08:00:00Z';
147
- delete entry.event;
148
- mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse([entry]));
149
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
150
- await screen.findByText(/2024-03-10/);
151
- });
152
- it('calls loadRecords with the search results', async () => {
153
- const items = [createMockHit({ howler: { id: 'hit-1' } })];
154
- mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse(items));
155
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
156
- await screen.findByText('HitCard:hit-1');
157
- expect(mockLoadRecords).toHaveBeenCalledWith(items);
158
- });
159
- it('renders MITRE autocomplete with options derived from the facet response', async () => {
160
- mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse());
161
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
162
- await screen.findByText('HitCard:hit-1');
163
- expect(screen.getByRole('combobox', { name: /page.cases.timeline.filter.mitre/i })).toBeTruthy();
164
- });
165
- it('renders escalation autocomplete pre-populated with "evidence"', async () => {
166
- mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse());
167
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
168
- await screen.findByText('HitCard:hit-1');
169
- // "evidence" chip should be pre-selected as a tag
170
- expect(screen.getByText('evidence')).toBeTruthy();
171
- });
172
- it('re-fetches with an escalation filter when a new escalation is selected', async () => {
173
- mockDispatchApi
174
- .mockResolvedValueOnce(mockFacetResponse)
175
- .mockResolvedValueOnce(mockSearchResponse())
176
- .mockResolvedValueOnce(mockSearchResponse());
177
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
178
- await screen.findByText('HitCard:hit-1');
179
- const escalationInput = screen.getByRole('combobox', { name: /page.cases.timeline.filter.escalation/i });
180
- await act(async () => {
181
- await userEvent.click(escalationInput);
182
- });
183
- const option = await screen.findByRole('option', { name: /\bhit\b/i });
184
- await act(async () => {
185
- await userEvent.click(option);
186
- });
187
- // The third dispatchApi call should carry a filter containing "hit"
188
- const thirdCallArgs = mockDispatchApi.mock.calls[2];
189
- expect(JSON.stringify(thirdCallArgs[0])).toContain('hit');
190
- });
191
- it('re-fetches when a MITRE tactic is selected', async () => {
192
- mockDispatchApi
193
- .mockResolvedValueOnce(mockFacetResponse)
194
- .mockResolvedValueOnce(mockSearchResponse())
195
- .mockResolvedValueOnce(mockSearchResponse());
196
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
197
- await screen.findByText('HitCard:hit-1');
198
- const mitreInput = screen.getByRole('combobox', { name: /page.cases.timeline.filter.mitre/i });
199
- await act(async () => {
200
- await userEvent.click(mitreInput);
201
- });
202
- const tacticOption = await screen.findByRole('option', { name: /TA0001/i });
203
- await act(async () => {
204
- await userEvent.click(tacticOption);
205
- });
206
- const thirdCallArgs = mockDispatchApi.mock.calls[2];
207
- expect(JSON.stringify(thirdCallArgs[0])).toContain('TA0001');
208
- });
209
- it('passes the case item ids in the base query on every search call', async () => {
210
- mockDispatchApi.mockResolvedValueOnce(mockFacetResponse).mockResolvedValueOnce(mockSearchResponse());
211
- render(_jsx(CaseTimeline, { case: mockCase }), { wrapper: Wrapper });
212
- await screen.findByText('HitCard:hit-1');
213
- const searchCallArg = mockDispatchApi.mock.calls[1][0];
214
- expect(JSON.stringify(searchCallArg)).toContain('hit-1');
215
- expect(JSON.stringify(searchCallArg)).toContain('obs-1');
216
- });
217
- it('renders nothing when _case is not yet available', () => {
218
- const emptyCaseWrapper = ({ children }) => createElement(ApiConfigContext.Provider, { value: { config: mockConfig, setConfig: vi.fn() }, children: null }, createElement(RecordContext.Provider, { value: { records: {}, loadRecords: mockLoadRecords }, children: null }, createElement(MemoryRouter, null, children)));
219
- const { container } = render(_jsx(CaseTimeline, {}), { wrapper: emptyCaseWrapper });
220
- expect(container.firstChild).toBeNull();
221
- });
222
- it('renders nothing and skips fetching when the case has no items', () => {
223
- mockDispatchApi.mockResolvedValue({});
224
- render(_jsx(CaseTimeline, { case: { case_id: 'empty', items: [] } }), { wrapper: Wrapper });
225
- expect(mockDispatchApi).not.toHaveBeenCalled();
226
- });
227
- });
@@ -1,6 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const ItemPage: FC<{
4
- case?: Case;
5
- }>;
6
- export default ItemPage;
@@ -1,99 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import api from '@cccsaurora/howler-ui/api';
3
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
4
- import NotFoundPage from '@cccsaurora/howler-ui/components/routes/404';
5
- import InformationPane from '@cccsaurora/howler-ui/components/routes/hits/search/InformationPane';
6
- import ObservableViewer from '@cccsaurora/howler-ui/components/routes/observables/ObservableViewer';
7
- import { useEffect, useMemo, useState } from 'react';
8
- import { useOutletContext, useParams } from 'react-router-dom';
9
- import useCase from '../hooks/useCase';
10
- import CaseDashboard from './CaseDashboard';
11
- const ItemPage = ({ case: providedCase }) => {
12
- const params = useParams();
13
- const routeCase = useOutletContext();
14
- const { case: fetchedCase } = useCase({ caseId: !providedCase && !routeCase ? params.id : undefined });
15
- const _case = providedCase ?? routeCase ?? fetchedCase;
16
- const { dispatchApi } = useMyApi();
17
- const [item, setItem] = useState(null);
18
- const [loading, setLoading] = useState(true);
19
- // When rendered as a child route, the wildcard segment is in params['*'].
20
- // When rendered directly with a case prop, fall back to parsing the pathname.
21
- const subPath = params['*'] ?? '';
22
- const normalizedSubPath = useMemo(() => subPath.replace(/^\/+|\/+$/g, ''), [subPath]);
23
- useEffect(() => {
24
- let cancelled = false;
25
- const resolveItem = async () => {
26
- setLoading(true);
27
- if (!normalizedSubPath) {
28
- if (!cancelled) {
29
- setItem(null);
30
- setLoading(false);
31
- }
32
- return;
33
- }
34
- let currentCase = _case;
35
- let remainingPath = normalizedSubPath;
36
- while (currentCase && remainingPath) {
37
- const currentRemainingPath = remainingPath;
38
- const matchedNestedCase = currentCase.items
39
- .filter(_item => _item?.path &&
40
- _item?.type?.toLowerCase() === 'case' &&
41
- (currentRemainingPath === _item.path || currentRemainingPath.startsWith(`${_item.path}/`)))
42
- .sort((a, b) => (b.path?.length || 0) - (a.path?.length || 0))[0];
43
- if (!matchedNestedCase) {
44
- break;
45
- }
46
- if (currentRemainingPath === matchedNestedCase.path) {
47
- if (!cancelled) {
48
- setItem(matchedNestedCase);
49
- setLoading(false);
50
- }
51
- return;
52
- }
53
- if (!matchedNestedCase.value) {
54
- if (!cancelled) {
55
- setItem(null);
56
- setLoading(false);
57
- }
58
- return;
59
- }
60
- const nextCase = await dispatchApi(api.v2.case.get(matchedNestedCase.value), { throwError: false });
61
- if (!nextCase) {
62
- if (!cancelled) {
63
- setItem(null);
64
- setLoading(false);
65
- }
66
- return;
67
- }
68
- remainingPath = currentRemainingPath.slice((matchedNestedCase.path?.length || 0) + 1);
69
- currentCase = nextCase;
70
- }
71
- const resolvedItem = currentCase?.items?.find(_item => _item.path === remainingPath);
72
- if (!cancelled) {
73
- setItem(resolvedItem || null);
74
- setLoading(false);
75
- }
76
- };
77
- resolveItem();
78
- return () => {
79
- cancelled = true;
80
- };
81
- }, [_case, dispatchApi, normalizedSubPath]);
82
- if (loading) {
83
- return null;
84
- }
85
- if (!item) {
86
- return _jsx(NotFoundPage, {});
87
- }
88
- if (item.type === 'hit') {
89
- return _jsx(InformationPane, { selected: item.value });
90
- }
91
- if (item.type === 'observable') {
92
- return _jsx(ObservableViewer, { observableId: item.value });
93
- }
94
- if (item.type === 'case') {
95
- return _jsx(CaseDashboard, { caseId: item.value });
96
- }
97
- return _jsx("h1", { children: JSON.stringify(item) });
98
- };
99
- export default ItemPage;
@@ -1,6 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const RelatedCasePanel: FC<{
4
- case: Case;
5
- }>;
6
- export default RelatedCasePanel;
@@ -1,34 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Divider, Pagination, Skeleton, Stack, Typography, useTheme } from '@mui/material';
3
- import { chunk, uniq } from 'lodash-es';
4
- import { useMemo, useState } from 'react';
5
- import { useTranslation } from 'react-i18next';
6
- import { Link } from 'react-router-dom';
7
- import CaseCard from '../../../elements/case/CaseCard';
8
- const RelatedCasePanel = ({ case: _case }) => {
9
- const { t } = useTranslation();
10
- const theme = useTheme();
11
- const [casePage, setCasePage] = useState(1);
12
- const casePages = useMemo(() => chunk(uniq((_case?.items ?? []).filter(item => item.type === 'case')), 5), [_case?.items]);
13
- if (!_case) {
14
- return _jsx(Skeleton, { height: 240 });
15
- }
16
- if (casePages.length < 1) {
17
- return null;
18
- }
19
- return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.cases') }), _jsx(Pagination, { count: casePages.length, page: casePage, onChange: (_, page) => setCasePage(page) })] }), _jsx(Divider, {}), casePages[casePage - 1]?.map(item => (_jsxs(Box, { position: "relative", children: [_jsx(CaseCard, { caseId: item.value }), _jsx(Box, { component: Link, to: item.path, sx: {
20
- position: 'absolute',
21
- top: 0,
22
- left: 0,
23
- width: '100%',
24
- height: '100%',
25
- cursor: 'pointer',
26
- zIndex: 100,
27
- borderRadius: '4px',
28
- '&:hover': {
29
- background: theme.palette.divider,
30
- border: `thin solid ${theme.palette.primary.light}`
31
- }
32
- } })] }, item.path)))] }));
33
- };
34
- export default RelatedCasePanel;
@@ -1,7 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const TaskPanel: FC<{
4
- case: Case;
5
- updateCase: (_case: Partial<Case>) => Promise<void>;
6
- }>;
7
- export default TaskPanel;
@@ -1,52 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Add } from '@mui/icons-material';
3
- import { Divider, Skeleton, Stack, Typography } from '@mui/material';
4
- import { useState } from 'react';
5
- import { useTranslation } from 'react-i18next';
6
- import CaseTask from './CaseTask';
7
- const TaskPanel = ({ case: _case, updateCase }) => {
8
- const { t } = useTranslation();
9
- const [addingTask, setAddingTask] = useState(false);
10
- const onEdit = (task) => async (newTask) => {
11
- if (task) {
12
- await updateCase({
13
- tasks: _case.tasks.map(_task => {
14
- if (_task.id !== task.id) {
15
- return _task;
16
- }
17
- return {
18
- ..._task,
19
- ...newTask
20
- };
21
- })
22
- });
23
- }
24
- else {
25
- await updateCase({
26
- tasks: [..._case.tasks, newTask]
27
- });
28
- }
29
- };
30
- if (!_case) {
31
- return _jsx(Skeleton, { height: 240 });
32
- }
33
- return (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.tasks') }), _jsx(Divider, {}), _case.tasks.map(task => (_jsx(CaseTask, { task: task, paths: _case.items.map(item => item.path), onEdit: onEdit(task), onDelete: () => updateCase({ tasks: _case.tasks.filter(_task => _task.id !== task.id) }) }, task.id))), addingTask && (_jsx(CaseTask, { newTask: true, paths: _case.items.map(item => item.path), onEdit: async (task) => {
34
- await onEdit()(task);
35
- setAddingTask(false);
36
- }, onDelete: async () => setAddingTask(false) })), _jsxs(Stack, { onClick: () => setAddingTask(true), direction: "row", spacing: 2, sx: theme => ({
37
- borderStyle: 'dashed',
38
- borderColor: theme.palette.text.secondary,
39
- borderWidth: '0.15rem',
40
- borderRadius: '0.15rem',
41
- opacity: 0.3,
42
- justifyContent: 'center',
43
- alignItems: 'center',
44
- padding: 1,
45
- transition: theme.transitions.create('opacity'),
46
- '&:hover': {
47
- opacity: 1,
48
- cursor: 'pointer'
49
- }
50
- }), children: [_jsx(Add, {}), _jsx(Typography, { children: t('page.cases.dashboard.tasks.add') })] })] }));
51
- };
52
- export default TaskPanel;
@@ -1,11 +0,0 @@
1
- import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
- import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
3
- declare const _default: import("react").NamedExoticComponent<{
4
- icon?: string;
5
- iconColor?: string;
6
- field?: string;
7
- records?: Partial<Hit | Observable>[];
8
- title?: string;
9
- subtitle?: string;
10
- }>;
11
- export default _default;
@@ -1,24 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Icon } from '@iconify/react';
3
- import { Card, CardContent, Skeleton, Stack, styled, Tooltip, tooltipClasses, Typography, useTheme } from '@mui/material';
4
- import { get, isEmpty, uniq } from 'lodash-es';
5
- import { memo } from 'react';
6
- const NoMaxWidthTooltip = styled(({ className, ...props }) => (_jsx(Tooltip, { ...props, classes: { popper: className } })))({
7
- [`& .${tooltipClasses.tooltip}`]: {
8
- maxWidth: 'none'
9
- }
10
- });
11
- const CaseAggregate = ({ icon, iconColor, field, records, title, subtitle }) => {
12
- const theme = useTheme();
13
- if (!title && (!records || !field)) {
14
- return _jsx(Skeleton, { height: 120 });
15
- }
16
- const values = records
17
- ? uniq(records
18
- .map(_record => get(_record, field))
19
- .flat()
20
- .filter(Boolean))
21
- : [];
22
- return (_jsx(Card, { sx: { height: '100%' }, children: _jsx(CardContent, { children: _jsxs(Stack, { alignItems: "center", spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [icon && _jsx(Icon, { fontSize: "96px", icon: icon, color: iconColor || theme.palette.grey[700] }), _jsx(NoMaxWidthTooltip, { title: !isEmpty(values) && (_jsx(Stack, { spacing: 0.5, children: uniq(values).map(value => (_jsx("span", { children: value }, value))) })), children: _jsxs(Typography, { variant: "h3", children: [values.length, !isEmpty(values) && !!title && ' - ', title] }) })] }), _jsx(Typography, { color: "textSecondary", children: subtitle })] }) }) }));
23
- };
24
- export default memo(CaseAggregate);
@@ -1,6 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const SourceAggregate: FC<{
4
- case: Case;
5
- }>;
6
- export default SourceAggregate;
@@ -1,26 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Chip, Grid, Skeleton } from '@mui/material';
3
- import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
4
- import { uniq } from 'lodash-es';
5
- import { useMemo } from 'react';
6
- import { useContextSelector } from 'use-context-selector';
7
- import useCase from '../../hooks/useCase';
8
- const SourceAggregate = ({ case: providedCase }) => {
9
- const { case: _case } = useCase({ case: providedCase });
10
- const records = useContextSelector(RecordContext, ctx => ctx.records);
11
- const analytics = useMemo(() => {
12
- if (!_case) {
13
- return [];
14
- }
15
- const hitIds = _case.items
16
- .filter(item => item.type === 'hit')
17
- .map(item => item.value)
18
- .filter(value => !!value);
19
- return uniq(hitIds.map(id => records[id]?.howler?.analytic).filter(analytic => !!analytic));
20
- }, [_case, records]);
21
- if (!_case) {
22
- return _jsx(Skeleton, { height: 12, variant: "rounded" });
23
- }
24
- return (_jsx(Grid, { container: true, spacing: 1, children: analytics.map(_analytic => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", label: _analytic }) }, _analytic))) }));
25
- };
26
- export default SourceAggregate;
@@ -1,14 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import type { FC } from 'react';
3
- export type AssetType = 'hash' | 'hosts' | 'ip' | 'user' | 'ids' | 'id' | 'uri' | 'signature';
4
- export interface AssetEntry {
5
- type: AssetType;
6
- value: string;
7
- /** IDs of the hits/observables this asset was seen in */
8
- seenIn: string[];
9
- }
10
- declare const Asset: FC<{
11
- asset: AssetEntry;
12
- case: Case;
13
- }>;
14
- export default Asset;
@@ -1,12 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Card, CardContent, Chip, Stack, Typography } from '@mui/material';
3
- import { useTranslation } from 'react-i18next';
4
- import { Link } from 'react-router-dom';
5
- const Asset = ({ asset, case: _case }) => {
6
- const { t } = useTranslation();
7
- return (_jsx(Card, { sx: { height: '100%' }, children: _jsx(CardContent, { children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Chip, { size: "small", label: t(`page.cases.assets.type.${asset.type}`), color: "primary", variant: "outlined" }), _jsx(Typography, { variant: "body2", sx: { wordBreak: 'break-all', fontFamily: 'monospace' }, children: asset.value })] }), asset.seenIn.length > 0 && (_jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.cases.assets.seen_in') }), _jsx(Stack, { direction: "row", flexWrap: "wrap", gap: 0.5, children: asset.seenIn.map(id => {
8
- const entry = _case.items.find(item => item.value === id);
9
- return (_jsx(Chip, { clickable: true, size: "small", label: entry.path, variant: "outlined", component: Link, to: `/cases/${_case.case_id}/${entry.path}` }, id));
10
- }) })] }))] }) }) }));
11
- };
12
- export default Asset;
@@ -1 +0,0 @@
1
- export {};
@@ -1,72 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- /// <reference types="vitest" />
3
- import { render, screen } from '@testing-library/react';
4
- import { MemoryRouter } from 'react-router-dom';
5
- import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
6
- import { describe, expect, it } from 'vitest';
7
- import Asset, {} from './Asset';
8
- const makeAsset = (overrides = {}) => ({
9
- type: 'ip',
10
- value: '192.168.1.1',
11
- seenIn: [],
12
- ...overrides
13
- });
14
- describe('Asset', () => {
15
- describe('type chip', () => {
16
- it('renders the correct label for each type', () => {
17
- const cases = ['hash', 'hosts', 'ip', 'user', 'ids', 'id', 'uri', 'signature'];
18
- for (const type of cases) {
19
- const { unmount } = render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ type, value: 'x' }), case: createMockCase() }) }));
20
- expect(screen.getByText(`page.cases.assets.type.${type}`)).toBeTruthy();
21
- unmount();
22
- }
23
- });
24
- });
25
- describe('value display', () => {
26
- it('renders the asset value', () => {
27
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ value: '10.0.0.1' }), case: createMockCase() }) }));
28
- expect(screen.getByText('10.0.0.1')).toBeTruthy();
29
- });
30
- it('renders long hash values without truncation', () => {
31
- const hash = 'a'.repeat(64);
32
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ type: 'hash', value: hash }), case: createMockCase() }) }));
33
- expect(screen.getByText(hash)).toBeTruthy();
34
- });
35
- });
36
- describe('seen-in chips', () => {
37
- it('renders nothing when seenIn is empty', () => {
38
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: [] }), case: createMockCase() }) }));
39
- expect(screen.queryByText('page.cases.assets.seen_in')).toBeNull();
40
- });
41
- it('renders "Seen in" label when seenIn has entries', () => {
42
- const _case = createMockCase({
43
- items: [{ path: 'alerts/test-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
44
- });
45
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001'] }), case: _case }) }));
46
- expect(screen.getByText('page.cases.assets.seen_in')).toBeTruthy();
47
- });
48
- it('renders a chip labelled with entry.path for each seenIn id', () => {
49
- const _case = createMockCase({
50
- items: [
51
- { path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' },
52
- { path: 'observables/obs-002', type: 'observable', value: 'obs-002' },
53
- { path: 'alerts/other-analytic (hit-003)', type: 'hit', value: 'hit-003' }
54
- ]
55
- });
56
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001', 'obs-002', 'hit-003'] }), case: _case }) }));
57
- expect(screen.getByText('alerts/my-analytic (hit-001)')).toBeTruthy();
58
- expect(screen.getByText('observables/obs-002')).toBeTruthy();
59
- expect(screen.getByText('alerts/other-analytic (hit-003)')).toBeTruthy();
60
- });
61
- it('links each chip to /cases/:case_id/:path', () => {
62
- const _case = createMockCase({
63
- case_id: 'case-abc',
64
- items: [{ path: 'alerts/my-analytic (hit-001)', type: 'hit', value: 'hit-001' }]
65
- });
66
- render(_jsx(MemoryRouter, { children: _jsx(Asset, { asset: makeAsset({ seenIn: ['hit-001'] }), case: _case }) }));
67
- const link = screen.getByText('alerts/my-analytic (hit-001)').closest('a');
68
- expect(link).not.toBeNull();
69
- expect(link?.getAttribute('href')).toBe('/cases/case-abc/alerts/my-analytic (hit-001)');
70
- });
71
- });
72
- });
@@ -1,20 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- import type { Tree } from './types';
4
- interface CaseFolderProps {
5
- case: Case;
6
- folder?: Tree;
7
- name?: string;
8
- step?: number;
9
- /**
10
- * The chain of `leaf.path` values for each case item traversed from the root
11
- * case to reach this nested case. Empty at the top level.
12
- *
13
- * Example: case1 → case2 (path "cases/caseone") → case3 (path "cases/casetwo")
14
- * gives parentCasePaths = ['cases/caseone', 'cases/casetwo'] inside case3.
15
- */
16
- parentCasePaths?: string[];
17
- onItemUpdated?: (newCase: Case) => void;
18
- }
19
- declare const CaseFolder: FC<CaseFolderProps>;
20
- export default CaseFolder;