@cccsaurora/howler-ui 2.18.0-dev.704 → 2.18.0-dev.705

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 (245) hide show
  1. package/api/index.d.ts +2 -0
  2. package/api/index.js +4 -2
  3. package/api/search/case.d.ts +4 -0
  4. package/api/search/case.js +8 -0
  5. package/api/search/index.d.ts +2 -1
  6. package/api/search/index.js +2 -1
  7. package/api/v2/case/index.d.ts +8 -0
  8. package/api/v2/case/index.js +20 -0
  9. package/api/v2/case/items.d.ts +6 -0
  10. package/api/v2/case/items.js +18 -0
  11. package/api/v2/index.d.ts +4 -0
  12. package/api/v2/index.js +6 -0
  13. package/api/v2/search/facet.d.ts +3 -0
  14. package/api/v2/search/facet.js +12 -0
  15. package/api/v2/search/index.d.ts +5 -0
  16. package/api/v2/search/index.js +24 -0
  17. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  18. package/components/app/App.js +34 -7
  19. package/components/app/hooks/useMatchers.js +2 -2
  20. package/components/app/hooks/useMatchers.test.js +22 -22
  21. package/components/app/hooks/useTitle.js +3 -3
  22. package/components/app/providers/FavouritesProvider.js +2 -2
  23. package/components/app/providers/ModalProvider.d.ts +1 -0
  24. package/components/app/providers/ParameterProvider.d.ts +9 -2
  25. package/components/app/providers/ParameterProvider.js +165 -240
  26. package/components/app/providers/ParameterProvider.test.js +307 -14
  27. package/components/app/providers/RecordProvider.d.ts +23 -0
  28. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  29. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  30. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  31. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
  32. package/components/elements/ContextMenu.d.ts +56 -0
  33. package/components/elements/ContextMenu.js +109 -0
  34. package/components/elements/ContextMenu.test.js +215 -0
  35. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  36. package/components/elements/ObjectDetails.d.ts +6 -0
  37. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  38. package/components/elements/PluginTypography.d.ts +2 -1
  39. package/components/elements/PluginTypography.js +3 -2
  40. package/components/elements/UserList.d.ts +5 -2
  41. package/components/elements/UserList.js +14 -5
  42. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  43. package/components/elements/case/CaseCard.d.ts +12 -0
  44. package/components/elements/case/CaseCard.js +42 -0
  45. package/components/elements/case/CasePreview.d.ts +6 -0
  46. package/components/elements/case/CasePreview.js +17 -0
  47. package/components/elements/case/StatusIcon.d.ts +5 -0
  48. package/components/elements/case/StatusIcon.js +13 -0
  49. package/components/elements/display/ChipPopper.d.ts +1 -1
  50. package/components/elements/display/HowlerCard.js +1 -1
  51. package/components/elements/display/Modal.js +2 -0
  52. package/components/elements/hit/HitActions.js +4 -4
  53. package/components/elements/hit/HitBanner.js +28 -48
  54. package/components/elements/hit/HitCard.js +5 -5
  55. package/components/elements/hit/HitLabels.js +2 -2
  56. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  57. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  58. package/components/elements/hit/HitSummary.d.ts +2 -1
  59. package/components/elements/hit/HitSummary.js +6 -5
  60. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  61. package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
  62. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  63. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  64. package/components/elements/hit/related/RelatedRecords.js +63 -0
  65. package/components/elements/observable/ObservableCard.d.ts +6 -0
  66. package/components/elements/observable/ObservableCard.js +23 -0
  67. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  68. package/components/elements/observable/ObservablePreview.js +12 -0
  69. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  70. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  71. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  72. package/components/elements/record/RecordContextMenu.js +247 -0
  73. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  74. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +94 -39
  75. package/components/elements/record/RecordRelated.d.ts +7 -0
  76. package/components/elements/record/RecordRelated.js +34 -0
  77. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  78. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  79. package/components/elements/view/ViewTitle.d.ts +1 -0
  80. package/components/elements/view/ViewTitle.js +9 -2
  81. package/components/hooks/useHitActions.d.ts +1 -1
  82. package/components/hooks/useHitActions.js +4 -4
  83. package/components/hooks/useMyPreferences.js +10 -1
  84. package/components/hooks/useMySearch.js +2 -2
  85. package/components/hooks/useMySitemap.js +4 -1
  86. package/components/hooks/useMyTheme.js +9 -2
  87. package/components/hooks/useParamState.test.js +3 -4
  88. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  89. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  90. package/components/hooks/useRelatedRecords.d.ts +13 -0
  91. package/components/hooks/useRelatedRecords.js +32 -0
  92. package/components/routes/action/edit/ActionEditor.js +2 -2
  93. package/components/routes/action/view/ActionSearch.js +1 -1
  94. package/components/routes/advanced/QueryBuilder.js +1 -1
  95. package/components/routes/advanced/QueryEditor.js +3 -3
  96. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  97. package/components/routes/analytics/AnalyticDetails.js +2 -2
  98. package/components/routes/analytics/AnalyticSearch.js +1 -1
  99. package/components/routes/cases/CaseViewer.d.ts +2 -0
  100. package/components/routes/cases/CaseViewer.js +22 -0
  101. package/components/routes/cases/Cases.d.ts +2 -0
  102. package/components/routes/cases/Cases.js +101 -0
  103. package/components/routes/cases/constants.d.ts +5 -0
  104. package/components/routes/cases/constants.js +5 -0
  105. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  106. package/components/routes/cases/detail/AlertPanel.js +33 -0
  107. package/components/routes/cases/detail/CaseAssets.d.ts +12 -0
  108. package/components/routes/cases/detail/CaseAssets.js +104 -0
  109. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  110. package/components/routes/cases/detail/CaseAssets.test.js +167 -0
  111. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  112. package/components/routes/cases/detail/CaseDashboard.js +54 -0
  113. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  114. package/components/routes/cases/detail/CaseDetails.js +61 -0
  115. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  116. package/components/routes/cases/detail/CaseOverview.js +43 -0
  117. package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
  118. package/components/routes/cases/detail/CaseSidebar.js +61 -0
  119. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  120. package/components/routes/cases/detail/CaseTask.js +57 -0
  121. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  122. package/components/routes/cases/detail/ItemPage.js +99 -0
  123. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  124. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  125. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  126. package/components/routes/cases/detail/TaskPanel.js +52 -0
  127. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  128. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  129. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  130. package/components/routes/cases/detail/aggregates/SourceAggregate.js +30 -0
  131. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  132. package/components/routes/cases/detail/assets/Asset.js +12 -0
  133. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  134. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  135. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +14 -0
  136. package/components/routes/cases/detail/sidebar/CaseFolder.js +133 -0
  137. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
  138. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +105 -0
  139. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
  140. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +351 -0
  141. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  142. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  143. package/components/routes/cases/detail/sidebar/utils.js +25 -0
  144. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  145. package/components/routes/cases/hooks/useCase.js +51 -0
  146. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  147. package/components/routes/cases/modals/AddToCaseModal.js +62 -0
  148. package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
  149. package/components/routes/cases/modals/RenameItemModal.js +48 -0
  150. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  151. package/components/routes/cases/modals/ResolveModal.js +62 -0
  152. package/components/routes/dossiers/DossierEditor.js +2 -2
  153. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  154. package/components/routes/help/ApiDocumentation.js +1 -1
  155. package/components/routes/help/HitBannerDocumentation.js +1 -0
  156. package/components/routes/help/HitDocumentation.js +1 -3
  157. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  158. package/components/routes/hits/search/InformationPane.js +47 -60
  159. package/components/routes/hits/search/LayoutSettings.js +3 -3
  160. package/components/routes/hits/search/QuerySettings.js +2 -1
  161. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  162. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  163. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  164. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  165. package/components/routes/hits/search/SearchPane.js +26 -49
  166. package/components/routes/hits/search/ViewLink.js +3 -3
  167. package/components/routes/hits/search/ViewLink.test.js +8 -8
  168. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  169. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  170. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  171. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  172. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  173. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  174. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  175. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  176. package/components/routes/hits/view/HitViewer.js +12 -13
  177. package/components/routes/home/ViewCard.js +47 -41
  178. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  179. package/components/routes/observables/ObservableViewer.js +27 -0
  180. package/components/routes/overviews/OverviewViewer.js +2 -2
  181. package/components/routes/views/ViewComposer.js +46 -19
  182. package/locales/en/translation.json +80 -3
  183. package/locales/fr/translation.json +78 -3
  184. package/models/WithMetadata.d.ts +2 -1
  185. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  186. package/models/entities/generated/Case.d.ts +28 -0
  187. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  188. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  189. package/models/entities/generated/EmailParent.d.ts +19 -0
  190. package/models/entities/generated/Enrichments.d.ts +7 -0
  191. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  192. package/models/entities/generated/Hit.d.ts +1 -0
  193. package/models/entities/generated/Howler.d.ts +0 -4
  194. package/models/entities/generated/HttpResponse.d.ts +11 -0
  195. package/models/entities/generated/Item.d.ts +9 -0
  196. package/models/entities/generated/Observable.d.ts +85 -0
  197. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  198. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  199. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  200. package/models/entities/generated/ObservableFile.d.ts +36 -0
  201. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  202. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  203. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  204. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  205. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  206. package/models/entities/generated/ObservableSource.d.ts +23 -0
  207. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  208. package/models/entities/generated/ObservableTls.d.ts +12 -0
  209. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  210. package/models/entities/generated/Rule.d.ts +2 -10
  211. package/models/entities/generated/Task.d.ts +10 -0
  212. package/models/entities/generated/Threat.d.ts +2 -2
  213. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  214. package/models/entities/generated/View.d.ts +1 -0
  215. package/package.json +18 -1
  216. package/plugins/clue/components/ClueTypography.js +2 -2
  217. package/plugins/clue/utils.d.ts +2 -1
  218. package/tests/server-handlers.js +6 -1
  219. package/tests/utils.d.ts +4 -0
  220. package/tests/utils.js +20 -0
  221. package/utils/constants.d.ts +3 -3
  222. package/utils/hitFunctions.d.ts +2 -1
  223. package/utils/hitFunctions.js +4 -4
  224. package/utils/typeUtils.d.ts +7 -0
  225. package/utils/typeUtils.js +27 -0
  226. package/utils/viewUtils.js +3 -0
  227. package/components/app/providers/HitProvider.d.ts +0 -22
  228. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  229. package/components/elements/display/icons/BundleButton.js +0 -32
  230. package/components/elements/hit/HitRelated.d.ts +0 -6
  231. package/components/elements/hit/HitRelated.js +0 -7
  232. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  233. package/components/routes/help/BundleDocumentation.js +0 -12
  234. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  235. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  236. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  237. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  238. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  239. package/components/routes/hits/search/BundleScroller.js +0 -6
  240. package/components/routes/hits/search/HitContextMenu.js +0 -227
  241. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  242. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  243. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  244. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  245. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -3,74 +3,80 @@ import { OpenInNew } from '@mui/icons-material';
3
3
  import { Card, CardContent, IconButton, Skeleton, Stack, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
6
- import { useHitContextSelector } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
6
+ import { useHitContextSelector as useRecordContextSelector } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
7
7
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
8
8
  import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
9
9
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
10
+ import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
11
+ import RecordContextMenu from '@cccsaurora/howler-ui/components/elements/record/RecordContextMenu';
10
12
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
11
- import HitContextMenu from '@cccsaurora/howler-ui/components/routes/hits/search/HitContextMenu';
12
13
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
14
  import { useTranslation } from 'react-i18next';
14
15
  import { Link, useNavigate } from 'react-router-dom';
15
16
  import { useContextSelector } from 'use-context-selector';
17
+ import { isObservable } from '@cccsaurora/howler-ui/utils/typeUtils';
16
18
  import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
17
- // Custom hook to select hits by IDs with proper memoization
18
- const useSelectHitsByIds = (hitIds) => {
19
- const hitIdsRef = useRef(hitIds);
19
+ // Custom hook to select records by IDs with proper memoization
20
+ const useSelectRecordsByIds = (recordIds) => {
21
+ const recordIdsRef = useRef(recordIds);
20
22
  const prevResultRef = useRef([]);
21
- const prevHitIdsRef = useRef([]);
22
- // Keep ref up to date with latest hitIds
23
- hitIdsRef.current = hitIds;
23
+ const prevRecordIdsRef = useRef([]);
24
+ // Keep ref up to date with latest recordIds
25
+ recordIdsRef.current = recordIds;
24
26
  const selector = useCallback(ctx => {
25
- const currentHitIds = hitIdsRef.current;
26
- // Fast path: if hitIds array didn't change, check if hit objects changed
27
- if (prevHitIdsRef.current.length === currentHitIds.length &&
28
- currentHitIds.every((id, i) => id === prevHitIdsRef.current[i])) {
29
- // HitIds unchanged - check if any hit objects changed by reference
30
- const anyHitChanged = currentHitIds.some((id, i) => ctx.hits[id] !== prevResultRef.current[i]);
31
- if (!anyHitChanged) {
27
+ const currentRecordIds = recordIdsRef.current;
28
+ // Fast path: if recordIds array didn't change, check if record objects changed
29
+ if (prevRecordIdsRef.current.length === currentRecordIds.length &&
30
+ currentRecordIds.every((id, i) => id === prevRecordIdsRef.current[i])) {
31
+ // RecordIds unchanged - check if any record objects changed by reference
32
+ const anyRecordChanged = currentRecordIds.some((id, i) => ctx.records[id] !== prevResultRef.current[i]);
33
+ if (!anyRecordChanged) {
32
34
  return prevResultRef.current;
33
35
  }
34
36
  }
35
37
  // Something changed - rebuild the array
36
- const currentHits = currentHitIds.map(id => ctx.hits[id]).filter(Boolean);
37
- prevHitIdsRef.current = currentHitIds;
38
- prevResultRef.current = currentHits;
39
- return currentHits;
38
+ const currentRecords = currentRecordIds.map(id => ctx.records[id]).filter(Boolean);
39
+ prevRecordIdsRef.current = currentRecordIds;
40
+ prevResultRef.current = currentRecords;
41
+ return currentRecords;
40
42
  }, []); // Empty deps - selector never changes
41
- return useHitContextSelector(selector);
43
+ return useRecordContextSelector(selector);
42
44
  };
43
45
  // Utility functions
44
46
  const normalize = (val) => (val == null ? '' : String(val));
45
47
  // Have to normalize the fields as websockets and api return null and undefined respectively. This causes false positives when comparing signatures if not normalized to a consistent value. We also stringify non-primitive values to ensure changes are detected.
46
- const createHitSignature = (hit) => {
47
- if (!hit)
48
+ const createRecordSignature = (record) => {
49
+ if (!record) {
48
50
  return '';
49
- return `${hit.howler?.id}:${normalize(hit.howler?.status)}:${normalize(hit.howler?.assignment)}:${normalize(hit.howler?.assessment)}`;
51
+ }
52
+ if (isObservable(record)) {
53
+ return record.howler?.id;
54
+ }
55
+ return `${record.howler?.id}:${normalize(record.howler?.status)}:${normalize(record.howler?.assignment)}:${normalize(record.howler?.assessment)}`;
50
56
  };
51
- const createSignatureFromHits = (hits) => {
52
- if (hits.length === 0)
57
+ const createSignatureFromRecords = (records) => {
58
+ if (records.length === 0)
53
59
  return '';
54
- return hits.map(createHitSignature).join('|');
60
+ return records.map(createRecordSignature).join('|');
55
61
  };
56
62
  const DEBOUNCE_TIME = 1000; // 1 second debounce for signature changes
57
63
  const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
58
64
  const navigate = useNavigate();
59
65
  const { t } = useTranslation();
60
66
  const { dispatchApi } = useMyApi();
61
- const [hitIds, setHitIds] = useState([]);
67
+ const [recordIds, setRecordIds] = useState([]);
62
68
  const [loading, setLoading] = useState(false);
63
69
  const debounceTimerRef = useRef(null);
64
70
  const isRefreshing = useRef(false);
65
71
  const lastSignature = useRef('');
66
72
  const view = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
67
73
  const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
68
- const loadHits = useHitContextSelector(ctx => ctx.loadHits);
74
+ const loadRecords = useRecordContextSelector(ctx => ctx.loadRecords);
69
75
  // Subscribe to hits from HitProvider cache based on current hitIds in the view
70
76
  // Uses memoized selector to avoid unnecessary re-renders on unrelated hit updates
71
- const hits = useSelectHitsByIds(hitIds);
77
+ const records = useSelectRecordsByIds(recordIds);
72
78
  // Create a stable signature that only changes when relevant fields change
73
- const hitsSignature = useMemo(() => createSignatureFromHits(hits), [hits]);
79
+ const recordsSignature = useMemo(() => createSignatureFromRecords(records), [records]);
74
80
  const refreshView = useCallback(async () => {
75
81
  if (!view?.query || isRefreshing.current) {
76
82
  onRefreshComplete?.();
@@ -78,21 +84,21 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
78
84
  }
79
85
  isRefreshing.current = true;
80
86
  try {
81
- const res = await dispatchApi(api.search.hit.post({
87
+ const res = await dispatchApi(api.v2.search.post(view.indexes, {
82
88
  query: view.query,
83
89
  rows: limit,
84
90
  metadata: ['analytic']
85
91
  }));
86
- const fetchedHits = res.items ?? [];
87
- loadHits(fetchedHits);
88
- setHitIds(fetchedHits.map(h => h.howler.id));
89
- lastSignature.current = createSignatureFromHits(fetchedHits);
92
+ const fetchedRecords = res.items ?? [];
93
+ loadRecords(fetchedRecords);
94
+ setRecordIds(fetchedRecords.map(r => r.howler.id));
95
+ lastSignature.current = createSignatureFromRecords(fetchedRecords);
90
96
  }
91
97
  finally {
92
98
  isRefreshing.current = false;
93
99
  onRefreshComplete?.();
94
100
  }
95
- }, [dispatchApi, limit, view?.query, loadHits, onRefreshComplete]);
101
+ }, [dispatchApi, limit, view?.query, view?.indexes, loadRecords, onRefreshComplete]);
96
102
  const debouncedRefresh = useCallback(() => {
97
103
  if (debounceTimerRef.current) {
98
104
  clearTimeout(debounceTimerRef.current);
@@ -125,16 +131,16 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
125
131
  }, [view?.query, limit, refreshView]);
126
132
  // Monitor hits currently in the view for changes that might affect query results
127
133
  useEffect(() => {
128
- if (!hitsSignature || hitIds.length === 0 || !lastSignature.current) {
129
- lastSignature.current = hitsSignature;
134
+ if (!recordsSignature || recordIds.length === 0 || !lastSignature.current) {
135
+ lastSignature.current = recordsSignature;
130
136
  return;
131
137
  }
132
138
  // Check if signature actually changed
133
- if (lastSignature.current === hitsSignature) {
139
+ if (lastSignature.current === recordsSignature) {
134
140
  return;
135
141
  }
136
142
  debouncedRefresh();
137
- }, [hitsSignature, hitIds, debouncedRefresh]);
143
+ }, [recordsSignature, recordIds, debouncedRefresh]);
138
144
  useEffect(() => {
139
145
  return () => {
140
146
  if (debounceTimerRef.current) {
@@ -151,6 +157,6 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
151
157
  }
152
158
  return selectedElement.id;
153
159
  }, []);
154
- return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", component: Link, disabled: !view, to: view ? buildViewUrl(view) : '', onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : hits.length > 0 ? (_jsx(HitContextMenu, { getSelectedId: getSelectedId, children: hits.map(h => (_jsx(Card, { id: h.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate((h.howler.is_bundle ? '/bundles/' : '/hits/') + h.howler.id), children: _jsx(CardContent, { children: _jsx(HitBanner, { layout: HitLayout.DENSE, hit: h }) }) }, h.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
160
+ return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", component: Link, disabled: !view, to: view ? buildViewUrl(view) : '', onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : records.length > 0 ? (_jsx(RecordContextMenu, { getSelectedId: getSelectedId, children: records.map((r) => (_jsx(Card, { id: r.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate(`/hits/${r.howler.id}`), children: _jsx(CardContent, { children: r.__index == 'hit' ? (_jsx(HitBanner, { layout: HitLayout.DENSE, hit: r })) : (_jsx(ObservableCard, { observable: r })) }) }, r.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
155
161
  };
156
162
  export default ViewCard;
@@ -0,0 +1,7 @@
1
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
2
+ import { type FC } from 'react';
3
+ declare const ObservableViewer: FC<{
4
+ observable?: Observable;
5
+ observableId?: string;
6
+ }>;
7
+ export default ObservableViewer;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Skeleton } from '@mui/material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
5
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
+ import { useEffect, useState } from 'react';
7
+ const ObservableViewer = ({ observable: provided, observableId }) => {
8
+ const { dispatchApi } = useMyApi();
9
+ const [observable, setObservable] = useState(null);
10
+ useEffect(() => {
11
+ if (provided) {
12
+ setObservable(provided);
13
+ }
14
+ }, [provided]);
15
+ useEffect(() => {
16
+ if (observableId) {
17
+ dispatchApi(api.v2.search.post('observable', { query: `howler.id:${observableId}`, rows: 1 }), {
18
+ throwError: false
19
+ }).then(res => setObservable(res.items[0]));
20
+ }
21
+ }, [dispatchApi, observableId]);
22
+ if (!observable) {
23
+ return;
24
+ }
25
+ return _jsx(Box, { p: 1, children: observable ? _jsx(ObjectDetails, { obj: observable }) : _jsx(Skeleton, { height: 120 }) });
26
+ };
27
+ export default ObservableViewer;
@@ -15,7 +15,7 @@ import useMyTheme from '@cccsaurora/howler-ui/components/hooks/useMyTheme';
15
15
  import { useSearchParams } from 'react-router-dom';
16
16
  import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
17
17
  import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
18
- import OverviewEditor from './OverviewEditor';
18
+ import MarkdownEditor from '../../elements/MarkdownEditor';
19
19
  import { useStartingTemplate } from './startingTemplate';
20
20
  const OverviewViewer = () => {
21
21
  const theme = useTheme();
@@ -188,7 +188,7 @@ const OverviewViewer = () => {
188
188
  right: `calc(50% + 7px - ${x}px)`,
189
189
  mr: -2.4,
190
190
  pr: 1.5
191
- }, children: _jsx(OverviewEditor, { height: "100%", content: content, setContent: setContent }) }) }), _jsx(Box, { sx: {
191
+ }, children: _jsx(MarkdownEditor, { height: "100%", content: content, setContent: setContent }) }) }), _jsx(Box, { sx: {
192
192
  position: 'absolute',
193
193
  top: 0,
194
194
  bottom: 0,
@@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next';
4
4
  import { HelpOutline, Save } from '@mui/icons-material';
5
5
  import { Alert, Checkbox, CircularProgress, LinearProgress, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
6
6
  import api from '@cccsaurora/howler-ui/api';
7
+ import {} from '@cccsaurora/howler-ui/api/search';
7
8
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
8
9
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
9
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
10
10
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
11
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
11
12
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
12
13
  import CustomButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomButton';
13
14
  import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexPort';
@@ -17,6 +18,7 @@ import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout
17
18
  import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
18
19
  import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
19
20
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
21
+ import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
20
22
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
21
23
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
22
24
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
@@ -26,8 +28,9 @@ import { DEFAULT_QUERY, StorageKey } from '@cccsaurora/howler-ui/utils/constants
26
28
  import { convertDateToLucene } from '@cccsaurora/howler-ui/utils/utils';
27
29
  import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
28
30
  import ErrorBoundary from '../ErrorBoundary';
29
- import HitQuery from '../hits/search/HitQuery';
31
+ import RecordQuery from '../hits/search/RecordQuery';
30
32
  import HitSort from '../hits/search/shared/HitSort';
33
+ import IndexPicker from '../hits/search/shared/IndexPicker';
31
34
  import SearchSpan from '../hits/search/shared/SearchSpan';
32
35
  const ViewComposer = () => {
33
36
  const { t } = useTranslation();
@@ -38,8 +41,10 @@ const ViewComposer = () => {
38
41
  const addView = useContextSelector(ViewContext, ctx => ctx.addView);
39
42
  const editView = useContextSelector(ViewContext, ctx => ctx.editView);
40
43
  const getCurrentViews = useContextSelector(ViewContext, ctx => ctx.getCurrentViews);
44
+ const indexes = useContextSelector(ParameterContext, ctx => ctx.indexes);
45
+ const setIndexes = useContextSelector(ParameterContext, ctx => ctx.setIndexes);
41
46
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
42
- const loadHits = useContextSelector(HitContext, ctx => ctx.loadHits);
47
+ const loadRecords = useContextSelector(RecordContext, ctx => ctx.loadRecords);
43
48
  // view state
44
49
  const [title, setTitle] = useState('');
45
50
  const [type, setType] = useState('global');
@@ -56,14 +61,17 @@ const ViewComposer = () => {
56
61
  const [searching, setSearching] = useState(false);
57
62
  const [error, setError] = useState(null);
58
63
  const [response, setResponse] = useState();
64
+ const [isLoadingView, setIsLoadingView] = useState(!!routeParams.id);
59
65
  const onSave = useCallback(async () => {
60
66
  setLoading(true);
61
67
  try {
68
+ const normalizedIndexes = indexes?.length > 0 ? indexes : ['hit'];
62
69
  if (!routeParams.id) {
63
70
  const newView = await addView({
64
71
  title,
65
72
  type,
66
73
  query,
74
+ indexes: normalizedIndexes,
67
75
  sort: sort || null,
68
76
  span: span || null,
69
77
  settings: {
@@ -77,6 +85,7 @@ const ViewComposer = () => {
77
85
  title,
78
86
  type,
79
87
  query,
88
+ indexes: normalizedIndexes,
80
89
  sort,
81
90
  span,
82
91
  settings: { advance_on_triage: advanceOnTriage }
@@ -101,23 +110,24 @@ const ViewComposer = () => {
101
110
  sort,
102
111
  span,
103
112
  advanceOnTriage,
113
+ indexes,
104
114
  navigate,
105
115
  editView,
106
116
  showErrorMessage
107
117
  ]);
108
- const search = useCallback(async (_query) => {
109
- setQuery(_query);
118
+ const performSearch = useCallback(async (searchQuery, searchIndexes, searchSort, searchSpan) => {
110
119
  setSearching(true);
111
120
  setError(null);
112
121
  try {
113
- const _response = await dispatchApi(api.search.hit.post({
122
+ const normalizedIndexes = searchIndexes?.length > 0 ? searchIndexes : ['hit'];
123
+ const _response = await dispatchApi(api.v2.search.post(normalizedIndexes, {
114
124
  rows: pageCount,
115
- query: _query,
116
- sort,
117
- filters: span ? [`event.created:${convertDateToLucene(span)}`] : [],
125
+ query: searchQuery,
126
+ sort: searchSort,
127
+ filters: searchSpan ? [`event.created:${convertDateToLucene(searchSpan)}`] : [],
118
128
  metadata: ['template', 'analytic']
119
129
  }), { showError: false, throwError: true });
120
- loadHits(_response.items);
130
+ loadRecords(_response.items);
121
131
  setResponse(_response);
122
132
  }
123
133
  catch (e) {
@@ -126,18 +136,25 @@ const ViewComposer = () => {
126
136
  finally {
127
137
  setSearching(false);
128
138
  }
129
- }, [dispatchApi, loadHits, pageCount, setQuery, sort, span]);
139
+ }, [dispatchApi, loadRecords, pageCount]);
140
+ const search = useCallback(async (_query) => {
141
+ setQuery(_query);
142
+ await performSearch(_query, indexes, sort, span);
143
+ }, [performSearch, indexes, sort, span, setQuery]);
130
144
  useEffect(() => {
131
- search(query || DEFAULT_QUERY);
145
+ // Only run initial search if we're NOT editing an existing view
146
+ if (!routeParams.id) {
147
+ search(query || DEFAULT_QUERY);
148
+ }
132
149
  // eslint-disable-next-line react-hooks/exhaustive-deps
133
- }, []);
150
+ }, [routeParams.id]);
134
151
  // We only run this when ancillary properties (i.e. filters, sorting) change
135
152
  useEffect(() => {
136
- if (query) {
153
+ if (query && !isLoadingView) {
137
154
  search(query);
138
155
  }
139
156
  // eslint-disable-next-line react-hooks/exhaustive-deps
140
- }, [sort, span]);
157
+ }, [sort, span, indexes, isLoadingView]);
141
158
  useEffect(() => {
142
159
  if (!routeParams.id) {
143
160
  return;
@@ -153,13 +170,23 @@ const ViewComposer = () => {
153
170
  }
154
171
  setTitle(viewToEdit.title);
155
172
  setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
156
- setQuery(viewToEdit.query);
173
+ const loadedQuery = viewToEdit.query || DEFAULT_QUERY;
174
+ const loadedIndexes = viewToEdit.indexes || indexes;
175
+ const loadedSort = viewToEdit.sort || sort;
176
+ const loadedSpan = viewToEdit.span || span;
177
+ setQuery(loadedQuery);
178
+ if (viewToEdit.indexes) {
179
+ setIndexes(loadedIndexes);
180
+ }
157
181
  if (viewToEdit.sort) {
158
- setSort(viewToEdit.sort);
182
+ setSort(loadedSort);
159
183
  }
160
184
  if (viewToEdit.span) {
161
- setSpan(viewToEdit.span);
185
+ setSpan(loadedSpan);
162
186
  }
187
+ // Perform search with the loaded values to avoid using stale state
188
+ await performSearch(loadedQuery, loadedIndexes, loadedSort, loadedSpan);
189
+ setIsLoadingView(false);
163
190
  })();
164
191
  // eslint-disable-next-line react-hooks/exhaustive-deps
165
192
  }, [routeParams.id]);
@@ -172,6 +199,6 @@ const ViewComposer = () => {
172
199
  fontSize: '0.9em',
173
200
  fontStyle: 'italic',
174
201
  mb: 0.5
175
- }), variant: "body2", children: t('hit.search.prompt') }), _jsx(HitQuery, { triggerSearch: search, searching: searching, onChange: (_query, isDirty) => setIsSearchDirty(isDirty) }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(HitSort, {}), _jsx(SearchSpan, { omitCustom: true }), _jsx("div", { style: { flex: 1 } }), _jsxs(Stack, { spacing: 1, direction: "row", alignItems: "center", sx: { flex: '0 !important', minWidth: '300px' }, children: [_jsx(Typography, { component: "span", children: t('view.settings.advance_on_triage') }), _jsx(Tooltip, { title: t('view.settings.advance_on_triage.description'), children: _jsx(HelpOutline, { sx: { fontSize: '16px' } }) }), _jsx(Checkbox, { size: "small", checked: advanceOnTriage, onChange: (_event, checked) => setAdvanceOnTriage(checked) })] })] }), response?.total ? (_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) })) : null, _jsx(LinearProgress, { sx: [!searching && { opacity: 0 }] })] }) }), _jsx(VSBoxContent, { children: _jsxs(Stack, { spacing: 1, children: [!response?.total && _jsx(AppListEmpty, {}), response?.items.map(hit => (_jsx(HitCard, { id: hit.howler.id, layout: HitLayout.DENSE }, hit.howler.id)))] }) })] }) }) }) }));
202
+ }), variant: "body2", children: t('hit.search.prompt') }), _jsx(RecordQuery, { triggerSearch: search, searching: searching, onChange: (_query, isDirty) => setIsSearchDirty(isDirty) }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(IndexPicker, {}), _jsx(HitSort, {}), _jsx(SearchSpan, { omitCustom: true }), _jsx("div", { style: { flex: 1 } }), _jsxs(Stack, { spacing: 1, direction: "row", alignItems: "center", sx: { flex: '0 !important', minWidth: '300px' }, children: [_jsx(Typography, { component: "span", children: t('view.settings.advance_on_triage') }), _jsx(Tooltip, { title: t('view.settings.advance_on_triage.description'), children: _jsx(HelpOutline, { sx: { fontSize: '16px' } }) }), _jsx(Checkbox, { size: "small", checked: advanceOnTriage, onChange: (_event, checked) => setAdvanceOnTriage(checked) })] })] }), response?.total ? (_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) })) : null, _jsx(LinearProgress, { sx: [!searching && { opacity: 0 }] })] }) }), _jsx(VSBoxContent, { children: _jsxs(Stack, { spacing: 1, children: [!response?.total && _jsx(AppListEmpty, {}), response?.items.map(record => record.__index === 'hit' ? (_jsx(HitCard, { id: record.howler.id, layout: HitLayout.DENSE }, record.howler.id)) : (_jsx(ObservableCard, { observable: record }, record.howler.id)))] }) })] }) }) }) }));
176
203
  };
177
204
  export default ViewComposer;
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "*": "All values",
3
+ "Protected B": "Protected B",
4
+ "Unclassified//Official Use Only": "Unclassified//Official Use Only",
3
5
  "actions.running": "Action \"{{action}}\" is executing.",
4
6
  "actions.succeeded": "Action \"{{action}}\" completed successfully.",
5
7
  "add": "Add",
@@ -11,6 +13,7 @@
11
13
  "analytic.notebook.error.minFields": "Name and link are required.",
12
14
  "analytic.notebook.link": "Link",
13
15
  "analytic.notebook.name": "Name",
16
+ "analytic.open": "Open Analytic",
14
17
  "any": "Any",
15
18
  "api.user.apikey.removed": "API key removed successfully.",
16
19
  "api.user.apikey.updated": "New API key added successfully.",
@@ -79,6 +82,8 @@
79
82
  "comments.edit.stop": "Stop Editing",
80
83
  "comments.edited": "Edited",
81
84
  "comments.quote": "Quote Reply",
85
+ "complete": "Complete",
86
+ "crisis": "Crisis",
82
87
  "custom": "Custom",
83
88
  "date.range.1.day": "The last day",
84
89
  "date.range.1.month": "The last month",
@@ -104,8 +109,11 @@
104
109
  "drawer.expand": "Expand Menu",
105
110
  "duplicates.omitted": "Some duplicate entries have been omitted.",
106
111
  "edit": "Edit",
112
+ "event.module": "Event Module",
113
+ "event.type": "Event Type",
107
114
  "features.warning.description": "This feature is undergoing active development, and is not yet in a finished state. You may encounter bugs or instability.",
108
115
  "features.warning.title": "Feature In Active Development",
116
+ "focus": "Main focus",
109
117
  "global": "Global",
110
118
  "help.actions.introduction": "Introduction to Actions",
111
119
  "help.hit.banner.description": "See the below hit banner example for the hit keys necessary to properly populate it. If you have any additional questions, ask in the HOWLER support channel.",
@@ -162,16 +170,20 @@
162
170
  "hit.header.assignment": "Assignment: {{user}}",
163
171
  "hit.header.assignment.add": "Assign to a user",
164
172
  "hit.header.assignment.change": "Change assignment",
165
- "hit.header.bundlesize": "{{hits}} hits",
166
173
  "hit.header.escalation": "Escalation Level: ",
167
174
  "hit.header.indicators": "Indicators",
168
175
  "hit.header.rationale": "Rationale",
176
+ "hit.header.related": "{{count}} related records",
169
177
  "hit.header.scrutiny": "Scrutiny: ",
170
178
  "hit.header.status": "Status: ",
171
179
  "hit.header.summary": "Summary",
172
180
  "hit.header.target": "Target",
173
181
  "hit.header.threat": "Threat",
182
+ "hit.header.view.case": "View case {{id}}",
183
+ "hit.header.view.hit": "View hit {{id}}",
184
+ "hit.header.view.observable": "View observable {{id}}",
174
185
  "hit.header.votes": "Votes: ",
186
+ "hit.howler.related": "{{count}} related records",
175
187
  "hit.label": "Labels",
176
188
  "hit.label.category.assignments": "Category: Assignments - Specifies what analyst or team of analysts is assigned to triaging this hit.",
177
189
  "hit.label.category.campaign": "Catergory: Campaign - Identifies ongoing campaigns from specific malware families or phishing initiatives. They should only be considered correct attributions if the hit is promoted to evidence.",
@@ -187,15 +199,16 @@
187
199
  "hit.label.edit.add.error.duplicate": "Duplicated label not allowed",
188
200
  "hit.label.edit.add.error.empty": "Can't add an empty label",
189
201
  "hit.label.edit.add.label": "New label value",
202
+ "hit.label.edit.desc": "Add or remove labels",
190
203
  "hit.notebook.confirm.dialog": "A notebook with that name already exists in your environment, do you wish to overwrite it?",
191
204
  "hit.notebook.confirm.title": "Overwrite existing notebook?",
192
205
  "hit.notebook.error.failToPost": "Failed to send notebook to Jupyterhub, make sure your user environment is running.",
193
206
  "hit.notebook.goTo": "Go to Jupyterhub",
194
207
  "hit.notebook.select": "Please Select a notebook",
195
208
  "hit.notebook.tooltip": "Open in Jupyterhub",
209
+ "hit.open": "Open Hit",
196
210
  "hit.overview.missing": "No overview has been created for this hit. In order to create an overview, press the add button to the right.",
197
211
  "hit.panel.aggregation.run": "Create Summary",
198
- "hit.panel.analytic.open": "Open Analytic",
199
212
  "hit.panel.bundles.open": "Parent Bundles",
200
213
  "hit.panel.bundles.open.prompt": "Open Parent Bundle",
201
214
  "hit.panel.close": "Close",
@@ -208,9 +221,12 @@
208
221
  "hit.panel.exclude": "Exclude By",
209
222
  "hit.panel.hit.noselection": "No hit has been selected",
210
223
  "hit.panel.include": "Include By",
211
- "hit.panel.open": "Open Hit Viewer",
212
224
  "hit.panel.view.layout": "Change View Panel",
213
225
  "hit.quicksearch": "Search by assignment, analytic, detection or status",
226
+ "hit.related.tab.case": "Cases",
227
+ "hit.related.tab.hit": "Hits",
228
+ "hit.related.tab.links": "Links",
229
+ "hit.related.tab.observable": "Observables",
214
230
  "hit.search.aggregate.button": "Create Summary",
215
231
  "hit.search.button": "Perform search",
216
232
  "hit.search.custom": "Custom Sort",
@@ -219,6 +235,8 @@
219
235
  "hit.search.filter.fields": "Fields",
220
236
  "hit.search.filter.label": "Lookup Filters",
221
237
  "hit.search.filter.values": "Values",
238
+ "hit.search.index.hit": "Hits",
239
+ "hit.search.index.observable": "Observables",
222
240
  "hit.search.invalid": "Invalid Query",
223
241
  "hit.search.keyboard": "Keyboard shortcuts",
224
242
  "hit.search.keyboard.no_shortcuts": "No shortcuts",
@@ -256,6 +274,7 @@
256
274
  "hit.summary.subtitle": "Limited to a maximum of 10 000 hits.",
257
275
  "hit.summary.title": "Summary of Hits Over Time",
258
276
  "hit.summary.zoom.reset": "Reset Zoom",
277
+ "hit.view.overview": "Overview",
259
278
  "hit.viewer.aggregate": "Summary",
260
279
  "hit.viewer.comments": "Comments",
261
280
  "hit.viewer.data": "Raw Data",
@@ -291,6 +310,19 @@
291
310
  "modal.action.empty": "Action Name cannot be empty.",
292
311
  "modal.action.label": "Action Name",
293
312
  "modal.action.title": "Save Action",
313
+ "modal.cases.add_to_case": "Add to Case",
314
+ "modal.cases.add_to_case.full_path": "Full path: {{path}}",
315
+ "modal.cases.add_to_case.select_case": "Search Cases",
316
+ "modal.cases.add_to_case.select_path": "Select Folder Path",
317
+ "modal.cases.add_to_case.title": "Item Title",
318
+ "modal.cases.rename_item": "Rename Item",
319
+ "modal.cases.rename_item.error.empty": "Name cannot be empty",
320
+ "modal.cases.rename_item.error.slash": "Name cannot contain '/'",
321
+ "modal.cases.rename_item.error.taken": "An item already exists at this path",
322
+ "modal.cases.rename_item.folder_path": "Folder: {{path}}",
323
+ "modal.cases.rename_item.new_name": "New Name",
324
+ "modal.cases.resolve": "Resolve Case",
325
+ "modal.cases.resolve.description": "When resolving a case, you must either assess all open alerts, or add an assessment to the alerts.",
294
326
  "modal.confirm.delete.description": "Are you sure you want to delete this item?",
295
327
  "modal.confirm.delete.title": "Confirm Deletion",
296
328
  "modal.rationale.description": "Provide a rationale that succinctly explains to other analysts why you are confident in this assessment.",
@@ -312,6 +344,8 @@
312
344
  "modal.rule.title": "Create a New Rule",
313
345
  "no.data": "No Data",
314
346
  "none": "None",
347
+ "normal": "Normal Priority",
348
+ "observable.open": "Open Observable",
315
349
  "on": "on",
316
350
  "open": "Open",
317
351
  "operations.add_label": "Add Label",
@@ -331,6 +365,37 @@
331
365
  "owner": "Owner",
332
366
  "page.404.description": "The page you are looking for cannot be found...",
333
367
  "page.404.title": "404: Not found",
368
+ "page.cases.assets": "Assets",
369
+ "page.cases.assets.empty": "No assets found.",
370
+ "page.cases.assets.filter_by_type": "Filter by type:",
371
+ "page.cases.assets.seen_in": "Seen in",
372
+ "page.cases.assets.type.hash": "Hash",
373
+ "page.cases.assets.type.hosts": "Host",
374
+ "page.cases.assets.type.id": "ID",
375
+ "page.cases.assets.type.ids": "ID",
376
+ "page.cases.assets.type.ip": "IP",
377
+ "page.cases.assets.type.signature": "Signature",
378
+ "page.cases.assets.type.uri": "URI",
379
+ "page.cases.assets.type.user": "User",
380
+ "page.cases.created": "Created",
381
+ "page.cases.dashboard": "Dashboard",
382
+ "page.cases.dashboard.alerts": "Alerts",
383
+ "page.cases.dashboard.cases": "Related Cases",
384
+ "page.cases.dashboard.duration": "Duration",
385
+ "page.cases.dashboard.indicators": "Indicators",
386
+ "page.cases.dashboard.target": "Targets",
387
+ "page.cases.dashboard.tasks": "Tasks",
388
+ "page.cases.dashboard.threat": "Threats",
389
+ "page.cases.detail.participants": "Participants",
390
+ "page.cases.detail.properties": "Properties",
391
+ "page.cases.detail.status": "Status",
392
+ "page.cases.escalation": "Escalation",
393
+ "page.cases.sidebar.folder.remove": "Remove folder",
394
+ "page.cases.sidebar.item.open": "Open item",
395
+ "page.cases.sidebar.item.remove": "Remove item",
396
+ "page.cases.sidebar.item.rename": "Rename item",
397
+ "page.cases.sources": "Sources",
398
+ "page.cases.updated": "Updated",
334
399
  "page.dashboard.settings.edit": "Edit Dashboard",
335
400
  "page.dashboard.settings.refreshRate": "Refresh Rate",
336
401
  "page.dashboard.title": "Dashboard",
@@ -355,6 +420,7 @@
355
420
  "page.help": "Help",
356
421
  "page.help.title": "Help Dashboard",
357
422
  "page.login.button": "Sign in",
423
+ "page.login.error": "Just type anything in the username and password fields...",
358
424
  "page.login.password": "Password",
359
425
  "page.login.username": "Username",
360
426
  "page.logout": "Logging out current user ... ",
@@ -401,6 +467,8 @@
401
467
  "page.user.search.column.groups": "Groups",
402
468
  "page.user.search.column.username": "Username",
403
469
  "page.user.search.prompt": "Search by username, fullname, email or group",
470
+ "pages.cases.detail.participants": "Participants",
471
+ "pages.cases.detail.status": "Status",
404
472
  "password": "New Password",
405
473
  "password.confirm": "Confirm Password",
406
474
  "password.match": "Password and Confirm Password must match",
@@ -414,6 +482,7 @@
414
482
  "personalization.showbreadcrumbs": "Show Breadcrumbs",
415
483
  "personalization.sticky": "Sticky Topbar",
416
484
  "query": "Query",
485
+ "query.invalid": "Invalid query",
417
486
  "quicksearch.aria": "search",
418
487
  "quicksearch.placeholder": "Search ...",
419
488
  "rationale.default": "Hit assessed as {{assessment}}",
@@ -529,6 +598,11 @@
529
598
  "route.analytics.triage.title": "Triage Settings",
530
599
  "route.analytics.updated": "Analytic Updated!",
531
600
  "route.analytics.view": "View Analytic",
601
+ "route.cases": "Cases",
602
+ "route.cases.create": "Create Case",
603
+ "route.cases.manager.search": "Search Cases",
604
+ "route.cases.search.prompt": "Search Cases via title, summary or indicators",
605
+ "route.cases.view": "View Case",
532
606
  "route.clear": "Clear query",
533
607
  "route.dossiers": "Dossiers",
534
608
  "route.dossiers.create": "New Dossier",
@@ -591,8 +665,10 @@
591
665
  "route.help.actions": "Action Documentation",
592
666
  "route.help.api": "API Documentation",
593
667
  "route.help.auth": "Authentication",
668
+ "route.help.bundles": "Hit Bundles",
594
669
  "route.help.client": "Howler Client",
595
670
  "route.help.hit": "Hit Documentation",
671
+ "route.help.hit.banner": "Hit Banner Documentation",
596
672
  "route.help.main": "Dashboard",
597
673
  "route.help.notebook": "Notebook Documentation",
598
674
  "route.help.overviews": "Overviews",
@@ -705,6 +781,7 @@
705
781
  "search.result.showing": "Showing {{offset}} to {{length}} of {{total}} results",
706
782
  "search.result.showing.single": "No results",
707
783
  "search.total": "There are a total of {{count}} hits matching this query.",
784
+ "started": "Started",
708
785
  "templates.edit.analytic": "Edit analytic template",
709
786
  "templates.edit.detection": "Edit detection template",
710
787
  "to": "to",