Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -2631,6 +2631,7 @@ x-pack/solutions/security/test/serverless/functional/configs/config.context_awar
/x-pack/solutions/security/plugins/security_solution/public/common/components/tables @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/components/top_n @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/containers/unified_alerts @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_create_data_view.ts @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/lib/cell_actions @elastic/security-threat-hunting-investigations
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './unified_alerts';
export * from './update_responses';
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { SearchUnifiedAlertsResponse } from '../../../../../common/api/detection_engine/unified_alerts';

export const getSearchUnifiedAlertsResponseMock = (
overrides?: Partial<SearchUnifiedAlertsResponse>
): SearchUnifiedAlertsResponse => ({
took: 5,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 2,
relation: 'eq' as const,
},
max_score: 1.0,
hits: [
{
_index: '.alerts-security.alerts-default',
_id: 'alert-1',
_score: 1.0,
_source: {
'@timestamp': '2024-01-01T00:00:00.000Z',
'kibana.alert.rule.name': 'Test Rule 1',
'kibana.alert.severity': 'high',
},
},
{
_index: '.alerts-security.alerts-default',
_id: 'alert-2',
_score: 1.0,
_source: {
'@timestamp': '2024-01-01T01:00:00.000Z',
'kibana.alert.rule.name': 'Test Rule 2',
'kibana.alert.severity': 'medium',
},
},
],
},
...overrides,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { estypes } from '@elastic/elasticsearch';

export const getUpdateByQueryResponseMock = (
overrides?: Partial<estypes.UpdateByQueryResponse>
): estypes.UpdateByQueryResponse => ({
took: 10,
timed_out: false,
total: 5,
updated: 5,
deleted: 0,
batches: 1,
version_conflicts: 0,
noops: 0,
retries: {
bulk: 0,
search: 0,
},
throttled_millis: 0,
requests_per_second: -1,
throttled_until_millis: 0,
...overrides,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { coreMock } from '@kbn/core/public/mocks';
import {
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
} from '../../../../../common/constants';
import { KibanaServices } from '../../../lib/kibana';
import * as api from '.';

jest.mock('../../../lib/kibana');
const mockKibanaServices = KibanaServices.get as jest.Mock;

const signal = {} as AbortSignal;

describe('Unified Alerts API', () => {
let mockHttp: ReturnType<typeof coreMock.createStart>['http'];

beforeEach(() => {
const coreStart = coreMock.createStart({ basePath: '/mock' });
mockHttp = coreStart.http;
mockKibanaServices.mockReturnValue(coreStart);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('searchUnifiedAlerts', () => {
it('calls http.post with correct params', async () => {
const query = { query: { match_all: {} } };
await api.searchUnifiedAlerts({ query, signal });
expect(mockHttp.post).toHaveBeenCalledWith(DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL, {
version: '1',
body: JSON.stringify(query),
signal,
});
});
});

describe('setUnifiedAlertsWorkflowStatus', () => {
it('calls http.post with correct params', async () => {
const body = {
signal_ids: ['alert-1', 'alert-2'],
status: 'closed' as const,
};
await api.setUnifiedAlertsWorkflowStatus({ body, signal });
expect(mockHttp.post).toHaveBeenCalledWith(
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
{
version: '1',
body: JSON.stringify(body),
signal,
}
);
});
});

describe('setUnifiedAlertsTags', () => {
it('calls http.post with correct params', async () => {
const body = {
tags: {
tags_to_add: ['tag-1'],
tags_to_remove: [],
},
ids: ['alert-1', 'alert-2'],
};
await api.setUnifiedAlertsTags({ body, signal });
expect(mockHttp.post).toHaveBeenCalledWith(DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL, {
version: '1',
body: JSON.stringify(body),
signal,
});
});
});

describe('setUnifiedAlertsAssignees', () => {
it('calls http.post with correct params', async () => {
const body = {
assignees: {
add: ['user-1'],
remove: [],
},
ids: ['alert-1', 'alert-2'],
};
await api.setUnifiedAlertsAssignees({ body, signal });
expect(mockHttp.post).toHaveBeenCalledWith(
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
{
version: '1',
body: JSON.stringify(body),
signal,
}
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { estypes } from '@elastic/elasticsearch';
import {
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
} from '../../../../../common/constants';
import type {
SearchUnifiedAlertsRequestBody,
SearchUnifiedAlertsResponse,
SetUnifiedAlertsWorkflowStatusRequestBody,
SetUnifiedAlertsTagsRequestBody,
SetUnifiedAlertsAssigneesRequestBody,
} from '../../../../../common/api/detection_engine/unified_alerts';
import { KibanaServices } from '../../../lib/kibana';

/**
* Parameters for searching unified alerts
*/
export interface SearchUnifiedAlertsParams {
/** The Elasticsearch query DSL object */
query: SearchUnifiedAlertsRequestBody;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}

/**
* Searches unified alerts (detection and attack alerts) by providing a query DSL.
* This endpoint searches across both detection alerts and attack alerts.
*
* @param params - The search parameters
* @param params.query - The Elasticsearch query DSL object
* @param params.signal - Optional AbortSignal for cancelling the request
* @returns Promise resolving to the search response containing alerts
*/
export const searchUnifiedAlerts = async ({
query,
signal,
}: SearchUnifiedAlertsParams): Promise<SearchUnifiedAlertsResponse> => {
return KibanaServices.get().http.post<SearchUnifiedAlertsResponse>(
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
{
version: '1',
body: JSON.stringify(query),
signal,
}
);
};

/**
* Parameters for setting workflow status on unified alerts
*/
export interface SetUnifiedAlertsWorkflowStatusParams {
/** The request body containing status and alert IDs */
body: SetUnifiedAlertsWorkflowStatusRequestBody;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}

/**
* Sets the workflow status (e.g., open, closed, acknowledged) for unified alerts.
* Updates both detection alerts and attack alerts that match the provided IDs.
*
* @param params - The update parameters
* @param params.body - The request body containing the status and alert IDs to update
* @param params.signal - Optional AbortSignal for cancelling the request
* @returns Promise resolving to the update by query response with the number of updated alerts
*/
export const setUnifiedAlertsWorkflowStatus = async ({
body,
signal,
}: SetUnifiedAlertsWorkflowStatusParams): Promise<estypes.UpdateByQueryResponse> => {
return KibanaServices.get().http.post<estypes.UpdateByQueryResponse>(
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
{
version: '1',
body: JSON.stringify(body),
signal,
}
);
};

/**
* Parameters for setting tags on unified alerts
*/
export interface SetUnifiedAlertsTagsParams {
/** The request body containing tags and alert IDs */
body: SetUnifiedAlertsTagsRequestBody;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}

/**
* Sets tags for unified alerts by adding or removing tags from the specified alerts.
* Updates both detection alerts and attack alerts that match the provided IDs.
*
* @param params - The update parameters
* @param params.body - The request body containing tags to add/remove and alert IDs to update
* @param params.signal - Optional AbortSignal for cancelling the request
* @returns Promise resolving to the update by query response with the number of updated alerts
*/
export const setUnifiedAlertsTags = async ({
body,
signal,
}: SetUnifiedAlertsTagsParams): Promise<estypes.UpdateByQueryResponse> => {
return KibanaServices.get().http.post<estypes.UpdateByQueryResponse>(
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
{
version: '1',
body: JSON.stringify(body),
signal,
}
);
};

/**
* Parameters for setting assignees on unified alerts
*/
export interface SetUnifiedAlertsAssigneesParams {
/** The request body containing assignees and alert IDs */
body: SetUnifiedAlertsAssigneesRequestBody;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}

/**
* Sets assignees for unified alerts by adding or removing assignees from the specified alerts.
* Updates both detection alerts and attack alerts that match the provided IDs.
*
* @param params - The update parameters
* @param params.body - The request body containing assignees to add/remove and alert IDs to update
* @param params.signal - Optional AbortSignal for cancelling the request
* @returns Promise resolving to the update by query response with the number of updated alerts
*/
export const setUnifiedAlertsAssignees = async ({
body,
signal,
}: SetUnifiedAlertsAssigneesParams): Promise<estypes.UpdateByQueryResponse> => {
return KibanaServices.get().http.post<estypes.UpdateByQueryResponse>(
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
{
version: '1',
body: JSON.stringify(body),
signal,
}
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import {
DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
} from '../../../../../common/constants';

const ONE_MINUTE = 60000;

export const DEFAULT_QUERY_OPTIONS = {
refetchIntervalInBackground: false,
staleTime: ONE_MINUTE * 5,
};

export const SEARCH_UNIFIED_ALERTS_QUERY_KEY = ['GET', DETECTION_ENGINE_SEARCH_UNIFIED_ALERTS_URL];
export const SET_UNIFIED_ALERTS_WORKFLOW_STATUS_MUTATION_KEY = [
'POST',
DETECTION_ENGINE_SET_UNIFIED_ALERTS_WORKFLOW_STATUS_URL,
];
export const SET_UNIFIED_ALERTS_TAGS_MUTATION_KEY = [
'POST',
DETECTION_ENGINE_SET_UNIFIED_ALERTS_TAGS_URL,
];
export const SET_UNIFIED_ALERTS_ASSIGNEES_MUTATION_KEY = [
'POST',
DETECTION_ENGINE_SET_UNIFIED_ALERTS_ASSIGNEES_URL,
];
Loading