[Security Solution][Exceptions] - Update rule.exceptions_list to include exception list list_id (#73349)

## Summary

This PR addresses the following:
- Adds `list_id` to `rule.exceptions_list` - this is needed in a number of features
- Updated `getExceptions` in `x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts` to use the latest exception item find endpoint that accepts an array of lists (previously was looping through lists and conducting a `find` for each)
- Updated prepackaged rule that makes reference to global endpoint list to include `list_id`
- Updates `formatAboutStepData` in `x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts` to include exception list `list_id`
This commit is contained in:
Yara Tercero 2020-07-28 23:27:14 -04:00 committed by GitHub
parent 7059270ce9
commit e645732319
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 122 additions and 137 deletions

View file

@ -41,7 +41,9 @@ describe('useExceptionList', () => {
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single', type: 'detection' }],
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
],
onError: onErrorMock,
pagination: {
page: 1,
@ -76,7 +78,9 @@ describe('useExceptionList', () => {
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single', type: 'detection' }],
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
],
onError: onErrorMock,
onSuccess: onSuccessMock,
pagination: {
@ -131,7 +135,9 @@ describe('useExceptionList', () => {
initialProps: {
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single', type: 'detection' }],
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
],
onError: onErrorMock,
onSuccess: onSuccessMock,
pagination: {
@ -146,7 +152,9 @@ describe('useExceptionList', () => {
rerender({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'newListId', namespaceType: 'single', type: 'detection' }],
lists: [
{ id: 'newListId', listId: 'new_list_id', namespaceType: 'single', type: 'detection' },
],
onError: onErrorMock,
onSuccess: onSuccessMock,
pagination: {
@ -173,7 +181,9 @@ describe('useExceptionList', () => {
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single', type: 'detection' }],
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
],
onError: onErrorMock,
pagination: {
page: 1,
@ -210,7 +220,9 @@ describe('useExceptionList', () => {
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single', type: 'detection' }],
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
],
onError: onErrorMock,
pagination: {
page: 1,
@ -238,7 +250,9 @@ describe('useExceptionList', () => {
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single', type: 'detection' }],
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
],
onError: onErrorMock,
pagination: {
page: 1,

View file

@ -60,6 +60,7 @@ export interface UseExceptionListProps {
export interface ExceptionIdentifiers {
id: string;
listId: string;
namespaceType: NamespaceType;
type: ExceptionListType;
}

View file

@ -1446,11 +1446,13 @@ describe('add prepackaged rules schema', () => {
exceptions_list: [
{
id: 'some_uuid',
list_id: 'list_id_single',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
@ -1535,6 +1537,7 @@ describe('add prepackaged rules schema', () => {
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "exceptions_list,list_id"',
'Invalid value "undefined" supplied to "exceptions_list,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);

View file

@ -1513,11 +1513,13 @@ describe('create rules schema', () => {
exceptions_list: [
{
id: 'some_uuid',
list_id: 'list_id_single',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
@ -1600,6 +1602,7 @@ describe('create rules schema', () => {
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "exceptions_list,list_id"',
'Invalid value "undefined" supplied to "exceptions_list,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);

View file

@ -1642,11 +1642,13 @@ describe('import rules schema', () => {
exceptions_list: [
{
id: 'some_uuid',
list_id: 'list_id_single',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
@ -1730,6 +1732,7 @@ describe('import rules schema', () => {
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "exceptions_list,list_id"',
'Invalid value "undefined" supplied to "exceptions_list,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);

View file

@ -1176,11 +1176,13 @@ describe('patch_rules_schema', () => {
exceptions_list: [
{
id: 'some_uuid',
list_id: 'list_id_single',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
@ -1251,6 +1253,7 @@ describe('patch_rules_schema', () => {
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "exceptions_list,list_id"',
'Invalid value "undefined" supplied to "exceptions_list,type"',
'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"',

View file

@ -1448,11 +1448,13 @@ describe('update rules schema', () => {
exceptions_list: [
{
id: 'some_uuid',
list_id: 'list_id_single',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
@ -1534,6 +1536,7 @@ describe('update rules schema', () => {
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "exceptions_list,list_id"',
'Invalid value "undefined" supplied to "exceptions_list,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);

View file

@ -4,17 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { List, ListArray } from './lists';
import { ENDPOINT_LIST_ID } from '../../../shared_imports';
export const getListMock = (): List => ({
id: 'some_uuid',
list_id: 'list_id_single',
namespace_type: 'single',
type: 'detection',
});
export const getListAgnosticMock = (): List => ({
id: 'some_uuid',
export const getEndpointListMock = (): List => ({
id: ENDPOINT_LIST_ID,
list_id: ENDPOINT_LIST_ID,
namespace_type: 'agnostic',
type: 'endpoint',
});
export const getListArrayMock = (): ListArray => [getListMock(), getListAgnosticMock()];
export const getListArrayMock = (): ListArray => [getListMock(), getEndpointListMock()];

View file

@ -9,7 +9,7 @@ import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { getListAgnosticMock, getListMock, getListArrayMock } from './lists.mock';
import { getEndpointListMock, getListMock, getListArrayMock } from './lists.mock';
import {
List,
ListArray,
@ -31,7 +31,7 @@ describe('Lists', () => {
});
test('it should validate a list with "namespace_type" of "agnostic"', () => {
const payload = getListAgnosticMock();
const payload = getEndpointListMock();
const decoded = list.decode(payload);
const message = pipe(decoded, foldLeftRight);
@ -91,7 +91,7 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<{| id: string, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}>"',
'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}>"',
]);
expect(message.schema).toEqual({});
});
@ -122,8 +122,8 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "(Array<{| id: string, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "[1]" supplied to "(Array<{| id: string, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"',
]);
expect(message.schema).toEqual({});
});

View file

@ -8,9 +8,12 @@ import * as t from 'io-ts';
import { exceptionListType, namespaceType } from '../../../shared_imports';
import { NonEmptyString } from './non_empty_string';
export const list = t.exact(
t.type({
id: t.string,
id: NonEmptyString,
list_id: NonEmptyString,
type: exceptionListType,
namespace_type: namespaceType,
})

View file

@ -102,6 +102,7 @@ export const useFetchOrCreateRuleExceptionList = ({
const newExceptionListReference = {
id: newExceptionList.id,
list_id: newExceptionList.list_id,
type: newExceptionList.type,
namespace_type: newExceptionList.namespace_type,
};

View file

@ -72,6 +72,7 @@ describe('ExceptionsViewer', () => {
exceptionListsMeta={[
{
id: '5b543420',
listId: 'list_id',
type: 'endpoint',
namespaceType: 'single',
},
@ -124,6 +125,7 @@ describe('ExceptionsViewer', () => {
exceptionListsMeta={[
{
id: '5b543420',
listId: 'list_id',
type: 'endpoint',
namespaceType: 'single',
},

View file

@ -176,8 +176,6 @@ const ExceptionsViewerComponent = ({
const handleEditException = useCallback(
(exception: ExceptionListItemSchema): void => {
// TODO: Added this just for testing. Update
// modal state logic as needed once ready
dispatch({
type: 'updateExceptionToEdit',
exception,

View file

@ -6,7 +6,6 @@
import { esFilters } from '../../../../../../../../../../src/plugins/data/public';
import { Rule, RuleError } from '../../../../../containers/detection_engine/rules';
import { List } from '../../../../../../../common/detection_engine/schemas/types';
import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types';
import { FieldValueQueryBar } from '../../../../../components/rules/query_bar';
import { fillEmptySeverityMappings } from '../../helpers';
@ -242,9 +241,3 @@ export const mockRules: Rule[] = [
mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'),
mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'),
];
export const mockExceptionsList: List = {
namespace_type: 'single',
id: '75cd4380-cc5e-11ea-9101-5b34f44aeb44',
type: 'detection',
};

View file

@ -5,9 +5,11 @@
*/
import { List } from '../../../../../../common/detection_engine/schemas/types';
import { ENDPOINT_LIST_ID } from '../../../../../shared_imports';
import { NewRule } from '../../../../containers/detection_engine/rules';
import {
getListMock,
getEndpointListMock,
} from '../../../../../../common/detection_engine/schemas/types/lists.mock';
import {
DefineStepRuleJson,
ScheduleStepRuleJson,
@ -29,19 +31,12 @@ import {
} from './helpers';
import {
mockDefineStepRule,
mockExceptionsList,
mockQueryBar,
mockScheduleStepRule,
mockAboutStepRule,
mockActionsStepRule,
} from '../all/__mocks__/mock';
const ENDPOINT_LIST = {
id: ENDPOINT_LIST_ID,
namespace_type: 'agnostic',
type: 'endpoint',
} as List;
describe('helpers', () => {
describe('getTimeTypeValue', () => {
test('returns timeObj with value 0 if no time value found', () => {
@ -391,14 +386,12 @@ describe('helpers', () => {
},
[]
);
expect(result.exceptions_list).toEqual([
{ id: ENDPOINT_LIST_ID, namespace_type: 'agnostic', type: 'endpoint' },
]);
expect(result.exceptions_list).toEqual([getEndpointListMock()]);
});
test('returns formatted object with detections exceptions_list', () => {
const result: AboutStepRuleJson = formatAboutStepData(mockData, [mockExceptionsList]);
expect(result.exceptions_list).toEqual([mockExceptionsList]);
const result: AboutStepRuleJson = formatAboutStepData(mockData, [getListMock()]);
expect(result.exceptions_list).toEqual([getListMock()]);
});
test('returns formatted object with both exceptions_lists', () => {
@ -407,13 +400,13 @@ describe('helpers', () => {
...mockData,
isAssociatedToEndpointList: true,
},
[mockExceptionsList]
[getListMock()]
);
expect(result.exceptions_list).toEqual([ENDPOINT_LIST, mockExceptionsList]);
expect(result.exceptions_list).toEqual([getEndpointListMock(), getListMock()]);
});
test('returns formatted object with pre-existing exceptions lists', () => {
const exceptionsLists: List[] = [ENDPOINT_LIST, mockExceptionsList];
const exceptionsLists: List[] = [getEndpointListMock(), getListMock()];
const result: AboutStepRuleJson = formatAboutStepData(
{
...mockData,
@ -425,9 +418,9 @@ describe('helpers', () => {
});
test('returns formatted object with pre-existing endpoint exceptions list disabled', () => {
const exceptionsLists: List[] = [ENDPOINT_LIST, mockExceptionsList];
const exceptionsLists: List[] = [getEndpointListMock(), getListMock()];
const result: AboutStepRuleJson = formatAboutStepData(mockData, exceptionsLists);
expect(result.exceptions_list).toEqual([mockExceptionsList]);
expect(result.exceptions_list).toEqual([getListMock()]);
});
test('returns formatted object with empty falsePositive and references filtered out', () => {

View file

@ -177,7 +177,12 @@ export const formatAboutStepData = (
...(isAssociatedToEndpointList
? {
exceptions_list: [
{ id: ENDPOINT_LIST_ID, namespace_type: 'agnostic', type: 'endpoint' },
{
id: ENDPOINT_LIST_ID,
list_id: ENDPOINT_LIST_ID,
namespace_type: 'agnostic',
type: 'endpoint',
},
...detectionExceptionLists,
] as AboutStepRuleJson['exceptions_list'],
}

View file

@ -328,13 +328,13 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
lists: ExceptionIdentifiers[];
allowedExceptionListTypes: ExceptionListTypeEnum[];
}>(
(acc, { id, namespace_type, type }) => {
(acc, { id, list_id, namespace_type, type }) => {
const { allowedExceptionListTypes, lists } = acc;
const shouldAddEndpoint =
type === ExceptionListTypeEnum.ENDPOINT &&
!allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT);
return {
lists: [...lists, { id, namespaceType: namespace_type, type }],
lists: [...lists, { id, listId: list_id, namespaceType: namespace_type, type }],
allowedExceptionListTypes: shouldAddEndpoint
? [...allowedExceptionListTypes, ExceptionListTypeEnum.ENDPOINT]
: allowedExceptionListTypes,

View file

@ -1,20 +1,17 @@
{
"author": [
"Elastic"
],
"author": ["Elastic"],
"description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Elastic Endpoint alerts.",
"enabled": true,
"exceptions_list": [
{
"id": "endpoint_list",
"list_id": "endpoint_list",
"namespace_type": "agnostic",
"type": "endpoint"
}
],
"from": "now-10m",
"index": [
"logs-endpoint.alerts-*"
],
"index": ["logs-endpoint.alerts-*"],
"language": "kuery",
"license": "Elastic License",
"max_signals": 10000,
@ -57,10 +54,7 @@
"value": "99"
}
],
"tags": [
"Elastic",
"Endpoint"
],
"tags": ["Elastic", "Endpoint"],
"timestamp_override": "event.ingested",
"type": "query",
"version": 1

View file

@ -1,12 +1,9 @@
{
"author": [
"Elastic"
],
"author": ["Elastic"],
"description": "Generates a detection alert for each external alert written to the configured indices. Enabling this rule allows you to immediately begin investigating external alerts in the app.",
"index": [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
@ -54,9 +51,7 @@
"value": "99"
}
],
"tags": [
"Elastic"
],
"tags": ["Elastic"],
"timestamp_override": "event.ingested",
"type": "query",
"version": 1

View file

@ -716,26 +716,31 @@ describe('utils', () => {
describe('#getExceptions', () => {
test('it successfully returns array of exception list items', async () => {
listMock.getExceptionListClient = () =>
(({
findExceptionListsItem: jest.fn().mockResolvedValue({
data: [getExceptionListItemSchemaMock()],
page: 1,
per_page: 10000,
total: 1,
}),
} as unknown) as ExceptionListClient);
const client = listMock.getExceptionListClient();
const exceptions = await getExceptions({
client,
lists: getListArrayMock(),
});
expect(client.getExceptionList).toHaveBeenNthCalledWith(1, {
id: 'some_uuid',
listId: undefined,
namespaceType: 'single',
expect(client.findExceptionListsItem).toHaveBeenCalledWith({
listId: ['list_id_single', 'endpoint_list'],
namespaceType: ['single', 'agnostic'],
page: 1,
perPage: 10000,
filter: [],
sortOrder: undefined,
sortField: undefined,
});
expect(client.getExceptionList).toHaveBeenNthCalledWith(2, {
id: 'some_uuid',
listId: undefined,
namespaceType: 'agnostic',
});
expect(exceptions).toEqual([
getExceptionListItemSchemaMock(),
getExceptionListItemSchemaMock(),
]);
expect(exceptions).toEqual([getExceptionListItemSchemaMock()]);
});
test('it throws if "client" is undefined', async () => {
@ -747,7 +752,7 @@ describe('utils', () => {
).rejects.toThrowError('lists plugin unavailable during rule execution');
});
test('it returns empty array if no "lists" is undefined', async () => {
test('it returns empty array if "lists" is undefined', async () => {
const exceptions = await getExceptions({
client: listMock.getExceptionListClient(),
lists: undefined,
@ -771,11 +776,11 @@ describe('utils', () => {
).rejects.toThrowError('unable to fetch exception list items');
});
test('it throws if "findExceptionListItem" fails', async () => {
test('it throws if "findExceptionListsItem" fails', async () => {
const err = new Error('error fetching list');
listMock.getExceptionListClient = () =>
(({
findExceptionListItem: jest.fn().mockRejectedValue(err),
findExceptionListsItem: jest.fn().mockRejectedValue(err),
} as unknown) as ExceptionListClient);
await expect(() =>
@ -786,24 +791,10 @@ describe('utils', () => {
).rejects.toThrowError('unable to fetch exception list items');
});
test('it returns empty array if "getExceptionList" returns null', async () => {
test('it returns empty array if "findExceptionListsItem" returns null', async () => {
listMock.getExceptionListClient = () =>
(({
getExceptionList: jest.fn().mockResolvedValue(null),
} as unknown) as ExceptionListClient);
const exceptions = await getExceptions({
client: listMock.getExceptionListClient(),
lists: undefined,
});
expect(exceptions).toEqual([]);
});
test('it returns empty array if "findExceptionListItem" returns null', async () => {
listMock.getExceptionListClient = () =>
(({
findExceptionListItem: jest.fn().mockResolvedValue(null),
findExceptionListsItem: jest.fn().mockResolvedValue(null),
} as unknown) as ExceptionListClient);
const exceptions = await getExceptions({

View file

@ -161,43 +161,20 @@ export const getExceptions = async ({
throw new Error('lists plugin unavailable during rule execution');
}
if (lists != null) {
if (lists != null && lists.length > 0) {
try {
// Gather all exception items of all exception lists linked to rule
const exceptions = await Promise.all(
lists
.map(async (list) => {
const { id, namespace_type: namespaceType } = list;
try {
// TODO update once exceptions client `findExceptionListItem`
// accepts an array of list ids
const foundList = await client.getExceptionList({
id,
namespaceType,
listId: undefined,
});
if (foundList == null) {
return [];
} else {
const items = await client.findExceptionListItem({
listId: foundList.list_id,
namespaceType,
page: 1,
perPage: MAX_EXCEPTION_LIST_SIZE,
filter: undefined,
sortOrder: undefined,
sortField: undefined,
});
return items != null ? items.data : [];
}
} catch {
throw new Error('unable to fetch exception list items');
}
})
.flat()
);
return exceptions.flat();
const listIds = lists.map(({ list_id: listId }) => listId);
const namespaceTypes = lists.map(({ namespace_type: namespaceType }) => namespaceType);
const items = await client.findExceptionListsItem({
listId: listIds,
namespaceType: namespaceTypes,
page: 1,
perPage: MAX_EXCEPTION_LIST_SIZE,
filter: [],
sortOrder: undefined,
sortField: undefined,
});
return items != null ? items.data : [];
} catch {
throw new Error('unable to fetch exception list items');
}