Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM][Detection Engine] - Update DE to work with new exceptions schema #69715

Merged
merged 10 commits into from
Jun 25, 2020
6 changes: 4 additions & 2 deletions x-pack/plugins/lists/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
6 changes: 2 additions & 4 deletions x-pack/plugins/lists/common/constants.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: Exported to use in detection engine.


type NamespaceType = t.TypeOf<typeof namespaceType>;

Expand Down
6 changes: 4 additions & 2 deletions x-pack/plugins/lists/common/schemas/types/entries.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
EXISTS,
FIELD,
LIST,
LIST_ID,
MATCH,
MATCH_ANY,
NESTED,
OPERATOR,
TYPE,
} from '../../constants.mock';

import {
Expand Down Expand Up @@ -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 => ({
Expand All @@ -52,7 +54,7 @@ export const getEntryExistsMock = (): EntryExists => ({
});

export const getEntryNestedMock = (): EntryNested => ({
entries: [getEntryMatchMock(), getEntryExistsMock()],
entries: [getEntryMatchMock(), getEntryMatchMock()],
field: FIELD,
type: NESTED,
});
Expand Down
22 changes: 18 additions & 4 deletions x-pack/plugins/lists/common/schemas/types/entries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntryList, 'value'> & { value: string } = {
test('it should not validate when "list" is not expected value', () => {
const payload: Omit<EntryList, 'list'> & { 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({});
});
Expand Down Expand Up @@ -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<EntryNested, 'entries'> & {
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;
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/lists/common/schemas/types/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -34,9 +34,9 @@ export type EntryMatchAny = t.TypeOf<typeof entriesMatchAny>;
export const entriesList = t.exact(
t.type({
field: t.string,
list: t.exact(t.type({ id: t.string, type })),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: Changed this to list from value, SOs were screaming when trying to index value as string | string[] | object so updated.

operator,
type: t.keyof({ list: null }),
value: DefaultStringArray,
})
);
export type EntryList = t.TypeOf<typeof entriesList>;
Expand All @@ -52,7 +52,7 @@ export type EntryExists = t.TypeOf<typeof entriesExists>;

export const entriesNested = t.exact(
t.type({
entries: t.array(t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists])),
entries: t.array(entriesMatch),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: Looking at KQL nested support, looks like it would only support our match and not any of the other operators. Please feel free to comment if that's wrong.

field: t.string,
type: t.keyof({ nested: null }),
})
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/lists/common/schemas/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
*/
export * from './default_comments_array';
export * from './default_entries_array';
export * from './default_namespace';
export * from './comments';
export * from './entries';
1 change: 1 addition & 0 deletions x-pack/plugins/lists/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/lists/server/saved_objects/exception_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = {
field: {
type: 'keyword',
},
list: {
properties: {
id: {
type: 'keyword',
},
type: {
type: 'keyword',
},
},
},
operator: {
type: 'keyword',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"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"],
"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" }
}
]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"id": "hand_inserted_item_id",
"list_id": "list-ip",
"value": "127.0.0.1"
"value": "10.4.2.140"
}
Original file line number Diff line number Diff line change
@@ -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 { EntriesArray, namespaceType } from '../../../lists/common/schemas';
Original file line number Diff line number Diff line change
Expand Up @@ -341,40 +341,3 @@ export type Note = t.TypeOf<typeof note>;

export const noteOrUndefined = t.union([note, t.undefined]);
export type NoteOrUndefined = t.TypeOf<typeof noteOrUndefined>;

// 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<typeof listAndOrUndefined>;
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
})
),
]);
Expand Down Expand Up @@ -130,5 +133,5 @@ export type AddPrepackagedRulesSchemaDecoded = Omit<
to: To;
threat: Threat;
throttle: ThrottleOrNull;
exceptions_list: ListsDefaultArraySchema;
exceptions_list: ListArray;
};
Loading