Skip to content

Commit 4fd0079

Browse files
authored
[Security Solution][Attacks/Alerts][Setup and miscellaneous] Unified Alerts: Hooks to work with the new endpoints (#247387) (#247389)
## Summary Closes #247387 This PR implements public-side logic to interact with the unified alerts management endpoints created in as part of this [ticket](#247065) in this [PR](#247068). It provides React Query hooks and API wrappers that enable the frontend to perform bulk operations (search, tag updates, assignee updates, and workflow status changes) on both detection alerts and attack alerts through a unified interface. ## Motivation The new **Attacks page** requires a unified way to manage different types of alerts (detection engine alerts and attack discovery alerts) in a single view. This PR provides the frontend infrastructure needed to interact with the backend unified alerts endpoints, enabling consistent user experience across alert management operations. ## Implementation Details ### API Layer (`api/index.ts`) - **`searchUnifiedAlerts`**: Wraps `POST /internal/detection_engine/unified_alerts/search` endpoint for searching unified alerts - **`setUnifiedAlertsWorkflowStatus`**: Wraps `POST /internal/detection_engine/unified_alerts/workflow_status` endpoint - **`setUnifiedAlertsTags`**: Wraps `POST /internal/detection_engine/unified_alerts/tags` endpoint - **`setUnifiedAlertsAssignees`**: Wraps `POST /internal/detection_engine/unified_alerts/assignees` endpoint ### React Query Hooks #### Query Hook - **`useSearchUnifiedAlerts`**: React Query hook for searching unified alerts - Accepts `SearchUnifiedAlertsRequestBody` as parameter - Provides automatic caching, error handling, and loading states - Includes cache invalidation support via `useInvalidateSearchUnifiedAlerts` #### Mutation Hooks - **`useSetUnifiedAlertsWorkflowStatus`**: Mutation hook for updating workflow status - **`useSetUnifiedAlertsTags`**: Mutation hook for adding/removing tags - **`useSetUnifiedAlertsAssignees`**: Mutation hook for adding/removing assignees ## File Structure ``` x-pack/solutions/security/plugins/security_solution/public/common/containers/unified_alerts/ ├── __mocks__/ │ ├── index.ts │ ├── unified_alerts.ts │ └── update_responses.ts ├── api/ │ ├── index.ts │ └── index.test.ts ├── hooks/ │ ├── constants.ts │ ├── translations.ts │ ├── use_search_unified_alerts.ts │ ├── use_search_unified_alerts.test.tsx │ ├── use_set_unified_alerts_workflow_status.ts │ ├── use_set_unified_alerts_workflow_status.test.tsx │ ├── use_set_unified_alerts_tags.ts │ ├── use_set_unified_alerts_tags.test.tsx │ ├── use_set_unified_alerts_assignees.ts │ └── use_set_unified_alerts_assignees.test.tsx └── index.ts ``` ## Testing Instructions 1. Run unit tests: ```bash yarn test:jest x-pack/solutions/security/plugins/security_solution/public/common/containers/unified_alerts ``` 2. Verify all tests pass (13 tests across 5 test suites)
1 parent 5dd4b0b commit 4fd0079

17 files changed

+1054
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2631,6 +2631,7 @@ x-pack/solutions/security/test/serverless/functional/configs/config.context_awar
26312631
/x-pack/solutions/security/plugins/security_solution/public/common/components/tables @elastic/security-threat-hunting-investigations
26322632
/x-pack/solutions/security/plugins/security_solution/public/common/components/top_n @elastic/security-threat-hunting-investigations
26332633
/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions @elastic/security-threat-hunting-investigations
2634+
/x-pack/solutions/security/plugins/security_solution/public/common/containers/unified_alerts @elastic/security-threat-hunting-investigations
26342635
/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx @elastic/security-threat-hunting-investigations
26352636
/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_create_data_view.ts @elastic/security-threat-hunting-investigations
26362637
/x-pack/solutions/security/plugins/security_solution/public/common/lib/cell_actions @elastic/security-threat-hunting-investigations
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
export * from './unified_alerts';
9+
export * from './update_responses';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { SearchUnifiedAlertsResponse } from '../../../../../common/api/detection_engine/unified_alerts';
9+
10+
export const getSearchUnifiedAlertsResponseMock = (
11+
overrides?: Partial<SearchUnifiedAlertsResponse>
12+
): SearchUnifiedAlertsResponse => ({
13+
took: 5,
14+
timed_out: false,
15+
_shards: {
16+
total: 1,
17+
successful: 1,
18+
skipped: 0,
19+
failed: 0,
20+
},
21+
hits: {
22+
total: {
23+
value: 2,
24+
relation: 'eq' as const,
25+
},
26+
max_score: 1.0,
27+
hits: [
28+
{
29+
_index: '.alerts-security.alerts-default',
30+
_id: 'alert-1',
31+
_score: 1.0,
32+
_source: {
33+
'@timestamp': '2024-01-01T00:00:00.000Z',
34+
'kibana.alert.rule.name': 'Test Rule 1',
35+
'kibana.alert.severity': 'high',
36+
},
37+
},
38+
{
39+
_index: '.alerts-security.alerts-default',
40+
_id: 'alert-2',
41+
_score: 1.0,
42+
_source: {
43+
'@timestamp': '2024-01-01T01:00:00.000Z',
44+
'kibana.alert.rule.name': 'Test Rule 2',
45+
'kibana.alert.severity': 'medium',
46+
},
47+
},
48+
],
49+
},
50+
...overrides,
51+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { estypes } from '@elastic/elasticsearch';
9+
10+
export const getUpdateByQueryResponseMock = (
11+
overrides?: Partial<estypes.UpdateByQueryResponse>
12+
): estypes.UpdateByQueryResponse => ({
13+
took: 10,
14+
timed_out: false,
15+
total: 5,
16+
updated: 5,
17+
deleted: 0,
18+
batches: 1,
19+
version_conflicts: 0,
20+
noops: 0,
21+
retries: {
22+
bulk: 0,
23+
search: 0,
24+
},
25+
throttled_millis: 0,
26+
requests_per_second: -1,
27+
throttled_until_millis: 0,
28+
...overrides,
29+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { coreMock } from '@kbn/core/public/mocks';
9+
import {
10+
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
11+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
12+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
13+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
14+
} from '../../../../../common/constants';
15+
import { KibanaServices } from '../../../lib/kibana';
16+
import * as api from '.';
17+
18+
jest.mock('../../../lib/kibana');
19+
const mockKibanaServices = KibanaServices.get as jest.Mock;
20+
21+
const signal = {} as AbortSignal;
22+
23+
describe('Unified Alerts API', () => {
24+
let mockHttp: ReturnType<typeof coreMock.createStart>['http'];
25+
26+
beforeEach(() => {
27+
const coreStart = coreMock.createStart({ basePath: '/mock' });
28+
mockHttp = coreStart.http;
29+
mockKibanaServices.mockReturnValue(coreStart);
30+
});
31+
32+
afterEach(() => {
33+
jest.clearAllMocks();
34+
});
35+
36+
describe('searchUnifiedAlerts', () => {
37+
it('calls http.post with correct params', async () => {
38+
const query = { query: { match_all: {} } };
39+
await api.searchUnifiedAlerts({ query, signal });
40+
expect(mockHttp.post).toHaveBeenCalledWith(DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL, {
41+
version: '1',
42+
body: JSON.stringify(query),
43+
signal,
44+
});
45+
});
46+
});
47+
48+
describe('setUnifiedAlertsWorkflowStatus', () => {
49+
it('calls http.post with correct params', async () => {
50+
const body = {
51+
signal_ids: ['alert-1', 'alert-2'],
52+
status: 'closed' as const,
53+
};
54+
await api.setUnifiedAlertsWorkflowStatus({ body, signal });
55+
expect(mockHttp.post).toHaveBeenCalledWith(
56+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
57+
{
58+
version: '1',
59+
body: JSON.stringify(body),
60+
signal,
61+
}
62+
);
63+
});
64+
});
65+
66+
describe('setUnifiedAlertsTags', () => {
67+
it('calls http.post with correct params', async () => {
68+
const body = {
69+
tags: {
70+
tags_to_add: ['tag-1'],
71+
tags_to_remove: [],
72+
},
73+
ids: ['alert-1', 'alert-2'],
74+
};
75+
await api.setUnifiedAlertsTags({ body, signal });
76+
expect(mockHttp.post).toHaveBeenCalledWith(DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL, {
77+
version: '1',
78+
body: JSON.stringify(body),
79+
signal,
80+
});
81+
});
82+
});
83+
84+
describe('setUnifiedAlertsAssignees', () => {
85+
it('calls http.post with correct params', async () => {
86+
const body = {
87+
assignees: {
88+
add: ['user-1'],
89+
remove: [],
90+
},
91+
ids: ['alert-1', 'alert-2'],
92+
};
93+
await api.setUnifiedAlertsAssignees({ body, signal });
94+
expect(mockHttp.post).toHaveBeenCalledWith(
95+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
96+
{
97+
version: '1',
98+
body: JSON.stringify(body),
99+
signal,
100+
}
101+
);
102+
});
103+
});
104+
});
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { estypes } from '@elastic/elasticsearch';
9+
import {
10+
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
11+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
12+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
13+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
14+
} from '../../../../../common/constants';
15+
import type {
16+
SearchUnifiedAlertsRequestBody,
17+
SearchUnifiedAlertsResponse,
18+
SetUnifiedAlertsWorkflowStatusRequestBody,
19+
SetUnifiedAlertsTagsRequestBody,
20+
SetUnifiedAlertsAssigneesRequestBody,
21+
} from '../../../../../common/api/detection_engine/unified_alerts';
22+
import { KibanaServices } from '../../../lib/kibana';
23+
24+
/**
25+
* Parameters for searching unified alerts
26+
*/
27+
export interface SearchUnifiedAlertsParams {
28+
/** The Elasticsearch query DSL object */
29+
query: SearchUnifiedAlertsRequestBody;
30+
/** Optional AbortSignal for cancelling request */
31+
signal?: AbortSignal;
32+
}
33+
34+
/**
35+
* Searches unified alerts (detection and attack alerts) by providing a query DSL.
36+
* This endpoint searches across both detection alerts and attack alerts.
37+
*
38+
* @param params - The search parameters
39+
* @param params.query - The Elasticsearch query DSL object
40+
* @param params.signal - Optional AbortSignal for cancelling the request
41+
* @returns Promise resolving to the search response containing alerts
42+
*/
43+
export const searchUnifiedAlerts = async ({
44+
query,
45+
signal,
46+
}: SearchUnifiedAlertsParams): Promise<SearchUnifiedAlertsResponse> => {
47+
return KibanaServices.get().http.post<SearchUnifiedAlertsResponse>(
48+
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
49+
{
50+
version: '1',
51+
body: JSON.stringify(query),
52+
signal,
53+
}
54+
);
55+
};
56+
57+
/**
58+
* Parameters for setting workflow status on unified alerts
59+
*/
60+
export interface SetUnifiedAlertsWorkflowStatusParams {
61+
/** The request body containing status and alert IDs */
62+
body: SetUnifiedAlertsWorkflowStatusRequestBody;
63+
/** Optional AbortSignal for cancelling request */
64+
signal?: AbortSignal;
65+
}
66+
67+
/**
68+
* Sets the workflow status (e.g., open, closed, acknowledged) for unified alerts.
69+
* Updates both detection alerts and attack alerts that match the provided IDs.
70+
*
71+
* @param params - The update parameters
72+
* @param params.body - The request body containing the status and alert IDs to update
73+
* @param params.signal - Optional AbortSignal for cancelling the request
74+
* @returns Promise resolving to the update by query response with the number of updated alerts
75+
*/
76+
export const setUnifiedAlertsWorkflowStatus = async ({
77+
body,
78+
signal,
79+
}: SetUnifiedAlertsWorkflowStatusParams): Promise<estypes.UpdateByQueryResponse> => {
80+
return KibanaServices.get().http.post<estypes.UpdateByQueryResponse>(
81+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
82+
{
83+
version: '1',
84+
body: JSON.stringify(body),
85+
signal,
86+
}
87+
);
88+
};
89+
90+
/**
91+
* Parameters for setting tags on unified alerts
92+
*/
93+
export interface SetUnifiedAlertsTagsParams {
94+
/** The request body containing tags and alert IDs */
95+
body: SetUnifiedAlertsTagsRequestBody;
96+
/** Optional AbortSignal for cancelling request */
97+
signal?: AbortSignal;
98+
}
99+
100+
/**
101+
* Sets tags for unified alerts by adding or removing tags from the specified alerts.
102+
* Updates both detection alerts and attack alerts that match the provided IDs.
103+
*
104+
* @param params - The update parameters
105+
* @param params.body - The request body containing tags to add/remove and alert IDs to update
106+
* @param params.signal - Optional AbortSignal for cancelling the request
107+
* @returns Promise resolving to the update by query response with the number of updated alerts
108+
*/
109+
export const setUnifiedAlertsTags = async ({
110+
body,
111+
signal,
112+
}: SetUnifiedAlertsTagsParams): Promise<estypes.UpdateByQueryResponse> => {
113+
return KibanaServices.get().http.post<estypes.UpdateByQueryResponse>(
114+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
115+
{
116+
version: '1',
117+
body: JSON.stringify(body),
118+
signal,
119+
}
120+
);
121+
};
122+
123+
/**
124+
* Parameters for setting assignees on unified alerts
125+
*/
126+
export interface SetUnifiedAlertsAssigneesParams {
127+
/** The request body containing assignees and alert IDs */
128+
body: SetUnifiedAlertsAssigneesRequestBody;
129+
/** Optional AbortSignal for cancelling request */
130+
signal?: AbortSignal;
131+
}
132+
133+
/**
134+
* Sets assignees for unified alerts by adding or removing assignees from the specified alerts.
135+
* Updates both detection alerts and attack alerts that match the provided IDs.
136+
*
137+
* @param params - The update parameters
138+
* @param params.body - The request body containing assignees to add/remove and alert IDs to update
139+
* @param params.signal - Optional AbortSignal for cancelling the request
140+
* @returns Promise resolving to the update by query response with the number of updated alerts
141+
*/
142+
export const setUnifiedAlertsAssignees = async ({
143+
body,
144+
signal,
145+
}: SetUnifiedAlertsAssigneesParams): Promise<estypes.UpdateByQueryResponse> => {
146+
return KibanaServices.get().http.post<estypes.UpdateByQueryResponse>(
147+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
148+
{
149+
version: '1',
150+
body: JSON.stringify(body),
151+
signal,
152+
}
153+
);
154+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
10+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
11+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
12+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
13+
} from '../../../../../common/constants';
14+
15+
const ONE_MINUTE = 60000;
16+
17+
export const DEFAULT_QUERY_OPTIONS = {
18+
refetchIntervalInBackground: false,
19+
staleTime: ONE_MINUTE * 5,
20+
};
21+
22+
export const SEARCH_UNIFIED_ALERTS_QUERY_KEY = ['GET', DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL];
23+
export const SET_UNIFIED_ALERTS_WORKFLOW_STATUS_MUTATION_KEY = [
24+
'POST',
25+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
26+
];
27+
export const SET_UNIFIED_ALERTS_TAGS_MUTATION_KEY = [
28+
'POST',
29+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
30+
];
31+
export const SET_UNIFIED_ALERTS_ASSIGNEES_MUTATION_KEY = [
32+
'POST',
33+
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
34+
];

0 commit comments

Comments
 (0)