@cccsaurora/howler-ui 2.13.0-dev.77 → 2.13.0

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 (118) hide show
  1. package/api/hit/index.d.ts +1 -1
  2. package/api/hit/index.js +6 -2
  3. package/api/search/index.d.ts +1 -0
  4. package/api/view/index.d.ts +1 -1
  5. package/api/view/index.js +2 -2
  6. package/commons/components/notification/elements/item/NotificationItemDate.js +2 -2
  7. package/commons/components/utils/hooks/useEnv.d.ts +1 -1
  8. package/components/app/App.js +1 -3
  9. package/components/app/drawers/ApiKeyDrawer.js +4 -4
  10. package/components/app/hooks/useMatchers.d.ts +9 -0
  11. package/components/app/hooks/useMatchers.js +82 -0
  12. package/components/app/hooks/useMatchers.test.d.ts +1 -0
  13. package/components/app/hooks/useMatchers.test.js +237 -0
  14. package/components/app/hooks/useTitle.js +5 -4
  15. package/components/app/providers/AnalyticProvider.d.ts +0 -4
  16. package/components/app/providers/AnalyticProvider.js +1 -44
  17. package/components/app/providers/ApiConfigProvider.js +2 -1
  18. package/components/app/providers/HitProvider.d.ts +2 -1
  19. package/components/app/providers/HitProvider.js +8 -2
  20. package/components/app/providers/HitSearchProvider.d.ts +2 -1
  21. package/components/app/providers/HitSearchProvider.js +2 -1
  22. package/components/app/providers/SocketProvider.js +1 -1
  23. package/components/app/providers/ViewProvider.d.ts +1 -1
  24. package/components/app/providers/ViewProvider.js +3 -3
  25. package/components/app/providers/ViewProvider.test.d.ts +1 -0
  26. package/components/app/providers/ViewProvider.test.js +150 -0
  27. package/components/elements/display/ActionButton.d.ts +8 -0
  28. package/components/elements/display/ActionButton.js +18 -0
  29. package/components/elements/display/handlebars/helpers.js +17 -2
  30. package/components/elements/display/json/JSONViewer.d.ts +2 -0
  31. package/components/elements/display/json/JSONViewer.js +6 -13
  32. package/components/elements/hit/HitActions.js +4 -6
  33. package/components/elements/hit/HitBanner.js +13 -5
  34. package/components/elements/hit/HitCard.js +0 -5
  35. package/components/elements/hit/HitComments.js +5 -4
  36. package/components/elements/hit/HitOutline.d.ts +2 -2
  37. package/components/elements/hit/HitOutline.js +11 -21
  38. package/components/elements/hit/HitOverview.js +7 -4
  39. package/components/elements/hit/HitSummary.d.ts +2 -1
  40. package/components/elements/hit/HitSummary.js +8 -7
  41. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -1
  42. package/components/elements/hit/aggregate/HitGraph.js +7 -7
  43. package/components/elements/hit/elements/HitTimestamp.js +8 -8
  44. package/components/elements/hit/related/PivotLink.js +11 -5
  45. package/components/elements/hit/related/RelatedIcon.d.ts +8 -0
  46. package/components/elements/hit/related/RelatedIcon.js +32 -0
  47. package/components/elements/hit/related/RelatedLink.js +4 -25
  48. package/components/hooks/useMyChart.d.ts +1 -1
  49. package/components/hooks/useMyChart.js +1 -1
  50. package/components/routes/advanced/QueryBuilder.js +47 -11
  51. package/components/routes/advanced/QueryEditor.js +8 -13
  52. package/components/routes/analytics/AnalyticOverview.d.ts +1 -1
  53. package/components/routes/analytics/AnalyticOverview.js +1 -1
  54. package/components/routes/analytics/AnalyticOverviews.d.ts +1 -1
  55. package/components/routes/analytics/AnalyticOverviews.js +1 -1
  56. package/components/routes/analytics/AnalyticSearch.js +14 -2
  57. package/components/routes/analytics/AnalyticTemplates.d.ts +1 -1
  58. package/components/routes/analytics/AnalyticTemplates.js +10 -7
  59. package/components/routes/analytics/RuleView.d.ts +1 -1
  60. package/components/routes/analytics/RuleView.js +1 -1
  61. package/components/routes/analytics/TriageSettings.d.ts +1 -1
  62. package/components/routes/analytics/TriageSettings.js +1 -1
  63. package/components/routes/analytics/widgets/Assessment.d.ts +1 -1
  64. package/components/routes/analytics/widgets/Assessment.js +1 -1
  65. package/components/routes/analytics/widgets/Created.d.ts +1 -1
  66. package/components/routes/analytics/widgets/Created.js +1 -1
  67. package/components/routes/analytics/widgets/Detection.d.ts +1 -1
  68. package/components/routes/analytics/widgets/Detection.js +1 -1
  69. package/components/routes/analytics/widgets/Escalation.d.ts +1 -1
  70. package/components/routes/analytics/widgets/Escalation.js +1 -1
  71. package/components/routes/analytics/widgets/Stacked.d.ts +1 -1
  72. package/components/routes/analytics/widgets/Stacked.js +1 -1
  73. package/components/routes/analytics/widgets/Status.d.ts +1 -1
  74. package/components/routes/analytics/widgets/Status.js +1 -1
  75. package/components/routes/help/SearchDocumentation.js +2 -1
  76. package/components/routes/help/TemplateDocumentation.js +5 -5
  77. package/components/routes/hits/search/HitBrowser.js +2 -2
  78. package/components/routes/hits/search/HitContextMenu.js +6 -7
  79. package/components/routes/hits/search/HitQuery.js +2 -1
  80. package/components/routes/hits/search/InformationPane.js +75 -78
  81. package/components/routes/hits/search/SearchPane.js +3 -9
  82. package/components/routes/hits/search/grid/AddColumnModal.js +10 -5
  83. package/components/routes/hits/search/grid/HitGrid.js +6 -5
  84. package/components/routes/hits/search/shared/CustomSpan.js +6 -6
  85. package/components/routes/hits/view/HitViewer.js +18 -26
  86. package/components/routes/home/index.js +4 -4
  87. package/components/routes/overviews/OverviewViewer.js +33 -31
  88. package/components/routes/settings/SecuritySection.js +2 -2
  89. package/components/routes/templates/TemplateViewer.js +27 -36
  90. package/components/routes/templates/Templates.js +4 -11
  91. package/components/routes/views/ViewComposer.js +8 -1
  92. package/components/routes/views/Views.js +25 -9
  93. package/index.js +7 -0
  94. package/locales/en/help/search.json +17 -0
  95. package/locales/en/translation.json +12 -3
  96. package/locales/fr/help/search.json +17 -0
  97. package/locales/fr/translation.json +12 -4
  98. package/models/WithMetadata.d.ts +10 -0
  99. package/models/WithMetadata.js +1 -0
  100. package/models/entities/generated/ApiType.d.ts +7 -0
  101. package/package.json +112 -111
  102. package/plugins/borealis/components/BorealisTypography.js +4 -2
  103. package/setupTests.d.ts +1 -0
  104. package/setupTests.js +12 -0
  105. package/tests/MockLocalStorage.d.ts +5 -0
  106. package/tests/MockLocalStorage.js +44 -0
  107. package/tests/server-handlers.d.ts +5 -0
  108. package/tests/server-handlers.js +97 -0
  109. package/tests/server.d.ts +3 -0
  110. package/tests/server.js +5 -0
  111. package/utils/constants.js +2 -2
  112. package/utils/stringUtils.d.ts +1 -0
  113. package/utils/stringUtils.js +9 -0
  114. package/utils/utils.js +3 -3
  115. package/components/app/providers/DossierProvider.d.ts +0 -16
  116. package/components/app/providers/DossierProvider.js +0 -82
  117. package/components/app/providers/TemplateProvider.d.ts +0 -14
  118. package/components/app/providers/TemplateProvider.js +0 -103
@@ -1,6 +1,7 @@
1
1
  import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
2
2
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
3
3
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
4
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
4
5
  import { type Dispatch, type FC, type PropsWithChildren, type SetStateAction } from 'react';
5
6
  export interface QueryEntry {
6
7
  [query: string]: string;
@@ -10,7 +11,7 @@ interface HitSearchProviderType {
10
11
  displayType: 'list' | 'grid';
11
12
  searching: boolean;
12
13
  error: string | null;
13
- response: HowlerSearchResponse<Hit> | null;
14
+ response: HowlerSearchResponse<WithMetadata<Hit>> | null;
14
15
  viewId: string | null;
15
16
  bundleId: string | null;
16
17
  queryHistory: QueryEntry;
@@ -86,7 +86,8 @@ const HitSearchProvider = ({ children }) => {
86
86
  query: fullQuery,
87
87
  sort,
88
88
  filters,
89
- track_total_hits: trackTotalHits
89
+ track_total_hits: trackTotalHits,
90
+ metadata: ['template', 'overview', 'analytic']
90
91
  }), { showError: false, throwError: true });
91
92
  if (_response.total < offset) {
92
93
  setOffset(0);
@@ -121,7 +121,7 @@ const SocketProvider = ({ children }) => {
121
121
  setRetry(false);
122
122
  // Here we go!
123
123
  setStatus(Status.CONNECTING);
124
- const host = window.location.host;
124
+ const host = window.location.host.includes('localhost') ? 'localhost:5000' : window.location.host;
125
125
  const protocol = window.location.protocol.startsWith('http:') ? 'ws' : 'wss';
126
126
  const ws = new WebSocket(`${protocol}://${host}/socket/v1/connect`);
127
127
  // Add our listeners to the websocket
@@ -10,7 +10,7 @@ export interface ViewContextType {
10
10
  removeFavourite: (id: string) => Promise<void>;
11
11
  fetchViews: (ids?: string[]) => Promise<View[]>;
12
12
  addView: (v: View) => Promise<View>;
13
- editView: (id: string, title: string, query: string, sort: string, span: string, advanceOnTriage: boolean) => Promise<View>;
13
+ editView: (id: string, newView: Partial<Omit<View, 'view_id' | 'owner'>>) => Promise<View>;
14
14
  removeView: (id: string) => Promise<void>;
15
15
  getCurrentView: (lazy?: boolean) => Promise<View>;
16
16
  }
@@ -69,11 +69,11 @@ const ViewProvider = ({ children }) => {
69
69
  }
70
70
  return views[id];
71
71
  }, [defaultView, fetchViews, location.pathname, routeParams.id, views]);
72
- const editView = useCallback(async (id, title, query, sort, span, advanceOnTriage) => {
73
- const result = await dispatchApi(api.view.put(id, title, query, sort, span, advanceOnTriage));
72
+ const editView = useCallback(async (id, partialView) => {
73
+ const result = await dispatchApi(api.view.put(id, partialView));
74
74
  setViews(_views => ({
75
75
  ..._views,
76
- [id]: { ...(_views[id] ?? {}), title, query, sort, span, settings: { advance_on_triage: advanceOnTriage } }
76
+ [id]: { ...(_views[id] ?? {}), ...partialView }
77
77
  }));
78
78
  return result;
79
79
  }, [dispatchApi]);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,150 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { act, renderHook, waitFor } from '@testing-library/react';
3
+ import { hget, hpost, hput } from '@cccsaurora/howler-ui/api';
4
+ import MockLocalStorage from '@cccsaurora/howler-ui/tests/MockLocalStorage';
5
+ import { MOCK_RESPONSES } from '@cccsaurora/howler-ui/tests/server-handlers';
6
+ import { useContextSelector } from 'use-context-selector';
7
+ import { MY_LOCAL_STORAGE_PREFIX, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
8
+ import ViewProvider, { ViewContext } from './ViewProvider';
9
+ let mockUser = {
10
+ favourite_views: ['favourited_view_id']
11
+ };
12
+ vi.mock('@cccsaurora/howler-ui/api', { spy: true });
13
+ vi.mock('react-router-dom', () => ({
14
+ useLocation: vi.fn(() => ({ pathname: '/views/searched_view_id' })),
15
+ useParams: vi.fn(() => ({ id: 'searched_view_id' }))
16
+ }));
17
+ vi.mock('@cccsaurora/howler-ui/commons/components/app/hooks', () => ({
18
+ useAppUser: () => ({
19
+ user: mockUser,
20
+ setUser: _user => (mockUser = _user)
21
+ })
22
+ }));
23
+ const mockLocalStorage = new MockLocalStorage();
24
+ // Replace localStorage in global scope
25
+ Object.defineProperty(window, 'localStorage', {
26
+ value: mockLocalStorage,
27
+ writable: true
28
+ });
29
+ const Wrapper = ({ children }) => {
30
+ return _jsx(ViewProvider, { children: children });
31
+ };
32
+ beforeEach(() => {
33
+ mockLocalStorage.clear();
34
+ });
35
+ describe('ViewContext', () => {
36
+ it('should fetch the defaultView on initialization', async () => {
37
+ mockLocalStorage.setItem(`${MY_LOCAL_STORAGE_PREFIX}.${StorageKey.DEFAULT_VIEW}`, JSON.stringify('searched_view_id'));
38
+ let hook = await act(async () => renderHook(() => useContextSelector(ViewContext, ctx => ctx.views), { wrapper: Wrapper }));
39
+ await waitFor(() => expect(hook.result.current.searched_view_id).not.toBeFalsy());
40
+ expect(hook.result.current.searched_view_id).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items[0]);
41
+ });
42
+ it('should allow the user to add and remove a favourite view', async () => {
43
+ const hook = await act(async () => {
44
+ return renderHook(() => useContextSelector(ViewContext, ctx => ({
45
+ addFavourite: ctx.addFavourite,
46
+ removeFavourite: ctx.removeFavourite
47
+ })), { wrapper: Wrapper });
48
+ });
49
+ await hook.result.current.addFavourite('example_view_id');
50
+ expect(mockUser.favourite_views).toEqual(['favourited_view_id', 'example_view_id']);
51
+ await hook.result.current.removeFavourite('example_view_id');
52
+ expect(mockUser.favourite_views).toEqual(['favourited_view_id']);
53
+ });
54
+ it('should allow the user to add and remove views', async () => {
55
+ const hook = await act(async () => {
56
+ return renderHook(() => useContextSelector(ViewContext, ctx => ({
57
+ addView: ctx.addView,
58
+ removeView: ctx.removeView,
59
+ views: ctx.views
60
+ })), { wrapper: Wrapper });
61
+ });
62
+ const result = await act(async () => hook.result.current.addView({
63
+ owner: 'user',
64
+ settings: {
65
+ advance_on_triage: false
66
+ },
67
+ view_id: 'example_created_view',
68
+ query: 'howler.id:*',
69
+ sort: 'event.created desc',
70
+ title: 'Example View',
71
+ type: 'personal',
72
+ span: 'date.range.1.month'
73
+ }));
74
+ hook.rerender();
75
+ expect(hook.result.current.views[result.view_id]).toEqual(result);
76
+ await act(async () => hook.result.current.removeView(result.view_id));
77
+ hook.rerender();
78
+ expect(hook.result.current.views[result.view_id]).toBeFalsy();
79
+ });
80
+ describe('fetchViews', () => {
81
+ let hook;
82
+ beforeEach(async () => {
83
+ hook = await act(async () => {
84
+ return renderHook(() => useContextSelector(ViewContext, ctx => ctx.fetchViews), { wrapper: Wrapper });
85
+ });
86
+ vi.mocked(hpost).mockClear();
87
+ vi.mocked(hget).mockClear();
88
+ });
89
+ it('Should fetch all views when no ids are provided', async () => {
90
+ const result = await act(async () => hook.result.current());
91
+ expect(result.length).toBe(2);
92
+ expect(result[0].view_id).toBe('example_view_id');
93
+ expect(result[1].view_id).toBe('another_view_id');
94
+ });
95
+ it('Should search for specified views when ids are provided', async () => {
96
+ const result = await act(async () => hook.result.current(['searched_view_id']));
97
+ expect(hpost).toHaveBeenCalledOnce();
98
+ expect(hpost).toBeCalledWith('/api/v1/search/view', { query: 'view_id:(searched_view_id)', rows: 1 });
99
+ expect(result).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items);
100
+ });
101
+ it('Should search only for new views when ids are provided', async () => {
102
+ await act(async () => hook.result.current(['searched_view_id']));
103
+ expect(hpost).toHaveBeenCalledOnce();
104
+ expect(hpost).toBeCalledWith('/api/v1/search/view', { query: 'view_id:(searched_view_id)', rows: 1 });
105
+ vi.mocked(hpost).mockClear();
106
+ await act(async () => hook.result.current(['searched_view_id', 'searched_view_id_2']));
107
+ expect(hpost).toHaveBeenCalledOnce();
108
+ expect(hpost).toBeCalledWith('/api/v1/search/view', { query: 'view_id:(searched_view_id_2)', rows: 1 });
109
+ });
110
+ it('Should provide cached instances as a response when the same views are requested', async () => {
111
+ let result = await act(async () => hook.result.current(['searched_view_id']));
112
+ expect(result).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items);
113
+ result = await act(async () => hook.result.current(['searched_view_id']));
114
+ expect(result).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items);
115
+ expect(hpost).toHaveBeenCalledOnce();
116
+ });
117
+ });
118
+ describe('getCurrentView', () => {
119
+ let hook;
120
+ beforeAll(async () => {
121
+ hook = await act(async () => {
122
+ return renderHook(() => useContextSelector(ViewContext, ctx => ctx.getCurrentView), { wrapper: Wrapper });
123
+ });
124
+ });
125
+ it('should allow the user to fetch their current view based on the location', async () => {
126
+ // lazy load should return nothing
127
+ await expect(hook.result.current(true)).resolves.toBeFalsy();
128
+ const result = await act(async () => hook.result.current());
129
+ expect(result).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items[0]);
130
+ });
131
+ });
132
+ describe('editView', () => {
133
+ let hook;
134
+ beforeAll(async () => {
135
+ hook = await act(async () => {
136
+ return renderHook(() => useContextSelector(ViewContext, ctx => ctx.editView), { wrapper: Wrapper });
137
+ });
138
+ });
139
+ beforeEach(() => {
140
+ vi.mocked(hput).mockClear();
141
+ vi.mocked(hpost).mockClear();
142
+ });
143
+ it('should allow users to edit views', async () => {
144
+ const result = await act(async () => hook.result.current('example_view_id', { query: 'howler.id:*' }));
145
+ expect(hput).toHaveBeenCalledOnce();
146
+ expect(hput).toBeCalledWith('/api/v1/view/example_view_id', { query: 'howler.id:*' });
147
+ expect(result).toEqual(MOCK_RESPONSES['/api/v1/view/example_view_id']);
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,8 @@
1
+ import { type ButtonProps } from '@mui/material';
2
+ import { type FC } from 'react';
3
+ declare const ActionButton: FC<{
4
+ actionId: string;
5
+ hitId: string;
6
+ label: string;
7
+ } & ButtonProps>;
8
+ export default ActionButton;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button } from '@mui/material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
+ import useMyActionFunctions from '@cccsaurora/howler-ui/components/routes/action/useMyActionFunctions';
6
+ import { useEffect, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ const ActionButton = ({ actionId, hitId, label, ...otherProps }) => {
9
+ const { t } = useTranslation();
10
+ const { dispatchApi } = useMyApi();
11
+ const { executeAction } = useMyActionFunctions();
12
+ const [action, setAction] = useState(null);
13
+ useEffect(() => {
14
+ dispatchApi(api.search.action.post({ query: `action_id:${actionId}`, rows: 1 })).then(result => setAction(result.items[0]));
15
+ }, [actionId, dispatchApi]);
16
+ return (_jsx(Button, { variant: otherProps.variant ?? 'outlined', disabled: !action, onClick: () => executeAction(actionId, `howler.id:${hitId}`), color: otherProps.color ?? 'primary', children: label ?? action?.name ?? t('loading') }));
17
+ };
18
+ export default ActionButton;
@@ -10,7 +10,9 @@ import { capitalize, get, groupBy, isObject } from 'lodash-es';
10
10
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
11
11
  import { useMemo } from 'react';
12
12
  import { usePluginStore } from 'react-pluggable';
13
+ import ActionButton from '../ActionButton';
13
14
  import JSONViewer from '../json/JSONViewer';
15
+ const FETCH_RESULTS = {};
14
16
  export const useHelpers = () => {
15
17
  const pluginStore = usePluginStore();
16
18
  const allHelpers = useMemo(() => [
@@ -55,8 +57,10 @@ export const useHelpers = () => {
55
57
  documentation: 'Fetches the url provided and returns the given (flattened) key from the returned JSON object. Note that the result must be JSON!',
56
58
  callback: async (url, key) => {
57
59
  try {
58
- const response = await fetch(url);
59
- const json = await response.json();
60
+ if (!FETCH_RESULTS[url]) {
61
+ FETCH_RESULTS[url] = fetch(url).then(res => res.json());
62
+ }
63
+ const json = await FETCH_RESULTS[url];
60
64
  return flatten(json)[key];
61
65
  }
62
66
  catch (e) {
@@ -144,6 +148,17 @@ export const useHelpers = () => {
144
148
  }) })] }) }));
145
149
  }
146
150
  },
151
+ {
152
+ keyword: 'action',
153
+ documentation: 'Execute a howler action given a specific action ID (from the URL when viewing the action, i.e. yaIKVqiKhWpyCsWdqsE4D)',
154
+ componentCallback: (actionId, hitId, context) => {
155
+ if (!actionId || !hitId) {
156
+ console.warn('Missing parameters for the action button.');
157
+ return null;
158
+ }
159
+ return _jsx(ActionButton, { actionId: actionId, hitId: hitId, ...(context.hash ?? {}) });
160
+ }
161
+ },
147
162
  ...howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.helpers`))
148
163
  ], [pluginStore]);
149
164
  return allHelpers;
@@ -2,5 +2,7 @@ import { type FC } from 'react';
2
2
  declare const JSONViewer: FC<{
3
3
  data: object;
4
4
  collapse?: boolean;
5
+ hideSearch?: boolean;
6
+ filter?: string;
5
7
  }>;
6
8
  export default JSONViewer;
@@ -8,10 +8,11 @@ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/us
8
8
  import { useCallback, useEffect, useMemo, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
11
+ import { validateRegex } from '@cccsaurora/howler-ui/utils/stringUtils';
11
12
  import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
12
13
  import { removeEmpty, searchObject } from '@cccsaurora/howler-ui/utils/utils';
13
14
  const THROTTLER = new Throttler(150);
14
- const JSONViewer = ({ data, collapse = true }) => {
15
+ const JSONViewer = ({ data, collapse = true, hideSearch = false, filter }) => {
15
16
  const { t } = useTranslation();
16
17
  const { isDark } = useAppTheme();
17
18
  const [compact] = useMyLocalStorageItem(StorageKey.COMPACT_JSON, true);
@@ -21,19 +22,11 @@ const JSONViewer = ({ data, collapse = true }) => {
21
22
  useEffect(() => {
22
23
  THROTTLER.debounce(() => {
23
24
  const filteredData = removeEmpty(data, compact);
24
- const searchedData = searchObject(filteredData, query, flat);
25
+ const searchedData = searchObject(filteredData, filter ?? query, flat);
25
26
  setResult(searchedData);
26
27
  });
27
- }, [compact, data, flat, query]);
28
- const hasError = useMemo(() => {
29
- try {
30
- new RegExp(query);
31
- return false;
32
- }
33
- catch (e) {
34
- return true;
35
- }
36
- }, [query]);
28
+ }, [compact, data, filter, flat, query]);
29
+ const hasError = useMemo(() => !validateRegex(filter ?? query), [query, filter]);
37
30
  const shouldCollapse = useCallback((field) => {
38
31
  return (field.name !== 'root' && field.type !== 'object') || field.namespace.length > 3;
39
32
  }, []);
@@ -48,6 +41,6 @@ const JSONViewer = ({ data, collapse = true }) => {
48
41
  // Type declaration is wrong - this is a valid prop
49
42
  displayArrayKey: !compact
50
43
  } })), [collapse, compact, isDark, result, shouldCollapse]);
51
- return data ? (_jsxs(Stack, { direction: "column", spacing: 1, sx: { '& > div:first-of-type': { mt: 1, mr: 0.5 } }, children: [_jsx(Phrase, { value: query, onChange: setQuery, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), disabled: !result, endAdornment: _jsx(IconButton, { onClick: () => setQuery(''), children: _jsx(Clear, {}) }) }), renderer] })) : (_jsx(Skeleton, { width: "100%", height: "95%", variant: "rounded" }));
44
+ return data ? (_jsxs(Stack, { direction: "column", spacing: 1, sx: { '& > div:first-of-type': { mt: 1, mr: 0.5 } }, children: [!hideSearch && (_jsx(Phrase, { value: query, onChange: setQuery, error: hasError, label: t('json.viewer.search.label'), placeholder: t('json.viewer.search.prompt'), disabled: !result, endAdornment: _jsx(IconButton, { onClick: () => setQuery(''), children: _jsx(Clear, {}) }) })), renderer] })) : (_jsx(Skeleton, { width: "100%", height: "95%", variant: "rounded" }));
52
45
  };
53
46
  export default JSONViewer;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { MoreHoriz } from '@mui/icons-material';
3
3
  import { Box, CircularProgress, Divider, FormControl, FormControlLabel, FormLabel, IconButton, Menu, Radio, RadioGroup, Stack, Switch, useMediaQuery } from '@mui/material';
4
- import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
4
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
5
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
6
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
7
7
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
@@ -27,7 +27,7 @@ const HitActions = ({ hit, orientation = 'horizontal' }) => {
27
27
  const { config } = useContext(ApiConfigContext);
28
28
  const { values, set } = useMyLocalStorageProvider();
29
29
  const pluginStore = usePluginStore();
30
- const { getAnalyticFromName } = useContext(AnalyticContext);
30
+ const { getMatchingAnalytic } = useMatchers();
31
31
  const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
32
32
  const selected = useContextSelector(ParameterContext, ctx => ctx?.selected);
33
33
  const setSelected = useContextSelector(ParameterContext, ctx => ctx?.setSelected);
@@ -112,11 +112,9 @@ const HitActions = ({ hit, orientation = 'horizontal' }) => {
112
112
  }
113
113
  }, [keyboardDownHandler]);
114
114
  useEffect(() => {
115
- (async () => {
116
- setAnalytic(await getAnalyticFromName(hit.howler.analytic));
117
- })();
115
+ getMatchingAnalytic(hit).then(setAnalytic);
118
116
  // eslint-disable-next-line react-hooks/exhaustive-deps
119
- }, [hit.howler.analytic]);
117
+ }, [hit?.howler.analytic]);
120
118
  const handleOpenSetting = useCallback((e) => setOpenSetting(e.currentTarget), []);
121
119
  const handleCloseSetting = useCallback(() => setOpenSetting(null), []);
122
120
  const onShortcutChange = useCallback((__, s) => set(StorageKey.HIT_SHORTCUTS, s), [set]);
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Chip, Divider, Grid, Stack, Tooltip, Typography, avatarClasses, iconButtonClasses, useTheme } from '@mui/material';
3
- import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
3
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
4
4
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
5
  import { uniq } from 'lodash-es';
6
6
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
@@ -19,15 +19,19 @@ import { HitLayout } from './HitLayout';
19
19
  const HitBanner = ({ hit, layout = HitLayout.NORMAL, showAssigned = true }) => {
20
20
  const { t } = useTranslation();
21
21
  const { config } = useContext(ApiConfigContext);
22
- const { getIdFromName } = useContext(AnalyticContext);
23
22
  const theme = useTheme();
24
23
  const pluginStore = usePluginStore();
24
+ const { getMatchingAnalytic } = useMatchers();
25
25
  const [analyticId, setAnalyticId] = useState();
26
26
  const compressed = useMemo(() => layout === HitLayout.DENSE, [layout]);
27
27
  const textVariant = useMemo(() => (layout === HitLayout.COMFY ? 'body1' : 'caption'), [layout]);
28
28
  useEffect(() => {
29
- getIdFromName(hit?.howler.analytic).then(setAnalyticId);
30
- }, [getIdFromName, hit]);
29
+ if (!hit?.howler.analytic) {
30
+ return;
31
+ }
32
+ getMatchingAnalytic(hit).then(analytic => setAnalyticId(analytic.analytic_id));
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ }, [hit?.howler.analytic]);
31
35
  const providerColor = useMemo(() => PROVIDER_COLORS[hit.event?.provider ?? 'unknown'] ?? stringToColor(hit.event.provider), [hit.event?.provider]);
32
36
  const mitreId = useMemo(() => {
33
37
  if (hit.threat?.framework?.toLowerCase().startsWith('mitre')) {
@@ -94,7 +98,11 @@ const HitBanner = ({ hit, layout = HitLayout.NORMAL, showAssigned = true }) => {
94
98
  }, spacing: layout !== HitLayout.COMFY ? 1 : 2, divider: _jsx(Divider, { orientation: "horizontal", sx: [
95
99
  layout !== HitLayout.COMFY && { marginTop: '4px !important' },
96
100
  { mr: `${theme.spacing(-1)} !important` }
97
- ] }), children: [_jsxs(Typography, { variant: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf: 'start', '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, onClick: e => e.stopPropagation(), children: hit.howler.analytic })) : (hit.howler.analytic), hit.howler.detection && ': ', hit.howler.detection] }), hit.howler?.rationale && (_jsxs(Typography, { flex: 1, variant: textVariant, color: ESCALATION_COLORS[hit.howler.escalation] + '.main', sx: { fontWeight: 'bold' }, children: [t('hit.header.rationale'), ": ", hit.howler.rationale] })), hit.howler?.outline && (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: layout !== HitLayout.COMFY ? 1 : 2, sx: { ml: `${theme.spacing(-1)} !important` }, children: [hit.howler.outline.threat && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.threat", value: hit.howler.outline.threat }) })), hit.howler.outline.target && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.target", value: hit.howler.outline.target }) }))] }), hit.howler.outline.indicators?.length > 0 && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Typography, { component: "span", variant: textVariant, children: [t('hit.header.indicators'), ":"] }), _jsx(Grid, { container: true, spacing: 0.5, sx: { mt: `${theme.spacing(-0.5)} !important`, ml: `${theme.spacing(0.25)} !important` }, children: uniq(hit.howler.outline.indicators).map((_indicator, index) => {
101
+ ] }), children: [_jsxs(Typography, { variant: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf: 'start', '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, onAuxClick: e => {
102
+ e.stopPropagation();
103
+ }, onClick: e => {
104
+ e.stopPropagation();
105
+ }, children: hit.howler.analytic })) : (hit.howler.analytic), hit.howler.detection && ': ', hit.howler.detection] }), hit.howler?.rationale && (_jsxs(Typography, { flex: 1, variant: textVariant, color: ESCALATION_COLORS[hit.howler.escalation] + '.main', sx: { fontWeight: 'bold' }, children: [t('hit.header.rationale'), ": ", hit.howler.rationale] })), hit.howler?.outline && (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: layout !== HitLayout.COMFY ? 1 : 2, sx: { ml: `${theme.spacing(-1)} !important` }, children: [hit.howler.outline.threat && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.threat", value: hit.howler.outline.threat }) })), hit.howler.outline.target && (_jsx(Grid, { item: true, children: _jsx(Wrapper, { i18nKey: "hit.header.target", value: hit.howler.outline.target }) }))] }), hit.howler.outline.indicators?.length > 0 && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Typography, { component: "span", variant: textVariant, children: [t('hit.header.indicators'), ":"] }), _jsx(Grid, { container: true, spacing: 0.5, sx: { mt: `${theme.spacing(-0.5)} !important`, ml: `${theme.spacing(0.25)} !important` }, children: uniq(hit.howler.outline.indicators).map((_indicator, index) => {
98
106
  return (_jsx(Grid, { item: true, children: _jsxs(Stack, { direction: "row", children: [_jsx(PluginTypography, { context: "indicators", variant: textVariant, value: _indicator, children: _indicator }), index < hit.howler.outline.indicators.length - 1 && (_jsx(Typography, { variant: textVariant, children: ',' }))] }) }, _indicator));
99
107
  }) })] })), hit.howler.outline.summary && (_jsx(Wrapper, { i18nKey: "hit.header.summary", value: hit.howler.outline.summary, paragraph: true, textOverflow: "wrap", sx: [compressed && { marginTop: `0 !important` }] }))] }))] }), _jsxs(Stack, { direction: "column", spacing: layout !== HitLayout.COMFY ? 0.5 : 1, alignSelf: "stretch", sx: [
100
108
  { minWidth: 0, alignItems: { sm: 'end', md: 'start' }, flex: 1, pl: 1 },
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { CardContent, Skeleton } from '@mui/material';
3
3
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
4
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
5
4
  import { memo, useEffect } from 'react';
6
5
  import { useContextSelector } from 'use-context-selector';
7
6
  import HowlerCard from '../display/HowlerCard';
@@ -10,12 +9,8 @@ import HitLabels from './HitLabels';
10
9
  import { HitLayout } from './HitLayout';
11
10
  import HitOutline from './HitOutline';
12
11
  const HitCard = ({ id, layout, readOnly = true }) => {
13
- const refresh = useContextSelector(TemplateContext, ctx => ctx.refresh);
14
12
  const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
15
13
  const hit = useContextSelector(HitContext, ctx => ctx.hits[id]);
16
- useEffect(() => {
17
- refresh();
18
- }, [refresh]);
19
14
  useEffect(() => {
20
15
  if (!hit) {
21
16
  getHit(id);
@@ -3,7 +3,7 @@ import { Clear, KeyboardArrowDown, Send } from '@mui/icons-material';
3
3
  import { Accordion, AccordionDetails, AccordionSummary, AvatarGroup, Chip, IconButton, Skeleton, Stack, TextField, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
- import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
6
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
7
7
  import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
8
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
9
9
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
@@ -20,8 +20,8 @@ const HitComments = ({ hit, users }) => {
20
20
  const { t } = useTranslation();
21
21
  const navigate = useNavigate();
22
22
  const { dispatchApi } = useMyApi();
23
- const { getAnalyticFromName } = useContext(AnalyticContext);
24
23
  const { addListener, removeListener, emit } = useContext(SocketContext);
24
+ const { getMatchingAnalytic } = useMatchers();
25
25
  const [typers, setTypers] = useState([]);
26
26
  const [loading, setLoading] = useState(false);
27
27
  const [showClear, setShowClear] = useState(false);
@@ -51,12 +51,13 @@ const HitComments = ({ hit, users }) => {
51
51
  }, [handler]);
52
52
  useEffect(() => {
53
53
  if (hit?.howler?.analytic) {
54
- getAnalyticFromName(hit?.howler?.analytic).then(analytic => {
54
+ getMatchingAnalytic(hit).then(analytic => {
55
55
  setAnalyticId(analytic?.analytic_id);
56
56
  setAnalyticComments(sortByTimestamp(analytic?.comment ?? []));
57
57
  });
58
58
  }
59
- }, [getAnalyticFromName, hit?.howler?.analytic]);
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [getMatchingAnalytic, hit?.howler?.analytic]);
60
61
  const onSubmit = useCallback(async () => {
61
62
  if (!input.current?.value || !hit || input.current.value.length > MAX_LENGTH)
62
63
  return;
@@ -1,9 +1,9 @@
1
1
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
2
3
  import { HitLayout } from './HitLayout';
3
4
  export declare const DEFAULT_FIELDS: string[];
4
5
  declare const _default: import("react").NamedExoticComponent<{
5
- hit: Hit;
6
+ hit: WithMetadata<Hit>;
6
7
  layout: HitLayout;
7
- type?: "global" | "personal";
8
8
  }>;
9
9
  export default _default;
@@ -1,30 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Divider, Skeleton, Typography } from '@mui/material';
3
- import { TemplateContext } from '@cccsaurora/howler-ui/components/app/providers/TemplateProvider';
4
- import { createElement, memo, useMemo } from 'react';
2
+ import { Box, Divider, Typography } from '@mui/material';
3
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
4
+ import { createElement, memo, useEffect, useMemo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
- import { useContextSelector } from 'use-context-selector';
7
6
  import { HitLayout } from './HitLayout';
8
7
  import DefaultOutline from './outlines/DefaultOutline';
9
8
  export const DEFAULT_FIELDS = ['howler.hash'];
10
- const HitOutline = ({ hit, layout, type }) => {
9
+ const HitOutline = ({ hit, layout }) => {
11
10
  const { t } = useTranslation();
12
- const loaded = useContextSelector(TemplateContext, ctx => ctx.loaded);
13
- const getMatchingTemplate = useContextSelector(TemplateContext, ctx => ctx.getMatchingTemplate);
14
- const template = useMemo(() => getMatchingTemplate(hit), [getMatchingTemplate, hit]);
11
+ const { getMatchingTemplate } = useMatchers();
12
+ const [template, setTemplate] = useState(null);
13
+ useEffect(() => {
14
+ getMatchingTemplate(hit).then(setTemplate);
15
+ }, [getMatchingTemplate, hit]);
15
16
  const outline = useMemo(() => {
16
- if (template && template.type === type) {
17
- return createElement(DefaultOutline, {
18
- hit,
19
- layout,
20
- template,
21
- fields: template.keys
22
- });
23
- }
24
- else if (!loaded) {
25
- return _jsx(Skeleton, { variant: "rounded", height: "50px" });
26
- }
27
- else if (template) {
17
+ if (template) {
28
18
  return createElement(DefaultOutline, {
29
19
  hit,
30
20
  layout,
@@ -40,7 +30,7 @@ const HitOutline = ({ hit, layout, type }) => {
40
30
  fields: DEFAULT_FIELDS
41
31
  });
42
32
  }
43
- }, [hit, layout, loaded, template, type]);
33
+ }, [hit, layout, template]);
44
34
  return (_jsxs(Box, { sx: { py: 1, width: '100%', pr: 2 }, children: [layout === HitLayout.COMFY && (_jsx(Typography, { variant: "body1", fontWeight: "bold", sx: { mb: 1 }, children: t('hit.details.title') })), layout !== HitLayout.DENSE && _jsx(Divider, { orientation: "horizontal", sx: { mb: 1 } }), outline] }));
45
35
  };
46
36
  export default memo(HitOutline);
@@ -1,14 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { InsertLink } from '@mui/icons-material';
3
3
  import { Box, IconButton, Skeleton } from '@mui/material';
4
- import { OverviewContext } from '@cccsaurora/howler-ui/components/app/providers/OverviewProvider';
4
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
5
  import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
6
- import { memo, useContext, useMemo } from 'react';
6
+ import { memo, useEffect, useMemo, useState } from 'react';
7
7
  import { Link } from 'react-router-dom';
8
8
  import HandlebarsMarkdown from '../display/HandlebarsMarkdown';
9
9
  const HitOverview = ({ content, hit }) => {
10
- const { getMatchingOverview } = useContext(OverviewContext);
11
- const matchingOverview = useMemo(() => (hit ? getMatchingOverview(hit) : null), [getMatchingOverview, hit]);
10
+ const { getMatchingOverview } = useMatchers();
11
+ const [matchingOverview, setMatchingOverview] = useState(null);
12
+ useEffect(() => {
13
+ getMatchingOverview(hit).then(setMatchingOverview);
14
+ }, [getMatchingOverview, hit]);
12
15
  const link = useMemo(() => matchingOverview
13
16
  ? `/overviews/view?analytic=${encodeURIComponent(matchingOverview.analytic)}${matchingOverview.detection && '&detection=' + encodeURIComponent(matchingOverview.detection)}`
14
17
  : hit
@@ -1,8 +1,9 @@
1
1
  import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
2
2
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
3
4
  declare const _default: import("react").NamedExoticComponent<{
4
5
  query: string;
5
- response?: HowlerSearchResponse<Hit>;
6
+ response?: HowlerSearchResponse<WithMetadata<Hit>>;
6
7
  execute?: boolean;
7
8
  onStart?: () => void;
8
9
  onComplete?: () => void;