diff --git a/x-pack/plugins/lists/README.md b/x-pack/plugins/lists/README.md index b6061368f6b13..dac6e8bb78fa5 100644 --- a/x-pack/plugins/lists/README.md +++ b/x-pack/plugins/lists/README.md @@ -57,7 +57,7 @@ which will: - Delete any existing exception list items you have - Delete any existing mapping, policies, and templates, you might have previously had. - Add the latest list and list item index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.lists.listIndex` and `xpack.lists.listItemIndex`. -- Posts the sample list from `./lists/new/list_ip.json` +- Posts the sample list from `./lists/new/ip_list.json` Now you can run @@ -69,7 +69,7 @@ You should see the new list created like so: ```sh { - "id": "list_ip", + "id": "ip_list", "created_at": "2020-05-28T19:15:22.344Z", "created_by": "yo", "description": "This list describes bad internet ip", @@ -96,7 +96,7 @@ You should see the new list item created and attached to the above list like so: "value": "127.0.0.1", "created_at": "2020-05-28T19:15:49.790Z", "created_by": "yo", - "list_id": "list_ip", + "list_id": "ip_list", "tie_breaker_id": "a881bf2e-1e17-4592-bba8-d567cb07d234", "updated_at": "2020-05-28T19:15:49.790Z", "updated_by": "yo" @@ -195,7 +195,7 @@ You can then do find for each one like so: "cursor": "WzIwLFsiYzU3ZWZiYzQtNDk3Ny00YTMyLTk5NWYtY2ZkMjk2YmVkNTIxIl1d", "data": [ { - "id": "list_ip", + "id": "ip_list", "created_at": "2020-05-28T19:15:22.344Z", "created_by": "yo", "description": "This list describes bad internet ip", diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 6bb6ee05034cb..6199a5f16f109 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -273,7 +273,6 @@ export const cursorOrUndefined = t.union([cursor, t.undefined]); export type CursorOrUndefined = t.TypeOf; export const namespace_type = DefaultNamespace; -export type NamespaceType = t.TypeOf; export const operator = t.keyof({ excluded: null, included: null }); export type Operator = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index fb452ac89576d..4b7db3eee35bc 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -10,7 +10,6 @@ import * as t from 'io-ts'; import { ItemId, - NamespaceType, Tags, _Tags, _tags, @@ -23,7 +22,12 @@ import { tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; -import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types'; +import { + CreateCommentsArray, + DefaultCreateCommentsArray, + DefaultEntryArray, + NamespaceType, +} from '../types'; import { EntriesArray } from '../types/entries'; import { DefaultUuid } from '../../siem_common_deps'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index a0aaa91c81427..66cca4ab9ca53 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -10,7 +10,6 @@ import * as t from 'io-ts'; import { ListId, - NamespaceType, Tags, _Tags, _tags, @@ -23,6 +22,7 @@ import { } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; import { DefaultUuid } from '../../siem_common_deps'; +import { NamespaceType } from '../types'; export const createExceptionListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts index 4c5b70d9a4073..909960c9fffc0 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts @@ -8,7 +8,8 @@ import * as t from 'io-ts'; -import { NamespaceType, id, item_id, namespace_type } from '../common/schemas'; +import { id, item_id, namespace_type } from '../common/schemas'; +import { NamespaceType } from '../types'; export const deleteExceptionListItemSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts index 2577d867031f0..3bf5e7a4d0782 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts @@ -8,7 +8,8 @@ import * as t from 'io-ts'; -import { NamespaceType, id, list_id, namespace_type } from '../common/schemas'; +import { id, list_id, namespace_type } from '../common/schemas'; +import { NamespaceType } from '../types'; export const deleteExceptionListSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index 31eb4925eb6d6..826da972fe7a3 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -8,27 +8,26 @@ import * as t from 'io-ts'; -import { - NamespaceType, - filter, - list_id, - namespace_type, - sort_field, - sort_order, -} from '../common/schemas'; +import { sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; import { StringToPositiveNumber } from '../types/string_to_positive_number'; +import { + DefaultNamespaceArray, + DefaultNamespaceArrayTypeDecoded, +} from '../types/default_namespace_array'; +import { NonEmptyStringArray } from '../types/non_empty_string_array'; +import { EmptyStringArray, EmptyStringArrayDecoded } from '../types/empty_string_array'; export const findExceptionListItemSchema = t.intersection([ t.exact( t.type({ - list_id, + list_id: NonEmptyStringArray, }) ), t.exact( t.partial({ - filter, // defaults to undefined if not set during decode - namespace_type, // defaults to 'single' if not set during decode + filter: EmptyStringArray, // defaults to undefined if not set during decode + namespace_type: DefaultNamespaceArray, // defaults to ['single'] if not set during decode page: StringToPositiveNumber, // defaults to undefined if not set during decode per_page: StringToPositiveNumber, // defaults to undefined if not set during decode sort_field, // defaults to undefined if not set during decode @@ -37,14 +36,15 @@ export const findExceptionListItemSchema = t.intersection([ ), ]); -export type FindExceptionListItemSchemaPartial = t.TypeOf; +export type FindExceptionListItemSchemaPartial = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. export type FindExceptionListItemSchemaPartialDecoded = Omit< - FindExceptionListItemSchemaPartial, - 'namespace_type' + t.TypeOf, + 'namespace_type' | 'filter' > & { - namespace_type: NamespaceType; + filter: EmptyStringArrayDecoded; + namespace_type: DefaultNamespaceArrayTypeDecoded; }; // This type is used after a decode since some things are defaults after a decode. diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index fa00c5b0dafb1..8b9b08ed387b1 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -8,9 +8,10 @@ import * as t from 'io-ts'; -import { NamespaceType, filter, namespace_type, sort_field, sort_order } from '../common/schemas'; +import { filter, namespace_type, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; import { StringToPositiveNumber } from '../types/string_to_positive_number'; +import { NamespaceType } from '../types'; export const findExceptionListSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts index 93a372ba383b0..d8864a6fc66e5 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts @@ -8,8 +8,9 @@ import * as t from 'io-ts'; -import { NamespaceType, id, item_id, namespace_type } from '../common/schemas'; +import { id, item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; +import { NamespaceType } from '../types'; export const readExceptionListItemSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts index 3947c88bf4c9c..613fb22a99d61 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts @@ -8,8 +8,9 @@ import * as t from 'io-ts'; -import { NamespaceType, id, list_id, namespace_type } from '../common/schemas'; +import { id, list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; +import { NamespaceType } from '../types'; export const readExceptionListSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index 582fabdc160f9..20a63e0fc7dac 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { - NamespaceType, Tags, _Tags, _tags, @@ -26,6 +25,7 @@ import { DefaultEntryArray, DefaultUpdateCommentsArray, EntriesArray, + NamespaceType, UpdateCommentsArray, } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index 76160c3419449..0b5f3a8a01794 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { - NamespaceType, Tags, _Tags, _tags, @@ -21,6 +20,7 @@ import { tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; +import { NamespaceType } from '../types'; export const updateExceptionListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts index 8f8f8d105b624..ecc45d3c84313 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts @@ -8,23 +8,18 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; export const namespaceType = t.keyof({ agnostic: null, single: null }); - -type NamespaceType = t.TypeOf; - -export type DefaultNamespaceC = t.Type; +export type NamespaceType = t.TypeOf; /** * Types the DefaultNamespace as: * - If null or undefined, then a default string/enumeration of "single" will be used. */ -export const DefaultNamespace: DefaultNamespaceC = new t.Type< - NamespaceType, - NamespaceType, - unknown ->( +export const DefaultNamespace = new t.Type( 'DefaultNamespace', namespaceType.is, (input, context): Either => input == null ? t.success('single') : namespaceType.validate(input, context), t.identity ); + +export type DefaultNamespaceC = typeof DefaultNamespace; diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts new file mode 100644 index 0000000000000..055f93069950e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts @@ -0,0 +1,99 @@ +/* + * 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 '../../siem_common_deps'; + +import { DefaultNamespaceArray, DefaultNamespaceArrayTypeEncoded } from './default_namespace_array'; + +describe('default_namespace_array', () => { + test('it should validate "null" single item as an array with a "single" value', () => { + const payload: DefaultNamespaceArrayTypeEncoded = null; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single']); + }); + + test('it should NOT validate a numeric value', () => { + const payload = 5; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultNamespaceArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate "undefined" item as an array with a "single" value', () => { + const payload: DefaultNamespaceArrayTypeEncoded = undefined; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single']); + }); + + test('it should validate "single" as an array of a "single" value', () => { + const payload: DefaultNamespaceArrayTypeEncoded = 'single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([payload]); + }); + + test('it should validate "agnostic" as an array of a "agnostic" value', () => { + const payload: DefaultNamespaceArrayTypeEncoded = 'agnostic'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([payload]); + }); + + test('it should validate "single,agnostic" as an array of 2 values of ["single", "agnostic"] values', () => { + const payload: DefaultNamespaceArrayTypeEncoded = 'agnostic,single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['agnostic', 'single']); + }); + + test('it should validate 3 elements of "single,agnostic,single" as an array of 3 values of ["single", "agnostic", "single"] values', () => { + const payload: DefaultNamespaceArrayTypeEncoded = 'single,agnostic,single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single', 'agnostic', 'single']); + }); + + test('it should validate 3 elements of "single,agnostic, single" as an array of 3 values of ["single", "agnostic", "single"] values when there are spaces', () => { + const payload: DefaultNamespaceArrayTypeEncoded = ' single, agnostic, single '; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single', 'agnostic', 'single']); + }); + + test('it should not validate 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { + const payload: DefaultNamespaceArrayTypeEncoded = 'single,agnostic,junk'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "junk" supplied to "DefaultNamespaceArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts new file mode 100644 index 0000000000000..c4099a48ffbcc --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts @@ -0,0 +1,45 @@ +/* + * 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'; + +import { namespaceType } from './default_namespace'; + +export const namespaceTypeArray = t.array(namespaceType); +export type NamespaceTypeArray = t.TypeOf; + +/** + * Types the DefaultNamespaceArray as: + * - If null or undefined, then a default string array of "single" will be used. + * - If it contains a string, then it is split along the commas and puts them into an array and validates it + */ +export const DefaultNamespaceArray = new t.Type< + NamespaceTypeArray, + string | undefined | null, + unknown +>( + 'DefaultNamespaceArray', + namespaceTypeArray.is, + (input, context): Either => { + if (input == null) { + return t.success(['single']); + } else if (typeof input === 'string') { + const commaSeparatedValues = input + .trim() + .split(',') + .map((value) => value.trim()); + return namespaceTypeArray.validate(commaSeparatedValues, context); + } + return t.failure(input, context); + }, + String +); + +export type DefaultNamespaceC = typeof DefaultNamespaceArray; + +export type DefaultNamespaceArrayTypeEncoded = t.OutputOf; +export type DefaultNamespaceArrayTypeDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts new file mode 100644 index 0000000000000..b14afab327fb0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts @@ -0,0 +1,79 @@ +/* + * 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 '../../siem_common_deps'; + +import { EmptyStringArray, EmptyStringArrayEncoded } from './empty_string_array'; + +describe('empty_string_array', () => { + test('it should validate "null" and create an empty array', () => { + const payload: EmptyStringArrayEncoded = null; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate "undefined" and create an empty array', () => { + const payload: EmptyStringArrayEncoded = undefined; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { + const payload: EmptyStringArrayEncoded = 'a'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a']); + }); + + test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { + const payload: EmptyStringArrayEncoded = 'a,b'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b']); + }); + + test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { + const payload: EmptyStringArrayEncoded = 'a,b,c'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); + + test('it should NOT validate a number', () => { + const payload: number = 5; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "EmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { + const payload: EmptyStringArrayEncoded = ' a, b, c '; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts new file mode 100644 index 0000000000000..389dc4a410cc9 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts @@ -0,0 +1,45 @@ +/* + * 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 EmptyStringArray as: + * - A value that can be undefined, or null (which will be turned into an empty array) + * - A comma separated string that can turn into an array by splitting on it + * - Example input converted to output: undefined -> [] + * - Example input converted to output: null -> [] + * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] + */ +export const EmptyStringArray = new t.Type( + 'EmptyStringArray', + t.array(t.string).is, + (input, context): Either => { + if (input == null) { + return t.success([]); + } else if (typeof input === 'string' && input.trim() !== '') { + const arrayValues = input + .trim() + .split(',') + .map((value) => value.trim()); + const emptyValueFound = arrayValues.some((value) => value === ''); + if (emptyValueFound) { + return t.failure(input, context); + } else { + return t.success(arrayValues); + } + } else { + return t.failure(input, context); + } + }, + String +); + +export type EmptyStringArrayC = typeof EmptyStringArray; + +export type EmptyStringArrayEncoded = t.OutputOf; +export type EmptyStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts new file mode 100644 index 0000000000000..6124487cdd7fb --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts @@ -0,0 +1,94 @@ +/* + * 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 '../../siem_common_deps'; + +import { NonEmptyStringArray, NonEmptyStringArrayEncoded } from './non_empty_string_array'; + +describe('non_empty_string_array', () => { + test('it should NOT validate "null"', () => { + const payload: NonEmptyStringArrayEncoded | null = null; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate "undefined"', () => { + const payload: NonEmptyStringArrayEncoded | undefined = undefined; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a single value of an empty string ""', () => { + const payload: NonEmptyStringArrayEncoded = ''; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { + const payload: NonEmptyStringArrayEncoded = 'a'; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a']); + }); + + test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { + const payload: NonEmptyStringArrayEncoded = 'a,b'; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b']); + }); + + test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { + const payload: NonEmptyStringArrayEncoded = 'a,b,c'; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); + + test('it should NOT validate a number', () => { + const payload: number = 5; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { + const payload: NonEmptyStringArrayEncoded = ' a, b, c '; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts new file mode 100644 index 0000000000000..c4a640e7cdbad --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts @@ -0,0 +1,41 @@ +/* + * 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 NonEmptyStringArray as: + * - A string that is not empty (which will be turned into an array of size 1) + * - A comma separated string that can turn into an array by splitting on it + * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] + */ +export const NonEmptyStringArray = new t.Type( + 'NonEmptyStringArray', + t.array(t.string).is, + (input, context): Either => { + if (typeof input === 'string' && input.trim() !== '') { + const arrayValues = input + .trim() + .split(',') + .map((value) => value.trim()); + const emptyValueFound = arrayValues.some((value) => value === ''); + if (emptyValueFound) { + return t.failure(input, context); + } else { + return t.success(arrayValues); + } + } else { + return t.failure(input, context); + } + }, + String +); + +export type NonEmptyStringArrayC = typeof NonEmptyStringArray; + +export type NonEmptyStringArrayEncoded = t.OutputOf; +export type NonEmptyStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index a6c2a18bb8c8a..a318d653450c7 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -44,26 +44,34 @@ export const findExceptionListItemRoute = (router: IRouter): void => { sort_field: sortField, sort_order: sortOrder, } = request.query; - const exceptionListItems = await exceptionLists.findExceptionListItem({ - filter, - listId, - namespaceType, - page, - perPage, - sortField, - sortOrder, - }); - if (exceptionListItems == null) { + + if (listId.length !== namespaceType.length) { return siemResponse.error({ - body: `list id: "${listId}" does not exist`, - statusCode: 404, + body: `list_id and namespace_id need to have the same comma separated number of values. Expected list_id length: ${listId.length} to equal namespace_type length: ${namespaceType.length}`, + statusCode: 400, }); - } - const [validated, errors] = validate(exceptionListItems, foundExceptionListItemSchema); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); } else { - return response.ok({ body: validated ?? {} }); + const exceptionListItems = await exceptionLists.findExceptionListsItem({ + filter, + listId, + namespaceType, + page, + perPage, + sortField, + sortOrder, + }); + if (exceptionListItems == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } + const [validated, errors] = validate(exceptionListItems, foundExceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } catch (err) { const error = transformError(err); diff --git a/x-pack/plugins/lists/server/scripts/delete_all_exception_lists.sh b/x-pack/plugins/lists/server/scripts/delete_all_exception_lists.sh index bb431800c56c3..3241bb8411916 100755 --- a/x-pack/plugins/lists/server/scripts/delete_all_exception_lists.sh +++ b/x-pack/plugins/lists/server/scripts/delete_all_exception_lists.sh @@ -7,7 +7,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_all_alerts.sh +# Example: ./delete_all_exception_lists.sh # https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json index 520bc4ddf1e09..19027ac189a47 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json @@ -1,8 +1,8 @@ { - "list_id": "endpoint_list", + "list_id": "simple_list", "_tags": ["endpoint", "process", "malware", "os:linux"], "tags": ["user added string for a tag", "malware"], - "type": "endpoint", + "type": "detection", "description": "This is a sample endpoint type exception", "name": "Sample Endpoint Exception List" } diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item.json index 8663be5d649e5..eede855aab199 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item.json @@ -1,6 +1,6 @@ { - "list_id": "endpoint_list", - "item_id": "endpoint_list_item", + "list_id": "simple_list", + "item_id": "simple_list_item", "_tags": ["endpoint", "process", "malware", "os:linux"], "tags": ["user added string for a tag", "malware"], "type": "simple", diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json index 3d6253fcb58ad..e0d401eff9269 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json @@ -18,7 +18,7 @@ "field": "source.ip", "operator": "excluded", "type": "list", - "list": { "id": "list-ip", "type": "ip" } + "list": { "id": "ip_list", "type": "ip" } } ] } diff --git a/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh index 5efad01e9a68e..ba8f1cd0477a1 100755 --- a/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh +++ b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh @@ -21,6 +21,6 @@ pushd ${FOLDER} > /dev/null curl -s -k -OJ \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=list-ip" + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=ip_list" popd > /dev/null diff --git a/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh b/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh index e3f21da56d1b7..ff720afba4157 100755 --- a/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh +++ b/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh @@ -9,12 +9,23 @@ set -e ./check_env_variables.sh -LIST_ID=${1:-endpoint_list} +LIST_ID=${1:-simple_list} NAMESPACE_TYPE=${2-single} -# Example: ./find_exception_list_items.sh {list-id} -# Example: ./find_exception_list_items.sh {list-id} single -# Example: ./find_exception_list_items.sh {list-id} agnostic +# First, post two different lists and two list items for the example to work +# ./post_exception_list.sh ./exception_lists/new/exception_list.json +# ./post_exception_list_item.sh ./exception_lists/new/exception_list_item.json +# +# ./post_exception_list.sh ./exception_lists/new/exception_list_agnostic.json +# ./post_exception_list_item.sh ./exception_lists/new/exception_list_item_agnostic.json + +# Querying a single list item aginst each type +# Example: ./find_exception_list_items.sh simple_list +# Example: ./find_exception_list_items.sh simple_list single +# Example: ./find_exception_list_items.sh endpoint_list agnostic +# +# Finding multiple list id's across multiple spaces +# Example: ./find_exception_list_items.sh simple_list,endpoint_list single,agnostic curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID}&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh b/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh index 57313275ccd0e..79e66be42e441 100755 --- a/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh +++ b/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -LIST_ID=${1:-endpoint_list} +LIST_ID=${1:-simple_list} FILTER=${2:-'exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List'} NAMESPACE_TYPE=${3-single} @@ -17,13 +17,23 @@ NAMESPACE_TYPE=${3-single} # The %22 is just an encoded quote of " # Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp -# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List -# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List single -# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List agnostic +# First, post two different lists and two list items for the example to work +# ./post_exception_list.sh ./exception_lists/new/exception_list.json +# ./post_exception_list_item.sh ./exception_lists/new/exception_list_item.json # -# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.entries.field:actingProcess.file.signer -# Example: ./find_exception_list_items_by_filter.sh endpoint_list "exception-list.attributes.entries.field:actingProcess.file.signe*" -# Example: ./find_exception_list_items_by_filter.sh endpoint_list "exception-list.attributes.entries.match:Elastic*%20AND%20exception-list.attributes.entries.field:actingProcess.file.signe*" +# ./post_exception_list.sh ./exception_lists/new/exception_list_agnostic.json +# ./post_exception_list_item.sh ./exception_lists/new/exception_list_item_agnostic.json + +# Example: ./find_exception_list_items_by_filter.sh simple_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List +# Example: ./find_exception_list_items_by_filter.sh simple_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List single +# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list-agnostic.attributes.name:%20Sample%20Endpoint%20Exception%20List agnostic +# +# Example: ./find_exception_list_items_by_filter.sh simple_list exception-list.attributes.entries.field:actingProcess.file.signer +# Example: ./find_exception_list_items_by_filter.sh simple_list "exception-list.attributes.entries.field:actingProcess.file.signe*" +# Example: ./find_exception_list_items_by_filter.sh simple_list "exception-list.attributes.entries.field:actingProcess.file.signe*%20AND%20exception-list.attributes.entries.field:actingProcess.file.signe*" +# +# Example with multiplie lists, and multiple filters +# Example: ./find_exception_list_items_by_filter.sh simple_list,endpoint_list "exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List,exception-list-agnostic.attributes.name:%20Sample%20Endpoint%20Exception%20List" single,agnostic curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID}&filter=${FILTER}&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items.sh b/x-pack/plugins/lists/server/scripts/find_list_items.sh index 9c8bfd2d5a490..d475da3db61f1 100755 --- a/x-pack/plugins/lists/server/scripts/find_list_items.sh +++ b/x-pack/plugins/lists/server/scripts/find_list_items.sh @@ -9,11 +9,11 @@ set -e ./check_env_variables.sh -LIST_ID=${1-list-ip} +LIST_ID=${1-ip_list} PAGE=${2-1} PER_PAGE=${3-20} -# Example: ./find_list_items.sh list-ip 1 20 +# Example: ./find_list_items.sh ip_list 1 20 curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh index 8924012cf62cf..38cef7c98994b 100755 --- a/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh +++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -LIST_ID=${1-list-ip} +LIST_ID=${1-ip_list} PAGE=${2-1} PER_PAGE=${3-20} CURSOR=${4-invalid} @@ -17,7 +17,7 @@ CURSOR=${4-invalid} # Example: # ./find_list_items.sh 1 20 | jq .cursor # Copy the cursor into the argument below like so -# ./find_list_items_with_cursor.sh list-ip 1 10 eyJwYWdlX2luZGV4IjoyMCwic2VhcmNoX2FmdGVyIjpbIjAyZDZlNGY3LWUzMzAtNGZkYi1iNTY0LTEzZjNiOTk1MjRiYSJdfQ== +# ./find_list_items_with_cursor.sh ip_list 1 10 eyJwYWdlX2luZGV4IjoyMCwic2VhcmNoX2FmdGVyIjpbIjAyZDZlNGY3LWUzMzAtNGZkYi1iNTY0LTEzZjNiOTk1MjRiYSJdfQ== curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&cursor=${CURSOR}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh index 37d80c3dd3f28..eb4b23236b7d4 100755 --- a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh +++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh @@ -9,13 +9,13 @@ set -e ./check_env_variables.sh -LIST_ID=${1-list-ip} +LIST_ID=${1-ip_list} PAGE=${2-1} PER_PAGE=${3-20} SORT_FIELD=${4-value} SORT_ORDER=${4-asc} -# Example: ./find_list_items_with_sort.sh list-ip 1 20 value asc +# Example: ./find_list_items_with_sort.sh ip_list 1 20 value asc curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh index 27d8deb2fc95a..289f9be82f209 100755 --- a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh +++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh @@ -9,14 +9,14 @@ set -e ./check_env_variables.sh -LIST_ID=${1-list-ip} +LIST_ID=${1-ip_list} PAGE=${2-1} PER_PAGE=${3-20} SORT_FIELD=${4-value} SORT_ORDER=${5-asc} CURSOR=${6-invalid} -# Example: ./find_list_items_with_sort_cursor.sh list-ip 1 20 value asc +# Example: ./find_list_items_with_sort_cursor.sh ip_list 1 20 value asc curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}&cursor=${CURSOR}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/import_list_items.sh b/x-pack/plugins/lists/server/scripts/import_list_items.sh index a39409cd08267..2ef01fdeed343 100755 --- a/x-pack/plugins/lists/server/scripts/import_list_items.sh +++ b/x-pack/plugins/lists/server/scripts/import_list_items.sh @@ -10,10 +10,10 @@ set -e ./check_env_variables.sh # Uses a defaults if no argument is specified -LIST_ID=${1:-list-ip} +LIST_ID=${1:-ip_list} FILE=${2:-./lists/files/ips.txt} -# ./import_list_items.sh list-ip ./lists/files/ips.txt +# ./import_list_items.sh ip_list ./lists/files/ips.txt curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json deleted file mode 100644 index d150cfaecc202..0000000000000 --- a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "hand_inserted_item_id", - "list_id": "list-ip", - "value": "10.4.3.11" -} diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index a731371a6ffac..1acc880c851a6 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -82,5 +82,5 @@ export const createExceptionListItem = async ({ type, updated_by: user, }); - return transformSavedObjectToExceptionListItem({ namespaceType, savedObject }); + return transformSavedObjectToExceptionListItem({ savedObject }); }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 73c52fb8b3ec9..62afda52bd79d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -21,6 +21,7 @@ import { DeleteExceptionListOptions, FindExceptionListItemOptions, FindExceptionListOptions, + FindExceptionListsItemOptions, GetExceptionListItemOptions, GetExceptionListOptions, UpdateExceptionListItemOptions, @@ -36,6 +37,7 @@ import { deleteExceptionList } from './delete_exception_list'; import { deleteExceptionListItem } 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'; export class ExceptionListClient { private readonly user: string; @@ -229,6 +231,28 @@ export class ExceptionListClient { }); }; + public findExceptionListsItem = async ({ + listId, + filter, + perPage, + page, + sortField, + sortOrder, + namespaceType, + }: FindExceptionListsItemOptions): Promise => { + const { savedObjectsClient } = this; + return findExceptionListsItem({ + filter, + listId, + namespaceType, + page, + perPage, + savedObjectsClient, + sortField, + sortOrder, + }); + }; + public findExceptionList = async ({ filter, perPage, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 3eff2c7e202e7..b3070f2d4a70d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -6,6 +6,9 @@ import { SavedObjectsClientContract } from 'kibana/server'; +import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; +import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_empty_string_array'; +import { EmptyStringArrayDecoded } from '../../../common/schemas/types/empty_string_array'; import { CreateCommentsArray, Description, @@ -127,6 +130,16 @@ export interface FindExceptionListItemOptions { sortOrder: SortOrderOrUndefined; } +export interface FindExceptionListsItemOptions { + listId: NonEmptyStringArrayDecoded; + namespaceType: NamespaceTypeArray; + filter: EmptyStringArrayDecoded; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +} + export interface FindExceptionListOptions { namespaceType: NamespaceType; filter: FilterOrUndefined; diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index 1c3103ad1db7e..e997ff5f9adf1 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -7,7 +7,6 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { - ExceptionListSoSchema, FilterOrUndefined, FoundExceptionListItemSchema, ListId, @@ -17,10 +16,8 @@ import { SortFieldOrUndefined, SortOrderOrUndefined, } from '../../../common/schemas'; -import { SavedObjectType } from '../../saved_objects'; -import { getSavedObjectType, transformSavedObjectsToFoundExceptionListItem } from './utils'; -import { getExceptionList } from './get_exception_list'; +import { findExceptionListsItem } from './find_exception_list_items'; interface FindExceptionListItemOptions { listId: ListId; @@ -43,43 +40,14 @@ export const findExceptionListItem = async ({ sortField, sortOrder, }: FindExceptionListItemOptions): Promise => { - const savedObjectType = getSavedObjectType({ namespaceType }); - const exceptionList = await getExceptionList({ - id: undefined, - listId, - namespaceType, + return findExceptionListsItem({ + filter: filter != null ? [filter] : [], + listId: [listId], + namespaceType: [namespaceType], + page, + perPage, savedObjectsClient, + sortField, + sortOrder, }); - if (exceptionList == null) { - return null; - } else { - const savedObjectsFindResponse = await savedObjectsClient.find({ - filter: getExceptionListItemFilter({ filter, listId, savedObjectType }), - page, - perPage, - sortField, - sortOrder, - type: savedObjectType, - }); - return transformSavedObjectsToFoundExceptionListItem({ - namespaceType, - savedObjectsFindResponse, - }); - } -}; - -export const getExceptionListItemFilter = ({ - filter, - listId, - savedObjectType, -}: { - listId: ListId; - filter: FilterOrUndefined; - savedObjectType: SavedObjectType; -}): string => { - if (filter == null) { - return `${savedObjectType}.attributes.list_type: item AND ${savedObjectType}.attributes.list_id: ${listId}`; - } else { - return `${savedObjectType}.attributes.list_type: item AND ${savedObjectType}.attributes.list_id: ${listId} AND ${filter}`; - } }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.test.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.test.ts new file mode 100644 index 0000000000000..a2fbb39103769 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { LIST_ID } from '../../../common/constants.mock'; + +import { getExceptionListsItemFilter } from './find_exception_list_items'; + +describe('find_exception_list_items', () => { + describe('getExceptionListsItemFilter', () => { + test('It should create a filter with a single listId with an empty filter', () => { + const filter = getExceptionListsItemFilter({ + filter: [], + listId: [LIST_ID], + savedObjectType: ['exception-list'], + }); + expect(filter).toEqual( + '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: some-list-id)' + ); + }); + + test('It should create a filter with a single listId with a single filter', () => { + const filter = getExceptionListsItemFilter({ + filter: ['exception-list.attributes.name: "Sample Endpoint Exception List"'], + listId: [LIST_ID], + savedObjectType: ['exception-list'], + }); + expect(filter).toEqual( + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: some-list-id) AND exception-list.attributes.name: "Sample Endpoint Exception List")' + ); + }); + + test('It should create a filter with 2 listIds and an empty filter', () => { + const filter = getExceptionListsItemFilter({ + filter: [], + listId: ['list-1', 'list-2'], + savedObjectType: ['exception-list', 'exception-list-agnostic'], + }); + expect(filter).toEqual( + '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2)' + ); + }); + + test('It should create a filter with 2 listIds and a single filter', () => { + const filter = getExceptionListsItemFilter({ + filter: ['exception-list.attributes.name: "Sample Endpoint Exception List"'], + listId: ['list-1', 'list-2'], + savedObjectType: ['exception-list', 'exception-list-agnostic'], + }); + expect(filter).toEqual( + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2)' + ); + }); + + test('It should create a filter with 3 listIds and an empty filter', () => { + const filter = getExceptionListsItemFilter({ + filter: [], + listId: ['list-1', 'list-2', 'list-3'], + savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'], + }); + expect(filter).toEqual( + '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-3)' + ); + }); + + test('It should create a filter with 3 listIds and a single filter for the first item', () => { + const filter = getExceptionListsItemFilter({ + filter: ['exception-list.attributes.name: "Sample Endpoint Exception List"'], + listId: ['list-1', 'list-2', 'list-3'], + savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'], + }); + expect(filter).toEqual( + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-3)' + ); + }); + + test('It should create a filter with 3 listIds and 3 filters for each', () => { + const filter = getExceptionListsItemFilter({ + filter: [ + 'exception-list.attributes.name: "Sample Endpoint Exception List 1"', + 'exception-list.attributes.name: "Sample Endpoint Exception List 2"', + 'exception-list.attributes.name: "Sample Endpoint Exception List 3"', + ], + listId: ['list-1', 'list-2', 'list-3'], + savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'], + }); + expect(filter).toEqual( + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) AND exception-list.attributes.name: "Sample Endpoint Exception List 1") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2) AND exception-list.attributes.name: "Sample Endpoint Exception List 2") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-3) AND exception-list.attributes.name: "Sample Endpoint Exception List 3")' + ); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts new file mode 100644 index 0000000000000..47a0d809cce67 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -0,0 +1,94 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; + +import { EmptyStringArrayDecoded } from '../../../common/schemas/types/empty_string_array'; +import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; +import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_empty_string_array'; +import { + ExceptionListSoSchema, + FoundExceptionListItemSchema, + PageOrUndefined, + PerPageOrUndefined, + SortFieldOrUndefined, + SortOrderOrUndefined, +} from '../../../common/schemas'; +import { SavedObjectType } from '../../saved_objects'; + +import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils'; +import { getExceptionList } from './get_exception_list'; + +interface FindExceptionListItemsOptions { + listId: NonEmptyStringArrayDecoded; + namespaceType: NamespaceTypeArray; + savedObjectsClient: SavedObjectsClientContract; + filter: EmptyStringArrayDecoded; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +} + +export const findExceptionListsItem = async ({ + listId, + namespaceType, + savedObjectsClient, + filter, + page, + perPage, + sortField, + sortOrder, +}: FindExceptionListItemsOptions): Promise => { + const savedObjectType = getSavedObjectTypes({ namespaceType }); + const exceptionLists = ( + await Promise.all( + listId.map((singleListId, index) => { + return getExceptionList({ + id: undefined, + listId: singleListId, + namespaceType: namespaceType[index], + savedObjectsClient, + }); + }) + ) + ).filter((list) => list != null); + if (exceptionLists.length === 0) { + return null; + } else { + const savedObjectsFindResponse = await savedObjectsClient.find({ + filter: getExceptionListsItemFilter({ filter, listId, savedObjectType }), + page, + perPage, + sortField, + sortOrder, + type: savedObjectType, + }); + return transformSavedObjectsToFoundExceptionListItem({ + savedObjectsFindResponse, + }); + } +}; + +export const getExceptionListsItemFilter = ({ + filter, + listId, + savedObjectType, +}: { + listId: NonEmptyStringArrayDecoded; + filter: EmptyStringArrayDecoded; + savedObjectType: SavedObjectType[]; +}): string => { + return listId.reduce((accum, singleListId, index) => { + const listItemAppend = `(${savedObjectType[index]}.attributes.list_type: item AND ${savedObjectType[index]}.attributes.list_id: ${singleListId})`; + const listItemAppendWithFilter = + filter[index] != null ? `(${listItemAppend} AND ${filter[index]})` : listItemAppend; + if (accum === '') { + return listItemAppendWithFilter; + } else { + return `${accum} OR ${listItemAppendWithFilter}`; + } + }, ''); +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts index d7efdc054c48c..d68863c02148f 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts @@ -35,7 +35,7 @@ export const getExceptionListItem = async ({ if (id != null) { try { const savedObject = await savedObjectsClient.get(savedObjectType, id); - return transformSavedObjectToExceptionListItem({ namespaceType, savedObject }); + return transformSavedObjectToExceptionListItem({ savedObject }); } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { return null; @@ -55,7 +55,6 @@ export const getExceptionListItem = async ({ }); if (savedObject.saved_objects[0] != null) { return transformSavedObjectToExceptionListItem({ - namespaceType, savedObject: savedObject.saved_objects[0], }); } else { diff --git a/x-pack/plugins/lists/server/services/exception_lists/index.ts b/x-pack/plugins/lists/server/services/exception_lists/index.ts index a66f00819605b..510b2c70c6c94 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/index.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/index.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './create_exception_list_item'; export * from './create_exception_list'; -export * from './delete_exception_list_item'; +export * from './create_exception_list_item'; export * from './delete_exception_list'; +export * from './delete_exception_list_item'; +export * from './delete_exception_list_items_by_list'; export * from './find_exception_list'; export * from './find_exception_list_item'; -export * from './get_exception_list_item'; +export * from './find_exception_list_items'; export * from './get_exception_list'; -export * from './update_exception_list_item'; +export * from './get_exception_list_item'; export * from './update_exception_list'; +export * from './update_exception_list_item'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index ab54647430b9b..ad1e1a3439d7c 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -6,6 +6,7 @@ import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server'; +import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; import { ErrorWithStatusCode } from '../../error_with_status_code'; import { Comments, @@ -42,6 +43,28 @@ export const getSavedObjectType = ({ } }; +export const getExceptionListType = ({ + savedObjectType, +}: { + savedObjectType: string; +}): NamespaceType => { + if (savedObjectType === exceptionListAgnosticSavedObjectType) { + return 'agnostic'; + } else { + return 'single'; + } +}; + +export const getSavedObjectTypes = ({ + namespaceType, +}: { + namespaceType: NamespaceTypeArray; +}): SavedObjectType[] => { + return namespaceType.map((singleNamespaceType) => + getSavedObjectType({ namespaceType: singleNamespaceType }) + ); +}; + export const transformSavedObjectToExceptionList = ({ savedObject, namespaceType, @@ -126,10 +149,8 @@ export const transformSavedObjectUpdateToExceptionList = ({ export const transformSavedObjectToExceptionListItem = ({ savedObject, - namespaceType, }: { savedObject: SavedObject; - namespaceType: NamespaceType; }): ExceptionListItemSchema => { const dateNow = new Date().toISOString(); const { @@ -167,7 +188,7 @@ export const transformSavedObjectToExceptionListItem = ({ list_id, meta, name, - namespace_type: namespaceType, + namespace_type: getExceptionListType({ savedObjectType: savedObject.type }), tags, tie_breaker_id, type: exceptionListItemType.is(type) ? type : 'simple', @@ -229,14 +250,12 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ export const transformSavedObjectsToFoundExceptionListItem = ({ savedObjectsFindResponse, - namespaceType, }: { savedObjectsFindResponse: SavedObjectsFindResponse; - namespaceType: NamespaceType; }): FoundExceptionListItemSchema => { return { data: savedObjectsFindResponse.saved_objects.map((savedObject) => - transformSavedObjectToExceptionListItem({ namespaceType, savedObject }) + transformSavedObjectToExceptionListItem({ savedObject }) ), page: savedObjectsFindResponse.page, per_page: savedObjectsFindResponse.per_page,