Skip to content

Commit f7acbbe

Browse files
authored
[SIEM][Detection Engine] - Update DE to work with new exceptions schema (#69715)
* Updates list entry schema, exposes exception list client, updates tests * create new de list schema and unit tests * updated route unit tests and types to match new list schema * updated existing DE exceptions code so it should now work as is with updated schema * test and types cleanup * cleanup * update unit test * updates per feedback
1 parent 7a55782 commit f7acbbe

71 files changed

Lines changed: 2513 additions & 2179 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

x-pack/plugins/lists/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,14 @@ And you can attach exception list items like so:
157157
{
158158
"field": "actingProcess.file.signer",
159159
"operator": "included",
160-
"match": "Elastic, N.V."
160+
"type": "match",
161+
"value": "Elastic, N.V."
161162
},
162163
{
163164
"field": "event.category",
164165
"operator": "included",
165-
"match_any": [
166+
"type": "match_any",
167+
"value": [
166168
"process",
167169
"malware"
168170
]

x-pack/plugins/lists/common/constants.mock.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ export const EXISTS = 'exists';
4646
export const NESTED = 'nested';
4747
export const ENTRIES: EntriesArray = [
4848
{
49-
entries: [
50-
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
51-
],
52-
field: 'some.field',
49+
entries: [{ field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }],
50+
field: 'some.parentField',
5351
type: 'nested',
5452
},
5553
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },

x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import { getEntriesArrayMock, getEntryMatchMock, getEntryNestedMock } from './en
1717
// it checks against every item in that union. Since entries consist of 5
1818
// different entry types, it returns 5 of these. To make more readable,
1919
// extracted here.
20-
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" |})>"`;
20+
const returnedSchemaError =
21+
'"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" |})>"';
2122

2223
describe('default_entries_array', () => {
2324
test('it should validate an empty array', () => {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { pipe } from 'fp-ts/lib/pipeable';
8+
import { left } from 'fp-ts/lib/Either';
9+
10+
import { foldLeftRight, getPaths } from '../../siem_common_deps';
11+
12+
import { DefaultNamespace } from './default_namespace';
13+
14+
describe('default_namespace', () => {
15+
test('it should validate "single"', () => {
16+
const payload = 'single';
17+
const decoded = DefaultNamespace.decode(payload);
18+
const message = pipe(decoded, foldLeftRight);
19+
20+
expect(getPaths(left(message.errors))).toEqual([]);
21+
expect(message.schema).toEqual(payload);
22+
});
23+
24+
test('it should validate "agnostic"', () => {
25+
const payload = 'agnostic';
26+
const decoded = DefaultNamespace.decode(payload);
27+
const message = pipe(decoded, foldLeftRight);
28+
29+
expect(getPaths(left(message.errors))).toEqual([]);
30+
expect(message.schema).toEqual(payload);
31+
});
32+
33+
test('it defaults to "single" if "undefined"', () => {
34+
const payload = undefined;
35+
const decoded = DefaultNamespace.decode(payload);
36+
const message = pipe(decoded, foldLeftRight);
37+
38+
expect(getPaths(left(message.errors))).toEqual([]);
39+
expect(message.schema).toEqual('single');
40+
});
41+
42+
test('it defaults to "single" if "null"', () => {
43+
const payload = null;
44+
const decoded = DefaultNamespace.decode(payload);
45+
const message = pipe(decoded, foldLeftRight);
46+
47+
expect(getPaths(left(message.errors))).toEqual([]);
48+
expect(message.schema).toEqual('single');
49+
});
50+
51+
test('it should NOT validate if not "single" or "agnostic"', () => {
52+
const payload = 'something else';
53+
const decoded = DefaultNamespace.decode(payload);
54+
const message = pipe(decoded, foldLeftRight);
55+
56+
expect(getPaths(left(message.errors))).toEqual([
57+
`Invalid value "something else" supplied to "DefaultNamespace"`,
58+
]);
59+
expect(message.schema).toEqual({});
60+
});
61+
});

x-pack/plugins/lists/common/schemas/types/default_namespace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import * as t from 'io-ts';
88
import { Either } from 'fp-ts/lib/Either';
99

10-
const namespaceType = t.keyof({ agnostic: null, single: null });
10+
export const namespaceType = t.keyof({ agnostic: null, single: null });
1111

1212
type NamespaceType = t.TypeOf<typeof namespaceType>;
1313

x-pack/plugins/lists/common/schemas/types/entries.mock.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
EXISTS,
1010
FIELD,
1111
LIST,
12+
LIST_ID,
1213
MATCH,
1314
MATCH_ANY,
1415
NESTED,
1516
OPERATOR,
17+
TYPE,
1618
} from '../../constants.mock';
1719

1820
import {
@@ -40,9 +42,9 @@ export const getEntryMatchAnyMock = (): EntryMatchAny => ({
4042

4143
export const getEntryListMock = (): EntryList => ({
4244
field: FIELD,
45+
list: { id: LIST_ID, type: TYPE },
4346
operator: OPERATOR,
4447
type: LIST,
45-
value: [ENTRY_VALUE],
4648
});
4749

4850
export const getEntryExistsMock = (): EntryExists => ({
@@ -52,7 +54,7 @@ export const getEntryExistsMock = (): EntryExists => ({
5254
});
5355

5456
export const getEntryNestedMock = (): EntryNested => ({
55-
entries: [getEntryMatchMock(), getEntryExistsMock()],
57+
entries: [getEntryMatchMock(), getEntryMatchMock()],
5658
field: FIELD,
5759
type: NESTED,
5860
});

x-pack/plugins/lists/common/schemas/types/entries.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,16 +251,16 @@ describe('Entries', () => {
251251
expect(message.schema).toEqual(payload);
252252
});
253253

254-
test('it should not validate when "value" is not string array', () => {
255-
const payload: Omit<EntryList, 'value'> & { value: string } = {
254+
test('it should not validate when "list" is not expected value', () => {
255+
const payload: Omit<EntryList, 'list'> & { list: string } = {
256256
...getEntryListMock(),
257-
value: 'someListId',
257+
list: 'someListId',
258258
};
259259
const decoded = entriesList.decode(payload);
260260
const message = pipe(decoded, foldLeftRight);
261261

262262
expect(getPaths(left(message.errors))).toEqual([
263-
'Invalid value "someListId" supplied to "value"',
263+
'Invalid value "someListId" supplied to "list"',
264264
]);
265265
expect(message.schema).toEqual({});
266266
});
@@ -338,6 +338,20 @@ describe('Entries', () => {
338338
expect(message.schema).toEqual({});
339339
});
340340

341+
test('it should NOT validate when "entries" contains an entry item that is not type "match"', () => {
342+
const payload: Omit<EntryNested, 'entries'> & {
343+
entries: EntryMatchAny[];
344+
} = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] };
345+
const decoded = entriesNested.decode(payload);
346+
const message = pipe(decoded, foldLeftRight);
347+
348+
expect(getPaths(left(message.errors))).toEqual([
349+
'Invalid value "match_any" supplied to "entries,type"',
350+
'Invalid value "["some host name"]" supplied to "entries,value"',
351+
]);
352+
expect(message.schema).toEqual({});
353+
});
354+
341355
test('it should strip out extra keys', () => {
342356
const payload: EntryNested & {
343357
extraKey?: string;

x-pack/plugins/lists/common/schemas/types/entries.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import * as t from 'io-ts';
1010

11-
import { operator } from '../common/schemas';
11+
import { operator, type } from '../common/schemas';
1212
import { DefaultStringArray } from '../../siem_common_deps';
1313

1414
export const entriesMatch = t.exact(
@@ -34,9 +34,9 @@ export type EntryMatchAny = t.TypeOf<typeof entriesMatchAny>;
3434
export const entriesList = t.exact(
3535
t.type({
3636
field: t.string,
37+
list: t.exact(t.type({ id: t.string, type })),
3738
operator,
3839
type: t.keyof({ list: null }),
39-
value: DefaultStringArray,
4040
})
4141
);
4242
export type EntryList = t.TypeOf<typeof entriesList>;
@@ -52,7 +52,7 @@ export type EntryExists = t.TypeOf<typeof entriesExists>;
5252

5353
export const entriesNested = t.exact(
5454
t.type({
55-
entries: t.array(t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists])),
55+
entries: t.array(entriesMatch),
5656
field: t.string,
5757
type: t.keyof({ nested: null }),
5858
})

x-pack/plugins/lists/common/schemas/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
*/
66
export * from './default_comments_array';
77
export * from './default_entries_array';
8+
export * from './default_namespace';
89
export * from './comments';
910
export * from './entries';

x-pack/plugins/lists/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ListPlugin } from './plugin';
1111

1212
// exporting these since its required at top level in siem plugin
1313
export { ListClient } from './services/lists/list_client';
14+
export { ExceptionListClient } from './services/exception_lists/exception_list_client';
1415
export { ListPluginSetup } from './types';
1516

1617
export const config = { schema: ConfigSchema };

0 commit comments

Comments
 (0)