[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:
parent
8325222c0a
commit
473806c3c8
|
@ -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",
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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>;
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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>;
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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>;
|
|
@ -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);
|
||||
|
|
|
@ -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" \
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"field": "source.ip",
|
||||
"operator": "excluded",
|
||||
"type": "list",
|
||||
"list": { "id": "list-ip", "type": "ip" }
|
||||
"list": { "id": "ip_list", "type": "ip" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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} \
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"id": "hand_inserted_item_id",
|
||||
"list_id": "list-ip",
|
||||
"value": "10.4.3.11"
|
||||
}
|
|
@ -82,5 +82,5 @@ export const createExceptionListItem = async ({
|
|||
type,
|
||||
updated_by: user,
|
||||
});
|
||||
return transformSavedObjectToExceptionListItem({ namespaceType, savedObject });
|
||||
return transformSavedObjectToExceptionListItem({ savedObject });
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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")'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}`;
|
||||
}
|
||||
}, '');
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue