From 3b19da11690418d2c860efecd21260f51576d72c Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 22 Jun 2020 16:22:04 -0400 Subject: [PATCH 1/8] Updates list entry schema, exposes exception list client, updates tests --- x-pack/plugins/lists/common/constants.mock.ts | 6 +- .../types/default_entries_array.test.ts | 3 +- .../schemas/types/default_namespace.test.ts | 61 +++++++++++++++++++ .../common/schemas/types/default_namespace.ts | 2 +- .../common/schemas/types/entries.mock.ts | 6 +- .../common/schemas/types/entries.test.ts | 22 +++++-- .../lists/common/schemas/types/entries.ts | 6 +- .../lists/common/schemas/types/index.ts | 1 + x-pack/plugins/lists/server/index.ts | 1 + .../server/saved_objects/exception_list.ts | 10 +++ 10 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 24cfe440bd7d8..185de02d555b7 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -46,10 +46,8 @@ export const EXISTS = 'exists'; export const NESTED = 'nested'; export const ENTRIES: EntriesArray = [ { - entries: [ - { field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' }, - ], - field: 'some.field', + entries: [{ field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }], + field: 'some.parentField', type: 'nested', }, { field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' }, diff --git a/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts index 9e615528ba775..e7910be6bf4b5 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts @@ -17,7 +17,8 @@ import { getEntriesArrayMock, getEntryMatchMock, getEntryNestedMock } from './en // it checks against every item in that union. Since entries consist of 5 // different entry types, it returns 5 of these. To make more readable, // extracted here. -const returnedSchemaError = `"Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "list", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "exists" |} | {| entries: Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "list", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "exists" |})>, field: string, type: "nested" |})>"`; +const returnedSchemaError = + '"Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, list: {| id: string, type: "ip" | "keyword" |}, operator: "excluded" | "included", type: "list" |} | {| field: string, operator: "excluded" | "included", type: "exists" |} | {| entries: Array<{| field: string, operator: "excluded" | "included", type: "match", value: string |}>, field: string, type: "nested" |})>"'; describe('default_entries_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts new file mode 100644 index 0000000000000..152f85233aa1a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { DefaultNamespace } from './default_namespace'; + +describe('default_namespace', () => { + test('it should validate "single"', () => { + const payload = 'single'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate "agnostic"', () => { + const payload = 'agnostic'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it defaults to "single" if "undefined"', () => { + const payload = undefined; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it defaults to "single" if "null"', () => { + const payload = null; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it should NOT validate if not "single" or "agnostic"', () => { + const payload = 'something else'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + `Invalid value "something else" supplied to "DefaultNamespace"`, + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts index c98cb8d2bba72..8f8f8d105b624 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -const namespaceType = t.keyof({ agnostic: null, single: null }); +export const namespaceType = t.keyof({ agnostic: null, single: null }); type NamespaceType = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index 1926cb09db119..8af18c970c6ae 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -9,10 +9,12 @@ import { EXISTS, FIELD, LIST, + LIST_ID, MATCH, MATCH_ANY, NESTED, OPERATOR, + TYPE, } from '../../constants.mock'; import { @@ -40,9 +42,9 @@ export const getEntryMatchAnyMock = (): EntryMatchAny => ({ export const getEntryListMock = (): EntryList => ({ field: FIELD, + list: { id: LIST_ID, type: TYPE }, operator: OPERATOR, type: LIST, - value: [ENTRY_VALUE], }); export const getEntryExistsMock = (): EntryExists => ({ @@ -52,7 +54,7 @@ export const getEntryExistsMock = (): EntryExists => ({ }); export const getEntryNestedMock = (): EntryNested => ({ - entries: [getEntryMatchMock(), getEntryExistsMock()], + entries: [getEntryMatchMock(), getEntryMatchMock()], field: FIELD, type: NESTED, }); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts index a13d4c0347e45..01f82f12f2b2c 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.test.ts @@ -251,16 +251,16 @@ describe('Entries', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "value" is not string array', () => { - const payload: Omit & { value: string } = { + test('it should not validate when "list" is not expected value', () => { + const payload: Omit & { list: string } = { ...getEntryListMock(), - value: 'someListId', + list: 'someListId', }; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "someListId" supplied to "value"', + 'Invalid value "someListId" supplied to "list"', ]); expect(message.schema).toEqual({}); }); @@ -338,6 +338,20 @@ describe('Entries', () => { expect(message.schema).toEqual({}); }); + test('it should NOT validate when "entries" contains an entry item that is not type "match"', () => { + const payload: Omit & { + entries: EntryMatchAny[]; + } = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "entries,type"', + 'Invalid value "["some host name"]" supplied to "entries,value"', + ]); + expect(message.schema).toEqual({}); + }); + test('it should strip out extra keys', () => { const payload: EntryNested & { extraKey?: string; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.ts b/x-pack/plugins/lists/common/schemas/types/entries.ts index e3625dbe08334..c379f77b862c8 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { operator } from '../common/schemas'; +import { operator, type } from '../common/schemas'; import { DefaultStringArray } from '../../siem_common_deps'; export const entriesMatch = t.exact( @@ -34,9 +34,9 @@ export type EntryMatchAny = t.TypeOf; export const entriesList = t.exact( t.type({ field: t.string, + list: t.exact(t.type({ id: t.string, type })), operator, type: t.keyof({ list: null }), - value: DefaultStringArray, }) ); export type EntryList = t.TypeOf; @@ -52,7 +52,7 @@ export type EntryExists = t.TypeOf; export const entriesNested = t.exact( t.type({ - entries: t.array(t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists])), + entries: t.array(entriesMatch), field: t.string, type: t.keyof({ nested: null }), }) diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts index 8e4b28b31d95c..97f2b0f59a5fd 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -5,5 +5,6 @@ */ export * from './default_comments_array'; export * from './default_entries_array'; +export * from './default_namespace'; export * from './comments'; export * from './entries'; diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 33f58ba65d3c3..31f22108028a6 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -11,6 +11,7 @@ import { ListPlugin } from './plugin'; // exporting these since its required at top level in siem plugin export { ListClient } from './services/lists/list_client'; +export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export { ListPluginSetup } from './types'; export const config = { schema: ConfigSchema }; diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 10f9b1f4383f5..57bc63e6f7e35 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -105,6 +105,16 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = { field: { type: 'keyword', }, + list: { + properties: { + id: { + type: 'keyword', + }, + type: { + type: 'keyword', + }, + }, + }, operator: { type: 'keyword', }, From 338891800ad330e719e5020aa9ca8011cc1b8468 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 22 Jun 2020 16:30:12 -0400 Subject: [PATCH 2/8] create new de list schema and unit tests --- .../detection_engine/lists_common_deps.ts | 7 + .../schemas/common/schemas.ts | 37 ---- .../schemas/types/lists.mock.ts | 18 ++ .../schemas/types/lists.test.ts | 131 +++++++++++++ .../detection_engine/schemas/types/lists.ts | 22 +++ .../schemas/types/lists_default_array.test.ts | 173 +++--------------- .../schemas/types/lists_default_array.ts | 28 +-- 7 files changed, 210 insertions(+), 206 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts new file mode 100644 index 0000000000000..efe1166ec1566 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EntryList, namespaceType } from '../../../lists/common/schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 0c7bcdefd360d..f6b732cd1f64e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -341,40 +341,3 @@ export type Note = t.TypeOf; export const noteOrUndefined = t.union([note, t.undefined]); export type NoteOrUndefined = t.TypeOf; - -// NOTE: Experimental list support not being shipped currently and behind a feature flag -// TODO: Remove this comment once we lists have passed testing and is ready for the release -export const list_field = t.string; -export const list_values_operator = t.keyof({ included: null, excluded: null }); -export const list_values_type = t.keyof({ match: null, match_all: null, list: null, exists: null }); -export const list_values = t.exact( - t.intersection([ - t.type({ - name: t.string, - }), - t.partial({ - id: t.string, - description: t.string, - created_at, - }), - ]) -); -export const list = t.exact( - t.intersection([ - t.type({ - field: t.string, - values_operator: list_values_operator, - values_type: list_values_type, - }), - t.partial({ values: t.array(list_values) }), - ]) -); -export const list_and = t.intersection([ - list, - t.partial({ - and: t.array(list), - }), -]); - -export const listAndOrUndefined = t.union([t.array(list_and), t.undefined]); -export type ListAndOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts new file mode 100644 index 0000000000000..d76e2ac78f3d3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts @@ -0,0 +1,18 @@ +/* + * 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, ListArray } from './lists'; + +export const getListMock = (): List => ({ + id: 'some_uuid', + namespace_type: 'single', +}); + +export const getListAgnosticMock = (): List => ({ + id: 'some_uuid', + namespace_type: 'agnostic', +}); + +export const getListArrayMock = (): ListArray => [getListMock(), getListAgnosticMock()]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts new file mode 100644 index 0000000000000..657a4b479f164 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../../test_utils'; + +import { getListAgnosticMock, getListMock, getListArrayMock } from './lists.mock'; +import { + List, + ListArray, + ListArrayOrUndefined, + list, + listArray, + listArrayOrUndefined, +} from './lists'; + +describe('Lists', () => { + describe('list', () => { + test('it should validate a list', () => { + const payload = getListMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a list with "namespace_type" of"agnostic"', () => { + const payload = getListAgnosticMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a list without an "id"', () => { + const payload = getListMock(); + delete payload.id; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a list without "namespace_type"', () => { + const payload = getListMock(); + delete payload.namespace_type; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: List & { + extraKey?: string; + } = getListMock(); + payload.extraKey = 'some value'; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getListMock()); + }); + }); + + describe('listArray', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when unexpected type found in array', () => { + const payload = ([1] as unknown) as ListArray; + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| id: string, namespace_type: "agnostic" | "single" |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('listArrayOrUndefined', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when undefined', () => { + const payload = undefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an item that is not of type "list" in array', () => { + const payload = ([1] as unknown) as ListArrayOrUndefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "(Array<{| id: string, namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: string, namespace_type: "agnostic" | "single" |}> | undefined)"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts new file mode 100644 index 0000000000000..07be038ff3526 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts @@ -0,0 +1,22 @@ +/* + * 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 { namespaceType } from '../../lists_common_deps'; + +export const list = t.exact( + t.type({ + id: t.string, + namespace_type: namespaceType, + }) +); + +export type List = t.TypeOf; +export const listArray = t.array(list); +export type ListArray = t.TypeOf; +export const listArrayOrUndefined = t.union([listArray, t.undefined]); +export type ListArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts index 9eb55c22756fa..2268e47bd1149 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts @@ -4,187 +4,60 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ListsDefaultArray } from './lists_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('lists_default_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - test('it should validate an array of lists', () => { - const payload = [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ]; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); +import { foldLeftRight, getPaths } from '../../../test_utils'; - test('it should not validate an array of lists that includes a values_operator other than included or excluded', () => { - const payload = [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'exists', - }, - { - field: 'host.hostname', - values_operator: 'jibber jabber', - values_type: 'exists', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); +import { DefaultListArray, DefaultListArrayC } from './lists_default_array'; +import { getListArrayMock } from './lists.mock'; - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "jibber jabber" supplied to "values_operator"', - ]); - expect(message.schema).toEqual({}); - }); - - // TODO - this scenario should never come up, as the values key is forbidden when values_type is "exists" in the incoming schema - need to find a good way to do this in io-ts - test('it will validate an array of lists that includes "values" when "values_type" is "exists"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'exists', - values: [ - { - name: '127.0.0.1', - }, - ], - }, - ]; - const decoded = ListsDefaultArray.decode(payload); +describe('lists_default_array', () => { + test('it should return a default array when null', () => { + const payload = null; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); + expect(message.schema).toEqual([]); }); - // TODO - this scenario should never come up, as the values key is required when values_type is "match" in the incoming schema - need to find a good way to do this in io-ts - test('it will validate an array of lists that does not include "values" when "values_type" is "match"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should return a default array when undefined', () => { + const payload = undefined; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); + expect(message.schema).toEqual([]); }); - // TODO - this scenario should never come up, as the values key is required when values_type is "match_all" in the incoming schema - need to find a good way to do this in io-ts - test('it will validate an array of lists that does not include "values" when "values_type" is "match_all"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match_all', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); - // TODO - this scenario should never come up, as the values key is required when values_type is "list" in the incoming schema - need to find a good way to do this in io-ts - test('it should not validate an array of lists that does not include "values" when "values_type" is "list"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'list', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); - test('it should not validate an array with a number', () => { - const payload = [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - values: [ - { - name: '127.0.0.1', - }, - ], - }, - 5, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should not validate an array of non accepted types', () => { + // Terrible casting for purpose of tests + const payload = ([1] as unknown) as DefaultListArrayC; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "listsWithDefaultArray"', - 'Invalid value "5" supplied to "listsWithDefaultArray"', + 'Invalid value "1" supplied to "DefaultListArray"', ]); expect(message.schema).toEqual({}); }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts index 7fe98cdc300ef..ac5666cad23a7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts @@ -7,28 +7,18 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { - list_and as listAnd, - list_values as listValues, - list_values_operator as listOperator, -} from '../common/schemas'; +import { ListArray, list } from './lists'; -export type List = t.TypeOf; -export type ListValues = t.TypeOf; -export type ListOperator = t.TypeOf; +export type DefaultListArrayC = t.Type; /** - * Types the ListsDefaultArray as: - * - If null or undefined, then a default array will be set for the list + * Types the DefaultListArray as: + * - If null or undefined, then a default array of type list will be set */ -export const ListsDefaultArray = new t.Type( - 'listsWithDefaultArray', - t.array(listAnd).is, - (input, context): Either => - input == null ? t.success([]) : t.array(listAnd).validate(input, context), +export const DefaultListArray: DefaultListArrayC = new t.Type( + 'DefaultListArray', + t.array(list).is, + (input, context): Either => + input == null ? t.success([]) : t.array(list).validate(input, context), t.identity ); - -export type ListsDefaultArrayC = typeof ListsDefaultArray; - -export type ListsDefaultArraySchema = t.TypeOf; From e977f0086b22ec123e7d0b38fd49b2c17e6d22fe Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 22 Jun 2020 22:41:33 -0400 Subject: [PATCH 3/8] updated route unit tests and types to match new list schema --- .../request/add_prepackaged_rules_schema.ts | 27 +-- .../add_prepackged_rules_schema.test.ts | 188 +++++++++++++++++- .../request/create_rules_schema.test.ts | 184 ++++++++++++++++- .../schemas/request/create_rules_schema.ts | 31 +-- .../request/import_rules_schema.test.ts | 187 ++++++++++++++++- .../schemas/request/import_rules_schema.ts | 33 +-- .../request/patch_rules_schema.test.ts | 155 ++++++++++++++- .../schemas/request/patch_rules_schema.ts | 4 +- .../request/update_rules_schema.test.ts | 181 ++++++++++++++++- .../schemas/request/update_rules_schema.ts | 27 +-- .../schemas/response/rules_schema.mocks.ts | 34 +--- .../schemas/response/rules_schema.test.ts | 44 ++++ .../schemas/response/rules_schema.ts | 4 +- .../detection_engine/schemas/types/index.ts | 34 ++++ .../routes/__mocks__/request_responses.ts | 34 +--- .../routes/__mocks__/utils.ts | 34 +--- .../routes/rules/validate.test.ts | 34 +--- .../rules/get_export_all.test.ts | 32 +-- .../rules/get_export_by_object_ids.test.ts | 64 +----- .../scripts/rules/patches/update_list.json | 27 +-- .../rules/queries/lists/query_with_and.json | 35 ---- .../queries/lists/query_with_excluded.json | 23 --- .../queries/lists/query_with_exists.json | 18 -- .../rules/queries/lists/query_with_list.json | 54 ----- .../queries/lists/query_with_list_plugin.json | 24 --- .../rules/queries/lists/query_with_match.json | 23 --- .../queries/lists/query_with_match_all.json | 26 --- .../rules/queries/lists/query_with_or.json | 32 --- .../scripts/rules/updates/update_list.json | 30 +-- .../signals/__mocks__/es_results.ts | 34 +--- .../signals/build_bulk_body.test.ts | 133 +------------ .../signals/build_rule.test.ts | 100 +--------- 32 files changed, 1040 insertions(+), 850 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index 3e7e7e5409c9c..43000f6d36f46 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -40,16 +40,19 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanFalse } from '../types/default_boolean_false'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanFalse, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultListArray, + ListArray, +} from '../types'; /** * Big differences between this schema and the createRulesSchema @@ -96,7 +99,7 @@ export const addPrepackagedRulesSchema = t.intersection([ throttle: DefaultThrottleNull, // defaults to "null" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode }) ), ]); @@ -130,5 +133,5 @@ export type AddPrepackagedRulesSchemaDecoded = Omit< to: To; threat: Threat; throttle: ThrottleOrNull; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts index f946b3ad3b39b..47a98166927b4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -19,6 +19,7 @@ import { getAddPrepackagedRulesSchemaDecodedMock, } from './add_prepackaged_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('add prepackaged rules schema', () => { test('empty objects do not validate', () => { @@ -1379,14 +1380,189 @@ describe('add prepackaged rules schema', () => { }); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, version, and exceptions_list] does validate', () => { + const payload: AddPrepackagedRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + version: 1, + exceptions_list: getListArrayMock(), + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, version, and empty exceptions_list] does validate', () => { + const payload: AddPrepackagedRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + version: 1, + note: '# some markdown', + exceptions_list: [], + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, version, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + version: 1, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, version, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: AddPrepackagedRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + version: 1, + note: '# some markdown', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts index a126b833ba461..1648044f5305a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts @@ -18,6 +18,7 @@ import { getCreateRulesSchemaDecodedMock, } from './create_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('create rules schema', () => { test('empty objects do not validate', () => { @@ -1435,14 +1436,185 @@ describe('create rules schema', () => { ); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { + const payload: CreateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: getListArrayMock(), + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: CreateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: CreateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts index 4e60201b8030e..d623cff8f1fc3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts @@ -41,18 +41,21 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanTrue } from '../types/default_boolean_true'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { DefaultVersionNumber } from '../types/default_version_number'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; -import { DefaultUuid } from '../types/default_uuid'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanTrue, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultVersionNumber, + DefaultListArray, + ListArray, + DefaultUuid, +} from '../types'; export const createRulesSchema = t.intersection([ t.exact( @@ -92,7 +95,7 @@ export const createRulesSchema = t.intersection([ references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version: DefaultVersionNumber, // defaults to 1 if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode }) ), ]); @@ -129,6 +132,6 @@ export type CreateRulesSchemaDecoded = Omit< threat: Threat; throttle: ThrottleOrNull; version: Version; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; rule_id: RuleId; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts index 9fe3e95a20621..12a13ab1a5ed1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -22,6 +22,7 @@ import { getImportRulesSchemaDecodedMock, } from './import_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('import rules schema', () => { test('empty objects do not validate', () => { @@ -1569,14 +1570,188 @@ describe('import rules schema', () => { }); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { + const payload: ImportRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: getListArrayMock(), + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + immutable: false, + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: ImportRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + immutable: false, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: ImportRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + immutable: false, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index a2110263e8e51..7d79861aacf38 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -47,19 +47,22 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanTrue } from '../types/default_boolean_true'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { DefaultVersionNumber } from '../types/default_version_number'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; -import { OnlyFalseAllowed } from '../types/only_false_allowed'; -import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanTrue, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultVersionNumber, + OnlyFalseAllowed, + DefaultStringBooleanFalse, + DefaultListArray, + ListArray, +} from '../types'; /** * Differences from this and the createRulesSchema are @@ -111,7 +114,7 @@ export const importRulesSchema = t.intersection([ references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version: DefaultVersionNumber, // defaults to 1 if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode created_at, // defaults "undefined" if not set during decode updated_at, // defaults "undefined" if not set during decode created_by, // defaults "undefined" if not set during decode @@ -153,7 +156,7 @@ export type ImportRulesSchemaDecoded = Omit< threat: Threat; throttle: ThrottleOrNull; version: Version; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; rule_id: RuleId; immutable: false; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts index 55363ffb18307..81a17df43daf6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts @@ -10,6 +10,7 @@ import { exactCheck } from '../../../exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; +import { getListArrayMock } from '../types/lists.mock'; describe('patch_rules_schema', () => { test('made up values do not validate', () => { @@ -1139,14 +1140,156 @@ describe('patch_rules_schema', () => { expect(message.schema).toEqual({}); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, note, and exceptions_list] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + note: '# some documentation markdown', + exceptions_list: getListArrayMock(), + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + note: '# some documentation markdown', + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + 'Invalid value "[{"id":"uuid_here","namespace_type":"not a namespace type"}]" supplied to "exceptions_list"', + ]); + expect(message.schema).toEqual({}); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 605e0272bbb4c..29d5467071a3d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -37,10 +37,10 @@ import { references, to, language, - listAndOrUndefined, query, id, } from '../common/schemas'; +import { listArrayOrUndefined } from '../types/lists'; /* eslint-enable @typescript-eslint/camelcase */ /** @@ -80,7 +80,7 @@ export const patchRulesSchema = t.exact( references, note, version, - exceptions_list: listAndOrUndefined, + exceptions_list: listArrayOrUndefined, }) ); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts index 1ff38f1351f59..02f8e7bbeb59b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts @@ -18,6 +18,7 @@ import { getUpdateRulesSchemaDecodedMock, } from './update_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('update rules schema', () => { test('empty objects do not validate', () => { @@ -1377,14 +1378,182 @@ describe('update rules schema', () => { }); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { + const payload: UpdateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + exceptions_list: getListArrayMock(), + }; + + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: UpdateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + filters: [], + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: UpdateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + exceptions_list: [], + }; + + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: UpdateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; + + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: UpdateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: UpdateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts index 504233f95986f..73078e617efc6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts @@ -43,16 +43,19 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanTrue } from '../types/default_boolean_true'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanTrue, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultListArray, + ListArray, +} from '../types'; /** * This almost identical to the create_rules_schema except for a few details. @@ -100,7 +103,7 @@ export const updateRulesSchema = t.intersection([ references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version, // defaults to "undefined" if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode }) ), ]); @@ -135,6 +138,6 @@ export type UpdateRulesSchemaDecoded = Omit< to: To; threat: Threat; throttle: ThrottleOrNull; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; rule_id: RuleId; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index ecbf0321cdc67..e63a7ad981e12 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { getListArrayMock } from '../types/lists.mock'; import { RulesSchema } from './rules_schema'; @@ -64,38 +65,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem language: 'kuery', rule_id: 'query-rule-id', interval: '5m', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }); export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index 90aef656db369..f4c1ff84e41b7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -22,6 +22,7 @@ import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; import { getRulesSchemaMock, getRulesMlSchemaMock } from './rules_schema.mocks'; +import { ListArray } from '../types/lists'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; @@ -650,4 +651,47 @@ describe('rules_schema', () => { expect(fields.length).toEqual(2); }); }); + + describe('exceptions_list', () => { + test('it should validate an empty array for "exceptions_list"', () => { + const payload = getRulesSchemaMock(); + payload.exceptions_list = []; + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getRulesSchemaMock(); + expected.exceptions_list = []; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "query" when it has extra data', () => { + const payload: Omit & { + exceptions_list?: string; + } = { ...getRulesSchemaMock(), exceptions_list: 'invalid_data' }; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid_data" supplied to "exceptions_list"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should default to empty array if "exceptions_list" is undefined ', () => { + const payload: Omit & { + exceptions_list?: ListArray; + } = getRulesSchemaMock(); + payload.exceptions_list = undefined; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ ...payload, exceptions_list: [] }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index a7a31ec9e1b59..9803a80f57857 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -56,7 +56,7 @@ import { meta, note, } from '../common/schemas'; -import { ListsDefaultArray } from '../types/lists_default_array'; +import { DefaultListArray } from '../types/lists_default_array'; /** * This is the required fields for the rules schema response. Put all required properties on @@ -87,7 +87,7 @@ export const requiredRulesSchema = t.type({ updated_at, created_by, version, - exceptions_list: ListsDefaultArray, + exceptions_list: DefaultListArray, }); export type RequiredRulesSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts new file mode 100644 index 0000000000000..368dd4922eec4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './default_actions_array'; +export * from './default_boolean_false'; +export * from './default_boolean_true'; +export * from './default_empty_string'; +export * from './default_export_file_name'; +export * from './default_from_string'; +export * from './default_interval_string'; +export * from './default_language_string'; +export * from './default_max_signals_number'; +export * from './default_page'; +export * from './default_per_page'; +export * from './default_string_array'; +export * from './default_string_boolean_false'; +export * from './default_threat_array'; +export * from './default_throttle_null'; +export * from './default_to_string'; +export * from './default_uuid'; +export * from './default_version_number'; +export * from './iso_date_string'; +export * from './lists'; +export * from './lists_default_array'; +export * from './non_empty_string'; +export * from './only_false_allowed'; +export * from './positive_integer'; +export * from './positive_integer_greater_than_zero'; +export * from './references_default_array'; +export * from './risk_score'; +export * from './uuid'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 9928ce4807da9..581946f2300b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -27,6 +27,7 @@ import { RuleNotificationAlertType } from '../../notifications/types'; import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -390,38 +391,7 @@ export const getResult = (): RuleAlertType => ({ references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', version: 1, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptionsList: getListArrayMock(), }, createdAt: new Date('2019-12-13T16:40:33.400Z'), updatedAt: new Date('2019-12-13T16:40:33.400Z'), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 063c9dffd66dd..7b7d3fbdea0bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -8,6 +8,7 @@ import { Readable } from 'stream'; import { HapiReadableStream } from '../../rules/types'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; /** * Given a string, builds a hapi stream as our @@ -76,38 +77,7 @@ export const getOutputRuleAlertForRest = (): Omit< ], }, ], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), filters: [ { query: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 1f5442e23d884..0065696712628 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -14,6 +14,7 @@ import { FindResult } from '../../../../../../alerts/server'; import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; export const ruleOutput: RulesSchema = { actions: [], @@ -68,38 +69,7 @@ export const ruleOutput: RulesSchema = { }, }, ], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], meta: { someMeta: 'someField', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index ee21c33540024..7d4bbfdced432 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -80,36 +80,8 @@ describe('getExportAll', () => { note: '# Investigative notes', version: 1, exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, + { id: 'some_uuid', namespace_type: 'single' }, + { id: 'some_uuid', namespace_type: 'agnostic' }, ], })}\n`, exportDetails: `${JSON.stringify({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index b00b7353a370f..043e563a4c8b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -88,36 +88,8 @@ describe('get_export_by_object_ids', () => { note: '# Investigative notes', version: 1, exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, + { id: 'some_uuid', namespace_type: 'single' }, + { id: 'some_uuid', namespace_type: 'agnostic' }, ], })}\n`, exportDetails: `${JSON.stringify({ @@ -216,36 +188,8 @@ describe('get_export_by_object_ids', () => { note: '# Investigative notes', version: 1, exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, + { id: 'some_uuid', namespace_type: 'single' }, + { id: 'some_uuid', namespace_type: 'agnostic' }, ], }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json index 8d831f3a961d8..6323597fc0946 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json @@ -2,31 +2,8 @@ "rule_id": "query-with-list", "exceptions_list": [ { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123456" - } - ] - } - ] + "id": "some_updated_fake_id", + "namespace_type": "single" } ] } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json deleted file mode 100644 index 1575a712e2cba..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "List - and", - "description": "Query with a list that includes and. This rule should only produce signals when host.name exists and when both event.module is endgame and event.category is anything other than file", - "rule_id": "query-with-list-and", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "endgame" - } - ], - "and": [ - { - "field": "event.category", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "file" - } - ] - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json deleted file mode 100644 index 4e6d9403a276f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "List - excluded", - "description": "Query with a list of values_operator excluded. This rule should only produce signals when host.name exists and event.module is suricata", - "rule_id": "query-with-list-excluded", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "suricata" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json deleted file mode 100644 index 97beace37633f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "List - exists", - "description": "Query with a list that includes exists. This rule should only produce signals when host.name exists and event.action does not exist", - "rule_id": "query-with-list-exists", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.action", - "values_operator": "included", - "values_type": "exists" - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json deleted file mode 100644 index ad0585b5a2ec5..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "Query with a list", - "description": "Query with a list. This rule should only produce signals when either host.name exists and event.module is system and user.name is zeek or gdm OR when host.name exists and event.module is not endgame or zeek or system.", - "rule_id": "query-with-list", - "risk_score": 2, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "system" - } - ], - "and": [ - { - "field": "user.name", - "values_operator": "excluded", - "values_type": "match_all", - "values": [ - { - "name": "zeek" - }, - { - "name": "gdm" - } - ] - } - ] - }, - { - "field": "event.module", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "endgame" - }, - { - "name": "zeek" - }, - { - "name": "system" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json deleted file mode 100644 index fa6fe6ac71117..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "Query with a list", - "description": "Query with a list only generate signals if source.ip is not in list", - "rule_id": "query-with-list", - "risk_score": 2, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "list", - "values": [ - { - "id": "ci-badguys.txt", - "name": "ip" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json deleted file mode 100644 index 6e6880cc28f24..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "List - match", - "description": "Query with a list that includes match. This rule should only produce signals when host.name exists and event.module is not suricata", - "rule_id": "query-with-list-match", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "suricata" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json deleted file mode 100644 index 44cc26ac3315e..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "List - match_all", - "description": "Query with a list that includes match_all. This rule should only produce signals when host.name exists and event.module is not suricata or auditd", - "rule_id": "query-with-list-match-all", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "suricata" - }, - { - "name": "auditd" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json deleted file mode 100644 index 9c4eda559d5bc..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "List - or", - "description": "Query with a list that includes or. This rule should only produce signals when host.name exists and event.module is suricata OR when host.name exists and event.category is file", - "rule_id": "query-with-list-or", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "suricata" - } - ] - }, - { - "field": "event.category", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "file" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json index df22dff5c046e..f7359d586bd86 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json @@ -6,33 +6,5 @@ "severity": "high", "type": "query", "query": "user.name: root or user.name: admin", - "exceptions_list": [ - { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123456" - } - ] - } - ] - } - ] + "exceptions_list": [{ "id": "some_updated_fake_id", "namespace_type": "single" }] } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 01ee41e3b877c..a7dc6036009d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -14,6 +14,7 @@ import { loggingServiceMock } from '../../../../../../../../src/core/server/mock import { RuleTypeParams } from '../../types'; import { IRuleStatusAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; export const sampleRuleAlertParams = ( maxSignals?: number | undefined, @@ -44,38 +45,7 @@ export const sampleRuleAlertParams = ( meta: undefined, threat: undefined, version: 1, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptionsList: getListArrayMock(), }); export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 80c2441193a0c..ad43932818836 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -12,6 +12,7 @@ import { } from './__mocks__/es_results'; import { buildBulkBody } from './build_bulk_body'; import { SignalHit } from './types'; +import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; describe('buildBulkBody', () => { beforeEach(() => { @@ -91,38 +92,7 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; @@ -218,38 +188,7 @@ describe('buildBulkBody', () => { updated_at: fakeSignalSourceHit.signal.rule?.updated_at, throttle: 'no_actions', threat: [], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; @@ -343,38 +282,7 @@ describe('buildBulkBody', () => { created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; @@ -461,38 +369,7 @@ describe('buildBulkBody', () => { updated_at: fakeSignalSourceHit.signal.rule?.updated_at, created_at: fakeSignalSourceHit.signal.rule?.created_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index eb87976a6fbab..9aef5a370b86a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -7,6 +7,7 @@ import { buildRule } from './build_rule'; import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; describe('buildRule', () => { beforeEach(() => { @@ -80,38 +81,7 @@ describe('buildRule', () => { query: 'host.name: Braden', }, ], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), version: 1, }; expect(rule).toEqual(expected); @@ -164,38 +134,7 @@ describe('buildRule', () => { updated_at: rule.updated_at, created_at: rule.created_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }; expect(rule).toEqual(expected); }); @@ -247,38 +186,7 @@ describe('buildRule', () => { updated_at: rule.updated_at, created_at: rule.created_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }; expect(rule).toEqual(expected); }); From 55e29c99261dc66f35f32937e46c1b74e10345a5 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 23 Jun 2020 09:49:18 -0400 Subject: [PATCH 4/8] updated existing DE exceptions code so it should now work as is with updated schema --- .../detection_engine/lists_common_deps.ts | 2 +- .../signals/build_exceptions_query.test.ts | 1199 +++++++---------- .../signals/build_exceptions_query.ts | 159 +-- .../signals/filter_events_with_list.test.ts | 169 +-- .../signals/filter_events_with_list.ts | 120 +- .../signals/get_filter.test.ts | 117 +- .../detection_engine/signals/get_filter.ts | 6 +- .../signals/search_after_bulk_create.test.ts | 132 +- .../signals/signal_rule_alert_type.test.ts | 14 +- .../signals/signal_rule_alert_type.ts | 44 +- .../lib/detection_engine/signals/utils.ts | 118 +- 11 files changed, 993 insertions(+), 1087 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts index efe1166ec1566..9a4a7b5617b4c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EntryList, namespaceType } from '../../../lists/common/schemas'; +export { EntryList, EntriesArray, namespaceType } from '../../../lists/common/schemas'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts index 07adfde71c1a9..ce7cc50e81d67 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts @@ -6,16 +6,24 @@ import { buildQueryExceptions, - buildExceptions, + buildExceptionItemEntries, operatorBuilder, buildExists, buildMatch, - buildMatchAll, + buildMatchAny, evaluateValues, formatQuery, getLanguageBooleanOperator, + buildNested, } from './build_exceptions_query'; -import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; +import { + EntriesArray, + EntryExists, + EntryMatch, + EntryMatchAny, + EntryNested, +} from '../../../../../lists/common/schemas'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; describe('build_exceptions_query', () => { describe('getLanguageBooleanOperator', () => { @@ -34,30 +42,30 @@ describe('build_exceptions_query', () => { describe('operatorBuilder', () => { describe('kuery', () => { - test('it returns "not " when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'kuery' }); + test('it returns "not " when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'kuery' }); - expect(operator).toEqual(' and '); + expect(operator).toEqual('not '); }); - test('it returns empty string when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'kuery' }); + test('it returns empty string when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'kuery' }); - expect(operator).toEqual(' and not '); + expect(operator).toEqual(''); }); }); describe('lucene', () => { - test('it returns "NOT " when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'lucene' }); + test('it returns "NOT " when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'lucene' }); - expect(operator).toEqual(' AND '); + expect(operator).toEqual('NOT '); }); - test('it returns empty string when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'lucene' }); + test('it returns empty string when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'lucene' }); - expect(operator).toEqual(' AND NOT '); + expect(operator).toEqual(''); }); }); }); @@ -65,161 +73,117 @@ describe('build_exceptions_query', () => { describe('buildExists', () => { describe('kuery', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'kuery' }); + const query = buildExists({ + item: { type: 'exists', operator: 'excluded', field: 'host.name' }, + language: 'kuery', + }); - expect(query).toEqual(' and host.name:*'); + expect(query).toEqual('host.name:*'); }); test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ operator: 'included', field: 'host.name', language: 'kuery' }); + const query = buildExists({ + item: { type: 'exists', operator: 'included', field: 'host.name' }, + language: 'kuery', + }); - expect(query).toEqual(' and not host.name:*'); + expect(query).toEqual('not host.name:*'); }); }); describe('lucene', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'lucene' }); + const query = buildExists({ + item: { type: 'exists', operator: 'excluded', field: 'host.name' }, + language: 'lucene', + }); - expect(query).toEqual(' AND _exists_host.name'); + expect(query).toEqual('_exists_host.name'); }); test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ operator: 'included', field: 'host.name', language: 'lucene' }); + const query = buildExists({ + item: { type: 'exists', operator: 'included', field: 'host.name' }, + language: 'lucene', + }); - expect(query).toEqual(' AND NOT _exists_host.name'); + expect(query).toEqual('NOT _exists_host.name'); }); }); }); describe('buildMatch', () => { describe('kuery', () => { - test('it returns empty string if no items in "values"', () => { - const query = buildMatch({ - operator: 'included', - field: 'host.name', - values: [], - language: 'kuery', - }); - - expect(query).toEqual(''); - }); - test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, + item: { + type: 'match', + operator: 'included', + field: 'host.name', + value: 'suricata', + }, language: 'kuery', }); - expect(query).toEqual(' and not host.name:suricata'); + expect(query).toEqual('not host.name:suricata'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'excluded', - field: 'host.name', - values, - language: 'kuery', - }); - - expect(query).toEqual(' and host.name:suricata'); - }); - - // TODO: need to clean up types and maybe restrict values to one if type is 'match' - test('it returns formatted string when "values" includes more than one item', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + item: { + type: 'match', + operator: 'excluded', + field: 'host.name', + value: 'suricata', }, - ]; - const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, language: 'kuery', }); - expect(query).toEqual(' and not host.name:suricata'); + expect(query).toEqual('host.name:suricata'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, + item: { + type: 'match', + operator: 'included', + field: 'host.name', + value: 'suricata', + }, language: 'lucene', }); - expect(query).toEqual(' AND NOT host.name:suricata'); + expect(query).toEqual('NOT host.name:suricata'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'excluded', - field: 'host.name', - values, - language: 'lucene', - }); - - expect(query).toEqual(' AND host.name:suricata'); - }); - - // TODO: need to clean up types and maybe restrict values to one if type is 'match' - test('it returns formatted string when "values" includes more than one item', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + item: { + type: 'match', + operator: 'excluded', + field: 'host.name', + value: 'suricata', }, - ]; - const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, language: 'lucene', }); - expect(query).toEqual(' AND NOT host.name:suricata'); + expect(query).toEqual('host.name:suricata'); }); }); }); - describe('buildMatchAll', () => { + describe('buildMatchAny', () => { describe('kuery', () => { test('it returns empty string if given an empty array for "values"', () => { - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values: [], + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: [], + type: 'match_any', + }, language: 'kuery', }); @@ -227,113 +191,180 @@ describe('build_exceptions_query', () => { }); test('it returns formatted string when "values" includes only one item', () => { - const values = [ - { - name: 'suricata', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'kuery', }); - expect(exceptionSegment).toEqual(' and not host.name:suricata'); + expect(exceptionSegment).toEqual('not host.name:(suricata)'); }); test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'kuery', }); - expect(exceptionSegment).toEqual(' and not host.name:(suricata or auditd)'); + expect(exceptionSegment).toEqual('not host.name:(suricata or auditd)'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'excluded', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'excluded', - field: 'host.name', - values, language: 'kuery', }); - expect(exceptionSegment).toEqual(' and host.name:(suricata or auditd)'); + expect(exceptionSegment).toEqual('host.name:(suricata or auditd)'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'lucene', }); - expect(exceptionSegment).toEqual(' AND NOT host.name:(suricata OR auditd)'); + expect(exceptionSegment).toEqual('NOT host.name:(suricata OR auditd)'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'excluded', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'excluded', - field: 'host.name', - values, language: 'lucene', }); - expect(exceptionSegment).toEqual(' AND host.name:(suricata OR auditd)'); + expect(exceptionSegment).toEqual('host.name:(suricata OR auditd)'); }); test('it returns formatted string when "values" includes only one item', () => { - const values = [ - { - name: 'suricata', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'lucene', }); - expect(exceptionSegment).toEqual(' AND NOT host.name:suricata'); + expect(exceptionSegment).toEqual('NOT host.name:(suricata)'); + }); + }); + }); + + describe('buildNested', () => { + describe('kuery', () => { + test('it returns formatted query when one item in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + ], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ nestedField:value-3 }'); + }); + + test('it returns formatted query when multiple items in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + { + field: 'nestedFieldB', + operator: 'excluded', + type: 'match', + value: 'value-4', + }, + ], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ nestedField:value-3 and nestedFieldB:value-4 }'); + }); + }); + + // TODO: Does lucene support nested query syntax? + describe.skip('lucene', () => { + test('it returns formatted query when one item in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + ], + }; + const result = buildNested({ item, language: 'lucene' }); + + expect(result).toEqual('parent:{ nestedField:value-3 }'); + }); + + test('it returns formatted query when multiple items in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + { + field: 'nestedFieldB', + operator: 'excluded', + type: 'match', + value: 'value-4', + }, + ], + }; + const result = buildNested({ item, language: 'lucene' }); + + expect(result).toEqual('parent:{ nestedField:value-3 AND nestedFieldB:value-4 }'); }); }); }); @@ -341,110 +372,96 @@ describe('build_exceptions_query', () => { describe('evaluateValues', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { - const list: List = { - values_operator: 'included', - values_type: 'exists', + const list: EntryExists = { + operator: 'included', + type: 'exists', field: 'host.name', }; const result = evaluateValues({ - list, + item: list, language: 'kuery', }); - expect(result).toEqual(' and not host.name:*'); + expect(result).toEqual('not host.name:*'); }); test('it returns formatted string when "type" is "match"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match', + const list: EntryMatch = { + operator: 'included', + type: 'match', field: 'host.name', - values: [{ name: 'suricata' }], + value: 'suricata', }; const result = evaluateValues({ - list, + item: list, language: 'kuery', }); - expect(result).toEqual(' and not host.name:suricata'); + expect(result).toEqual('not host.name:suricata'); }); - test('it returns formatted string when "type" is "match_all"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match_all', + test('it returns formatted string when "type" is "match_any"', () => { + const list: EntryMatchAny = { + operator: 'included', + type: 'match_any', field: 'host.name', - values: [ - { - name: 'suricata', - }, - { - name: 'auditd', - }, - ], + value: ['suricata', 'auditd'], }; const result = evaluateValues({ - list, + item: list, language: 'kuery', }); - expect(result).toEqual(' and not host.name:(suricata or auditd)'); + expect(result).toEqual('not host.name:(suricata or auditd)'); }); }); describe('lucene', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { - const list: List = { - values_operator: 'included', - values_type: 'exists', + const list: EntryExists = { + operator: 'included', + type: 'exists', field: 'host.name', }; const result = evaluateValues({ - list, + item: list, language: 'lucene', }); - expect(result).toEqual(' AND NOT _exists_host.name'); + expect(result).toEqual('NOT _exists_host.name'); }); test('it returns formatted string when "type" is "match"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match', + const list: EntryMatch = { + operator: 'included', + type: 'match', field: 'host.name', - values: [{ name: 'suricata' }], + value: 'suricata', }; const result = evaluateValues({ - list, + item: list, language: 'lucene', }); - expect(result).toEqual(' AND NOT host.name:suricata'); + expect(result).toEqual('NOT host.name:suricata'); }); - test('it returns formatted string when "type" is "match_all"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match_all', + test('it returns formatted string when "type" is "match_any"', () => { + const list: EntryMatchAny = { + operator: 'included', + type: 'match_any', field: 'host.name', - values: [ - { - name: 'suricata', - }, - { - name: 'auditd', - }, - ], + value: ['suricata', 'auditd'], }; const result = evaluateValues({ - list, + item: list, language: 'lucene', }); - expect(result).toEqual(' AND NOT host.name:(suricata OR auditd)'); + expect(result).toEqual('NOT host.name:(suricata OR auditd)'); }); }); }); @@ -459,7 +476,7 @@ describe('build_exceptions_query', () => { test('it returns expected query string when single exception in array', () => { const formattedQuery = formatQuery({ - exceptions: [' and b:(value-1 or value-2) and not c:*'], + exceptions: ['b:(value-1 or value-2) and not c:*'], query: 'a:*', language: 'kuery', }); @@ -469,7 +486,7 @@ describe('build_exceptions_query', () => { test('it returns expected query string when multiple exceptions in array', () => { const formattedQuery = formatQuery({ - exceptions: [' and b:(value-1 or value-2) and not c:*', ' and not d:*'], + exceptions: ['b:(value-1 or value-2) and not c:*', 'not d:*'], query: 'a:*', language: 'kuery', }); @@ -480,149 +497,70 @@ describe('build_exceptions_query', () => { }); }); - describe('buildExceptions', () => { - test('it returns empty array if empty lists array passed in', () => { - const query = buildExceptions({ - query: 'a:*', + describe('buildExceptionItemEntries', () => { + test('it returns empty string if empty lists array passed in', () => { + const query = buildExceptionItemEntries({ language: 'kuery', lists: [], }); - expect(query).toEqual([]); + expect(query).toEqual(''); }); test('it returns expected query when more than one item in list', () => { // Equal to query && !(b && !c) -> (query AND NOT b) OR (query AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const payload: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], }, { field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], - }, - ]; - const query = buildExceptions({ - query: 'a:*', - language: 'kuery', - lists, - }); - const expectedQuery = [' and not b:(value-1 or value-2)', ' and c:value-3']; - - expect(query).toEqual(expectedQuery); - }); - - test('it returns expected query when list item includes nested "and" value', () => { - // Equal to query && !(b || !c) -> (query AND NOT b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ - { - field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [ - { - field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], - }, - ], + operator: 'excluded', + type: 'match', + value: 'value-3', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', - lists, + lists: payload, }); - const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3']; + const expectedQuery = 'not b:(value-1 or value-2) and c:value-3'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list item includes nested "and" value of empty array', () => { + test('it returns expected query when list item includes nested value', () => { // Equal to query && !(b || !c) -> (query AND NOT b AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [], + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], }, - ]; - const query = buildExceptions({ - query: 'a:*', - language: 'kuery', - lists, - }); - const expectedQuery = [' and not b:(value-1 or value-2)']; - - expect(query).toEqual(expectedQuery); - }); - - test('it returns expected query when list item includes nested "and" value of null', () => { - // Equal to query && !(b || !c) -> (query AND NOT b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ { - field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ + field: 'parent', + type: 'nested', + entries: [ { - name: 'value-1', - }, - { - name: 'value-2', + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', }, ], - and: undefined, }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:(value-1 or value-2)']; + const expectedQuery = 'not b:(value-1 or value-2) and parent:{ nestedField:value-3 }'; expect(query).toEqual(expectedQuery); }); @@ -630,130 +568,112 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items and nested "and" values', () => { // Equal to query && !((b || !c) && d) -> (query AND NOT b AND c) OR (query AND NOT d) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { - field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', }, ], }, { field: 'd', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3', ' and not d:*']; - + const expectedQuery = + 'not b:(value-1 or value-2) and parent:{ nestedField:value-3 } and not d:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when language is "lucene"', () => { // Equal to query && !((b || !c) && !d) -> (query AND NOT b AND c) OR (query AND d) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { - field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', }, ], }, { field: 'e', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'lucene', lists, }); - const expectedQuery = [' AND NOT b:(value-1 OR value-2) AND c:value-3', ' AND _exists_e']; - + const expectedQuery = + 'NOT b:(value-1 OR value-2) AND parent:{ nestedField:value-3 } AND _exists_e'; expect(query).toEqual(expectedQuery); }); describe('exists', () => { - test('it returns expected query when list includes single list item with values_operator of "included"', () => { + test('it returns expected query when list includes single list item with operator of "included"', () => { // Equal to query && !(b) -> (query AND NOT b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:*']; + const expectedQuery = 'not b:*'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + test('it returns expected query when list includes single list item with operator of "excluded"', () => { // Equal to query && !(!b) -> (query AND b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:*']; + const expectedQuery = 'b:*'; expect(query).toEqual(expectedQuery); }); @@ -761,26 +681,30 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { // Equal to query && !(!b || !c) -> (query AND b AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'exists', - and: [ + operator: 'excluded', + type: 'exists', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'match', + value: 'value-1', }, ], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:* and c:*']; + const expectedQuery = 'b:* and parent:{ c:value-1 }'; expect(query).toEqual(expectedQuery); }); @@ -788,88 +712,83 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'exists', - and: [ + operator: 'included', + type: 'exists', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'match', + value: 'value-1', }, { field: 'd', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'match', + value: 'value-2', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:* and c:* and not d:*', ' and not e:*']; + const expectedQuery = 'not b:* and parent:{ c:value-1 and d:value-2 } and not e:*'; expect(query).toEqual(expectedQuery); }); }); describe('match', () => { - test('it returns expected query when list includes single list item with values_operator of "included"', () => { + test('it returns expected query when list includes single list item with operator of "included"', () => { // Equal to query && !(b) -> (query AND NOT b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], + operator: 'included', + type: 'match', + value: 'value', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:value']; + const expectedQuery = 'not b:value'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + test('it returns expected query when list includes single list item with operator of "excluded"', () => { // Equal to query && !(!b) -> (query AND b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], + operator: 'excluded', + type: 'match', + value: 'value', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:value']; + const expectedQuery = 'b:value'; expect(query).toEqual(expectedQuery); }); @@ -877,36 +796,31 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { // Equal to query && !(!b || !c) -> (query AND b AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], - and: [ + operator: 'excluded', + type: 'match', + value: 'value', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, ], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:value and c:valueC']; + const expectedQuery = 'b:value and parent:{ c:valueC }'; expect(query).toEqual(expectedQuery); }); @@ -914,160 +828,117 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], - and: [ + operator: 'included', + type: 'match', + value: 'value', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, { field: 'd', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'included', + type: 'match', + value: 'valueC', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [ - ' and not b:value and c:valueC and not d:valueC', - ' and not e:valueC', - ]; + const expectedQuery = 'not b:value and parent:{ c:valueC and d:valueC } and not e:valueC'; expect(query).toEqual(expectedQuery); }); }); - describe('match_all', () => { - test('it returns expected query when list includes single list item with values_operator of "included"', () => { + describe('match_any', () => { + test('it returns expected query when list includes single list item with operator of "included"', () => { // Equal to query && !(b) -> (query AND NOT b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:(value or value-1)']; + const expectedQuery = 'not b:(value or value-1)'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + test('it returns expected query when list includes single list item with operator of "excluded"', () => { // Equal to query && !(!b) -> (query AND b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], + operator: 'excluded', + type: 'match_any', + value: ['value', 'value-1'], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:(value or value-1)']; + const expectedQuery = 'b:(value or value-1)'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes list item with "and" values', () => { + test('it returns expected query when list includes list item with nested values', () => { // Equal to query && !(!b || c) -> (query AND b AND NOT c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ + operator: 'excluded', + type: 'match_any', + value: ['value', 'value-1'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, ], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:(value or value-1) and not c:(valueC or value-2)']; + const expectedQuery = 'b:(value or value-1) and parent:{ c:valueC }'; expect(query).toEqual(expectedQuery); }); @@ -1075,71 +946,25 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ - { - field: 'c', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], - }, - { - field: 'd', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueD', - }, - { - name: 'value-3', - }, - ], - }, - ], + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], }, { field: 'e', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueE', - }, - { - name: 'value-4', - }, - ], + operator: 'included', + type: 'match_any', + value: ['valueE', 'value-4'], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [ - ' and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)', - ' and not e:(valueE or value-4)', - ]; + const expectedQuery = 'not b:(value or value-1) and not e:(valueE or value-4)'; expect(query).toEqual(expectedQuery); }); @@ -1157,65 +982,47 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "kuery"', () => { // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const payload = getExceptionListItemSchemaMock(); + const payload2 = getExceptionListItemSchemaMock(); + payload2.entries = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, { field: 'd', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueD', - }, - { - name: 'value-3', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueD', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueE', - }, - { - name: 'value-4', - }, - ], + operator: 'included', + type: 'match_any', + value: ['valueE', 'value-4'], }, ]; - const query = buildQueryExceptions({ query: 'a:*', language: 'kuery', lists }); + const query = buildQueryExceptions({ + query: 'a:*', + language: 'kuery', + lists: [payload, payload2], + }); const expectedQuery = - '(a:* and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)) or (a:* and not e:(valueE or value-4))'; + '(a:* and some.parentField:{ nested.field:some value } and not some.not.nested.field:some value) or (a:* and not b:(value or value-1) and parent:{ c:valueC and d:valueD } and not e:(valueE or value-4))'; expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); }); @@ -1223,65 +1030,47 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "lucene"', () => { // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const payload = getExceptionListItemSchemaMock(); + const payload2 = getExceptionListItemSchemaMock(); + payload2.entries = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, { field: 'd', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueD', - }, - { - name: 'value-3', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueD', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueE', - }, - { - name: 'value-4', - }, - ], + operator: 'included', + type: 'match_any', + value: ['valueE', 'value-4'], }, ]; - const query = buildQueryExceptions({ query: 'a:*', language: 'lucene', lists }); + const query = buildQueryExceptions({ + query: 'a:*', + language: 'lucene', + lists: [payload, payload2], + }); const expectedQuery = - '(a:* AND NOT b:(value OR value-1) AND c:(valueC OR value-2) AND NOT d:(valueD OR value-3)) OR (a:* AND NOT e:(valueE OR value-4))'; + '(a:* AND some.parentField:{ nested.field:some value } AND NOT some.not.nested.field:some value) OR (a:* AND NOT b:(value OR value-1) AND parent:{ c:valueC AND d:valueD } AND NOT e:(valueE OR value-4))'; expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts index 233b20792299b..03bf463ed0d07 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -3,17 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - ListAndOrUndefined, - Language, - Query, -} from '../../../../common/detection_engine/schemas/common/schemas'; -import { - ListOperator, - ListValues, - List, -} from '../../../../common/detection_engine/schemas/types/lists_default_array'; +import { Language, Query } from '../../../../common/detection_engine/schemas/common/schemas'; import { Query as DataQuery } from '../../../../../../../src/plugins/data/server'; +import { + Entry, + ExceptionListItemSchema, + EntryMatch, + EntryMatchAny, + EntryNested, + EntryExists, + Operator, + entriesMatchAny, + entriesExists, + entriesMatch, + entriesNested, + EntriesArray, +} from '../../../../../lists/common/schemas'; type Operators = 'and' | 'or' | 'not'; type LuceneOperators = 'AND' | 'OR' | 'NOT'; @@ -41,37 +46,32 @@ export const operatorBuilder = ({ operator, language, }: { - operator: ListOperator; + operator: Operator; language: Language; }): string => { - const and = getLanguageBooleanOperator({ - language, - value: 'and', - }); - const or = getLanguageBooleanOperator({ + const not = getLanguageBooleanOperator({ language, value: 'not', }); switch (operator) { - case 'excluded': - return ` ${and} `; + // case 'excluded': + // return ` ${and} `; case 'included': - return ` ${and} ${or} `; + return `${not} `; default: return ''; } }; export const buildExists = ({ - operator, - field, + item, language, }: { - operator: ListOperator; - field: string; + item: EntryExists; language: Language; }): string => { + const { operator, field } = item; const exceptionOperator = operatorBuilder({ operator, language }); switch (language) { @@ -85,64 +85,70 @@ export const buildExists = ({ }; export const buildMatch = ({ - operator, - field, - values, + item, language, }: { - operator: ListOperator; - field: string; - values: ListValues[]; + item: EntryMatch; language: Language; }): string => { - if (values.length > 0) { - const exceptionOperator = operatorBuilder({ operator, language }); - const [exception] = values; + const { value, operator, field } = item; + const exceptionOperator = operatorBuilder({ operator, language }); - return `${exceptionOperator}${field}:${exception.name}`; - } else { - return ''; - } + return `${exceptionOperator}${field}:${value}`; }; -export const buildMatchAll = ({ - operator, - field, - values, +export const buildMatchAny = ({ + item, language, }: { - operator: ListOperator; - field: string; - values: ListValues[]; + item: EntryMatchAny; language: Language; }): string => { - switch (values.length) { + const { value, operator, field } = item; + + switch (value.length) { case 0: return ''; - case 1: - return buildMatch({ operator, field, values, language }); default: const or = getLanguageBooleanOperator({ language, value: 'or' }); const exceptionOperator = operatorBuilder({ operator, language }); - const matchAllValues = values.map((value) => { - return value.name; - }); + const matchAnyValues = value.map((v) => v); - return `${exceptionOperator}${field}:(${matchAllValues.join(` ${or} `)})`; + return `${exceptionOperator}${field}:(${matchAnyValues.join(` ${or} `)})`; } }; -export const evaluateValues = ({ list, language }: { list: List; language: Language }): string => { - const { values_operator: operator, values_type: type, field, values } = list; - switch (type) { - case 'exists': - return buildExists({ operator, field, language }); - case 'match': - return buildMatch({ operator, field, values: values ?? [], language }); - case 'match_all': - return buildMatchAll({ operator, field, values: values ?? [], language }); - default: - return ''; +export const buildNested = ({ + item, + language, +}: { + item: EntryNested; + language: Language; +}): string => { + const { field, entries } = item; + const and = getLanguageBooleanOperator({ language, value: 'and' }); + const values = entries.map((entry) => `${entry.field}:${entry.value}`); + + return `${field}:{ ${values.join(` ${and} `)} }`; +}; + +export const evaluateValues = ({ + item, + language, +}: { + item: Entry | EntryNested; + language: Language; +}): string => { + if (entriesExists.is(item)) { + return buildExists({ item, language }); + } else if (entriesMatch.is(item)) { + return buildMatch({ item, language }); + } else if (entriesMatchAny.is(item)) { + return buildMatchAny({ item, language }); + } else if (entriesNested.is(item)) { + return buildNested({ item, language }); + } else { + return ''; } }; @@ -157,8 +163,9 @@ export const formatQuery = ({ }): string => { if (exceptions.length > 0) { const or = getLanguageBooleanOperator({ language, value: 'or' }); + const and = getLanguageBooleanOperator({ language, value: 'and' }); const formattedExceptions = exceptions.map((exception) => { - return `(${query}${exception})`; + return `(${query} ${and} ${exception})`; }); return formattedExceptions.join(` ${or} `); @@ -167,23 +174,20 @@ export const formatQuery = ({ } }; -export const buildExceptions = ({ - query, +export const buildExceptionItemEntries = ({ lists, language, }: { - query: string; - lists: List[]; + lists: EntriesArray; language: Language; -}): string[] => { - return lists.reduce((accum, listItem) => { - const { and, ...exceptionDetails } = { ...listItem }; - const andExceptionsSegments = and ? buildExceptions({ query, lists: and, language }) : []; - const exceptionSegment = evaluateValues({ list: exceptionDetails, language }); - const exception = [...exceptionSegment, ...andExceptionsSegments]; - - return [...accum, exception.join('')]; +}): string => { + const and = getLanguageBooleanOperator({ language, value: 'and' }); + const exceptionItem = lists.reduce((accum, listItem) => { + const exceptionSegment = evaluateValues({ item: listItem, language }); + return [...accum, exceptionSegment]; }, []); + + return exceptionItem.join(` ${and} `); }; export const buildQueryExceptions = ({ @@ -193,12 +197,13 @@ export const buildQueryExceptions = ({ }: { query: Query; language: Language; - lists: ListAndOrUndefined; + lists: ExceptionListItemSchema[] | undefined; }): DataQuery[] => { if (lists && lists !== null) { - const exceptions = buildExceptions({ lists, language, query }); + const exceptions = lists.map((exceptionItem) => + buildExceptionItemEntries({ lists: exceptionItem.entries, language }) + ); const formattedQuery = formatQuery({ exceptions, language, query }); - return [ { query: formattedQuery, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 4e9eb8587484f..bb56926390af9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -8,6 +8,7 @@ import uuid from 'uuid'; import { filterEventsAgainstList } from './filter_events_with_list'; import { mockLogger, repeatedSearchResultsWithSortId } from './__mocks__/es_results'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; import { listMock } from '../../../../../lists/server/mocks'; @@ -36,92 +37,42 @@ describe('filterEventsAgainstList', () => { expect(res.hits.hits.length).toEqual(4); }); - it('should throw an error if malformed exception list present', async () => { - let message = ''; - try { - await filterEventsAgainstList({ - logger: mockLogger, - listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: undefined, + describe('operator_type is included', () => { + it('should respond with same list if no items match value list', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', }, - ], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', - '7.7.7.7', - ]), - }); - } catch (exc) { - message = exc.message; - } - expect(message).toEqual( - 'Failed to query lists index. Reason: Malformed exception list provided' - ); - }); + }, + ]; - it('should throw an error if unsupported exception type', async () => { - let message = ''; - try { - await filterEventsAgainstList({ - logger: mockLogger, - listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'unsupportedListPluginType', - }, - ], - }, - ], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', - '7.7.7.7', - ]), - }); - } catch (exc) { - message = exc.message; - } - expect(message).toEqual( - 'Failed to query lists index. Reason: Unsupported list type used, please use one of ip,keyword' - ); - }); - - describe('operator_type is includes', () => { - it('should respond with same list if no items match value list', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), }); expect(res.hits.hits.length).toEqual(4); }); it('should respond with less items in the list if some values match', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; listClient.getListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ @@ -133,19 +84,7 @@ describe('filterEventsAgainstList', () => { const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', @@ -162,27 +101,39 @@ describe('filterEventsAgainstList', () => { }); describe('operator type is excluded', () => { it('should respond with empty list if no items match value list', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), }); expect(res.hits.hits.length).toEqual(0); }); it('should respond with less items in the list if some values match', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; listClient.getListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ @@ -194,19 +145,7 @@ describe('filterEventsAgainstList', () => { const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index 48b120d1b5806..f1f5f4bf3c75e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -6,15 +6,13 @@ import { get } from 'lodash/fp'; import { Logger } from 'src/core/server'; -import { ListAndOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; -import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; -import { type } from '../../../../../lists/common/schemas/common'; import { ListClient } from '../../../../../lists/server'; import { SignalSearchResponse, SearchTypes } from './types'; +import { entriesList, ExceptionListItemSchema } from '../../../../../lists/common/schemas'; interface FilterEventsAgainstList { listClient: ListClient; - exceptionsList: ListAndOrUndefined; + exceptionsList: ExceptionListItemSchema[]; logger: Logger; eventSearchResult: SignalSearchResponse; } @@ -34,63 +32,69 @@ export const filterEventsAgainstList = async ({ const isStringableType = (val: SearchTypes) => ['string', 'number', 'boolean'].includes(typeof val); // grab the signals with values found in the given exception lists. - const filteredHitsPromises = exceptionsList - .filter((exceptionItem: List) => exceptionItem.values_type === 'list') - .map(async (exceptionItem: List) => { - if (exceptionItem.values == null || exceptionItem.values.length === 0) { - throw new Error('Malformed exception list provided'); - } - if (!type.is(exceptionItem.values[0].name)) { - throw new Error( - `Unsupported list type used, please use one of ${Object.keys(type.keys).join()}` - ); - } - if (!exceptionItem.values[0].id) { - throw new Error(`Missing list id for exception on field ${exceptionItem.field}`); - } - // acquire the list values we are checking for. - const valuesOfGivenType = eventSearchResult.hits.hits.reduce((acc, searchResultItem) => { - const valueField = get(exceptionItem.field, searchResultItem._source); - if (valueField != null && isStringableType(valueField)) { - acc.add(valueField.toString()); - } - return acc; - }, new Set()); + const filteredHitsPromises = exceptionsList.map( + async (exceptionItem: ExceptionListItemSchema) => { + const { entries } = exceptionItem; - // matched will contain any list items that matched with the - // values passed in from the Set. - const matchedListItems = await listClient.getListItemByValues({ - listId: exceptionItem.values[0].id, - type: exceptionItem.values[0].name, - value: [...valuesOfGivenType], - }); + const filteredHitsEntries = entries + .filter((t) => entriesList.is(t)) + .map(async (entry) => { + // TODO: If this check is removed, then typescript doesn't + // recognize "entry" as EntryList despite .filter() + if (!entriesList.is(entry)) { + throw new Error('Malformed exception list provided'); + } - // create a set of list values that were a hit - easier to work with - const matchedListItemsSet = new Set( - matchedListItems.map((item) => item.value) - ); + // acquire the list values we are checking for. + const valuesOfGivenType = eventSearchResult.hits.hits.reduce( + (acc, searchResultItem) => { + const valueField = get(entry.field, searchResultItem._source); + if (valueField != null && isStringableType(valueField)) { + acc.add(valueField.toString()); + } + return acc; + }, + new Set() + ); - // do a single search after with these values. - // painless script to do nested query in elasticsearch - // filter out the search results that match with the values found in the list. - const operator = exceptionItem.values_operator; - const filteredEvents = eventSearchResult.hits.hits.filter((item) => { - const eventItem = get(exceptionItem.field, item._source); - if (operator === 'included') { - if (eventItem != null) { - return !matchedListItemsSet.has(eventItem); - } - } else if (operator === 'excluded') { - if (eventItem != null) { - return matchedListItemsSet.has(eventItem); - } - } - return false; - }); - const diff = eventSearchResult.hits.hits.length - filteredEvents.length; - logger.debug(`Lists filtered out ${diff} events`); - return filteredEvents; - }); + // matched will contain any list items that matched with the + // values passed in from the Set. + const matchedListItems = await listClient.getListItemByValues({ + listId: entry.list.id, + type: entry.list.type, + value: [...valuesOfGivenType], + }); + + // create a set of list values that were a hit - easier to work with + const matchedListItemsSet = new Set( + matchedListItems.map((item) => item.value) + ); + + // do a single search after with these values. + // painless script to do nested query in elasticsearch + // filter out the search results that match with the values found in the list. + const operator = entry.operator; + const filteredEvents = eventSearchResult.hits.hits.filter((item) => { + const eventItem = get(entry.field, item._source); + if (operator === 'included') { + if (eventItem != null) { + return !matchedListItemsSet.has(eventItem); + } + } else if (operator === 'excluded') { + if (eventItem != null) { + return matchedListItemsSet.has(eventItem); + } + } + return false; + }); + const diff = eventSearchResult.hits.hits.length - filteredEvents.length; + logger.debug(`Lists filtered out ${diff} events`); + return filteredEvents; + }); + + return (await Promise.all(filteredHitsEntries)).flat(); + } + ); const filteredHits = await Promise.all(filteredHitsPromises); const toReturn: SignalSearchResponse = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts index 61cd9cfedd94f..9b3a446bc666d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts @@ -7,6 +7,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { PartialFilter } from '../types'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; describe('get_filter', () => { let servicesMock: AlertServicesMock; @@ -381,18 +382,7 @@ describe('get_filter', () => { 'kuery', [], ['auditbeat-*'], - [ - { - field: 'event.module', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'suricata', - }, - ], - }, - ] + [getExceptionListItemSchemaMock()] ); expect(esQuery).toEqual({ bool: { @@ -414,11 +404,39 @@ describe('get_filter', () => { }, { bool: { - minimum_should_match: 1, - should: [ + filter: [ { - match: { - 'event.module': 'suricata', + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.parentField.nested.field': 'some value', + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.not.nested.field': 'some value', + }, + }, + ], + }, + }, }, }, ], @@ -450,7 +468,7 @@ describe('get_filter', () => { }); test('it should work when lists has value undefined', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], undefined); + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); expect(esQuery).toEqual({ bool: { filter: [ @@ -529,7 +547,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }); expect(filter).toEqual({ bool: { @@ -564,7 +582,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -579,7 +597,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -594,7 +612,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: undefined, - lists: undefined, + lists: [], }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -608,7 +626,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }); expect(filter).toEqual({ bool: { @@ -632,7 +650,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -647,7 +665,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, - lists: undefined, + lists: [], }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -662,7 +680,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, - lists: undefined, + lists: [], }) ).rejects.toThrow('Unsupported Rule of type "machine_learning" supplied to getFilter'); }); @@ -812,18 +830,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: [ - { - field: 'event.module', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'suricata', - }, - ], - }, - ], + lists: [getExceptionListItemSchemaMock()], }); expect(filter).toEqual({ bool: { @@ -845,11 +852,39 @@ describe('get_filter', () => { }, { bool: { - minimum_should_match: 1, - should: [ + filter: [ { - match: { - 'event.module': 'suricata', + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.parentField.nested.field': 'some value', + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.not.nested.field': 'some value', + }, + }, + ], + }, + }, }, }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 3e9f79c67d8ca..50ce01aaa6f74 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -10,11 +10,11 @@ import { Type, SavedIdOrUndefined, IndexOrUndefined, - ListAndOrUndefined, Language, Index, Query, } from '../../../../common/detection_engine/schemas/common/schemas'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { assertUnreachable } from '../../../utils/build_query'; import { @@ -33,7 +33,7 @@ export const getQueryFilter = ( language: Language, filters: PartialFilter[], index: Index, - lists: ListAndOrUndefined + lists: ExceptionListItemSchema[] ) => { const indexPattern = { fields: [], @@ -64,7 +64,7 @@ interface GetFilterArgs { savedId: SavedIdOrUndefined; services: AlertServices; index: IndexOrUndefined; - lists: ListAndOrUndefined; + lists: ExceptionListItemSchema[]; } interface QueryAttributes { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 163ed76d0c6c3..3fe1f3551da4b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -17,6 +17,7 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mock import uuid from 'uuid'; import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; import { listMock } from '../../../../../lists/server/mocks'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; @@ -94,22 +95,23 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -168,22 +170,22 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -281,25 +283,25 @@ describe('searchAfterAndBulkCreate', () => { }); test('if unsuccessful first bulk create', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const sampleParams = sampleRuleAlertParams(10); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockRejectedValue(new Error('bulk failed')); // Added this recently const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -327,6 +329,18 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success with 0 total hits', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); listClient.getListItemByValues = jest.fn(({ value }) => @@ -339,19 +353,7 @@ describe('searchAfterAndBulkCreate', () => { ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -405,21 +407,21 @@ describe('searchAfterAndBulkCreate', () => { })) ) ); + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], ruleParams: sampleParams, services: mockService, logger: mockLogger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index a2dc33ba1c2bf..92b05d1643745 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -10,7 +10,7 @@ import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; -import { getGapBetweenRuns } from './utils'; +import { getGapBetweenRuns, getListsClient, getExceptions, sortExceptionItems } from './utils'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; @@ -18,6 +18,9 @@ import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; import { listMock } from '../../../../../lists/server/mocks'; +import { getListClientMock } from '../../../../../lists/server/services/lists/list_client.mock'; +import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -84,6 +87,15 @@ describe('rules_notification_alert_type', () => { }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); + (getListsClient as jest.Mock).mockReturnValue({ + listClient: getListClientMock(), + exceptionsClient: getExceptionListClientMock(), + }); + (getExceptions as jest.Mock).mockReturnValue([getExceptionListItemSchemaMock()]); + (sortExceptionItems as jest.Mock).mockReturnValue({ + exceptionsWithoutValueLists: [getExceptionListItemSchemaMock()], + exceptionsWithValueLists: [], + }); (searchAfterAndBulkCreate as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 728bd66b7d65c..4f883f665aeee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -15,9 +15,6 @@ import { } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; import { SetupPlugins } from '../../../plugin'; - -import { ListClient } from '../../../../../lists/server'; - import { getInputIndex } from './get_input_output_index'; import { searchAfterAndBulkCreate, @@ -25,7 +22,13 @@ import { } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; -import { getGapBetweenRuns, parseScheduleDates } from './utils'; +import { + getGapBetweenRuns, + parseScheduleDates, + getListsClient, + getExceptions, + sortExceptionItems, +} from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; @@ -38,7 +41,6 @@ import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; import { getNotificationResultsLink } from '../notifications/utils'; -import { hasListsFeature } from '../feature_flags'; export const signalRulesAlertType = ({ logger, @@ -140,6 +142,21 @@ export const signalRulesAlertType = ({ await ruleStatusService.error(gapMessage, { gap: gapString }); } try { + const { listClient, exceptionsClient } = await getListsClient({ + services, + updatedByUser, + spaceId, + lists, + savedObjectClient: services.savedObjectsClient, + }); + const exceptionItems = await getExceptions({ + client: exceptionsClient, + lists: exceptionsList, + }); + const { exceptionsWithValueLists, exceptionsWithoutValueLists } = sortExceptionItems( + exceptionItems ?? [] + ); + if (isMlRule(type)) { if (ml == null) { throw new Error('ML plugin unavailable during rule execution'); @@ -214,18 +231,6 @@ export const signalRulesAlertType = ({ result.bulkCreateTimes.push(bulkCreateDuration); } } else { - let listClient: ListClient | undefined; - if (hasListsFeature()) { - if (lists == null) { - throw new Error('lists plugin unavailable during rule execution'); - } - listClient = await lists.getListClient( - services.callCluster, - spaceId, - updatedByUser ?? 'elastic' - ); - } - const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ type, @@ -235,13 +240,12 @@ export const signalRulesAlertType = ({ savedId, services, index: inputIndex, - // temporary filter out list type - lists: exceptionsList?.filter((item) => item.values_type !== 'list'), + lists: exceptionsWithoutValueLists, }); result = await searchAfterAndBulkCreate({ listClient, - exceptionsList, + exceptionsList: exceptionsWithValueLists, ruleParams: params, services, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index f0ca08b73fac6..e9f389fbc5c98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -7,9 +7,125 @@ import { createHash } from 'crypto'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { parseDuration } from '../../../../../alerts/server'; +import { SavedObjectsClientContract } from '../../../../../../../src/core/server'; +import { AlertServices, parseDuration } from '../../../../../alerts/server'; +import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; +import { EntriesArray, ExceptionListItemSchema } from '../../../../../lists/common/schemas'; +import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; +import { hasListsFeature } from '../feature_flags'; import { BulkResponse, BulkResponseErrorAggregation } from './types'; +interface SortExceptionsReturn { + exceptionsWithValueLists: ExceptionListItemSchema[]; + exceptionsWithoutValueLists: ExceptionListItemSchema[]; +} + +export const getListsClient = async ({ + lists, + spaceId, + updatedByUser, + services, + savedObjectClient, +}: { + lists: ListPluginSetup | undefined; + spaceId: string; + updatedByUser: string | null; + services: AlertServices; + savedObjectClient: SavedObjectsClientContract; +}): Promise<{ + listClient: ListClient | undefined; + exceptionsClient: ExceptionListClient | undefined; +}> => { + // TODO Remove check once feature is no longer behind flag + if (hasListsFeature()) { + if (lists == null) { + throw new Error('lists plugin unavailable during rule execution'); + } + + const listClient = await lists.getListClient( + services.callCluster, + spaceId, + updatedByUser ?? 'elastic' + ); + const exceptionsClient = await lists.getExceptionListClient( + savedObjectClient, + updatedByUser ?? 'elastic' + ); + + return { listClient, exceptionsClient }; + } else { + return { listClient: undefined, exceptionsClient: undefined }; + } +}; + +export const hasLargeValueList = (entries: EntriesArray): boolean => { + const found = entries.filter(({ type }) => type === 'list'); + return found.length > 0; +}; + +export const getExceptions = async ({ + client, + lists, +}: { + client: ExceptionListClient | undefined; + lists: ListArrayOrUndefined; +}): Promise => { + // TODO Remove check once feature is no longer behind flag + if (hasListsFeature()) { + if (client == null) { + throw new Error('lists plugin unavailable during rule execution'); + } + + if (lists != null) { + try { + // Gather all exception items of all exception lists linked to rule + const exceptions: ExceptionListItemSchema[] = await Promise.all( + lists + .map(async (list) => { + const { id, namespace_type: namespaceType } = list; + const items = await client.findExceptionListItem({ + listId: id, + namespaceType, + page: 1, + perPage: 5000, + filter: undefined, + sortOrder: undefined, + sortField: undefined, + }); + return items != null ? items.data : []; + }) + .flat() + ); + return exceptions.flat(); + } catch { + return []; + } + } + } +}; + +export const sortExceptionItems = (exceptions: ExceptionListItemSchema[]): SortExceptionsReturn => { + return exceptions.reduce( + (acc, exception) => { + const { entries } = exception; + const { exceptionsWithValueLists, exceptionsWithoutValueLists } = acc; + + if (hasLargeValueList(entries)) { + return { + exceptionsWithValueLists: [...exceptionsWithValueLists, { ...exception }], + exceptionsWithoutValueLists, + }; + } else { + return { + exceptionsWithValueLists, + exceptionsWithoutValueLists: [...exceptionsWithoutValueLists, { ...exception }], + }; + } + }, + { exceptionsWithValueLists: [], exceptionsWithoutValueLists: [] } + ); +}; + export const generateId = ( docIndex: string, docId: string, From 65e47ca25d0c8f03d5f9832fc41b0e12d365e068 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 23 Jun 2020 12:55:06 -0400 Subject: [PATCH 5/8] test and types cleanup --- .../new/exception_list_item_with_list.json | 23 +++++++++++++++++++ .../components/exceptions/helpers.test.tsx | 2 +- .../common/components/exceptions/helpers.tsx | 13 ++++++++++- .../public/lists_plugin_deps.ts | 1 + .../lib/detection_engine/rules/types.ts | 9 ++++---- .../lib/detection_engine/rules/utils.ts | 4 ++-- .../rules/queries/query_with_list.json | 10 ++++++++ .../signals/build_exceptions_query.ts | 13 +++++++---- .../signals/search_after_bulk_create.test.ts | 2 +- .../signals/search_after_bulk_create.ts | 4 ++-- .../signals/signal_rule_alert_type.ts | 15 +++--------- .../lib/detection_engine/signals/utils.ts | 2 +- .../server/lib/detection_engine/types.ts | 4 ++-- 13 files changed, 70 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json new file mode 100644 index 0000000000000..dffcfd6fdd90e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json @@ -0,0 +1,23 @@ +{ + "item_id": "endpoint_list_item_lg_val_list", + "_tags": ["endpoint", "process", "malware", "os:windows"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample exception list item with a large value list included", + "name": "Sample Endpoint Exception List Item with large value list", + "comments": [], + "entries": [ + { + "field": "event.module", + "operator": "excluded", + "type": "match_any", + "value": ["zeek"] + }, + { + "field": "source.ip", + "operator": "excluded", + "type": "list", + "list": { "id": "list-ip", "type": "ip" } + } + ] +} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 2239de3764326..bab507f4207db 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -215,7 +215,7 @@ describe('Exception helpers', () => { fieldName: 'host.name', isNested: false, operator: 'is in list', - value: ['some host name'], + value: 'some-list-id', }, { fieldName: 'host.name', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index f8b9c39801ae5..164940db619f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -19,6 +19,7 @@ import { OperatorTypeEnum, entriesNested, entriesExists, + entriesList, } from '../../../lists_plugin_deps'; /** @@ -87,6 +88,16 @@ export const getFormattedEntries = (entries: EntriesArray): FormattedEntry[] => return formattedEntries.flat(); }; +export const getEntryValue = (entry: Entry): string | string[] | null => { + if (entriesList.is(entry)) { + return entry.list.id; + } else if (entriesExists.is(entry)) { + return null; + } else { + return entry.value; + } +}; + /** * Helper method for `getFormattedEntries` */ @@ -100,7 +111,7 @@ export const formatEntry = ({ item: Entry; }): FormattedEntry => { const operator = getExceptionOperatorSelect(item); - const value = !entriesExists.is(item) ? item.value : null; + const value = getEntryValue(item); return { fieldName: isNested ? `${parent}.${item.field}` : item.field, diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index 22732c86bd9a9..575ff26330a46 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -27,4 +27,5 @@ export { OperatorTypeEnum, entriesNested, entriesExists, + entriesList, } from '../../lists/common/schemas'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 4b84057f6d795..1be667751f6a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -14,7 +14,6 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { ListsDefaultArraySchema } from '../../../../common/detection_engine/schemas/types/lists_default_array'; import { FalsePositives, From, @@ -62,7 +61,6 @@ import { ThreatOrUndefined, TypeOrUndefined, ReferencesOrUndefined, - ListAndOrUndefined, PerPageOrUndefined, PageOrUndefined, SortFieldOrUndefined, @@ -80,6 +78,7 @@ import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleTypeParams, PartialFilter } from '../types'; +import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types'; export interface RuleAlertType extends Alert { params: RuleTypeParams; @@ -194,7 +193,7 @@ export interface CreateRulesOptions { references: References; note: NoteOrUndefined; version: Version; - exceptionsList: ListsDefaultArraySchema; + exceptionsList: ListArrayOrUndefined; actions: RuleAlertAction[]; } @@ -230,7 +229,7 @@ export interface UpdateRulesOptions { references: References; note: NoteOrUndefined; version: VersionOrUndefined; - exceptionsList: ListsDefaultArraySchema; + exceptionsList: ListArrayOrUndefined; actions: RuleAlertAction[]; } @@ -264,7 +263,7 @@ export interface PatchRulesOptions { references: ReferencesOrUndefined; note: NoteOrUndefined; version: VersionOrUndefined; - exceptionsList: ListAndOrUndefined; + exceptionsList: ListArrayOrUndefined; actions: RuleAlertAction[] | undefined; rule: SanitizedAlert | null; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index d40cb5d96669b..5c620a5df61f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -31,9 +31,9 @@ import { ThreatOrUndefined, TypeOrUndefined, ReferencesOrUndefined, - ListAndOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; import { PartialFilter } from '../types'; +import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types'; export const calculateInterval = ( interval: string | undefined, @@ -74,7 +74,7 @@ export interface UpdateProperties { references: ReferencesOrUndefined; note: NoteOrUndefined; version: VersionOrUndefined; - exceptionsList: ListAndOrUndefined; + exceptionsList: ListArrayOrUndefined; anomalyThreshold: AnomalyThresholdOrUndefined; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json new file mode 100644 index 0000000000000..1cb4c144aa293 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json @@ -0,0 +1,10 @@ +{ + "name": "Rule w exceptions", + "description": "Sample rule with exception list", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "exceptions_list": [{ "id": "endpoint_list", "namespace_type": "single" }] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts index 03bf463ed0d07..378a57001073b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -12,12 +12,13 @@ import { EntryMatchAny, EntryNested, EntryExists, + EntriesArray, Operator, entriesMatchAny, entriesExists, entriesMatch, entriesNested, - EntriesArray, + entriesList, } from '../../../../../lists/common/schemas'; type Operators = 'and' | 'or' | 'not'; @@ -182,10 +183,12 @@ export const buildExceptionItemEntries = ({ language: Language; }): string => { const and = getLanguageBooleanOperator({ language, value: 'and' }); - const exceptionItem = lists.reduce((accum, listItem) => { - const exceptionSegment = evaluateValues({ item: listItem, language }); - return [...accum, exceptionSegment]; - }, []); + const exceptionItem = lists + .filter((t) => !entriesList.is(t)) + .reduce((accum, listItem) => { + const exceptionSegment = evaluateValues({ item: listItem, language }); + return [...accum, exceptionSegment]; + }, []); return exceptionItem.join(` ${and} `); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 3fe1f3551da4b..1923f43c47b92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -256,7 +256,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, listClient, - exceptionsList: undefined, + exceptionsList: [], services: mockService, logger: mockLogger, id: sampleRuleGuid, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 65679dc23e64f..7475257121552 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ListAndOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { ListClient } from '../../../../../lists/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; @@ -14,12 +13,13 @@ import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; import { SignalSearchResponse } from './types'; import { filterEventsAgainstList } from './filter_events_with_list'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; interface SearchAfterAndBulkCreateParams { ruleParams: RuleTypeParams; services: AlertServices; listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged - exceptionsList: ListAndOrUndefined; + exceptionsList: ExceptionListItemSchema[]; logger: Logger; id: string; inputIndexPattern: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 4f883f665aeee..1bf27dc6e26b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -22,13 +22,7 @@ import { } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; -import { - getGapBetweenRuns, - parseScheduleDates, - getListsClient, - getExceptions, - sortExceptionItems, -} from './utils'; +import { getGapBetweenRuns, parseScheduleDates, getListsClient, getExceptions } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; @@ -153,9 +147,6 @@ export const signalRulesAlertType = ({ client: exceptionsClient, lists: exceptionsList, }); - const { exceptionsWithValueLists, exceptionsWithoutValueLists } = sortExceptionItems( - exceptionItems ?? [] - ); if (isMlRule(type)) { if (ml == null) { @@ -240,12 +231,12 @@ export const signalRulesAlertType = ({ savedId, services, index: inputIndex, - lists: exceptionsWithoutValueLists, + lists: exceptionItems ?? [], }); result = await searchAfterAndBulkCreate({ listClient, - exceptionsList: exceptionsWithValueLists, + exceptionsList: exceptionItems ?? [], ruleParams: params, services, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index e9f389fbc5c98..e431e65fad623 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -79,7 +79,7 @@ export const getExceptions = async ({ if (lists != null) { try { // Gather all exception items of all exception lists linked to rule - const exceptions: ExceptionListItemSchema[] = await Promise.all( + const exceptions = await Promise.all( lists .map(async (list) => { const { id, namespace_type: namespaceType } = list; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 6e284908e3358..90484a46dc6d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -28,11 +28,11 @@ import { Version, MetaOrUndefined, RuleId, - ListAndOrUndefined, } from '../../../common/detection_engine/schemas/common/schemas'; import { CallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; import { RuleType } from '../../../common/detection_engine/types'; +import { ListArrayOrUndefined } from '../../../common/detection_engine/schemas/types'; export type PartialFilter = Partial; @@ -62,7 +62,7 @@ export interface RuleTypeParams { type: RuleType; references: References; version: Version; - exceptionsList: ListAndOrUndefined; + exceptionsList: ListArrayOrUndefined; } // eslint-disable-next-line @typescript-eslint/no-explicit-any From a2f6286dc450cbbf8866dadf886c42841e00f569 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 23 Jun 2020 13:26:50 -0400 Subject: [PATCH 6/8] cleanup --- .../detection_engine/schemas/response/rules_schema.test.ts | 2 +- .../server/lib/detection_engine/rules/types.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index f4c1ff84e41b7..b3f9096b51483 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -665,7 +665,7 @@ describe('rules_schema', () => { expect(message.schema).toEqual(expected); }); - test('it should NOT validate a type of "query" when it has extra data', () => { + test('it should NOT validate when "exceptions_list" is not expected type', () => { const payload: Omit & { exceptions_list?: string; } = { ...getRulesSchemaMock(), exceptions_list: 'invalid_data' }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 1be667751f6a9..fc95f0cfeb78e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -78,7 +78,7 @@ import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleTypeParams, PartialFilter } from '../types'; -import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types'; +import { ListArrayOrUndefined, ListArray } from '../../../../common/detection_engine/schemas/types'; export interface RuleAlertType extends Alert { params: RuleTypeParams; @@ -193,7 +193,7 @@ export interface CreateRulesOptions { references: References; note: NoteOrUndefined; version: Version; - exceptionsList: ListArrayOrUndefined; + exceptionsList: ListArray; actions: RuleAlertAction[]; } @@ -229,7 +229,7 @@ export interface UpdateRulesOptions { references: References; note: NoteOrUndefined; version: VersionOrUndefined; - exceptionsList: ListArrayOrUndefined; + exceptionsList: ListArray; actions: RuleAlertAction[]; } From 53536f8fcd1f8e80c7cd9ef46f62d5ad42a3a751 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 23 Jun 2020 17:05:30 -0400 Subject: [PATCH 7/8] update unit test --- .../public/common/components/exceptions/helpers.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index bab507f4207db..244819080c93d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -238,8 +238,8 @@ describe('Exception helpers', () => { { fieldName: 'host.name.host.name', isNested: true, - operator: 'exists', - value: null, + operator: 'is', + value: 'some host name', }, ]; expect(result).toEqual(expected); From 373fe8cf462b894a3406b24f6d88709e3e7ea6a0 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 24 Jun 2020 21:58:43 -0400 Subject: [PATCH 8/8] updates per feedback --- x-pack/plugins/lists/README.md | 6 +- .../new/exception_list_item_with_list.json | 1 + .../scripts/lists/new/list_ip_item.json | 2 +- .../detection_engine/lists_common_deps.ts | 2 +- .../signals/build_exceptions_query.ts | 2 - .../signals/filter_events_with_list.ts | 14 +-- .../detection_engine/signals/utils.test.ts | 113 +++++++++++++++++- 7 files changed, 124 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/lists/README.md b/x-pack/plugins/lists/README.md index cdd7813792fc3..5c97107cf2282 100644 --- a/x-pack/plugins/lists/README.md +++ b/x-pack/plugins/lists/README.md @@ -157,12 +157,14 @@ And you can attach exception list items like so: { "field": "actingProcess.file.signer", "operator": "included", - "match": "Elastic, N.V." + "type": "match", + "value": "Elastic, N.V." }, { "field": "event.category", "operator": "included", - "match_any": [ + "type": "match_any", + "value": [ "process", "malware" ] diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json index dffcfd6fdd90e..e1dab72c1c7f6 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json @@ -1,4 +1,5 @@ { + "list_id": "endpoint_list", "item_id": "endpoint_list_item_lg_val_list", "_tags": ["endpoint", "process", "malware", "os:windows"], "tags": ["user added string for a tag", "malware"], diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json index 1516fa5057e50..1ece2268f3cf6 100644 --- a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json @@ -1,5 +1,5 @@ { "id": "hand_inserted_item_id", "list_id": "list-ip", - "value": "127.0.0.1" + "value": "10.4.2.140" } diff --git a/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts index 9a4a7b5617b4c..a8b177f587a48 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EntryList, EntriesArray, namespaceType } from '../../../lists/common/schemas'; +export { EntriesArray, namespaceType } from '../../../lists/common/schemas'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts index 378a57001073b..ba0d9dec7d1b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -56,8 +56,6 @@ export const operatorBuilder = ({ }); switch (operator) { - // case 'excluded': - // return ` ${and} `; case 'included': return `${not} `; default: diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index f1f5f4bf3c75e..1a2f648eb8562 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -8,7 +8,11 @@ import { Logger } from 'src/core/server'; import { ListClient } from '../../../../../lists/server'; import { SignalSearchResponse, SearchTypes } from './types'; -import { entriesList, ExceptionListItemSchema } from '../../../../../lists/common/schemas'; +import { + entriesList, + EntryList, + ExceptionListItemSchema, +} from '../../../../../lists/common/schemas'; interface FilterEventsAgainstList { listClient: ListClient; @@ -37,14 +41,8 @@ export const filterEventsAgainstList = async ({ const { entries } = exceptionItem; const filteredHitsEntries = entries - .filter((t) => entriesList.is(t)) + .filter((t): t is EntryList => entriesList.is(t)) .map(async (entry) => { - // TODO: If this check is removed, then typescript doesn't - // recognize "entry" as EntryList despite .filter() - if (!entriesList.is(entry)) { - throw new Error('Malformed exception list provided'); - } - // acquire the list values we are checking for. const valuesOfGivenType = eventSearchResult.hits.hits.reduce( (acc, searchResultItem) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index f74694df613ce..24c2d24ee972e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -7,6 +7,12 @@ import moment from 'moment'; import sinon from 'sinon'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { listMock } from '../../../../../lists/server/mocks'; +import { EntriesArray } from '../../../../common/detection_engine/lists_common_deps'; + +import * as featureFlags from '../feature_flags'; + import { generateId, parseInterval, @@ -14,10 +20,10 @@ import { getDriftTolerance, getGapBetweenRuns, errorAggregator, + getListsClient, + hasLargeValueList, } from './utils'; - import { BulkResponseErrorAggregation } from './types'; - import { sampleBulkResponse, sampleEmptyBulkResponse, @@ -529,4 +535,107 @@ describe('utils', () => { expect(aggregated).toEqual(expected); }); }); + + describe('#getListsClient', () => { + let alertServices: AlertServicesMock; + + beforeEach(() => { + alertServices = alertsMock.createAlertServices(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it successfully returns list and exceptions list client', async () => { + jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(true); + + const { listClient, exceptionsClient } = await getListsClient({ + services: alertServices, + savedObjectClient: alertServices.savedObjectsClient, + updatedByUser: 'some_user', + spaceId: '', + lists: listMock.createSetup(), + }); + + expect(listClient).toBeDefined(); + expect(exceptionsClient).toBeDefined(); + }); + + test('it returns list and exceptions client of "undefined" if lists feature flag is off', async () => { + jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(false); + + const listsClient = await getListsClient({ + services: alertServices, + savedObjectClient: alertServices.savedObjectsClient, + updatedByUser: 'some_user', + spaceId: '', + lists: listMock.createSetup(), + }); + + expect(listsClient).toEqual({ listClient: undefined, exceptionsClient: undefined }); + }); + + test('it throws if "lists" is undefined', async () => { + jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(true); + + await expect(() => + getListsClient({ + services: alertServices, + savedObjectClient: alertServices.savedObjectsClient, + updatedByUser: 'some_user', + spaceId: '', + lists: undefined, + }) + ).rejects.toThrowError('lists plugin unavailable during rule execution'); + }); + }); + + describe('#hasLargeValueList', () => { + test('it returns false if empty array', () => { + const hasLists = hasLargeValueList([]); + + expect(hasLists).toBeFalsy(); + }); + + test('it returns true if item of type EntryList exists', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'list', + operator: 'included', + list: { id: 'some id', type: 'ip' }, + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasLargeValueList(entries); + + expect(hasLists).toBeTruthy(); + }); + + test('it returns false if item of type EntryList does not exist', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'match', + operator: 'included', + value: 'Elastic, N.V.', + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasLargeValueList(entries); + + expect(hasLists).toBeFalsy(); + }); + }); });