mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Modify open_close_signals route to avoid using update_by_query when possible (#159703)
issue: https://github.com/elastic/security-team/issues/6829 ## Summary * Update `open_close_signals_route` to call `_update_by_query` API only when a query is provided. When the alert ids are provided it will call `_bulk` update instead. * It also unifies the response so both `updateAlerts` calls return the same interface. ### Context We still can call updateByQuery as many times as we need if the feature works with eventual consistency (search after writing returns stale data). If we need to show the updated data after writing, we need refresh: true, which is expensive (updateByQuery doesn't support refresh: wait_for). So, it makes sense to avoid calling refresh: true when we have the ids by using _bulk update with refresh: wait_for. ### How to test it? #### Old Alerts Table/Grouped Alerts Table * Select one or more rows * Open the bulk action menu and update the alert status <img width="531" alt="Screenshot 2023-06-27 at 16 51 42" src="b0628921
-b034-4dee-9f75-9b760e33f512"> * It should update Alerts by id instead of sending a query and calling `update_by_query` API * Bulk updating all alert statuses doesn't change #### Close Alerts From Exceptions * Click on the row of actions for an alert * Select the 'Add rule exception' option <img width="366" alt="Screenshot 2023-06-27 at 16 55 54" src="2faac947
-a004-4a80-b026-ffeeabbf1695"> * Fill all required fields on the flyout + `Close this alert` * Click on "Add rule exception" * It should update the Alerts by id instead of sending a query and calling the `update_by_query` API * Bulk updating all alert statuses doesn't change ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
023b23f2a6
commit
9685de25e0
39 changed files with 466 additions and 238 deletions
|
@ -86,7 +86,6 @@ interface Props {
|
|||
data: TimelineEventsDetailsItem[];
|
||||
detailsEcsData: Ecs | null;
|
||||
id: string;
|
||||
indexName: string;
|
||||
isAlert: boolean;
|
||||
isDraggable?: boolean;
|
||||
rawEventData: object | undefined;
|
||||
|
@ -154,7 +153,6 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
data,
|
||||
detailsEcsData,
|
||||
id,
|
||||
indexName,
|
||||
isAlert,
|
||||
isDraggable,
|
||||
rawEventData,
|
||||
|
@ -235,7 +233,6 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
contextId={scopeId}
|
||||
data={data}
|
||||
eventId={id}
|
||||
indexName={indexName}
|
||||
scopeId={scopeId}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
isReadOnly={isReadOnly}
|
||||
|
@ -328,7 +325,6 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
scopeId,
|
||||
data,
|
||||
id,
|
||||
indexName,
|
||||
handleOnEventClosed,
|
||||
isReadOnly,
|
||||
renderer,
|
||||
|
|
|
@ -42,22 +42,12 @@ interface Props {
|
|||
data: TimelineEventsDetailsItem[];
|
||||
eventId: string;
|
||||
handleOnEventClosed: () => void;
|
||||
indexName: string;
|
||||
scopeId: string;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
export const Overview = React.memo<Props>(
|
||||
({
|
||||
browserFields,
|
||||
contextId,
|
||||
data,
|
||||
eventId,
|
||||
handleOnEventClosed,
|
||||
indexName,
|
||||
scopeId,
|
||||
isReadOnly,
|
||||
}) => {
|
||||
({ browserFields, contextId, data, eventId, handleOnEventClosed, scopeId, isReadOnly }) => {
|
||||
const statusData = useMemo(() => {
|
||||
const item = find({ field: SIGNAL_STATUS_FIELD_NAME, category: 'kibana' }, data);
|
||||
return (
|
||||
|
@ -128,7 +118,6 @@ export const Overview = React.memo<Props>(
|
|||
eventId={eventId}
|
||||
contextId={contextId}
|
||||
enrichedFieldInfo={statusData}
|
||||
indexName={indexName}
|
||||
scopeId={scopeId}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
/>
|
||||
|
|
|
@ -45,7 +45,6 @@ const props = {
|
|||
fields: {},
|
||||
},
|
||||
},
|
||||
indexName: '.internal.alerts-security.alerts-default-000001',
|
||||
scopeId: 'alerts-page',
|
||||
handleOnEventClosed: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -24,13 +24,12 @@ interface StatusPopoverButtonProps {
|
|||
eventId: string;
|
||||
contextId: string;
|
||||
enrichedFieldInfo: EnrichedFieldInfoWithValues;
|
||||
indexName: string;
|
||||
scopeId: string;
|
||||
handleOnEventClosed: () => void;
|
||||
}
|
||||
|
||||
export const StatusPopoverButton = React.memo<StatusPopoverButtonProps>(
|
||||
({ eventId, contextId, enrichedFieldInfo, indexName, scopeId, handleOnEventClosed }) => {
|
||||
({ eventId, contextId, enrichedFieldInfo, scopeId, handleOnEventClosed }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
@ -51,7 +50,6 @@ export const StatusPopoverButton = React.memo<StatusPopoverButtonProps>(
|
|||
closePopover: closeAfterAction,
|
||||
eventId,
|
||||
scopeId,
|
||||
indexName,
|
||||
alertStatus: enrichedFieldInfo.values[0] as Status,
|
||||
refetch: refetchGlobalQuery,
|
||||
});
|
||||
|
|
|
@ -496,7 +496,6 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
|
|||
tableId,
|
||||
data: nonDeletedEvents,
|
||||
totalItems: totalCountMinusDeleted,
|
||||
indexNames: selectedPatterns,
|
||||
hasAlertsCrud: hasCrudPermissions,
|
||||
showCheckboxes,
|
||||
filterStatus: currentFilter,
|
||||
|
|
|
@ -18,7 +18,6 @@ interface OwnProps {
|
|||
tableId: TableId;
|
||||
data: TimelineItem[];
|
||||
totalItems: number;
|
||||
indexNames: string[];
|
||||
hasAlertsCrud: boolean;
|
||||
showCheckboxes: boolean;
|
||||
filterStatus?: AlertWorkflowStatus;
|
||||
|
@ -31,7 +30,6 @@ export const useAlertBulkActions = ({
|
|||
tableId,
|
||||
data,
|
||||
totalItems,
|
||||
indexNames,
|
||||
hasAlertsCrud,
|
||||
showCheckboxes,
|
||||
filterStatus,
|
||||
|
@ -102,7 +100,6 @@ export const useAlertBulkActions = ({
|
|||
totalItems={totalItems}
|
||||
filterStatus={filterStatus}
|
||||
query={filterQuery}
|
||||
indexName={indexNames.join()}
|
||||
onActionSuccess={onAlertStatusActionSuccess}
|
||||
onActionFailure={onAlertStatusActionFailure}
|
||||
customBulkActions={additionalBulkActions}
|
||||
|
@ -115,7 +112,6 @@ export const useAlertBulkActions = ({
|
|||
additionalBulkActions,
|
||||
filterQuery,
|
||||
filterStatus,
|
||||
indexNames,
|
||||
onAlertStatusActionFailure,
|
||||
onAlertStatusActionSuccess,
|
||||
showAlertStatusActions,
|
||||
|
|
|
@ -37,7 +37,6 @@ interface OwnProps {
|
|||
totalItems: number;
|
||||
filterStatus?: AlertWorkflowStatus;
|
||||
query?: string;
|
||||
indexName: string;
|
||||
showAlertStatusActions?: boolean;
|
||||
onActionSuccess?: OnUpdateAlertStatusSuccess;
|
||||
onActionFailure?: OnUpdateAlertStatusError;
|
||||
|
@ -59,7 +58,6 @@ export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProp
|
|||
selectedEventIds,
|
||||
isSelectAllChecked,
|
||||
clearSelected,
|
||||
indexName,
|
||||
showAlertStatusActions,
|
||||
onActionSuccess,
|
||||
onActionFailure,
|
||||
|
@ -177,7 +175,6 @@ export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProp
|
|||
);
|
||||
|
||||
const bulkActionItems = useBulkActionItems({
|
||||
indexName,
|
||||
eventIds: Object.keys(selectedEventIds),
|
||||
currentStatus: filterStatus,
|
||||
...(showClearSelection ? { query } : {}),
|
||||
|
|
|
@ -14,12 +14,12 @@ import { BulkAlertTagsPanel } from './alert_bulk_tags';
|
|||
import { ALERT_WORKFLOW_TAGS } from '@kbn/rule-data-utils';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { useSetAlertTags } from './use_set_alert_tags';
|
||||
import { getUpdateAlertsQuery } from '../../../../detections/components/alerts_table/actions';
|
||||
import { getUpdateAlertsQuery } from './helpers';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
jest.mock('../../../hooks/use_app_toasts');
|
||||
jest.mock('./use_set_alert_tags');
|
||||
jest.mock('../../../../detections/components/alerts_table/actions');
|
||||
jest.mock('./helpers');
|
||||
|
||||
const mockTagItems = [
|
||||
{
|
||||
|
|
|
@ -45,3 +45,21 @@ export const createInitialTagsState = (existingTags: string[][], defaultTags: st
|
|||
})
|
||||
.sort(checkedSortCallback);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Please avoid using update_by_query API with `refresh:true` on serverless, use `_bulk` update by id instead.
|
||||
*/
|
||||
export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
|
||||
return {
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
terms: {
|
||||
_id: eventIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { updateAlertStatus } from './update_alerts';
|
||||
|
||||
const mockUpdateAlertStatusByIds = jest.fn().mockReturnValue(new Promise(() => {}));
|
||||
const mockUpdateAlertStatusByQuery = jest.fn().mockReturnValue(new Promise(() => {}));
|
||||
|
||||
jest.mock('../../../../detections/containers/detection_engine/alerts/api', () => {
|
||||
return {
|
||||
updateAlertStatusByQuery: (params: unknown) => mockUpdateAlertStatusByQuery(params),
|
||||
updateAlertStatusByIds: (params: unknown) => mockUpdateAlertStatusByIds(params),
|
||||
};
|
||||
});
|
||||
|
||||
const status = 'open';
|
||||
|
||||
describe('updateAlertStatus', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw an error if neither query nor signalIds are provided', () => {
|
||||
expect(() => {
|
||||
updateAlertStatus({ status });
|
||||
}).toThrowError('Either query or signalIds must be provided');
|
||||
});
|
||||
|
||||
it('should call updateAlertStatusByIds if signalIds are provided', () => {
|
||||
const signalIds = ['1', '2'];
|
||||
updateAlertStatus({
|
||||
status,
|
||||
signalIds,
|
||||
});
|
||||
expect(mockUpdateAlertStatusByIds).toHaveBeenCalledWith({
|
||||
status,
|
||||
signalIds,
|
||||
});
|
||||
expect(mockUpdateAlertStatusByQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call mockUpdateAlertStatusByQuery if query is provided', () => {
|
||||
const query = { query: 'query' };
|
||||
updateAlertStatus({
|
||||
status,
|
||||
query,
|
||||
});
|
||||
expect(mockUpdateAlertStatusByIds).not.toHaveBeenCalled();
|
||||
expect(mockUpdateAlertStatusByQuery).toHaveBeenCalledWith({
|
||||
status,
|
||||
query,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { UpdateByQueryResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { Status } from '../../../../../common/detection_engine/schemas/common';
|
||||
import {
|
||||
updateAlertStatusByIds,
|
||||
updateAlertStatusByQuery,
|
||||
} from '../../../../detections/containers/detection_engine/alerts/api';
|
||||
|
||||
interface UpdatedAlertsResponse {
|
||||
updated: number;
|
||||
version_conflicts: UpdateByQueryResponse['version_conflicts'];
|
||||
}
|
||||
|
||||
interface UpdatedAlertsProps {
|
||||
status: Status;
|
||||
query?: object;
|
||||
signalIds?: string[];
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update alert status by query or signalIds.
|
||||
* Either query or signalIds must be provided
|
||||
* `signalIds` is the preferred way to update alerts because it is more cost effective on Serverless.
|
||||
*
|
||||
* @param status to update to('open' / 'closed' / 'acknowledged')
|
||||
* @param index index to be updated
|
||||
* @param query optional query object to update alerts by query.
|
||||
* @param signalIds optional signalIds to update alerts by signalIds.
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const updateAlertStatus = ({
|
||||
status,
|
||||
query,
|
||||
signalIds,
|
||||
signal,
|
||||
}: UpdatedAlertsProps): Promise<UpdatedAlertsResponse> => {
|
||||
if (signalIds && signalIds.length > 0) {
|
||||
return updateAlertStatusByIds({ status, signalIds, signal }).then(({ items }) => ({
|
||||
updated: items.length,
|
||||
version_conflicts: 0,
|
||||
}));
|
||||
} else if (query) {
|
||||
return updateAlertStatusByQuery({ status, query, signal }).then(
|
||||
({ updated, version_conflicts: conflicts }) => ({
|
||||
updated: updated ?? 0,
|
||||
version_conflicts: conflicts,
|
||||
})
|
||||
);
|
||||
}
|
||||
throw new Error('Either query or signalIds must be provided');
|
||||
};
|
|
@ -14,22 +14,17 @@ import type {
|
|||
SetEventsLoading,
|
||||
} from '../../../../../common/types';
|
||||
import * as i18n from './translations';
|
||||
import { useUpdateAlertsStatus } from './use_update_alerts';
|
||||
import { updateAlertStatus } from './update_alerts';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { useStartTransaction } from '../../../lib/apm/use_start_transaction';
|
||||
import { APM_USER_INTERACTIONS } from '../../../lib/apm/constants';
|
||||
import type { AlertWorkflowStatus } from '../../../types';
|
||||
import type { OnUpdateAlertStatusError, OnUpdateAlertStatusSuccess } from './types';
|
||||
|
||||
export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
|
||||
return { bool: { filter: { terms: { _id: eventIds } } } };
|
||||
};
|
||||
|
||||
export interface BulkActionsProps {
|
||||
eventIds: string[];
|
||||
currentStatus?: AlertWorkflowStatus;
|
||||
query?: string;
|
||||
indexName: string;
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
showAlertStatusActions?: boolean;
|
||||
|
@ -42,7 +37,6 @@ export const useBulkActionItems = ({
|
|||
eventIds,
|
||||
currentStatus,
|
||||
query,
|
||||
indexName,
|
||||
setEventsLoading,
|
||||
showAlertStatusActions = true,
|
||||
setEventsDeleted,
|
||||
|
@ -50,7 +44,6 @@ export const useBulkActionItems = ({
|
|||
onUpdateFailure,
|
||||
customBulkActions,
|
||||
}: BulkActionsProps) => {
|
||||
const { updateAlertStatus } = useUpdateAlertsStatus();
|
||||
const { addSuccess, addError, addWarning } = useAppToasts();
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
|
@ -116,11 +109,10 @@ export const useBulkActionItems = ({
|
|||
|
||||
try {
|
||||
setEventsLoading({ eventIds, isLoading: true });
|
||||
|
||||
const response = await updateAlertStatus({
|
||||
index: indexName,
|
||||
status,
|
||||
query: query ? JSON.parse(query) : getUpdateAlertsQuery(eventIds),
|
||||
query: query && JSON.parse(query),
|
||||
signalIds: eventIds,
|
||||
});
|
||||
|
||||
// TODO: Only delete those that were successfully updated from updatedRules
|
||||
|
@ -140,8 +132,6 @@ export const useBulkActionItems = ({
|
|||
[
|
||||
setEventsLoading,
|
||||
eventIds,
|
||||
updateAlertStatus,
|
||||
indexName,
|
||||
query,
|
||||
setEventsDeleted,
|
||||
onAlertStatusUpdateSuccess,
|
||||
|
|
|
@ -10,11 +10,11 @@ import type { CoreStart } from '@kbn/core/public';
|
|||
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { getUpdateAlertsQuery } from '../../../../detections/components/alerts_table/actions';
|
||||
import type { AlertTags } from '../../../../../common/detection_engine/schemas/common';
|
||||
import { DETECTION_ENGINE_ALERT_TAGS_URL } from '../../../../../common/constants';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import * as i18n from './translations';
|
||||
import { getUpdateAlertsQuery } from './helpers';
|
||||
|
||||
export type SetAlertTagsFunc = (
|
||||
tags: AlertTags,
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants';
|
||||
import type { AlertWorkflowStatus } from '../../../types';
|
||||
|
||||
/**
|
||||
* Update alert status by query
|
||||
*
|
||||
* @param status to update to('open' / 'closed' / 'acknowledged')
|
||||
* @param index index to be updated
|
||||
* @param query optional query object to update alerts by query.
|
||||
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const useUpdateAlertsStatus = (): {
|
||||
updateAlertStatus: (params: {
|
||||
status: AlertWorkflowStatus;
|
||||
index: string;
|
||||
query: object;
|
||||
}) => Promise<estypes.UpdateByQueryResponse>;
|
||||
} => {
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
return {
|
||||
updateAlertStatus: async ({ status, index, query }) => {
|
||||
return http.fetch<estypes.UpdateByQueryResponse>(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ status, query }),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -8,9 +8,6 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { updateAlertStatus } from '../../../detections/containers/detection_engine/alerts/api';
|
||||
import { getUpdateAlertsQuery } from '../../../detections/components/alerts_table/actions';
|
||||
import {
|
||||
buildAlertStatusesFilter,
|
||||
buildAlertsFilter,
|
||||
|
@ -21,6 +18,7 @@ import { prepareExceptionItemsForBulkClose } from '../utils/helpers';
|
|||
import * as i18nCommon from '../../../common/translations';
|
||||
import * as i18n from './translations';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { updateAlertStatus } from '../../../common/components/toolbar/bulk_actions/update_alerts';
|
||||
|
||||
/**
|
||||
* Closes alerts.
|
||||
|
@ -65,7 +63,7 @@ export const useCloseAlertsFromExceptions = (): ReturnUseCloseAlertsFromExceptio
|
|||
let bulkResponse: estypes.UpdateByQueryResponse | undefined;
|
||||
if (alertIdToClose != null) {
|
||||
alertIdResponse = await updateAlertStatus({
|
||||
query: getUpdateAlertsQuery([alertIdToClose]),
|
||||
signalIds: [alertIdToClose],
|
||||
status: 'closed',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
@ -88,9 +86,7 @@ export const useCloseAlertsFromExceptions = (): ReturnUseCloseAlertsFromExceptio
|
|||
);
|
||||
|
||||
bulkResponse = await updateAlertStatus({
|
||||
query: {
|
||||
query: filter,
|
||||
},
|
||||
query: filter,
|
||||
status: 'closed',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
|
|
@ -44,7 +44,6 @@ import {
|
|||
import type { TimelineResult } from '../../../../common/types/timeline/api';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineType } from '../../../../common/types/timeline/api';
|
||||
import { updateAlertStatus } from '../../containers/detection_engine/alerts/api';
|
||||
import type {
|
||||
SendAlertToTimelineActionProps,
|
||||
ThresholdAggregationData,
|
||||
|
@ -83,20 +82,7 @@ import {
|
|||
DEFAULT_FROM_MOMENT,
|
||||
DEFAULT_TO_MOMENT,
|
||||
} from '../../../common/utils/default_date_settings';
|
||||
|
||||
export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
|
||||
return {
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
terms: {
|
||||
_id: eventIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
import { updateAlertStatus } from '../../../common/components/toolbar/bulk_actions/update_alerts';
|
||||
|
||||
export const updateAlertStatusAction = async ({
|
||||
query,
|
||||
|
@ -110,8 +96,12 @@ export const updateAlertStatusAction = async ({
|
|||
try {
|
||||
setEventsLoading({ eventIds: alertIds, isLoading: true });
|
||||
|
||||
const queryObject = query ? { query: JSON.parse(query) } : getUpdateAlertsQuery(alertIds);
|
||||
const response = await updateAlertStatus({ query: queryObject, status: selectedStatus });
|
||||
const response = await updateAlertStatus({
|
||||
query: query && JSON.parse(query),
|
||||
status: selectedStatus,
|
||||
signalIds: alertIds,
|
||||
});
|
||||
|
||||
// TODO: Only delete those that were successfully updated from updatedRules
|
||||
setEventsDeleted({ eventIds: alertIds, isDeleted: true });
|
||||
|
||||
|
|
|
@ -235,7 +235,6 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
);
|
||||
|
||||
const takeActionItems = useGroupTakeActionsItems({
|
||||
indexName: indexPattern.title,
|
||||
currentStatus: currentAlertStatusFilterValue,
|
||||
showAlertStatusActions: hasIndexWrite && hasIndexMaintenance,
|
||||
});
|
||||
|
|
|
@ -35,7 +35,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
const { result, waitForNextUpdate } = renderHook(
|
||||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: true,
|
||||
}),
|
||||
{
|
||||
|
@ -53,7 +52,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
currentStatus: [],
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: true,
|
||||
}),
|
||||
{
|
||||
|
@ -71,7 +69,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
currentStatus: ['open', 'closed'],
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: true,
|
||||
}),
|
||||
{
|
||||
|
@ -89,7 +86,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
currentStatus: ['open'],
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: true,
|
||||
}),
|
||||
{
|
||||
|
@ -110,7 +106,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
currentStatus: ['closed'],
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: true,
|
||||
}),
|
||||
{
|
||||
|
@ -131,7 +126,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
currentStatus: ['acknowledged'],
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: true,
|
||||
}),
|
||||
{
|
||||
|
@ -151,7 +145,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
const { result, waitForNextUpdate } = renderHook(
|
||||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: false,
|
||||
}),
|
||||
{
|
||||
|
@ -167,7 +160,6 @@ describe('useGroupTakeActionsItems', () => {
|
|||
const { result, waitForNextUpdate } = renderHook(
|
||||
() =>
|
||||
useGroupTakeActionsItems({
|
||||
indexName: '.alerts-security.alerts-default',
|
||||
showAlertStatusActions: true,
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ import { useStartTransaction } from '../../../../common/lib/apm/use_start_transa
|
|||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import type { AlertWorkflowStatus } from '../../../../common/types';
|
||||
import { APM_USER_INTERACTIONS } from '../../../../common/lib/apm/constants';
|
||||
import { useUpdateAlertsStatus } from '../../../../common/components/toolbar/bulk_actions/use_update_alerts';
|
||||
import { updateAlertStatus } from '../../../../common/components/toolbar/bulk_actions/update_alerts';
|
||||
import {
|
||||
BULK_ACTION_ACKNOWLEDGED_SELECTED,
|
||||
BULK_ACTION_CLOSE_SELECTED,
|
||||
|
@ -33,16 +33,13 @@ import type { StartServices } from '../../../../types';
|
|||
|
||||
export interface TakeActionsProps {
|
||||
currentStatus?: Status[];
|
||||
indexName: string;
|
||||
showAlertStatusActions?: boolean;
|
||||
}
|
||||
|
||||
export const useGroupTakeActionsItems = ({
|
||||
currentStatus,
|
||||
indexName,
|
||||
showAlertStatusActions = true,
|
||||
}: TakeActionsProps) => {
|
||||
const { updateAlertStatus } = useUpdateAlertsStatus();
|
||||
const { addSuccess, addError, addWarning } = useAppToasts();
|
||||
const { startTransaction } = useStartTransaction();
|
||||
const getGlobalQuerySelector = inputsSelectors.globalQuery();
|
||||
|
@ -163,7 +160,6 @@ export const useGroupTakeActionsItems = ({
|
|||
|
||||
try {
|
||||
const response = await updateAlertStatus({
|
||||
index: indexName,
|
||||
status,
|
||||
query: query ? JSON.parse(query) : {},
|
||||
});
|
||||
|
@ -176,8 +172,6 @@ export const useGroupTakeActionsItems = ({
|
|||
[
|
||||
startTransaction,
|
||||
reportAlertsGroupingTakeActionClick,
|
||||
updateAlertStatus,
|
||||
indexName,
|
||||
onAlertStatusUpdateSuccess,
|
||||
onAlertStatusUpdateFailure,
|
||||
]
|
||||
|
|
|
@ -177,7 +177,6 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps & PropsFromRedux
|
|||
const { actionItems: statusActionItems } = useAlertsActions({
|
||||
alertStatus,
|
||||
eventId: ecsRowData?._id,
|
||||
indexName: ecsRowData?._index ?? '',
|
||||
scopeId,
|
||||
refetch: refetchAll,
|
||||
closePopover,
|
||||
|
|
|
@ -19,7 +19,6 @@ interface Props {
|
|||
closePopover: () => void;
|
||||
eventId: string;
|
||||
scopeId: string;
|
||||
indexName: string;
|
||||
refetch?: () => void;
|
||||
}
|
||||
|
||||
|
@ -28,7 +27,6 @@ export const useAlertsActions = ({
|
|||
closePopover,
|
||||
eventId,
|
||||
scopeId,
|
||||
indexName,
|
||||
refetch,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -63,7 +61,6 @@ export const useAlertsActions = ({
|
|||
const actionItems = useBulkActionItems({
|
||||
eventIds: [eventId],
|
||||
currentStatus: alertStatus as AlertWorkflowStatus,
|
||||
indexName,
|
||||
setEventsLoading: localSetEventsLoading,
|
||||
setEventsDeleted,
|
||||
onUpdateSuccess: onStatusUpdate,
|
||||
|
|
|
@ -99,7 +99,6 @@ describe('take action dropdown', () => {
|
|||
detailsData: generateAlertDetailsDataMock() as TimelineEventsDetailsItem[],
|
||||
ecsData: getDetectionAlertMock(),
|
||||
handleOnEventClosed: jest.fn(),
|
||||
indexName: 'index',
|
||||
isHostIsolationPanelOpen: false,
|
||||
loadingEventDetails: false,
|
||||
onAddEventFilterClick: jest.fn(),
|
||||
|
|
|
@ -48,7 +48,6 @@ export interface TakeActionDropdownProps {
|
|||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
ecsData?: Ecs;
|
||||
handleOnEventClosed: () => void;
|
||||
indexName: string;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
loadingEventDetails: boolean;
|
||||
onAddEventFilterClick: () => void;
|
||||
|
@ -65,7 +64,6 @@ export const TakeActionDropdown = React.memo(
|
|||
detailsData,
|
||||
ecsData,
|
||||
handleOnEventClosed,
|
||||
indexName,
|
||||
isHostIsolationPanelOpen,
|
||||
loadingEventDetails,
|
||||
onAddEventFilterClick,
|
||||
|
@ -180,7 +178,6 @@ export const TakeActionDropdown = React.memo(
|
|||
alertStatus: actionsData.alertStatus,
|
||||
closePopover: closePopoverAndFlyout,
|
||||
eventId: actionsData.eventId,
|
||||
indexName,
|
||||
refetch,
|
||||
scopeId,
|
||||
});
|
||||
|
|
|
@ -16,11 +16,12 @@ import {
|
|||
} from './mock';
|
||||
import {
|
||||
fetchQueryAlerts,
|
||||
updateAlertStatus,
|
||||
getSignalIndex,
|
||||
getUserPrivilege,
|
||||
createSignalIndex,
|
||||
createHostIsolation,
|
||||
updateAlertStatusByQuery,
|
||||
updateAlertStatusByIds,
|
||||
} from './api';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
|
@ -57,40 +58,40 @@ describe('Detections Alerts API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('updateAlertStatus', () => {
|
||||
describe('updateAlertStatusByQuery', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue({});
|
||||
});
|
||||
|
||||
test('check parameter url, body when closing an alert', async () => {
|
||||
await updateAlertStatus({
|
||||
await updateAlertStatusByQuery({
|
||||
query: mockStatusAlertQuery,
|
||||
signal: abortCtrl.signal,
|
||||
status: 'closed',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', {
|
||||
body: '{"conflicts":"proceed","status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}',
|
||||
body: '{"conflicts":"proceed","status":"closed","query":{"bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}}',
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('check parameter url, body when opening an alert', async () => {
|
||||
await updateAlertStatus({
|
||||
await updateAlertStatusByQuery({
|
||||
query: mockStatusAlertQuery,
|
||||
signal: abortCtrl.signal,
|
||||
status: 'open',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', {
|
||||
body: '{"conflicts":"proceed","status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}',
|
||||
body: '{"conflicts":"proceed","status":"open","query":{"bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}}',
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
const alertsResp = await updateAlertStatus({
|
||||
const alertsResp = await updateAlertStatusByQuery({
|
||||
query: mockStatusAlertQuery,
|
||||
signal: abortCtrl.signal,
|
||||
status: 'open',
|
||||
|
@ -99,6 +100,48 @@ describe('Detections Alerts API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('updateAlertStatusById', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue({});
|
||||
});
|
||||
|
||||
test('check parameter url, body when closing an alert', async () => {
|
||||
await updateAlertStatusByIds({
|
||||
signalIds: ['123'],
|
||||
signal: abortCtrl.signal,
|
||||
status: 'closed',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', {
|
||||
body: '{"status":"closed","signal_ids":["123"]}',
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('check parameter url, body when opening an alert', async () => {
|
||||
await updateAlertStatusByIds({
|
||||
signalIds: ['123'],
|
||||
signal: abortCtrl.signal,
|
||||
status: 'open',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', {
|
||||
body: '{"status":"open","signal_ids":["123"]}',
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
const alertsResp = await updateAlertStatusByIds({
|
||||
signalIds: ['123'],
|
||||
signal: abortCtrl.signal,
|
||||
status: 'open',
|
||||
});
|
||||
expect(alertsResp).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSignalIndex', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
|
|
|
@ -24,9 +24,10 @@ import type {
|
|||
QueryAlerts,
|
||||
AlertSearchResponse,
|
||||
AlertsIndex,
|
||||
UpdateAlertStatusProps,
|
||||
UpdateAlertStatusByQueryProps,
|
||||
CasesFromAlertsResponse,
|
||||
CheckSignalIndex,
|
||||
UpdateAlertStatusByIdsProps,
|
||||
} from './types';
|
||||
import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isolation';
|
||||
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
|
||||
|
@ -84,14 +85,34 @@ export const fetchQueryRuleRegistryAlerts = async <Hit, Aggregations>({
|
|||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const updateAlertStatus = async ({
|
||||
export const updateAlertStatusByQuery = async ({
|
||||
query,
|
||||
status,
|
||||
signal,
|
||||
}: UpdateAlertStatusProps): Promise<estypes.UpdateByQueryResponse> =>
|
||||
}: UpdateAlertStatusByQueryProps): Promise<estypes.UpdateByQueryResponse> =>
|
||||
KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ conflicts: 'proceed', status, ...query }),
|
||||
body: JSON.stringify({ conflicts: 'proceed', status, query }),
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Update alert status by signalIds
|
||||
*
|
||||
* @param signalIds List of signal ids to update
|
||||
* @param status to update to('open' / 'closed' / 'acknowledged')
|
||||
* @param signal AbortSignal for cancelling request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const updateAlertStatusByIds = async ({
|
||||
signalIds,
|
||||
status,
|
||||
signal,
|
||||
}: UpdateAlertStatusByIdsProps): Promise<estypes.BulkResponse> =>
|
||||
KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ status, signal_ids: signalIds }),
|
||||
signal,
|
||||
});
|
||||
|
||||
|
|
|
@ -38,10 +38,16 @@ export interface AlertSearchResponse<Hit = {}, Aggregations = {} | undefined>
|
|||
};
|
||||
}
|
||||
|
||||
export interface UpdateAlertStatusProps {
|
||||
export interface UpdateAlertStatusByQueryProps {
|
||||
query: object;
|
||||
status: Status;
|
||||
signal?: AbortSignal; // TODO: implement cancelling
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface UpdateAlertStatusByIdsProps {
|
||||
signalIds: string[];
|
||||
status: Status;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface AlertsIndex {
|
||||
|
|
|
@ -12,14 +12,12 @@ import { buildEsQuery } from '@kbn/es-query';
|
|||
import type { TableId } from '@kbn/securitysolution-data-table';
|
||||
import type { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { APM_USER_INTERACTIONS } from '../../../common/lib/apm/constants';
|
||||
import { useUpdateAlertsStatus } from '../../../common/components/toolbar/bulk_actions/use_update_alerts';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { updateAlertStatus } from '../../../common/components/toolbar/bulk_actions/update_alerts';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
|
||||
import type { AlertWorkflowStatus } from '../../../common/types';
|
||||
import { FILTER_CLOSED, FILTER_OPEN, FILTER_ACKNOWLEDGED } from '../../../../common/types';
|
||||
import * as i18n from '../translations';
|
||||
import { getUpdateAlertsQuery } from '../../components/alerts_table/actions';
|
||||
import { buildTimeRangeFilter } from '../../components/alerts_table/helpers';
|
||||
|
||||
interface UseBulkAlertActionItemsArgs {
|
||||
|
@ -45,7 +43,6 @@ export const useBulkAlertActionItems = ({
|
|||
}: UseBulkAlertActionItemsArgs) => {
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
const { updateAlertStatus } = useUpdateAlertsStatus();
|
||||
const { addSuccess, addError, addWarning } = useAppToasts();
|
||||
|
||||
const onAlertStatusUpdateSuccess = useCallback(
|
||||
|
@ -92,8 +89,6 @@ export const useBulkAlertActionItems = ({
|
|||
[addError]
|
||||
);
|
||||
|
||||
const { selectedPatterns } = useSourcererDataView(scopeId);
|
||||
|
||||
const getOnAction = useCallback(
|
||||
(status: AlertWorkflowStatus) => {
|
||||
const onActionClick: BulkActionsConfig['onClick'] = async (
|
||||
|
@ -103,14 +98,13 @@ export const useBulkAlertActionItems = ({
|
|||
clearSelection,
|
||||
refresh
|
||||
) => {
|
||||
const ids = items.map((item) => item._id);
|
||||
let query: Record<string, unknown> = getUpdateAlertsQuery(ids).query;
|
||||
let ids: string[] | undefined = items.map((item) => item._id);
|
||||
let query: Record<string, unknown> | undefined;
|
||||
|
||||
if (isSelectAllChecked) {
|
||||
const timeFilter = buildTimeRangeFilter(from, to);
|
||||
query = buildEsQuery(undefined, [], [...timeFilter, ...filters], undefined);
|
||||
}
|
||||
if (query) {
|
||||
ids = undefined;
|
||||
startTransaction({ name: APM_USER_INTERACTIONS.BULK_QUERY_STATUS_UPDATE });
|
||||
} else if (items.length > 1) {
|
||||
startTransaction({ name: APM_USER_INTERACTIONS.BULK_STATUS_UPDATE });
|
||||
|
@ -121,9 +115,9 @@ export const useBulkAlertActionItems = ({
|
|||
try {
|
||||
setAlertLoading(true);
|
||||
const response = await updateAlertStatus({
|
||||
index: selectedPatterns.join(','),
|
||||
status,
|
||||
query,
|
||||
signalIds: ids,
|
||||
});
|
||||
|
||||
setAlertLoading(false);
|
||||
|
@ -150,8 +144,6 @@ export const useBulkAlertActionItems = ({
|
|||
[
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
updateAlertStatus,
|
||||
selectedPatterns,
|
||||
startTransaction,
|
||||
filters,
|
||||
from,
|
||||
|
|
|
@ -17,14 +17,8 @@ import { useHostIsolationTools } from '../../timelines/components/side_panel/eve
|
|||
*/
|
||||
export const PanelFooter: FC = memo(() => {
|
||||
const { closeFlyout } = useExpandableFlyoutContext();
|
||||
const {
|
||||
eventId,
|
||||
indexName,
|
||||
dataFormattedForFieldBrowser,
|
||||
dataAsNestedObject,
|
||||
refetchFlyoutData,
|
||||
scopeId,
|
||||
} = useRightPanelContext();
|
||||
const { dataFormattedForFieldBrowser, dataAsNestedObject, refetchFlyoutData, scopeId } =
|
||||
useRightPanelContext();
|
||||
|
||||
const { isHostIsolationPanelOpen, showHostIsolationPanel } = useHostIsolationTools();
|
||||
|
||||
|
@ -36,7 +30,6 @@ export const PanelFooter: FC = memo(() => {
|
|||
<FlyoutFooter
|
||||
detailsData={dataFormattedForFieldBrowser}
|
||||
detailsEcsData={dataAsNestedObject}
|
||||
expandedEvent={{ eventId, indexName }}
|
||||
handleOnEventClosed={closeFlyout}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
isReadOnly={false}
|
||||
|
|
|
@ -220,7 +220,6 @@ export const ExpandableEvent = React.memo<Props>(
|
|||
detailsEcsData={detailsEcsData}
|
||||
id={event.eventId}
|
||||
isAlert={isAlert}
|
||||
indexName={event.indexName}
|
||||
isDraggable={isDraggable}
|
||||
rawEventData={rawEventData}
|
||||
scopeId={scopeId}
|
||||
|
|
|
@ -115,7 +115,6 @@ const defaultProps = {
|
|||
isHostIsolationPanelOpen: false,
|
||||
handleOnEventClosed: jest.fn(),
|
||||
onAddIsolationStatusClick: jest.fn(),
|
||||
expandedEvent: { eventId: ecsData._id, indexName: '' },
|
||||
detailsData: mockAlertDetailsDataWithIsObject,
|
||||
refetchFlyoutData: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -26,11 +26,6 @@ import { OsqueryFlyout } from '../../../../../detections/components/osquery/osqu
|
|||
interface FlyoutFooterProps {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
detailsEcsData: Ecs | null;
|
||||
expandedEvent: {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
refetch?: () => void;
|
||||
};
|
||||
handleOnEventClosed: () => void;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
isReadOnly?: boolean;
|
||||
|
@ -52,7 +47,6 @@ export const FlyoutFooterComponent = React.memo(
|
|||
({
|
||||
detailsData,
|
||||
detailsEcsData,
|
||||
expandedEvent,
|
||||
handleOnEventClosed,
|
||||
isHostIsolationPanelOpen,
|
||||
isReadOnly,
|
||||
|
@ -162,7 +156,6 @@ export const FlyoutFooterComponent = React.memo(
|
|||
onAddIsolationStatusClick={onAddIsolationStatusClick}
|
||||
refetchFlyoutData={refetchFlyoutData}
|
||||
refetch={refetchAll}
|
||||
indexName={expandedEvent.indexName}
|
||||
scopeId={scopeId}
|
||||
onOsqueryClick={setOsqueryFlyoutOpenWithAgentId}
|
||||
/>
|
||||
|
|
|
@ -136,7 +136,6 @@ export const useToGetInternalFlyout = () => {
|
|||
<FlyoutFooter
|
||||
detailsData={detailsData}
|
||||
detailsEcsData={ecsData}
|
||||
expandedEvent={{ eventId: localAlert._id, indexName: localAlert._index }}
|
||||
refetchFlyoutData={refetchFlyoutData}
|
||||
handleOnEventClosed={noop}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
|
|
|
@ -257,7 +257,6 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
<FlyoutFooter
|
||||
detailsData={detailsData}
|
||||
detailsEcsData={ecsData}
|
||||
expandedEvent={expandedEvent}
|
||||
refetchFlyoutData={refetchFlyoutData}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
|
|
|
@ -84,6 +84,53 @@ describe('set signal status', () => {
|
|||
status_code: 500,
|
||||
});
|
||||
});
|
||||
|
||||
test('calls "esClient.updateByQuery" with queryId when query is defined', async () => {
|
||||
await server.inject(
|
||||
getSetSignalStatusByQueryRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(context.core.elasticsearch.client.asCurrentUser.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
query: expect.objectContaining({
|
||||
bool: { filter: typicalSetStatusSignalByQueryPayload().query },
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('calls "esClient.bulk" with signalIds when ids are defined', async () => {
|
||||
await server.inject(
|
||||
getSetSignalStatusByIdsRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(context.core.elasticsearch.client.asCurrentUser.bulk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.arrayContaining([
|
||||
{
|
||||
update: {
|
||||
_id: 'somefakeid1',
|
||||
_index: '.alerts-security.alerts-default',
|
||||
},
|
||||
},
|
||||
{
|
||||
script: expect.anything(),
|
||||
},
|
||||
{
|
||||
update: {
|
||||
_id: 'somefakeid2',
|
||||
_index: '.alerts-security.alerts-default',
|
||||
},
|
||||
},
|
||||
{
|
||||
script: expect.anything(),
|
||||
},
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('request validation', () => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { get } from 'lodash';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import { setSignalStatusValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/set_signal_status_type_dependents';
|
||||
import type { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema';
|
||||
import { setSignalsStatusSchema } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema';
|
||||
|
@ -87,40 +87,20 @@ export const setSignalsStatusRoute = (
|
|||
}
|
||||
}
|
||||
|
||||
let queryObject;
|
||||
if (signalIds) {
|
||||
queryObject = { ids: { values: signalIds } };
|
||||
}
|
||||
if (query) {
|
||||
queryObject = {
|
||||
bool: {
|
||||
filter: query,
|
||||
},
|
||||
};
|
||||
}
|
||||
try {
|
||||
const body = await esClient.updateByQuery({
|
||||
index: `${DEFAULT_ALERTS_INDEX}-${spaceId}`,
|
||||
conflicts: conflicts ?? 'abort',
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html#_refreshing_shards_2
|
||||
// Note: Before we tried to use "refresh: wait_for" but I do not think that was available and instead it defaulted to "refresh: true"
|
||||
// but the tests do not pass with "refresh: false". If at some point a "refresh: wait_for" is implemented, we should use that instead.
|
||||
refresh: true,
|
||||
body: {
|
||||
script: {
|
||||
source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) {
|
||||
ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}'
|
||||
}
|
||||
if (ctx._source.signal != null && ctx._source.signal.status != null) {
|
||||
ctx._source.signal.status = '${status}'
|
||||
}`,
|
||||
lang: 'painless',
|
||||
},
|
||||
query: queryObject,
|
||||
},
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
return response.ok({ body });
|
||||
if (signalIds) {
|
||||
const body = await updateSignalsStatusByIds(status, signalIds, spaceId, esClient);
|
||||
return response.ok({ body });
|
||||
} else {
|
||||
const body = await updateSignalsStatusByQuery(
|
||||
status,
|
||||
query,
|
||||
{ conflicts: conflicts ?? 'abort' },
|
||||
spaceId,
|
||||
esClient
|
||||
);
|
||||
return response.ok({ body });
|
||||
}
|
||||
} catch (err) {
|
||||
// error while getting or updating signal with id: id in signal index .siem-signals
|
||||
const error = transformError(err);
|
||||
|
@ -132,3 +112,62 @@ export const setSignalsStatusRoute = (
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
const updateSignalsStatusByIds = async (
|
||||
status: SetSignalsStatusSchemaDecoded['status'],
|
||||
signalsId: string[],
|
||||
spaceId: string,
|
||||
esClient: ElasticsearchClient
|
||||
) =>
|
||||
esClient.bulk({
|
||||
index: `${DEFAULT_ALERTS_INDEX}-${spaceId}`,
|
||||
refresh: 'wait_for',
|
||||
body: signalsId.flatMap((signalId) => [
|
||||
{
|
||||
update: { _id: signalId, _index: `${DEFAULT_ALERTS_INDEX}-${spaceId}` },
|
||||
},
|
||||
{
|
||||
script: getUpdateSignalStatusScript(status),
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Please avoid using `updateSignalsStatusByQuery` when possible, use `updateSignalsStatusByIds` instead.
|
||||
*
|
||||
* This method calls `updateByQuery` with `refresh: true` which is expensive on serverless.
|
||||
*/
|
||||
const updateSignalsStatusByQuery = async (
|
||||
status: SetSignalsStatusSchemaDecoded['status'],
|
||||
query: object | undefined,
|
||||
options: { conflicts: 'abort' | 'proceed' },
|
||||
spaceId: string,
|
||||
esClient: ElasticsearchClient
|
||||
) =>
|
||||
esClient.updateByQuery({
|
||||
index: `${DEFAULT_ALERTS_INDEX}-${spaceId}`,
|
||||
conflicts: options.conflicts,
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html#_refreshing_shards_2
|
||||
// Note: Before we tried to use "refresh: wait_for" but I do not think that was available and instead it defaulted to "refresh: true"
|
||||
// but the tests do not pass with "refresh: false". If at some point a "refresh: wait_for" is implemented, we should use that instead.
|
||||
refresh: true,
|
||||
body: {
|
||||
script: getUpdateSignalStatusScript(status),
|
||||
query: {
|
||||
bool: {
|
||||
filter: query,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
|
||||
const getUpdateSignalStatusScript = (status: SetSignalsStatusSchemaDecoded['status']) => ({
|
||||
source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) {
|
||||
ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}'
|
||||
}
|
||||
if (ctx._source.signal != null && ctx._source.signal.status != null) {
|
||||
ctx._source.signal.status = '${status}'
|
||||
}`,
|
||||
lang: 'painless',
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
setSignalStatus,
|
||||
getAlertUpdateEmptyResponse,
|
||||
getAlertUpdateByQueryEmptyResponse,
|
||||
getQuerySignalIds,
|
||||
deleteAllRules,
|
||||
createRule,
|
||||
|
@ -41,33 +41,106 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
describe('open_close_signals', () => {
|
||||
describe('validation checks', () => {
|
||||
it('should not give errors when querying and the signals index does not exist yet', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(setSignalStatus({ signalIds: ['123'], status: 'open' }))
|
||||
.expect(200);
|
||||
describe('update by ids', () => {
|
||||
it('should not give errors when querying and the signals index does not exist yet', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(setSignalStatus({ signalIds: ['123'], status: 'open' }))
|
||||
.expect(200);
|
||||
|
||||
// remove any server generated items that are indeterministic
|
||||
delete body.took;
|
||||
// remove any server generated items that are nondeterministic
|
||||
body.items.forEach((_: any, index: number) => {
|
||||
delete body.items[index].update.error.index_uuid;
|
||||
});
|
||||
delete body.took;
|
||||
|
||||
expect(body).to.eql(getAlertUpdateEmptyResponse());
|
||||
expect(body).to.eql({
|
||||
errors: true,
|
||||
items: [
|
||||
{
|
||||
update: {
|
||||
_id: '123',
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
error: {
|
||||
index: '.internal.alerts-security.alerts-default-000001',
|
||||
reason: '[123]: document missing',
|
||||
shard: '0',
|
||||
type: 'document_missing_exception',
|
||||
},
|
||||
status: 404,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not give errors when querying and the signals index does exist and is empty', async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(setSignalStatus({ signalIds: ['123'], status: 'open' }))
|
||||
.expect(200);
|
||||
|
||||
// remove any server generated items that are nondeterministic
|
||||
body.items.forEach((_: any, index: number) => {
|
||||
delete body.items[index].update.error.index_uuid;
|
||||
});
|
||||
delete body.took;
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: true,
|
||||
items: [
|
||||
{
|
||||
update: {
|
||||
_id: '123',
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
error: {
|
||||
index: '.internal.alerts-security.alerts-default-000001',
|
||||
reason: '[123]: document missing',
|
||||
shard: '0',
|
||||
type: 'document_missing_exception',
|
||||
},
|
||||
status: 404,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await deleteAllAlerts(supertest, log, es);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not give errors when querying and the signals index does exist and is empty', async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(setSignalStatus({ signalIds: ['123'], status: 'open' }))
|
||||
.expect(200);
|
||||
describe('update by query', () => {
|
||||
it('should not give errors when querying and the signals index does not exist yet', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(setSignalStatus({ query: { match_all: {} }, status: 'open' }))
|
||||
.expect(200);
|
||||
|
||||
// remove any server generated items that are indeterministic
|
||||
delete body.took;
|
||||
// remove any server generated items that are indeterministic
|
||||
delete body.took;
|
||||
|
||||
expect(body).to.eql(getAlertUpdateEmptyResponse());
|
||||
expect(body).to.eql(getAlertUpdateByQueryEmptyResponse());
|
||||
});
|
||||
|
||||
await deleteAllAlerts(supertest, log, es);
|
||||
it('should not give errors when querying and the signals index does exist and is empty', async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(setSignalStatus({ query: { match_all: {} }, status: 'open' }))
|
||||
.expect(200);
|
||||
|
||||
// remove any server generated items that are indeterministic
|
||||
delete body.took;
|
||||
|
||||
expect(body).to.eql(getAlertUpdateByQueryEmptyResponse());
|
||||
|
||||
await deleteAllAlerts(supertest, log, es);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tests with auditbeat data', () => {
|
||||
|
|
|
@ -17,7 +17,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context';
|
|||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
getAlertUpdateEmptyResponse,
|
||||
getQuerySignalIds,
|
||||
deleteAllRules,
|
||||
createRule,
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
getSignalsByIds,
|
||||
waitForRuleSuccess,
|
||||
getRuleForSignalTesting,
|
||||
getAlertUpdateByQueryEmptyResponse,
|
||||
} from '../../utils';
|
||||
import { buildAlertTagsQuery, setAlertTags } from '../../utils/set_alert_tags';
|
||||
|
||||
|
@ -47,7 +47,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
// remove any server generated items that are indeterministic
|
||||
delete body.took;
|
||||
|
||||
expect(body).to.eql(getAlertUpdateEmptyResponse());
|
||||
expect(body).to.eql(getAlertUpdateByQueryEmptyResponse());
|
||||
});
|
||||
|
||||
it('should give errors when duplicate tags exist in both tags_to_add and tags_to_remove', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const getAlertUpdateEmptyResponse = () => ({
|
||||
export const getAlertUpdateByQueryEmptyResponse = () => ({
|
||||
timed_out: false,
|
||||
total: 0,
|
||||
updated: 0,
|
||||
|
|
|
@ -12,11 +12,14 @@ import type {
|
|||
|
||||
export const setSignalStatus = ({
|
||||
signalIds,
|
||||
query,
|
||||
status,
|
||||
}: {
|
||||
signalIds: SignalIds;
|
||||
signalIds?: SignalIds;
|
||||
query?: object;
|
||||
status: Status;
|
||||
}) => ({
|
||||
signal_ids: signalIds,
|
||||
query,
|
||||
status,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue