Skip to content

Commit

Permalink
Added multiple filtering capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankHassanabad committed Jul 13, 2020
1 parent 254de4a commit 66a7246
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

import * as t from 'io-ts';

import { filter, 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(
Expand All @@ -25,7 +26,7 @@ export const findExceptionListItemSchema = t.intersection([
),
t.exact(
t.partial({
filter, // defaults to undefined 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
Expand All @@ -40,8 +41,9 @@ export type FindExceptionListItemSchemaPartial = t.OutputOf<typeof findException
// This type is used after a decode since some things are defaults after a decode.
export type FindExceptionListItemSchemaPartialDecoded = Omit<
t.TypeOf<typeof findExceptionListItemSchema>,
'namespace_type'
'namespace_type' | 'filter'
> & {
filter: EmptyStringArrayDecoded;
namespace_type: DefaultNamespaceArrayTypeDecoded;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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']);
});
});
45 changes: 45 additions & 0 deletions x-pack/plugins/lists/common/schemas/types/empty_string_array.ts
Original file line number Diff line number Diff line change
@@ -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>;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ NAMESPACE_TYPE=${3-single}

# 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 simple_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List agnostic
# 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.match:Elastic*%20AND%20simple_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 .
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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,
Expand Down Expand Up @@ -132,7 +133,7 @@ export interface FindExceptionListItemOptions {
export interface FindExceptionListsItemOptions {
listId: NonEmptyStringArrayDecoded;
namespaceType: NamespaceTypeArray;
filter: FilterOrUndefined;
filter: EmptyStringArrayDecoded;
perPage: PerPageOrUndefined;
page: PageOrUndefined;
sortField: SortFieldOrUndefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const findExceptionListItem = async ({
sortOrder,
}: FindExceptionListItemOptions): Promise<FoundExceptionListItemSchema | null> => {
return findExceptionListsItem({
filter,
filter: filter != null ? [filter] : [],
listId: [listId],
namespaceType: [namespaceType],
page,
Expand Down
Original file line number Diff line number Diff line change
@@ -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")'
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
*/
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,
FilterOrUndefined,
FoundExceptionListItemSchema,
PageOrUndefined,
PerPageOrUndefined,
Expand All @@ -25,7 +25,7 @@ interface FindExceptionListItemsOptions {
listId: NonEmptyStringArrayDecoded;
namespaceType: NamespaceTypeArray;
savedObjectsClient: SavedObjectsClientContract;
filter: FilterOrUndefined;
filter: EmptyStringArrayDecoded;
perPage: PerPageOrUndefined;
page: PageOrUndefined;
sortField: SortFieldOrUndefined;
Expand Down Expand Up @@ -78,19 +78,17 @@ export const getExceptionListsItemFilter = ({
savedObjectType,
}: {
listId: NonEmptyStringArrayDecoded;
filter: FilterOrUndefined;
filter: EmptyStringArrayDecoded;
savedObjectType: SavedObjectType[];
}): string => {
const listIdsFilter = listId.reduce((accum, singleListId, index) => {
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 `(${savedObjectType[index]}.attributes.list_type: item AND ${savedObjectType[index]}.attributes.list_id: ${singleListId})`;
return listItemAppendWithFilter;
} else {
return `${accum} OR (${savedObjectType[index]}.attributes.list_type: item AND ${savedObjectType[index]}.attributes.list_id: ${singleListId})`;
return `${accum} OR ${listItemAppendWithFilter}`;
}
}, '');
if (filter == null) {
return listIdsFilter;
} else {
return `${listIdsFilter} AND ${filter}`;
}
};

0 comments on commit 66a7246

Please sign in to comment.