[Security Solution][Exceptions] Exception modal bulk close alerts that match exception attributes (#71321)
* progress on bulk close * works but could be slow * clean up, add tests * fix reduce types * address 'event.' fields * remove duplicate import * don't replace nested fields * my best friend typescript
This commit is contained in:
parent
c86ad7bbec
commit
f4091df289
File diff suppressed because it is too large
Load diff
|
@ -17,6 +17,7 @@ import {
|
|||
entriesMatch,
|
||||
entriesNested,
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
} from '../shared_imports';
|
||||
import { Language, Query } from './schemas/common/schemas';
|
||||
|
||||
|
@ -45,32 +46,35 @@ export const getLanguageBooleanOperator = ({
|
|||
export const operatorBuilder = ({
|
||||
operator,
|
||||
language,
|
||||
exclude,
|
||||
}: {
|
||||
operator: Operator;
|
||||
language: Language;
|
||||
exclude: boolean;
|
||||
}): string => {
|
||||
const not = getLanguageBooleanOperator({
|
||||
language,
|
||||
value: 'not',
|
||||
});
|
||||
|
||||
switch (operator) {
|
||||
case 'included':
|
||||
return `${not} `;
|
||||
default:
|
||||
return '';
|
||||
if ((exclude && operator === 'included') || (!exclude && operator === 'excluded')) {
|
||||
return `${not} `;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const buildExists = ({
|
||||
item,
|
||||
language,
|
||||
exclude,
|
||||
}: {
|
||||
item: EntryExists;
|
||||
language: Language;
|
||||
exclude: boolean;
|
||||
}): string => {
|
||||
const { operator, field } = item;
|
||||
const exceptionOperator = operatorBuilder({ operator, language });
|
||||
const exceptionOperator = operatorBuilder({ operator, language, exclude });
|
||||
|
||||
switch (language) {
|
||||
case 'kuery':
|
||||
|
@ -85,12 +89,14 @@ export const buildExists = ({
|
|||
export const buildMatch = ({
|
||||
item,
|
||||
language,
|
||||
exclude,
|
||||
}: {
|
||||
item: EntryMatch;
|
||||
language: Language;
|
||||
exclude: boolean;
|
||||
}): string => {
|
||||
const { value, operator, field } = item;
|
||||
const exceptionOperator = operatorBuilder({ operator, language });
|
||||
const exceptionOperator = operatorBuilder({ operator, language, exclude });
|
||||
|
||||
return `${exceptionOperator}${field}:${value}`;
|
||||
};
|
||||
|
@ -98,9 +104,11 @@ export const buildMatch = ({
|
|||
export const buildMatchAny = ({
|
||||
item,
|
||||
language,
|
||||
exclude,
|
||||
}: {
|
||||
item: EntryMatchAny;
|
||||
language: Language;
|
||||
exclude: boolean;
|
||||
}): string => {
|
||||
const { value, operator, field } = item;
|
||||
|
||||
|
@ -109,7 +117,7 @@ export const buildMatchAny = ({
|
|||
return '';
|
||||
default:
|
||||
const or = getLanguageBooleanOperator({ language, value: 'or' });
|
||||
const exceptionOperator = operatorBuilder({ operator, language });
|
||||
const exceptionOperator = operatorBuilder({ operator, language, exclude });
|
||||
const matchAnyValues = value.map((v) => v);
|
||||
|
||||
return `${exceptionOperator}${field}:(${matchAnyValues.join(` ${or} `)})`;
|
||||
|
@ -133,16 +141,18 @@ export const buildNested = ({
|
|||
export const evaluateValues = ({
|
||||
item,
|
||||
language,
|
||||
exclude,
|
||||
}: {
|
||||
item: Entry | EntryNested;
|
||||
language: Language;
|
||||
exclude: boolean;
|
||||
}): string => {
|
||||
if (entriesExists.is(item)) {
|
||||
return buildExists({ item, language });
|
||||
return buildExists({ item, language, exclude });
|
||||
} else if (entriesMatch.is(item)) {
|
||||
return buildMatch({ item, language });
|
||||
return buildMatch({ item, language, exclude });
|
||||
} else if (entriesMatchAny.is(item)) {
|
||||
return buildMatchAny({ item, language });
|
||||
return buildMatchAny({ item, language, exclude });
|
||||
} else if (entriesNested.is(item)) {
|
||||
return buildNested({ item, language });
|
||||
} else {
|
||||
|
@ -163,7 +173,11 @@ export const formatQuery = ({
|
|||
const or = getLanguageBooleanOperator({ language, value: 'or' });
|
||||
const and = getLanguageBooleanOperator({ language, value: 'and' });
|
||||
const formattedExceptions = exceptions.map((exception) => {
|
||||
return `(${query} ${and} ${exception})`;
|
||||
if (query === '') {
|
||||
return `(${exception})`;
|
||||
} else {
|
||||
return `(${query} ${and} ${exception})`;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedExceptions.join(` ${or} `);
|
||||
|
@ -175,15 +189,17 @@ export const formatQuery = ({
|
|||
export const buildExceptionItemEntries = ({
|
||||
lists,
|
||||
language,
|
||||
exclude,
|
||||
}: {
|
||||
lists: EntriesArray;
|
||||
language: Language;
|
||||
exclude: boolean;
|
||||
}): string => {
|
||||
const and = getLanguageBooleanOperator({ language, value: 'and' });
|
||||
const exceptionItem = lists
|
||||
.filter(({ type }) => type !== 'list')
|
||||
.reduce<string[]>((accum, listItem) => {
|
||||
const exceptionSegment = evaluateValues({ item: listItem, language });
|
||||
const exceptionSegment = evaluateValues({ item: listItem, language, exclude });
|
||||
return [...accum, exceptionSegment];
|
||||
}, []);
|
||||
|
||||
|
@ -194,15 +210,22 @@ export const buildQueryExceptions = ({
|
|||
query,
|
||||
language,
|
||||
lists,
|
||||
exclude = true,
|
||||
}: {
|
||||
query: Query;
|
||||
language: Language;
|
||||
lists: ExceptionListItemSchema[] | undefined;
|
||||
lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema> | undefined;
|
||||
exclude?: boolean;
|
||||
}): DataQuery[] => {
|
||||
if (lists != null) {
|
||||
const exceptions = lists.map((exceptionItem) =>
|
||||
buildExceptionItemEntries({ lists: exceptionItem.entries, language })
|
||||
);
|
||||
const exceptions = lists.reduce<string[]>((acc, exceptionItem) => {
|
||||
return [
|
||||
...acc,
|
||||
...(exceptionItem.entries !== undefined
|
||||
? [buildExceptionItemEntries({ lists: exceptionItem.entries, language, exclude })]
|
||||
: []),
|
||||
];
|
||||
}, []);
|
||||
const formattedQuery = formatQuery({ exceptions, language, query });
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -456,6 +456,96 @@ describe('get_filter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when "excludeExceptions" is false', () => {
|
||||
test('it should work with a list', () => {
|
||||
const esQuery = getQueryFilter(
|
||||
'host.name: linux',
|
||||
'kuery',
|
||||
[],
|
||||
['auditbeat-*'],
|
||||
[getExceptionListItemSchemaMock()],
|
||||
false
|
||||
);
|
||||
expect(esQuery).toEqual({
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'host.name': 'linux',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
nested: {
|
||||
path: 'some.parentField',
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'some.parentField.nested.field': 'some value',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
score_mode: 'none',
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'some.not.nested.field': 'some value',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it should work with an empty list', () => {
|
||||
const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], [], false);
|
||||
expect(esQuery).toEqual({
|
||||
bool: {
|
||||
filter: [
|
||||
{ bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } },
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('it should work with a nested object queries', () => {
|
||||
const esQuery = getQueryFilter(
|
||||
'category:{ name:Frank and trusted:true }',
|
||||
|
|
|
@ -11,7 +11,10 @@ import {
|
|||
buildEsQuery,
|
||||
Query as DataQuery,
|
||||
} from '../../../../../src/plugins/data/common';
|
||||
import { ExceptionListItemSchema } from '../../../lists/common/schemas';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
} from '../../../lists/common/schemas';
|
||||
import { buildQueryExceptions } from './build_exceptions_query';
|
||||
import { Query, Language, Index } from './schemas/common/schemas';
|
||||
|
||||
|
@ -20,14 +23,20 @@ export const getQueryFilter = (
|
|||
language: Language,
|
||||
filters: Array<Partial<Filter>>,
|
||||
index: Index,
|
||||
lists: ExceptionListItemSchema[]
|
||||
lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
excludeExceptions: boolean = true
|
||||
) => {
|
||||
const indexPattern: IIndexPattern = {
|
||||
fields: [],
|
||||
title: index.join(),
|
||||
};
|
||||
|
||||
const queries: DataQuery[] = buildQueryExceptions({ query, language, lists });
|
||||
const queries: DataQuery[] = buildQueryExceptions({
|
||||
query,
|
||||
language,
|
||||
lists,
|
||||
exclude: excludeExceptions,
|
||||
});
|
||||
|
||||
const config = {
|
||||
allowLeadingWildcards: true,
|
||||
|
|
|
@ -251,13 +251,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
|
||||
const onAddExceptionConfirm = useCallback(() => {
|
||||
if (addOrUpdateExceptionItems !== null) {
|
||||
if (shouldCloseAlert && alertData) {
|
||||
addOrUpdateExceptionItems(enrichExceptionItems(), alertData.ecsData._id);
|
||||
} else {
|
||||
addOrUpdateExceptionItems(enrichExceptionItems());
|
||||
}
|
||||
const alertIdToClose = shouldCloseAlert && alertData ? alertData.ecsData._id : undefined;
|
||||
const bulkCloseIndex =
|
||||
shouldBulkCloseAlert && signalIndexName !== null ? [signalIndexName] : undefined;
|
||||
addOrUpdateExceptionItems(enrichExceptionItems(), alertIdToClose, bulkCloseIndex);
|
||||
}
|
||||
}, [addOrUpdateExceptionItems, enrichExceptionItems, shouldCloseAlert, alertData]);
|
||||
}, [
|
||||
addOrUpdateExceptionItems,
|
||||
enrichExceptionItems,
|
||||
shouldCloseAlert,
|
||||
shouldBulkCloseAlert,
|
||||
alertData,
|
||||
signalIndexName,
|
||||
]);
|
||||
|
||||
const isSubmitButtonDisabled = useCallback(
|
||||
() => fetchOrCreateListError || exceptionItemsToAdd.length === 0,
|
||||
|
@ -330,7 +336,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
<EuiHorizontalRule />
|
||||
<ModalBodySection>
|
||||
{alertData !== undefined && (
|
||||
<EuiFormRow>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiCheckbox
|
||||
id="close-alert-on-add-add-exception-checkbox"
|
||||
label="Close this alert"
|
||||
|
@ -339,10 +345,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiCheckbox
|
||||
id="bulk-close-alert-on-add-add-exception-checkbox"
|
||||
label={i18n.BULK_CLOSE_LABEL}
|
||||
label={
|
||||
shouldDisableBulkClose
|
||||
? i18n.BULK_CLOSE_LABEL_DISABLED
|
||||
: i18n.BULK_CLOSE_LABEL
|
||||
}
|
||||
checked={shouldBulkCloseAlert}
|
||||
onChange={onBulkCloseAlertCheckboxChange}
|
||||
disabled={shouldDisableBulkClose}
|
||||
|
|
|
@ -60,6 +60,14 @@ export const BULK_CLOSE_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const BULK_CLOSE_LABEL_DISABLED = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled',
|
||||
{
|
||||
defaultMessage:
|
||||
'Close all alerts that match attributes in this exception (Lists and non-ECS fields are not supported)',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_BUILDER_INFO = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.addException.infoLabel',
|
||||
{
|
||||
|
|
|
@ -181,9 +181,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
|
||||
const onEditExceptionConfirm = useCallback(() => {
|
||||
if (addOrUpdateExceptionItems !== null) {
|
||||
addOrUpdateExceptionItems(enrichExceptionItems());
|
||||
const bulkCloseIndex =
|
||||
shouldBulkCloseAlert && signalIndexName !== null ? [signalIndexName] : undefined;
|
||||
addOrUpdateExceptionItems(enrichExceptionItems(), undefined, bulkCloseIndex);
|
||||
}
|
||||
}, [addOrUpdateExceptionItems, enrichExceptionItems]);
|
||||
}, [addOrUpdateExceptionItems, enrichExceptionItems, shouldBulkCloseAlert, signalIndexName]);
|
||||
|
||||
const indexPatternConfig = useCallback(() => {
|
||||
if (exceptionListType === 'endpoint') {
|
||||
|
@ -239,10 +241,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
</ModalBodySection>
|
||||
<EuiHorizontalRule />
|
||||
<ModalBodySection>
|
||||
<EuiFormRow>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiCheckbox
|
||||
id="close-alert-on-add-add-exception-checkbox"
|
||||
label={i18n.BULK_CLOSE_LABEL}
|
||||
label={
|
||||
shouldDisableBulkClose ? i18n.BULK_CLOSE_LABEL_DISABLED : i18n.BULK_CLOSE_LABEL
|
||||
}
|
||||
checked={shouldBulkCloseAlert}
|
||||
onChange={onBulkCloseAlertCheckboxChange}
|
||||
disabled={shouldDisableBulkClose}
|
||||
|
|
|
@ -38,6 +38,14 @@ export const BULK_CLOSE_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const BULK_CLOSE_LABEL_DISABLED = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled',
|
||||
{
|
||||
defaultMessage:
|
||||
'Close all alerts that match attributes in this exception (Lists and non-ECS fields are not supported)',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENDPOINT_QUARANTINE_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.editException.endpointQuarantineText',
|
||||
{
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
enrichExceptionItemsWithOS,
|
||||
entryHasListType,
|
||||
entryHasNonEcsType,
|
||||
prepareExceptionItemsForBulkClose,
|
||||
} from './helpers';
|
||||
import { FormattedEntry, DescriptionListItem, EmptyEntry } from './types';
|
||||
import {
|
||||
|
@ -683,4 +684,65 @@ describe('Exception helpers', () => {
|
|||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#prepareExceptionItemsForBulkClose', () => {
|
||||
test('it should return no exceptionw when passed in an empty array', () => {
|
||||
const payload: ExceptionListItemSchema[] = [];
|
||||
const result = prepareExceptionItemsForBulkClose(payload);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test("should not make any updates when the exception entries don't contain 'event.'", () => {
|
||||
const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
|
||||
const result = prepareExceptionItemsForBulkClose(payload);
|
||||
expect(result).toEqual(payload);
|
||||
});
|
||||
|
||||
test("should update entry fields when they start with 'event.'", () => {
|
||||
const payload = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryMatchMock(),
|
||||
field: 'event.kind',
|
||||
},
|
||||
getEntryMatchMock(),
|
||||
],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryMatchMock(),
|
||||
field: 'event.module',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryMatchMock(),
|
||||
field: 'signal.original_event.kind',
|
||||
},
|
||||
getEntryMatchMock(),
|
||||
],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryMatchMock(),
|
||||
field: 'signal.original_event.module',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = prepareExceptionItemsForBulkClose(payload);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
exceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
EntryNested,
|
||||
} from '../../../lists_plugin_deps';
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { TimelineNonEcsData } from '../../../graphql/types';
|
||||
|
@ -380,6 +381,35 @@ export const formatExceptionItemForUpdate = (
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps "event." fields to "signal.original_event.". This is because when a rule is created
|
||||
* the "event" field is copied over to "original_event". When the user creates an exception,
|
||||
* they expect it to match against the original_event's fields, not the signal event's.
|
||||
* @param exceptionItems new or existing ExceptionItem[]
|
||||
*/
|
||||
export const prepareExceptionItemsForBulkClose = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
|
||||
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
|
||||
return exceptionItems.map((item: ExceptionListItemSchema | CreateExceptionListItemSchema) => {
|
||||
if (item.entries !== undefined) {
|
||||
const newEntries = item.entries.map((itemEntry: Entry | EntryNested) => {
|
||||
return {
|
||||
...itemEntry,
|
||||
field: itemEntry.field.startsWith('event.')
|
||||
? itemEntry.field.replace(/^event./, 'signal.original_event.')
|
||||
: itemEntry.field,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...item,
|
||||
entries: newEntries,
|
||||
};
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds new and existing comments to all new exceptionItems if not present already
|
||||
* @param exceptionItems new or existing ExceptionItem[]
|
||||
|
|
|
@ -9,6 +9,8 @@ import { KibanaServices } from '../../../common/lib/kibana';
|
|||
|
||||
import * as alertsApi from '../../../detections/containers/detection_engine/alerts/api';
|
||||
import * as listsApi from '../../../../../lists/public/exceptions/api';
|
||||
import * as getQueryFilterHelper from '../../../../common/detection_engine/get_query_filter';
|
||||
import * as buildAlertStatusFilterHelper from '../../../detections/components/alerts_table/default_config';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getCreateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/create_exception_list_item_schema.mock';
|
||||
import { getUpdateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/update_exception_list_item_schema.mock';
|
||||
|
@ -38,11 +40,16 @@ describe('useAddOrUpdateException', () => {
|
|||
let updateExceptionListItem: jest.SpyInstance<ReturnType<
|
||||
typeof listsApi.updateExceptionListItem
|
||||
>>;
|
||||
let getQueryFilter: jest.SpyInstance<ReturnType<typeof getQueryFilterHelper.getQueryFilter>>;
|
||||
let buildAlertStatusFilter: jest.SpyInstance<ReturnType<
|
||||
typeof buildAlertStatusFilterHelper.buildAlertStatusFilter
|
||||
>>;
|
||||
let addOrUpdateItemsArgs: Parameters<AddOrUpdateExceptionItemsFunc>;
|
||||
let render: () => RenderHookResult<UseAddOrUpdateExceptionProps, ReturnUseAddOrUpdateException>;
|
||||
const onError = jest.fn();
|
||||
const onSuccess = jest.fn();
|
||||
const alertIdToClose = 'idToClose';
|
||||
const bulkCloseIndex = ['.signals'];
|
||||
const itemsToAdd: CreateExceptionListItemSchema[] = [
|
||||
{
|
||||
...getCreateExceptionListItemSchemaMock(),
|
||||
|
@ -113,6 +120,10 @@ describe('useAddOrUpdateException', () => {
|
|||
.spyOn(listsApi, 'updateExceptionListItem')
|
||||
.mockResolvedValue(getExceptionListItemSchemaMock());
|
||||
|
||||
getQueryFilter = jest.spyOn(getQueryFilterHelper, 'getQueryFilter');
|
||||
|
||||
buildAlertStatusFilter = jest.spyOn(buildAlertStatusFilterHelper, 'buildAlertStatusFilter');
|
||||
|
||||
addOrUpdateItemsArgs = [itemsToAddOrUpdate];
|
||||
render = () =>
|
||||
renderHook<UseAddOrUpdateExceptionProps, ReturnUseAddOrUpdateException>(() =>
|
||||
|
@ -244,4 +255,92 @@ describe('useAddOrUpdateException', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when bulkCloseIndex is passed in', () => {
|
||||
beforeEach(() => {
|
||||
addOrUpdateItemsArgs = [itemsToAddOrUpdate, undefined, bulkCloseIndex];
|
||||
});
|
||||
it('should update the status of only alerts that are open', async () => {
|
||||
await act(async () => {
|
||||
const { rerender, result, waitForNextUpdate } = render();
|
||||
const addOrUpdateItems = await waitForAddOrUpdateFunc({
|
||||
rerender,
|
||||
result,
|
||||
waitForNextUpdate,
|
||||
});
|
||||
if (addOrUpdateItems) {
|
||||
addOrUpdateItems(...addOrUpdateItemsArgs);
|
||||
}
|
||||
await waitForNextUpdate();
|
||||
expect(buildAlertStatusFilter).toHaveBeenCalledTimes(1);
|
||||
expect(buildAlertStatusFilter.mock.calls[0][0]).toEqual('open');
|
||||
});
|
||||
});
|
||||
it('should generate the query filter using exceptions', async () => {
|
||||
await act(async () => {
|
||||
const { rerender, result, waitForNextUpdate } = render();
|
||||
const addOrUpdateItems = await waitForAddOrUpdateFunc({
|
||||
rerender,
|
||||
result,
|
||||
waitForNextUpdate,
|
||||
});
|
||||
if (addOrUpdateItems) {
|
||||
addOrUpdateItems(...addOrUpdateItemsArgs);
|
||||
}
|
||||
await waitForNextUpdate();
|
||||
expect(getQueryFilter).toHaveBeenCalledTimes(1);
|
||||
expect(getQueryFilter.mock.calls[0][4]).toEqual(itemsToAddOrUpdate);
|
||||
expect(getQueryFilter.mock.calls[0][5]).toEqual(false);
|
||||
});
|
||||
});
|
||||
it('should update the alert status', async () => {
|
||||
await act(async () => {
|
||||
const { rerender, result, waitForNextUpdate } = render();
|
||||
const addOrUpdateItems = await waitForAddOrUpdateFunc({
|
||||
rerender,
|
||||
result,
|
||||
waitForNextUpdate,
|
||||
});
|
||||
if (addOrUpdateItems) {
|
||||
addOrUpdateItems(...addOrUpdateItemsArgs);
|
||||
}
|
||||
await waitForNextUpdate();
|
||||
expect(updateAlertStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
it('creates new items', async () => {
|
||||
await act(async () => {
|
||||
const { rerender, result, waitForNextUpdate } = render();
|
||||
const addOrUpdateItems = await waitForAddOrUpdateFunc({
|
||||
rerender,
|
||||
result,
|
||||
waitForNextUpdate,
|
||||
});
|
||||
if (addOrUpdateItems) {
|
||||
addOrUpdateItems(...addOrUpdateItemsArgs);
|
||||
}
|
||||
await waitForNextUpdate();
|
||||
expect(addExceptionListItem).toHaveBeenCalledTimes(2);
|
||||
expect(addExceptionListItem.mock.calls[1][0].listItem).toEqual(itemsToAdd[1]);
|
||||
});
|
||||
});
|
||||
it('updates existing items', async () => {
|
||||
await act(async () => {
|
||||
const { rerender, result, waitForNextUpdate } = render();
|
||||
const addOrUpdateItems = await waitForAddOrUpdateFunc({
|
||||
rerender,
|
||||
result,
|
||||
waitForNextUpdate,
|
||||
});
|
||||
if (addOrUpdateItems) {
|
||||
addOrUpdateItems(...addOrUpdateItemsArgs);
|
||||
}
|
||||
await waitForNextUpdate();
|
||||
expect(updateExceptionListItem).toHaveBeenCalledTimes(2);
|
||||
expect(updateExceptionListItem.mock.calls[1][0].listItem).toEqual(
|
||||
itemsToUpdateFormatted[1]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,18 +16,23 @@ import {
|
|||
} from '../../../lists_plugin_deps';
|
||||
import { updateAlertStatus } from '../../../detections/containers/detection_engine/alerts/api';
|
||||
import { getUpdateAlertsQuery } from '../../../detections/components/alerts_table/actions';
|
||||
import { formatExceptionItemForUpdate } from './helpers';
|
||||
import { buildAlertStatusFilter } from '../../../detections/components/alerts_table/default_config';
|
||||
import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter';
|
||||
import { Index } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from './helpers';
|
||||
|
||||
/**
|
||||
* Adds exception items to the list. Also optionally closes alerts.
|
||||
*
|
||||
* @param exceptionItemsToAddOrUpdate array of ExceptionListItemSchema to add or update
|
||||
* @param alertIdToClose - optional string representing alert to close
|
||||
* @param bulkCloseIndex - optional index used to create bulk close query
|
||||
*
|
||||
*/
|
||||
export type AddOrUpdateExceptionItemsFunc = (
|
||||
exceptionItemsToAddOrUpdate: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
alertIdToClose?: string
|
||||
alertIdToClose?: string,
|
||||
bulkCloseIndex?: Index
|
||||
) => Promise<void>;
|
||||
|
||||
export type ReturnUseAddOrUpdateException = [
|
||||
|
@ -100,7 +105,8 @@ export const useAddOrUpdateException = ({
|
|||
|
||||
const addOrUpdateExceptionItems: AddOrUpdateExceptionItemsFunc = async (
|
||||
exceptionItemsToAddOrUpdate,
|
||||
alertIdToClose
|
||||
alertIdToClose,
|
||||
bulkCloseIndex
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
@ -111,6 +117,23 @@ export const useAddOrUpdateException = ({
|
|||
});
|
||||
}
|
||||
|
||||
if (bulkCloseIndex != null) {
|
||||
const filter = getQueryFilter(
|
||||
'',
|
||||
'kuery',
|
||||
buildAlertStatusFilter('open'),
|
||||
bulkCloseIndex,
|
||||
prepareExceptionItemsForBulkClose(exceptionItemsToAddOrUpdate),
|
||||
false
|
||||
);
|
||||
await updateAlertStatus({
|
||||
query: {
|
||||
query: filter,
|
||||
},
|
||||
status: 'closed',
|
||||
});
|
||||
}
|
||||
|
||||
await addOrUpdateItems(exceptionItemsToAddOrUpdate);
|
||||
|
||||
if (isSubscribed) {
|
||||
|
|
Loading…
Reference in a new issue