mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[SecuritySolution][Detections] Resolves referential integrity issues when deleting value lists (#85925)
## Summary Resolves https://github.com/elastic/kibana/issues/77324, https://github.com/elastic/kibana/issues/77325, resolves https://github.com/elastic/kibana/issues/77325, and resolves https://github.com/elastic/kibana/issues/81302 This PR addresses referential integrity issues when deleting value lists. Previously when deleting value lists, any references in Exception Lists/Items would be left behind. This PR introduces a new confirmation modal when deleting value lists that are referenced in either space aware (`simple`) or space `agnostic` exception lists. Also includes: * Fixed Lists plugin `quick_start.sh` as it was using endpoint exception list + value lists (unsupported) * Adds `quick_start_value_list_references.sh` to create exception lists/items, value lists, and references to easily test * Add support to `findExceptionList` for searching for both `simple` and `agnostic` list types * Two new query params have been added to the `deleteListRoute` * `ignoreReferences` (default:false) when true, maintains pre-7.11 behavior of deleting value list without performing any additional checks. * NOTE: As written, this becomes an API breaking change as existing existing calls to the same API will `409` conflict if references exist. cc @jmikell821 @DonNateR * `deleteReferences` (default:false) to perform dry run and identify referenced exception lists/items ## Testing To test, run `quick_start_value_list_references.sh` and it will create all the necessary resources/references to easily exercise the above functionality. The below diagram details the resources created and how the references are wired up. > Creates three different exception lists and value lists, and associates as > below to test referential integrity functionality. > > NOTE: Endpoint lists don't support value lists, and are not tested here > > EL: Exception list > ELI Exception list Item > VL: Value list > > EL1 EL2 (Agnostic) EL3 > | | | > ELI1 ELI2 ELI3 > |\ /| | > | \ / | | > | \ / | | > | \ / | | > | \/ | | > | /\ | | > | / \ | | > | / \ | | > | / \ | | > |/ \| | > VL1 VL2 VL3 VL4 > ips.txt ip_range.txt text.txt hosts.txt > Corner cases to be aware of: * An exception item may have multiple value list entries -- only referenced value list entries should be removed * There is no API for removing individual entries. If all entries are references the entire item is deleted. If only some entries are references, the item is updated via a `PUT` (no `PATCH` support for exception items) * It's not possible via the UI to create a space agnostic list that has value list exception items (only agnostic endpoint exception lists can be created and they do not support value lists). Please use above script to exercise this behavior. Additional notes: * Once the Exception List table is introduced (https://github.com/elastic/kibana/pull/85465), we can add an enhancement for deeplinking to exception lists from the reference error modal. * The `deleteListRoute` response has been updated to include the responses from the reference checks to provide maximum flexibility * There is no bulk API for deleting exception list items, and so they are iterated over via the `deleteExceptionListItem` API. ##### Reference error modal <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/102199153-813e1e80-3e80-11eb-8a9b-af116ca13df9.gif" /> </p> ##### Overflow example <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/102199032-5784f780-3e80-11eb-81c7-17283d002ce4.gif" /> </p> ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [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 - [X] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) ### For maintainers - [X] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
92a805f56f
commit
4dccbcad33
45 changed files with 1267 additions and 53 deletions
|
@ -41,6 +41,7 @@ export const NESTED_FIELD = 'parent.field';
|
|||
// Exception List specific
|
||||
export const ID = 'uuid_here';
|
||||
export const ITEM_ID = 'some-list-item-id';
|
||||
export const DETECTION_TYPE = 'detection';
|
||||
export const ENDPOINT_TYPE = 'endpoint';
|
||||
export const FIELD = 'host.name';
|
||||
export const OPERATOR = 'included';
|
||||
|
|
188
x-pack/plugins/lists/common/format_errors.test.ts
Normal file
188
x-pack/plugins/lists/common/format_errors.test.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { formatErrors } from './format_errors';
|
||||
|
||||
describe('utils', () => {
|
||||
test('returns an empty error message string if there are no errors', () => {
|
||||
const errors: t.Errors = [];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns a single error message if given one', () => {
|
||||
const validationError: t.ValidationError = {
|
||||
context: [],
|
||||
message: 'some error',
|
||||
value: 'Some existing error',
|
||||
};
|
||||
const errors: t.Errors = [validationError];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['some error']);
|
||||
});
|
||||
|
||||
test('returns a two error messages if given two', () => {
|
||||
const validationError1: t.ValidationError = {
|
||||
context: [],
|
||||
message: 'some error 1',
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const validationError2: t.ValidationError = {
|
||||
context: [],
|
||||
message: 'some error 2',
|
||||
value: 'Some existing error 2',
|
||||
};
|
||||
const errors: t.Errors = [validationError1, validationError2];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['some error 1', 'some error 2']);
|
||||
});
|
||||
|
||||
test('it filters out duplicate error messages', () => {
|
||||
const validationError1: t.ValidationError = {
|
||||
context: [],
|
||||
message: 'some error 1',
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const validationError2: t.ValidationError = {
|
||||
context: [],
|
||||
message: 'some error 1',
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1, validationError2];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['some error 1']);
|
||||
});
|
||||
|
||||
test('will use message before context if it is set', () => {
|
||||
const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
message: 'I should be used first',
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['I should be used first']);
|
||||
});
|
||||
|
||||
test('will use context entry of a single string', () => {
|
||||
const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']);
|
||||
});
|
||||
|
||||
test('will use two context entries of two strings', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: 'some string key 1' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will filter out and not use any strings of numbers', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '5' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will filter out and not use null', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: null },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will filter out and not use empty strings', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will use a name context if it cannot find a keyContext', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '' },
|
||||
{ key: '', type: { name: 'someName' } },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "someName"']);
|
||||
});
|
||||
|
||||
test('will return an empty string if name does not exist but type does', () => {
|
||||
const context: t.Context = ([{ key: '' }, { key: '', type: {} }] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: 'Some existing error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to ""']);
|
||||
});
|
||||
|
||||
test('will stringify an error value', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
context,
|
||||
value: { foo: 'some error' },
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "{"foo":"some error"}" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
});
|
31
x-pack/plugins/lists/common/format_errors.ts
Normal file
31
x-pack/plugins/lists/common/format_errors.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { isObject } from 'lodash/fp';
|
||||
|
||||
export const formatErrors = (errors: t.Errors): string[] => {
|
||||
const err = errors.map((error) => {
|
||||
if (error.message != null) {
|
||||
return error.message;
|
||||
} else {
|
||||
const keyContext = error.context
|
||||
.filter(
|
||||
(entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== ''
|
||||
)
|
||||
.map((entry) => entry.key)
|
||||
.join(',');
|
||||
|
||||
const nameContext = error.context.find((entry) => entry.type?.name?.length > 0);
|
||||
const suppliedValue =
|
||||
keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : '';
|
||||
const value = isObject(error.value) ? JSON.stringify(error.value) : error.value;
|
||||
return `Invalid value "${value}" supplied to "${suppliedValue}"`;
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(err)];
|
||||
};
|
|
@ -9,5 +9,7 @@ import { LIST_ID } from '../../constants.mock';
|
|||
import { DeleteListSchema } from './delete_list_schema';
|
||||
|
||||
export const getDeleteListSchemaMock = (): DeleteListSchema => ({
|
||||
deleteReferences: false,
|
||||
id: LIST_ID,
|
||||
ignoreReferences: true,
|
||||
});
|
||||
|
|
|
@ -8,12 +8,21 @@ import * as t from 'io-ts';
|
|||
|
||||
import { id } from '../common/schemas';
|
||||
import { RequiredKeepUndefined } from '../../types';
|
||||
import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false';
|
||||
|
||||
export const deleteListSchema = t.exact(
|
||||
t.type({
|
||||
id,
|
||||
})
|
||||
);
|
||||
export const deleteListSchema = t.intersection([
|
||||
t.exact(
|
||||
t.type({
|
||||
id,
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
deleteReferences: DefaultStringBooleanFalse,
|
||||
ignoreReferences: DefaultStringBooleanFalse,
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
export type DeleteListSchema = RequiredKeepUndefined<t.TypeOf<typeof deleteListSchema>>;
|
||||
export type DeleteListSchemaEncoded = t.OutputOf<typeof deleteListSchema>;
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../test_utils';
|
||||
|
||||
import { DefaultStringBooleanFalse } from './default_string_boolean_false';
|
||||
|
||||
describe('default_string_boolean_false', () => {
|
||||
test('it should validate a boolean false', () => {
|
||||
const payload = false;
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate a boolean true', () => {
|
||||
const payload = true;
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate a number', () => {
|
||||
const payload = 5;
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "5" supplied to "DefaultStringBooleanFalse"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return a default false', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(false);
|
||||
});
|
||||
|
||||
test('it should return a default false when given a string of "false"', () => {
|
||||
const payload = 'false';
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(false);
|
||||
});
|
||||
|
||||
test('it should return a default true when given a string of "true"', () => {
|
||||
const payload = 'true';
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(true);
|
||||
});
|
||||
|
||||
test('it should return a default true when given a string of "TruE"', () => {
|
||||
const payload = 'TruE';
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(true);
|
||||
});
|
||||
|
||||
test('it should not work with a string of junk "junk"', () => {
|
||||
const payload = 'junk';
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "junk" supplied to "DefaultStringBooleanFalse"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not work with an empty string', () => {
|
||||
const payload = '';
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "" supplied to "DefaultStringBooleanFalse"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
|
||||
/**
|
||||
* Types the DefaultStringBooleanFalse as:
|
||||
* - If a string this will convert the string to a boolean
|
||||
* - If null or undefined, then a default false will be set
|
||||
*/
|
||||
export const DefaultStringBooleanFalse = new t.Type<boolean, boolean | undefined | string, unknown>(
|
||||
'DefaultStringBooleanFalse',
|
||||
t.boolean.is,
|
||||
(input, context): Either<t.Errors, boolean> => {
|
||||
if (input == null) {
|
||||
return t.success(false);
|
||||
} else if (typeof input === 'string' && input.toLowerCase() === 'true') {
|
||||
return t.success(true);
|
||||
} else if (typeof input === 'string' && input.toLowerCase() === 'false') {
|
||||
return t.success(false);
|
||||
} else {
|
||||
return t.boolean.validate(input, context);
|
||||
}
|
||||
},
|
||||
t.identity
|
||||
);
|
||||
|
||||
export type DefaultStringBooleanFalseC = typeof DefaultStringBooleanFalse;
|
50
x-pack/plugins/lists/common/test_utils.ts
Normal file
50
x-pack/plugins/lists/common/test_utils.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { formatErrors } from './format_errors';
|
||||
|
||||
interface Message<T> {
|
||||
errors: t.Errors;
|
||||
schema: T | {};
|
||||
}
|
||||
|
||||
const onLeft = <T>(errors: t.Errors): Message<T> => {
|
||||
return { errors, schema: {} };
|
||||
};
|
||||
|
||||
const onRight = <T>(schema: T): Message<T> => {
|
||||
return {
|
||||
errors: [],
|
||||
schema,
|
||||
};
|
||||
};
|
||||
|
||||
export const foldLeftRight = fold(onLeft, onRight);
|
||||
|
||||
/**
|
||||
* Convenience utility to keep the error message handling within tests to be
|
||||
* very concise.
|
||||
* @param validation The validation to get the errors from
|
||||
*/
|
||||
export const getPaths = <A>(validation: t.Validation<A>): string[] => {
|
||||
return pipe(
|
||||
validation,
|
||||
fold(
|
||||
(errors) => formatErrors(errors),
|
||||
() => ['no errors']
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience utility to remove text appended to links by EUI
|
||||
*/
|
||||
export const removeExternalLinkText = (str: string): string =>
|
||||
str.replace(/\(opens in a new tab or window\)/g, '');
|
|
@ -41,7 +41,11 @@ describe('Value Lists API', () => {
|
|||
|
||||
it('DELETEs specifying the id as a query parameter', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
const payload: ApiPayload<DeleteListParams> = { id: 'list-id' };
|
||||
const payload: ApiPayload<DeleteListParams> = {
|
||||
deleteReferences: false,
|
||||
id: 'list-id',
|
||||
ignoreReferences: true,
|
||||
};
|
||||
await deleteList({
|
||||
http: httpMock,
|
||||
...payload,
|
||||
|
@ -52,7 +56,7 @@ describe('Value Lists API', () => {
|
|||
'/api/lists',
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
query: { id: 'list-id' },
|
||||
query: { deleteReferences: false, id: 'list-id', ignoreReferences: true },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -129,23 +129,27 @@ const importListWithValidation = async ({
|
|||
export { importListWithValidation as importList };
|
||||
|
||||
const deleteList = async ({
|
||||
deleteReferences = false,
|
||||
http,
|
||||
id,
|
||||
ignoreReferences = false,
|
||||
signal,
|
||||
}: ApiParams & DeleteListSchemaEncoded): Promise<ListSchema> =>
|
||||
http.fetch<ListSchema>(LIST_URL, {
|
||||
method: 'DELETE',
|
||||
query: { id },
|
||||
query: { deleteReferences, id, ignoreReferences },
|
||||
signal,
|
||||
});
|
||||
|
||||
const deleteListWithValidation = async ({
|
||||
deleteReferences,
|
||||
http,
|
||||
id,
|
||||
ignoreReferences,
|
||||
signal,
|
||||
}: DeleteListParams): Promise<ListSchema> =>
|
||||
pipe(
|
||||
{ id },
|
||||
{ deleteReferences, id, ignoreReferences },
|
||||
(payload) => fromEither(validateEither(deleteListSchema, payload)),
|
||||
chain((payload) => tryCatch(() => deleteList({ http, signal, ...payload }), toError)),
|
||||
chain((response) => fromEither(validateEither(listSchema, response))),
|
||||
|
|
|
@ -26,7 +26,9 @@ export interface ImportListParams extends ApiParams {
|
|||
}
|
||||
|
||||
export interface DeleteListParams extends ApiParams {
|
||||
deleteReferences?: boolean;
|
||||
id: string;
|
||||
ignoreReferences?: boolean;
|
||||
}
|
||||
|
||||
export interface ExportListParams extends ApiParams {
|
||||
|
|
|
@ -9,9 +9,18 @@ import { IRouter } from 'kibana/server';
|
|||
import { LIST_URL } from '../../common/constants';
|
||||
import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps';
|
||||
import { validate } from '../../common/shared_imports';
|
||||
import { deleteListSchema, listSchema } from '../../common/schemas';
|
||||
import {
|
||||
EntriesArray,
|
||||
ExceptionListItemSchema,
|
||||
FoundExceptionListSchema,
|
||||
deleteListSchema,
|
||||
exceptionListItemSchema,
|
||||
listSchema,
|
||||
} from '../../common/schemas';
|
||||
import { getSavedObjectType } from '../services/exception_lists/utils';
|
||||
import { ExceptionListClient } from '../services/exception_lists/exception_list_client';
|
||||
|
||||
import { getListClient } from '.';
|
||||
import { getExceptionListClient, getListClient } from '.';
|
||||
|
||||
export const deleteListRoute = (router: IRouter): void => {
|
||||
router.delete(
|
||||
|
@ -28,7 +37,68 @@ export const deleteListRoute = (router: IRouter): void => {
|
|||
const siemResponse = buildSiemResponse(response);
|
||||
try {
|
||||
const lists = getListClient(context);
|
||||
const { id } = request.query;
|
||||
const exceptionLists = getExceptionListClient(context);
|
||||
const { id, deleteReferences, ignoreReferences } = request.query;
|
||||
let deleteExceptionItemResponses;
|
||||
|
||||
// ignoreReferences=true maintains pre-7.11 behavior of deleting value list without performing any additional checks
|
||||
if (!ignoreReferences) {
|
||||
const referencedExceptionListItems = await exceptionLists.findValueListExceptionListItems(
|
||||
{
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
valueListId: id,
|
||||
}
|
||||
);
|
||||
|
||||
if (referencedExceptionListItems?.data?.length) {
|
||||
// deleteReferences=false to perform dry run and identify referenced exception lists/items
|
||||
if (deleteReferences) {
|
||||
// Delete referenced exception list items
|
||||
// TODO: Create deleteListItems to delete in batch
|
||||
deleteExceptionItemResponses = await Promise.all(
|
||||
referencedExceptionListItems.data.map(async (listItem) => {
|
||||
// Ensure only the single entry is deleted as there could be a separate value list referenced that is okay to keep // TODO: Add API to delete single entry
|
||||
// @ts-ignore inline way of verifying entry type is EntryList?
|
||||
const remainingEntries = listItem.entries.filter((e) => e?.list?.id !== id);
|
||||
if (remainingEntries.length === 0) {
|
||||
// All entries reference value list specified in request, delete entire exception list item
|
||||
return deleteExceptionListItem(exceptionLists, listItem);
|
||||
} else {
|
||||
// Contains more entries than just value list specified in request , patch (doesn't exist yet :) exception list item to remove entry
|
||||
return updateExceptionListItems(exceptionLists, listItem, remainingEntries);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const referencedExceptionLists = await getReferencedExceptionLists(
|
||||
exceptionLists,
|
||||
referencedExceptionListItems.data
|
||||
);
|
||||
const refError = `Value list '${id}' is referenced in existing exception list(s)`;
|
||||
const references = referencedExceptionListItems.data.map((item) => ({
|
||||
exception_item: item,
|
||||
exception_list: referencedExceptionLists.data.find(
|
||||
(l) => l.list_id === item.list_id
|
||||
),
|
||||
}));
|
||||
|
||||
return siemResponse.error({
|
||||
body: {
|
||||
error: {
|
||||
message: refError,
|
||||
references,
|
||||
value_list_id: id,
|
||||
},
|
||||
},
|
||||
statusCode: 409,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await lists.deleteList({ id });
|
||||
if (deleted == null) {
|
||||
return siemResponse.error({
|
||||
|
@ -40,7 +110,11 @@ export const deleteListRoute = (router: IRouter): void => {
|
|||
if (errors != null) {
|
||||
return siemResponse.error({ body: errors, statusCode: 500 });
|
||||
} else {
|
||||
return response.ok({ body: validated ?? {} });
|
||||
return response.ok({
|
||||
body: validated ?? {
|
||||
deleteItemResponses: deleteExceptionItemResponses,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -53,3 +127,98 @@ export const deleteListRoute = (router: IRouter): void => {
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches ExceptionLists for given ExceptionListItems
|
||||
* @param exceptionLists ExceptionListClient
|
||||
* @param exceptionListItems ExceptionListItemSchema[]
|
||||
*/
|
||||
const getReferencedExceptionLists = async (
|
||||
exceptionLists: ExceptionListClient,
|
||||
exceptionListItems: ExceptionListItemSchema[]
|
||||
): Promise<FoundExceptionListSchema> => {
|
||||
const filter = exceptionListItems
|
||||
.map(
|
||||
(item) =>
|
||||
`${getSavedObjectType({
|
||||
namespaceType: item.namespace_type,
|
||||
})}.attributes.list_id: ${item.list_id}`
|
||||
)
|
||||
.join(' OR ');
|
||||
return exceptionLists.findExceptionList({
|
||||
filter: `(${filter})`,
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adapted from deleteExceptionListItemRoute
|
||||
* @param exceptionLists ExceptionListClient
|
||||
* @param listItem ExceptionListItemSchema
|
||||
*/
|
||||
const deleteExceptionListItem = async (
|
||||
exceptionLists: ExceptionListClient,
|
||||
listItem: ExceptionListItemSchema
|
||||
): Promise<unknown> => {
|
||||
const deletedExceptionListItem = await exceptionLists.deleteExceptionListItem({
|
||||
id: listItem.id,
|
||||
itemId: listItem.item_id,
|
||||
namespaceType: listItem.namespace_type,
|
||||
});
|
||||
if (deletedExceptionListItem == null) {
|
||||
return {
|
||||
body: `list item with id: "${listItem.id}" not found`,
|
||||
statusCode: 404,
|
||||
};
|
||||
} else {
|
||||
const [validated, errors] = validate(deletedExceptionListItem, exceptionListItemSchema);
|
||||
if (errors != null) {
|
||||
return { body: errors, statusCode: 500 };
|
||||
} else {
|
||||
return { body: validated ?? {} };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adapted from updateExceptionListItemRoute
|
||||
* @param exceptionLists ExceptionListClient
|
||||
* @param listItem ExceptionListItemSchema
|
||||
* @param remainingEntries EntriesArray
|
||||
*/
|
||||
const updateExceptionListItems = async (
|
||||
exceptionLists: ExceptionListClient,
|
||||
listItem: ExceptionListItemSchema,
|
||||
remainingEntries: EntriesArray
|
||||
): Promise<unknown> => {
|
||||
const updateExceptionListItem = await exceptionLists.updateExceptionListItem({
|
||||
_version: listItem._version,
|
||||
comments: listItem.comments,
|
||||
description: listItem.description,
|
||||
entries: remainingEntries,
|
||||
id: listItem.id,
|
||||
itemId: listItem.item_id,
|
||||
meta: listItem.meta,
|
||||
name: listItem.name,
|
||||
namespaceType: listItem.namespace_type,
|
||||
osTypes: listItem.os_types,
|
||||
tags: listItem.tags,
|
||||
type: listItem.type,
|
||||
});
|
||||
if (updateExceptionListItem == null) {
|
||||
return {
|
||||
body: `exception list item id: "${listItem.item_id}" does not exist`,
|
||||
statusCode: 404,
|
||||
};
|
||||
} else {
|
||||
const [validated, errors] = validate(updateExceptionListItem, exceptionListItemSchema);
|
||||
if (errors != null) {
|
||||
return { body: errors, statusCode: 500 };
|
||||
} else {
|
||||
return { body: validated ?? {} };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
{
|
||||
"list_id": "endpoint_list",
|
||||
"item_id": "endpoint_list_item_lg_val_list",
|
||||
"list_id": "simple_list",
|
||||
"item_id": "simple_list_item_lg_val_list",
|
||||
"tags": ["user added string for a tag", "malware"],
|
||||
"type": "simple",
|
||||
"description": "This is a sample exception list item with a large value list included",
|
||||
"name": "Sample Endpoint Exception List Item with large value list",
|
||||
"name": "Sample Simple List Item with large value list",
|
||||
"os_types": ["windows"],
|
||||
"comments": [],
|
||||
"entries": [
|
||||
{
|
||||
"field": "event.module",
|
||||
"operator": "excluded",
|
||||
"type": "match_any",
|
||||
"value": ["suricata"]
|
||||
},
|
||||
{
|
||||
"field": "source.ip",
|
||||
"operator": "excluded",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"list_id": "detection_list_1",
|
||||
"tags": ["user added string for a tag", "malware"],
|
||||
"type": "detection",
|
||||
"description": "This is a sample detection type exception list",
|
||||
"name": "Detection Exception List (1)"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"list_id": "detection_list_2",
|
||||
"tags": ["user added string for a tag", "malware"],
|
||||
"type": "detection",
|
||||
"description": "This is a sample agnostic detection type exception list",
|
||||
"name": "Detection Exception List (2)",
|
||||
"namespace_type": "agnostic"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"list_id": "detection_list_3",
|
||||
"tags": ["user added string for a tag", "malware"],
|
||||
"type": "detection",
|
||||
"description": "This is a sample detection type exception list",
|
||||
"name": "Detection Exception List (3)"
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"list_id": "detection_list_1",
|
||||
"item_id": "simple_list_item_two_value_lists",
|
||||
"tags": [
|
||||
"user added string for a tag",
|
||||
"malware"
|
||||
],
|
||||
"type": "simple",
|
||||
"description": "This is a sample exception list item with a large value list included",
|
||||
"name": "Simple List Item with ip value list",
|
||||
"os_types": [
|
||||
"windows"
|
||||
],
|
||||
"comments": [],
|
||||
"entries": [
|
||||
{
|
||||
"field": "source.ip",
|
||||
"operator": "excluded",
|
||||
"type": "list",
|
||||
"list": {
|
||||
"id": "ips.txt",
|
||||
"type": "ip"
|
||||
}
|
||||
},
|
||||
{
|
||||
"field": "source.ip",
|
||||
"operator": "excluded",
|
||||
"type": "list",
|
||||
"list": {
|
||||
"id": "ip_range.txt",
|
||||
"type": "ip_range"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"list_id": "detection_list_1",
|
||||
"item_id": "simple_list_item_two_non-value_list",
|
||||
"tags": [
|
||||
"user added string for a tag",
|
||||
"malware"
|
||||
],
|
||||
"type": "simple",
|
||||
"description": "This is a sample exception list item with two non-value list entries",
|
||||
"name": "Sample Detection Exception List Item",
|
||||
"os_types": [
|
||||
"windows"
|
||||
],
|
||||
"comments": [],
|
||||
"entries": [
|
||||
{
|
||||
"field": "actingProcess.file.signer",
|
||||
"operator": "excluded",
|
||||
"type": "exists"
|
||||
},
|
||||
{
|
||||
"field": "host.name",
|
||||
"operator": "included",
|
||||
"type": "match_any",
|
||||
"value": ["some host", "another host"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"list_id": "detection_list_2",
|
||||
"item_id": "simple_list_item_two_value_lists",
|
||||
"tags": [
|
||||
"user added string for a tag",
|
||||
"malware"
|
||||
],
|
||||
"type": "simple",
|
||||
"description": "This is a sample exception list item with a large value list included",
|
||||
"name": "Simple List Item with ip value list",
|
||||
"namespace_type": "agnostic",
|
||||
"os_types": [
|
||||
"windows"
|
||||
],
|
||||
"comments": [],
|
||||
"entries": [
|
||||
{
|
||||
"field": "source.ip",
|
||||
"operator": "excluded",
|
||||
"type": "list",
|
||||
"list": {
|
||||
"id": "ips.txt",
|
||||
"type": "ip"
|
||||
}
|
||||
},
|
||||
{
|
||||
"field": "source.ip",
|
||||
"operator": "excluded",
|
||||
"type": "list",
|
||||
"list": {
|
||||
"id": "ip_range.txt",
|
||||
"type": "ip_range"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"list_id": "detection_list_3",
|
||||
"item_id": "simple_list_item_one_value_list",
|
||||
"tags": [
|
||||
"user added string for a tag",
|
||||
"malware"
|
||||
],
|
||||
"type": "simple",
|
||||
"description": "This is a sample exception list item with a large value list included",
|
||||
"name": "Simple List Item with ip value list",
|
||||
"os_types": [
|
||||
"windows"
|
||||
],
|
||||
"comments": [],
|
||||
"entries": [
|
||||
{
|
||||
"field": "source.ip",
|
||||
"operator": "excluded",
|
||||
"type": "list",
|
||||
"list": {
|
||||
"id": "text.txt",
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
47
x-pack/plugins/lists/server/scripts/quick_start_value_list_references.sh
Executable file
47
x-pack/plugins/lists/server/scripts/quick_start_value_list_references.sh
Executable file
|
@ -0,0 +1,47 @@
|
|||
#!/bin/sh
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
# or more contributor license agreements. Licensed under the Elastic License;
|
||||
# you may not use this file except in compliance with the Elastic License.
|
||||
#
|
||||
# Creates three different exception lists and value lists, and associates as
|
||||
# below to test referential integrity functionality.
|
||||
#
|
||||
# NOTE: Endpoint lists don't support value lists, and are not tested here
|
||||
#
|
||||
# EL: Exception list
|
||||
# ELI Exception list Item
|
||||
# VL: Value list
|
||||
#
|
||||
#
|
||||
# EL1 EL2 (Agnostic) EL3
|
||||
# | | |
|
||||
# ELI1 ELI2 ELI3
|
||||
# |\ /| |
|
||||
# | \ / | |
|
||||
# | \ / | |
|
||||
# | \ / | |
|
||||
# | \/ | |
|
||||
# | /\ | |
|
||||
# | / \ | |
|
||||
# | / \ | |
|
||||
# | / \ | |
|
||||
# |/ \| |
|
||||
# VL1 VL2 VL3 VL4
|
||||
# ips.txt ip_range.txt text.txt hosts.txt
|
||||
|
||||
./hard_reset.sh && \
|
||||
# Create value lists
|
||||
./import_list_items_by_filename.sh ip ./lists/files/ips.txt && \
|
||||
./import_list_items_by_filename.sh ip_range ./lists/files/ip_range.txt && \
|
||||
./import_list_items_by_filename.sh keyword ./lists/files/text.txt && \
|
||||
./import_list_items_by_filename.sh keyword ./lists/files/hosts.txt && \
|
||||
# Create exception lists 1, 2 (agnostic), 3
|
||||
./post_exception_list.sh ./exception_lists/new/references/exception_list_detection_1.json && \
|
||||
./post_exception_list.sh ./exception_lists/new/references/exception_list_detection_2_agnostic.json && \
|
||||
./post_exception_list.sh ./exception_lists/new/references/exception_list_detection_3.json && \
|
||||
# Create exception list items with value lists
|
||||
./post_exception_list_item.sh ./exception_lists/new/references/exception_list_item_1_multi_value_list.json && \
|
||||
./post_exception_list_item.sh ./exception_lists/new/references/exception_list_item_2_multi_value_list.json && \
|
||||
./post_exception_list_item.sh ./exception_lists/new/references/exception_list_item_3_single_value_list.json && \
|
||||
# Create exception list items (non value lists, to ensure they're not deleted on cleanup)
|
||||
./post_exception_list_item.sh ./exception_lists/new/references/exception_list_item_1_non_value_list.json
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
FindExceptionListItemOptions,
|
||||
FindExceptionListOptions,
|
||||
FindExceptionListsItemOptions,
|
||||
FindValueListExceptionListsItems,
|
||||
GetEndpointListItemOptions,
|
||||
GetExceptionListItemOptions,
|
||||
GetExceptionListOptions,
|
||||
|
@ -44,7 +45,10 @@ import { deleteExceptionList } from './delete_exception_list';
|
|||
import { deleteExceptionListItem, deleteExceptionListItemById } from './delete_exception_list_item';
|
||||
import { findExceptionListItem } from './find_exception_list_item';
|
||||
import { findExceptionList } from './find_exception_list';
|
||||
import { findExceptionListsItem } from './find_exception_list_items';
|
||||
import {
|
||||
findExceptionListsItem,
|
||||
findValueListExceptionListItems,
|
||||
} from './find_exception_list_items';
|
||||
import { createEndpointList } from './create_endpoint_list';
|
||||
import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list';
|
||||
|
||||
|
@ -139,7 +143,7 @@ export class ExceptionListClient {
|
|||
};
|
||||
|
||||
/**
|
||||
* This is the same as "updateListItem" except it applies specifically to the endpoint list and will
|
||||
* This is the same as "updateExceptionListItem" except it applies specifically to the endpoint list and will
|
||||
* auto-call the "createEndpointList" for you so that you have the best chance of the endpoint
|
||||
* being there if it did not exist before. If the list did not exist before, then creating it here will still cause a
|
||||
* return of null but at least the list exists again.
|
||||
|
@ -410,6 +414,24 @@ export class ExceptionListClient {
|
|||
});
|
||||
};
|
||||
|
||||
public findValueListExceptionListItems = async ({
|
||||
perPage,
|
||||
page,
|
||||
sortField,
|
||||
sortOrder,
|
||||
valueListId,
|
||||
}: FindValueListExceptionListsItems): Promise<FoundExceptionListItemSchema | null> => {
|
||||
const { savedObjectsClient } = this;
|
||||
return findValueListExceptionListItems({
|
||||
page,
|
||||
perPage,
|
||||
savedObjectsClient,
|
||||
sortField,
|
||||
sortOrder,
|
||||
valueListId,
|
||||
});
|
||||
};
|
||||
|
||||
public findExceptionList = async ({
|
||||
filter,
|
||||
perPage,
|
||||
|
|
|
@ -196,8 +196,16 @@ export interface FindExceptionListsItemOptions {
|
|||
sortOrder: SortOrderOrUndefined;
|
||||
}
|
||||
|
||||
export interface FindValueListExceptionListsItems {
|
||||
valueListId: Id;
|
||||
perPage: PerPageOrUndefined;
|
||||
page: PageOrUndefined;
|
||||
sortField: SortFieldOrUndefined;
|
||||
sortOrder: SortOrderOrUndefined;
|
||||
}
|
||||
|
||||
export interface FindExceptionListOptions {
|
||||
namespaceType: NamespaceType;
|
||||
namespaceType?: NamespaceType;
|
||||
filter: FilterOrUndefined;
|
||||
perPage: PerPageOrUndefined;
|
||||
page: PageOrUndefined;
|
||||
|
|
|
@ -16,12 +16,16 @@ import {
|
|||
SortFieldOrUndefined,
|
||||
SortOrderOrUndefined,
|
||||
} from '../../../common/schemas';
|
||||
import { SavedObjectType } from '../../saved_objects';
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../saved_objects';
|
||||
|
||||
import { getSavedObjectType, transformSavedObjectsToFoundExceptionList } from './utils';
|
||||
|
||||
interface FindExceptionListOptions {
|
||||
namespaceType: NamespaceType;
|
||||
namespaceType?: NamespaceType;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
filter: FilterOrUndefined;
|
||||
perPage: PerPageOrUndefined;
|
||||
|
@ -39,7 +43,9 @@ export const findExceptionList = async ({
|
|||
sortField,
|
||||
sortOrder,
|
||||
}: FindExceptionListOptions): Promise<FoundExceptionListSchema> => {
|
||||
const savedObjectType = getSavedObjectType({ namespaceType });
|
||||
const savedObjectType: SavedObjectType[] = namespaceType
|
||||
? [getSavedObjectType({ namespaceType })]
|
||||
: [exceptionListSavedObjectType, exceptionListAgnosticSavedObjectType];
|
||||
const savedObjectsFindResponse = await savedObjectsClient.find<ExceptionListSoSchema>({
|
||||
filter: getExceptionListFilter({ filter, savedObjectType }),
|
||||
page,
|
||||
|
@ -56,11 +62,18 @@ export const getExceptionListFilter = ({
|
|||
savedObjectType,
|
||||
}: {
|
||||
filter: FilterOrUndefined;
|
||||
savedObjectType: SavedObjectType;
|
||||
savedObjectType: SavedObjectType[];
|
||||
}): string => {
|
||||
const savedObjectTypeFilter = `(${savedObjectType
|
||||
.map((sot) => `${sot}.attributes.list_type: list`)
|
||||
.join(' OR ')})`;
|
||||
if (filter == null) {
|
||||
return `${savedObjectType}.attributes.list_type: list`;
|
||||
return savedObjectTypeFilter;
|
||||
} else {
|
||||
return `${savedObjectType}.attributes.list_type: list AND ${filter}`;
|
||||
if (Array.isArray(savedObjectType)) {
|
||||
return `${savedObjectTypeFilter} AND ${filter}`;
|
||||
} else {
|
||||
return `${savedObjectType}.attributes.list_type: list AND ${filter}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,12 +11,17 @@ import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_em
|
|||
import {
|
||||
ExceptionListSoSchema,
|
||||
FoundExceptionListItemSchema,
|
||||
Id,
|
||||
PageOrUndefined,
|
||||
PerPageOrUndefined,
|
||||
SortFieldOrUndefined,
|
||||
SortOrderOrUndefined,
|
||||
} from '../../../common/schemas';
|
||||
import { SavedObjectType } from '../../saved_objects';
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../saved_objects';
|
||||
|
||||
import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils';
|
||||
import { getExceptionList } from './get_exception_list';
|
||||
|
@ -92,3 +97,33 @@ export const getExceptionListsItemFilter = ({
|
|||
}
|
||||
}, '');
|
||||
};
|
||||
|
||||
interface FindValueListExceptionListsItems {
|
||||
valueListId: Id;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
perPage: PerPageOrUndefined;
|
||||
page: PageOrUndefined;
|
||||
sortField: SortFieldOrUndefined;
|
||||
sortOrder: SortOrderOrUndefined;
|
||||
}
|
||||
|
||||
export const findValueListExceptionListItems = async ({
|
||||
valueListId,
|
||||
savedObjectsClient,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
}: FindValueListExceptionListsItems): Promise<FoundExceptionListItemSchema | null> => {
|
||||
const savedObjectsFindResponse = await savedObjectsClient.find<ExceptionListSoSchema>({
|
||||
filter: `(exception-list.attributes.list_type: item AND exception-list.attributes.entries.list.id:${valueListId}) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.entries.list.id:${valueListId}) `,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
type: [exceptionListSavedObjectType, exceptionListAgnosticSavedObjectType],
|
||||
});
|
||||
return transformSavedObjectsToFoundExceptionListItem({
|
||||
savedObjectsFindResponse,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -75,7 +75,7 @@ describe('default_string_boolean_false', () => {
|
|||
expect(message.schema).toEqual(true);
|
||||
});
|
||||
|
||||
test('it should not work with a strong of junk "junk"', () => {
|
||||
test('it should not work with a string of junk "junk"', () => {
|
||||
const payload = 'junk';
|
||||
const decoded = DefaultStringBooleanFalse.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
|
@ -32,12 +33,27 @@ import * as i18n from './translations';
|
|||
import { buildColumns } from './table_helpers';
|
||||
import { ValueListsForm } from './form';
|
||||
import { AutoDownload } from './auto_download';
|
||||
import { ReferenceErrorModal } from './reference_error_modal';
|
||||
|
||||
interface ValueListsModalProps {
|
||||
onClose: () => void;
|
||||
showModal: boolean;
|
||||
}
|
||||
|
||||
interface ReferenceModalState {
|
||||
contentText: string;
|
||||
exceptionListReferences: string[];
|
||||
isLoading: boolean;
|
||||
valueListId: string;
|
||||
}
|
||||
|
||||
const referenceModalInitialState: ReferenceModalState = {
|
||||
contentText: '',
|
||||
exceptionListReferences: [],
|
||||
isLoading: false,
|
||||
valueListId: '',
|
||||
};
|
||||
|
||||
export const ValueListsModalComponent: React.FC<ValueListsModalProps> = ({
|
||||
onClose,
|
||||
showModal,
|
||||
|
@ -47,24 +63,42 @@ export const ValueListsModalComponent: React.FC<ValueListsModalProps> = ({
|
|||
const [cursor, setCursor] = useCursor({ pageIndex, pageSize });
|
||||
const { http } = useKibana().services;
|
||||
const { start: findLists, ...lists } = useFindLists();
|
||||
const { start: deleteList, result: deleteResult } = useDeleteList();
|
||||
const { start: deleteList, result: deleteResult, error: deleteError } = useDeleteList();
|
||||
const [deletingListIds, setDeletingListIds] = useState<string[]>([]);
|
||||
const [exportingListIds, setExportingListIds] = useState<string[]>([]);
|
||||
const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({});
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
const [showReferenceErrorModal, setShowReferenceErrorModal] = useState<boolean>(false);
|
||||
const [referenceModalState, setReferenceModalState] = useState<ReferenceModalState>(
|
||||
referenceModalInitialState
|
||||
);
|
||||
|
||||
const fetchLists = useCallback(() => {
|
||||
findLists({ cursor, http, pageIndex: pageIndex + 1, pageSize });
|
||||
}, [cursor, http, findLists, pageIndex, pageSize]);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
({ id }: { id: string }) => {
|
||||
({
|
||||
deleteReferences,
|
||||
id,
|
||||
}: {
|
||||
deleteReferences?: boolean;
|
||||
id: string;
|
||||
ignoreReferences?: boolean;
|
||||
}) => {
|
||||
setDeletingListIds([...deletingListIds, id]);
|
||||
deleteList({ http, id });
|
||||
deleteList({ deleteReferences, http, id });
|
||||
},
|
||||
[deleteList, deletingListIds, http]
|
||||
);
|
||||
|
||||
const handleReferenceDelete = useCallback(async () => {
|
||||
setShowReferenceErrorModal(false);
|
||||
deleteList({ deleteReferences: true, http, id: referenceModalState.valueListId });
|
||||
setReferenceModalState(referenceModalInitialState);
|
||||
setDeletingListIds([]);
|
||||
}, [deleteList, http, referenceModalState.valueListId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteResult != null) {
|
||||
setDeletingListIds((ids) => [...ids.filter((id) => id !== deleteResult.id)]);
|
||||
|
@ -72,6 +106,26 @@ export const ValueListsModalComponent: React.FC<ValueListsModalProps> = ({
|
|||
}
|
||||
}, [deleteResult, fetchLists]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(deleteError)) {
|
||||
const references: string[] =
|
||||
// @ts-ignore-next-line deleteError response unknown message.error.references
|
||||
deleteError?.body?.message?.error?.references?.map(
|
||||
// @ts-ignore-next-line response not typed
|
||||
(ref) => ref?.exception_list.name
|
||||
) ?? [];
|
||||
const uniqueExceptionListReferences = Array.from(new Set(references));
|
||||
setShowReferenceErrorModal(true);
|
||||
setReferenceModalState({
|
||||
contentText: i18n.referenceErrorMessage(uniqueExceptionListReferences.length),
|
||||
exceptionListReferences: uniqueExceptionListReferences,
|
||||
isLoading: false,
|
||||
// @ts-ignore-next-line deleteError response unknown
|
||||
valueListId: deleteError?.body?.message?.error?.value_list_id,
|
||||
});
|
||||
}
|
||||
}, [deleteError]);
|
||||
|
||||
const handleExport = useCallback(
|
||||
async ({ id }: { id: string }) => {
|
||||
try {
|
||||
|
@ -126,6 +180,17 @@ export const ValueListsModalComponent: React.FC<ValueListsModalProps> = ({
|
|||
}
|
||||
}, [lists.loading, lists.result, setCursor]);
|
||||
|
||||
const handleCloseReferenceErrorModal = useCallback(() => {
|
||||
setDeletingListIds([]);
|
||||
setShowReferenceErrorModal(false);
|
||||
setReferenceModalState({
|
||||
contentText: '',
|
||||
exceptionListReferences: [],
|
||||
isLoading: false,
|
||||
valueListId: '',
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!showModal) {
|
||||
return null;
|
||||
}
|
||||
|
@ -173,6 +238,17 @@ export const ValueListsModalComponent: React.FC<ValueListsModalProps> = ({
|
|||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
<ReferenceErrorModal
|
||||
cancelText={i18n.REFERENCE_MODAL_CANCEL_BUTTON}
|
||||
confirmText={i18n.REFERENCE_MODAL_CONFIRM_BUTTON}
|
||||
contentText={referenceModalState.contentText}
|
||||
onCancel={handleCloseReferenceErrorModal}
|
||||
onClose={handleCloseReferenceErrorModal}
|
||||
onConfirm={handleReferenceDelete}
|
||||
references={referenceModalState.exceptionListReferences}
|
||||
showModal={showReferenceErrorModal}
|
||||
titleText={i18n.REFERENCE_MODAL_TITLE}
|
||||
/>
|
||||
<AutoDownload
|
||||
blob={exportDownload.blob}
|
||||
name={exportDownload.name}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ReferenceErrorModal } from './reference_error_modal';
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiConfirmModal, EuiListGroup, EuiListGroupItem, EuiOverlayMask } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const MarkdownContainer = styled.div`
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
width: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-clip: content-box;
|
||||
background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
|
||||
border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ReferenceErrorModalProps {
|
||||
cancelText: string;
|
||||
confirmText: string;
|
||||
contentText: string;
|
||||
onCancel: () => void;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
references: string[];
|
||||
showModal: boolean;
|
||||
titleText: string;
|
||||
}
|
||||
|
||||
export const ReferenceErrorModalComponent: React.FC<ReferenceErrorModalProps> = ({
|
||||
cancelText,
|
||||
confirmText,
|
||||
contentText,
|
||||
onClose,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
references = [],
|
||||
showModal,
|
||||
titleText,
|
||||
}) => {
|
||||
if (!showModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiOverlayMask onClick={onClose}>
|
||||
<EuiConfirmModal
|
||||
maxWidth={460}
|
||||
title={titleText}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
cancelButtonText={cancelText}
|
||||
confirmButtonText={confirmText}
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<p>{contentText}</p>
|
||||
<MarkdownContainer>
|
||||
<EuiListGroup gutterSize="none" showToolTips>
|
||||
{references.map((r, index) => (
|
||||
<EuiListGroupItem key={`${index}-${r}`} label={r} />
|
||||
))}
|
||||
</EuiListGroup>
|
||||
</MarkdownContainer>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
||||
|
||||
ReferenceErrorModalComponent.displayName = 'ReferenceErrorModalComponent';
|
||||
|
||||
export const ReferenceErrorModal = React.memo(ReferenceErrorModalComponent);
|
||||
|
||||
ReferenceErrorModal.displayName = 'ReferenceErrorModal';
|
|
@ -167,3 +167,31 @@ export const TEXT_RADIO = i18n.translate(
|
|||
defaultMessage: 'Text',
|
||||
}
|
||||
);
|
||||
|
||||
export const REFERENCE_MODAL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.lists.referenceModalTitle',
|
||||
{
|
||||
defaultMessage: 'Remove value list',
|
||||
}
|
||||
);
|
||||
|
||||
export const REFERENCE_MODAL_CANCEL_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.lists.referenceModalCancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const REFERENCE_MODAL_CONFIRM_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.lists.referenceModalDeleteButton',
|
||||
{
|
||||
defaultMessage: 'Remove value list',
|
||||
}
|
||||
);
|
||||
|
||||
export const referenceErrorMessage = (referenceCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.lists.referenceModalDescription', {
|
||||
defaultMessage:
|
||||
'This value list is associated with ({referenceCount}) exception {referenceCount, plural, =1 {list} other {lists}}. Removing this list will remove all exception items that reference this value list.',
|
||||
values: { referenceCount },
|
||||
});
|
||||
|
|
|
@ -7,19 +7,32 @@
|
|||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { LIST_URL } from '../../../../plugins/lists/common/constants';
|
||||
import {
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
EXCEPTION_LIST_URL,
|
||||
LIST_ITEM_URL,
|
||||
LIST_URL,
|
||||
} from '../../../../plugins/lists/common/constants';
|
||||
|
||||
import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock';
|
||||
import {
|
||||
getCreateMinimalListSchemaMock,
|
||||
getCreateMinimalListSchemaMockWithoutId,
|
||||
} from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock';
|
||||
import {
|
||||
createListsIndex,
|
||||
deleteAllExceptions,
|
||||
deleteListsIndex,
|
||||
removeListServerGeneratedProperties,
|
||||
} from '../../utils';
|
||||
import { getListResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/list_schema.mock';
|
||||
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
|
||||
import { getCreateExceptionListItemMinimalSchemaMockWithoutId } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock';
|
||||
import { DETECTION_TYPE, LIST_ID } from '../../../../plugins/lists/common/constants.mock';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('delete_lists', () => {
|
||||
describe('deleting lists', () => {
|
||||
|
@ -54,7 +67,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const { body: bodyWithCreatedList } = await supertest
|
||||
.post(LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateMinimalListSchemaMock())
|
||||
.send(getCreateMinimalListSchemaMockWithoutId())
|
||||
.expect(200);
|
||||
|
||||
// delete that list by its auto-generated id
|
||||
|
@ -78,6 +91,148 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
status_code: 404,
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleting lists referenced in exceptions', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllExceptions(es);
|
||||
});
|
||||
|
||||
it('should return an error when deleting a list referenced within an exception list item', async () => {
|
||||
// create a list
|
||||
const { body: valueListBody } = await supertest
|
||||
.post(LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateMinimalListSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
// create an exception list
|
||||
const { body: exceptionListBody } = await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ...getCreateExceptionListMinimalSchemaMock(), type: DETECTION_TYPE })
|
||||
.expect(200);
|
||||
|
||||
// create an exception list item referencing value list
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...getCreateExceptionListItemMinimalSchemaMockWithoutId(),
|
||||
list_id: exceptionListBody.list_id,
|
||||
entries: [
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
operator: 'included',
|
||||
type: 'list',
|
||||
list: { id: valueListBody.id, type: 'keyword' },
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// try to delete that list by its auto-generated id
|
||||
await supertest
|
||||
.delete(`${LIST_URL}?id=${valueListBody.id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(409);
|
||||
|
||||
// really delete that list by its auto-generated id
|
||||
await supertest
|
||||
.delete(`${LIST_URL}?id=${valueListBody.id}&ignoreReferences=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
// Tests in development
|
||||
it.skip('should delete a single list referenced within an exception list item if ignoreReferences=true', async () => {
|
||||
// create a list
|
||||
const { body: valueListBody } = await supertest
|
||||
.post(LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateMinimalListSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
// create an exception list
|
||||
const { body: exceptionListBody } = await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ...getCreateExceptionListMinimalSchemaMock(), type: DETECTION_TYPE })
|
||||
.expect(200);
|
||||
|
||||
// create an exception list item referencing value list
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...getCreateExceptionListItemMinimalSchemaMockWithoutId(),
|
||||
list_id: exceptionListBody.list_id,
|
||||
entries: [
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
operator: 'included',
|
||||
type: 'list',
|
||||
list: { id: valueListBody.id, type: 'keyword' },
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// delete that list by its auto-generated id and ignoreReferences
|
||||
supertest
|
||||
.delete(`${LIST_URL}?id=${valueListBody.id}&ignoreReferences=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
// Tests in development
|
||||
it.skip('should delete a single list referenced within an exception list item and referenced exception list items if deleteReferences=true', async () => {
|
||||
// create a list
|
||||
const { body: valueListBody } = await supertest
|
||||
.post(LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateMinimalListSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
// create an exception list
|
||||
const { body: exceptionListBody } = await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ...getCreateExceptionListMinimalSchemaMock(), type: DETECTION_TYPE })
|
||||
.expect(200);
|
||||
|
||||
// create an exception list item referencing value list
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...getCreateExceptionListItemMinimalSchemaMockWithoutId(),
|
||||
list_id: exceptionListBody.list_id,
|
||||
entries: [
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
operator: 'included',
|
||||
type: 'list',
|
||||
list: { id: valueListBody.id, type: 'keyword' },
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// delete that list by its auto-generated id and delete referenced list items
|
||||
const deleteListBody = await supertest
|
||||
.delete(`${LIST_URL}?id=${valueListBody.id}&ignoreReferences=true`)
|
||||
.set('kbn-xsrf', 'true');
|
||||
|
||||
const bodyToCompare = removeListServerGeneratedProperties(deleteListBody.body);
|
||||
expect(bodyToCompare).to.eql(getListResponseMockWithoutAutoGeneratedValues());
|
||||
|
||||
await supertest
|
||||
.get(`${LIST_ITEM_URL}/_find?list_id=${LIST_ID}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue