[SIEM][Detection Engine][Lists] Adds the ability for exception lists to be multi-list queried. (#71540)

## Summary

* Adds the ability for exception lists to be multi-list queried 
* Fixes a bunch of script issues where I did not update everywhere I needed to use `ip_list` and deletes an old list that now lives within the new/lists folder
* Fixes a few io-ts issues with Encode Decode while I was in there.
* Adds two more types and their tests for supporting converting between comma separated strings and arrays for GET calls.
* Fixes one weird circular dep issue while adding more types.

You now send into the find an optional comma separated list of exception lists their namespace type and any filters like so:

```ts
GET /api/exception_lists/items/_find?list_id=simple_list,endpoint_list&namespace_type=single,agnostic&filtering=filter1,filter2"
```

And this will return the results of both together with each filter applied to each list. If you use a sort field and ordering it will order across the lists together as if they are one list. Filter is optional like before. If you provide less filters than there are lists, the lists will only apply the filters to each list until it runs out of filters and then not filter the other lists.

If at least one list is found this will _not_ return a 404 but it will _only_ query the list(s) it did find. If none of the lists are found, then this will return a 404 not found exception.

**Script testing**

See these files for more information:
* find_exception_list_items.sh
* find_exception_list_items_by_filter.sh 

But basically you can create two lists and an item for each of the lists:

```ts
./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
```

And then you can query these two lists together:
```ts
./find_exception_list_items.sh simple_list,endpoint_list single,agnostic
```

Or for filtering you can query both and add a filter for each one:
```ts
./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
```

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
This commit is contained in:
Frank Hassanabad 2020-07-13 19:07:35 -06:00 committed by GitHub
parent 8325222c0a
commit 473806c3c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 786 additions and 143 deletions

View file

@ -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",

View file

@ -273,7 +273,6 @@ export const cursorOrUndefined = t.union([cursor, t.undefined]);
export type CursorOrUndefined = t.TypeOf<typeof cursorOrUndefined>;
export const namespace_type = DefaultNamespace;
export type NamespaceType = t.TypeOf<typeof namespace_type>;
export const operator = t.keyof({ excluded: null, included: null });
export type Operator = t.TypeOf<typeof operator>;

View file

@ -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';

View file

@ -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(

View file

@ -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({

View file

@ -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({

View file

@ -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<typeof findExceptionListItemSchema>;
export type FindExceptionListItemSchemaPartial = t.OutputOf<typeof findExceptionListItemSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type FindExceptionListItemSchemaPartialDecoded = Omit<
FindExceptionListItemSchemaPartial,
'namespace_type'
t.TypeOf<typeof findExceptionListItemSchema>,
'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.

View file

@ -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({

View file

@ -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({

View file

@ -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({

View file

@ -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';

View file

@ -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(

View file

@ -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<typeof namespaceType>;
export type DefaultNamespaceC = t.Type<NamespaceType, NamespaceType, unknown>;
export type NamespaceType = t.TypeOf<typeof namespaceType>;
/**
* 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<NamespaceType, NamespaceType, unknown>(
'DefaultNamespace',
namespaceType.is,
(input, context): Either<t.Errors, NamespaceType> =>
input == null ? t.success('single') : namespaceType.validate(input, context),
t.identity
);
export type DefaultNamespaceC = typeof DefaultNamespace;

View file

@ -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({});
});
});

View file

@ -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<typeof namespaceTypeArray>;
/**
* 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<t.Errors, NamespaceTypeArray> => {
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<typeof DefaultNamespaceArray>;
export type DefaultNamespaceArrayTypeDecoded = t.TypeOf<typeof DefaultNamespaceArray>;

View file

@ -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']);
});
});

View file

@ -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<string[], string | undefined | null, unknown>(
'EmptyStringArray',
t.array(t.string).is,
(input, context): Either<t.Errors, string[]> => {
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<typeof EmptyStringArray>;
export type EmptyStringArrayDecoded = t.TypeOf<typeof EmptyStringArray>;

View file

@ -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']);
});
});

View file

@ -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<string[], string, unknown>(
'NonEmptyStringArray',
t.array(t.string).is,
(input, context): Either<t.Errors, string[]> => {
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<typeof NonEmptyStringArray>;
export type NonEmptyStringArrayDecoded = t.TypeOf<typeof NonEmptyStringArray>;

View file

@ -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);

View file

@ -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" \

View file

@ -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"
}

View file

@ -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",

View file

@ -18,7 +18,7 @@
"field": "source.ip",
"operator": "excluded",
"type": "list",
"list": { "id": "list-ip", "type": "ip" }
"list": { "id": "ip_list", "type": "ip" }
}
]
}

View file

@ -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

View file

@ -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 .

View file

@ -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 .

View file

@ -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 .

View file

@ -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 .

View file

@ -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 .

View file

@ -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 <cursor>
# Example: ./find_list_items_with_sort_cursor.sh ip_list 1 20 value asc <cursor>
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 .

View file

@ -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} \

View file

@ -1,5 +0,0 @@
{
"id": "hand_inserted_item_id",
"list_id": "list-ip",
"value": "10.4.3.11"
}

View file

@ -82,5 +82,5 @@ export const createExceptionListItem = async ({
type,
updated_by: user,
});
return transformSavedObjectToExceptionListItem({ namespaceType, savedObject });
return transformSavedObjectToExceptionListItem({ savedObject });
};

View file

@ -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<FoundExceptionListItemSchema | null> => {
const { savedObjectsClient } = this;
return findExceptionListsItem({
filter,
listId,
namespaceType,
page,
perPage,
savedObjectsClient,
sortField,
sortOrder,
});
};
public findExceptionList = async ({
filter,
perPage,

View file

@ -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;

View file

@ -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<FoundExceptionListItemSchema | null> => {
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<ExceptionListSoSchema>({
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}`;
}
};

View file

@ -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")'
);
});
});
});

View file

@ -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<FoundExceptionListItemSchema | null> => {
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<ExceptionListSoSchema>({
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}`;
}
}, '');
};

View file

@ -35,7 +35,7 @@ export const getExceptionListItem = async ({
if (id != null) {
try {
const savedObject = await savedObjectsClient.get<ExceptionListSoSchema>(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 {

View file

@ -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';

View file

@ -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<ExceptionListSoSchema>;
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<ExceptionListSoSchema>;
namespaceType: NamespaceType;
}): FoundExceptionListItemSchema => {
return {
data: savedObjectsFindResponse.saved_objects.map((savedObject) =>
transformSavedObjectToExceptionListItem({ namespaceType, savedObject })
transformSavedObjectToExceptionListItem({ savedObject })
),
page: savedObjectsFindResponse.page,
per_page: savedObjectsFindResponse.per_page,