[Security Solution][Exceptions] - Tie server and client code together (#70918)

## Summary

This PR tries to start to tie together the server and client changes for exceptions lists. 

- Updates graphql types to allow UI access to a rule's `exceptions_list` property
- Updates the exception viewer component to now dynamically take the rule `exceptions_list`, up until now we just had an empty array in it's place
- Updates the viewer logic to check if a rule has an endpoint list associated with it. If it does, then it displays both detections and endpoint UIs (in the viewer), if it does not, then it only displays the detections UI
- Updates the viewer UI to better deal with spacing when an exception list item only has one or two entries (before the and badge with the antennas was stretching passed the exception items to fill the space)
- Updates the detections engine exceptions logic to fetch list items using an exception list's `id` as opposed to it's `list_id`, this now aligns with the UI using the same params on its end
- Adds exception list `type` to information kept by the rule for exception lists
- Updates the exception list type from `string` to `endpoint | detection`
- Updates the exception list _item_ type from `string` to `simple`
- Adds unit tests for the detection engine server side util that fetches the exception list items
This commit is contained in:
Yara Tercero 2020-07-07 15:49:43 -04:00 committed by GitHub
parent aeff8c154b
commit 37c2c925d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 417 additions and 146 deletions

View file

@ -9,7 +9,7 @@ import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../siem_common_deps';
import { operator, operator_type as operatorType } from './schemas';
import { exceptionListType, operator, operator_type as operatorType } from './schemas';
describe('Common schemas', () => {
describe('operatorType', () => {
@ -91,4 +91,35 @@ describe('Common schemas', () => {
expect(keys.length).toEqual(2);
});
});
describe('exceptionListType', () => {
test('it should validate for "detection"', () => {
const payload = 'detection';
const decoded = exceptionListType.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate for "endpoint"', () => {
const payload = 'endpoint';
const decoded = exceptionListType.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should contain 2 keys', () => {
// Might seem like a weird test, but its meant to
// ensure that if exceptionListType is updated, you
// also update the ExceptionListTypeEnum, a workaround
// for io-ts not yet supporting enums
// https://github.com/gcanti/io-ts/issues/67
const keys = Object.keys(exceptionListType.keys);
expect(keys.length).toEqual(2);
});
});
});

View file

@ -73,15 +73,19 @@ export type _Tags = t.TypeOf<typeof _tags>;
export const _tagsOrUndefined = t.union([_tags, t.undefined]);
export type _TagsOrUndefined = t.TypeOf<typeof _tagsOrUndefined>;
// TODO: Change this into a t.keyof enumeration when we know what types of lists we going to have.
export const exceptionListType = t.string;
export const exceptionListType = t.keyof({ detection: null, endpoint: null });
export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]);
export type ExceptionListType = t.TypeOf<typeof exceptionListType>;
export type ExceptionListTypeOrUndefined = t.TypeOf<typeof exceptionListTypeOrUndefined>;
export enum ExceptionListTypeEnum {
DETECTION = 'detection',
ENDPOINT = 'endpoint',
}
// TODO: Change this into a t.keyof enumeration when we know what types of lists we going to have.
export const exceptionListItemType = t.string;
export const exceptionListItemType = t.keyof({ simple: null });
export const exceptionListItemTypeOrUndefined = t.union([exceptionListItemType, t.undefined]);
export type ExceptionListItemType = t.TypeOf<typeof exceptionListItemType>;
export type ExceptionListItemTypeOrUndefined = t.TypeOf<typeof exceptionListItemTypeOrUndefined>;
export const list_type = t.keyof({ item: null, list: null });
export type ListType = t.TypeOf<typeof list_type>;

View file

@ -342,7 +342,7 @@ describe('Exceptions Lists API', () => {
});
test('it returns error if response payload fails decode', async () => {
const badPayload = getExceptionListItemSchemaMock();
const badPayload = getExceptionListSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);

View file

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

View file

@ -8,7 +8,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api';
import { ExceptionIdentifiers, ExceptionList, Pagination, UseExceptionListProps } from '../types';
import { ExceptionListItemSchema } from '../../../common/schemas';
import { ExceptionListItemSchema, NamespaceType } from '../../../common/schemas';
type Func = () => void;
export type ReturnExceptionListAndItems = [
@ -73,7 +73,13 @@ export const useExceptionList = ({
let exceptions: ExceptionListItemSchema[] = [];
let exceptionListsReturned: ExceptionList[] = [];
const fetchData = async ({ id, namespaceType }: ExceptionIdentifiers): Promise<void> => {
const fetchData = async ({
id,
namespaceType,
}: {
id: string;
namespaceType: NamespaceType;
}): Promise<void> => {
try {
setLoading(true);

View file

@ -9,6 +9,7 @@ import {
CreateExceptionListSchema,
ExceptionListItemSchema,
ExceptionListSchema,
ExceptionListType,
NamespaceType,
Page,
PerPage,
@ -60,7 +61,7 @@ export interface UseExceptionListProps {
export interface ExceptionIdentifiers {
id: string;
namespaceType: NamespaceType;
type?: string;
type: ExceptionListType;
}
export interface ApiCallByListIdProps {

View file

@ -12,7 +12,7 @@
"field": "event.module",
"operator": "excluded",
"type": "match_any",
"value": ["zeek"]
"value": ["suricata"]
},
{
"field": "source.ip",

View file

@ -1,5 +1,5 @@
{
"id": "hand_inserted_item_id",
"list_id": "list-ip",
"value": "10.4.2.140"
"value": "10.4.3.11"
}

View file

@ -12,8 +12,8 @@ import {
Description,
EntriesArray,
ExceptionListItemSchema,
ExceptionListItemType,
ExceptionListSoSchema,
ExceptionListType,
ItemId,
ListId,
MetaOrUndefined,
@ -43,7 +43,7 @@ interface CreateExceptionListItemOptions {
user: string;
tags: Tags;
tieBreaker?: string;
type: ExceptionListType;
type: ExceptionListItemType;
}
export const createExceptionListItem = async ({

View file

@ -12,6 +12,8 @@ import {
DescriptionOrUndefined,
EntriesArray,
EntriesArrayOrUndefined,
ExceptionListItemType,
ExceptionListItemTypeOrUndefined,
ExceptionListType,
ExceptionListTypeOrUndefined,
FilterOrUndefined,
@ -98,7 +100,7 @@ export interface CreateExceptionListItemOptions {
description: Description;
meta: MetaOrUndefined;
tags: Tags;
type: ExceptionListType;
type: ExceptionListItemType;
}
export interface UpdateExceptionListItemOptions {
@ -112,7 +114,7 @@ export interface UpdateExceptionListItemOptions {
description: DescriptionOrUndefined;
meta: MetaOrUndefined;
tags: TagsOrUndefined;
type: ExceptionListTypeOrUndefined;
type: ExceptionListItemTypeOrUndefined;
}
export interface FindExceptionListItemOptions {

View file

@ -10,8 +10,8 @@ import {
DescriptionOrUndefined,
EntriesArrayOrUndefined,
ExceptionListItemSchema,
ExceptionListItemTypeOrUndefined,
ExceptionListSoSchema,
ExceptionListTypeOrUndefined,
IdOrUndefined,
ItemIdOrUndefined,
MetaOrUndefined,
@ -43,7 +43,7 @@ interface UpdateExceptionListItemOptions {
user: string;
tags: TagsOrUndefined;
tieBreaker?: string;
type: ExceptionListTypeOrUndefined;
type: ExceptionListItemTypeOrUndefined;
}
export const updateExceptionListItem = async ({

View file

@ -21,6 +21,8 @@ import {
NamespaceType,
UpdateCommentsArrayOrUndefined,
comments as commentsSchema,
exceptionListItemType,
exceptionListType,
} from '../../../common/schemas';
import {
SavedObjectType,
@ -80,7 +82,7 @@ export const transformSavedObjectToExceptionList = ({
namespace_type: namespaceType,
tags,
tie_breaker_id,
type,
type: exceptionListType.is(type) ? type : 'detection',
updated_at: updatedAt ?? dateNow,
updated_by,
};
@ -116,7 +118,7 @@ export const transformSavedObjectUpdateToExceptionList = ({
namespace_type: namespaceType,
tags: tags ?? exceptionList.tags,
tie_breaker_id: exceptionList.tie_breaker_id,
type: type ?? exceptionList.type,
type: exceptionListType.is(type) ? type : exceptionList.type,
updated_at: updatedAt ?? dateNow,
updated_by: updatedBy ?? exceptionList.updated_by,
};
@ -168,7 +170,7 @@ export const transformSavedObjectToExceptionListItem = ({
namespace_type: namespaceType,
tags,
tie_breaker_id,
type,
type: exceptionListItemType.is(type) ? type : 'simple',
updated_at: updatedAt ?? dateNow,
updated_by,
};
@ -202,6 +204,8 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
// TODO: Change this to do a decode and throw if the saved object is not as expected.
// TODO: Do a throw if after the decode this is not the correct "list_type: list"
// TODO: Update exception list and item types (perhaps separating out) so as to avoid
// defaulting
return {
_tags: _tags ?? exceptionListItem._tags,
comments: comments ?? exceptionListItem.comments,
@ -217,7 +221,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
namespace_type: namespaceType,
tags: tags ?? exceptionListItem.tags,
tie_breaker_id: exceptionListItem.tie_breaker_id,
type: type ?? exceptionListItem.type,
type: exceptionListItemType.is(type) ? type : exceptionListItem.type,
updated_at: updatedAt ?? dateNow,
updated_by: updatedBy ?? exceptionListItem.updated_by,
};

View file

@ -16,7 +16,6 @@ import {
entriesExists,
entriesMatch,
entriesNested,
entriesList,
ExceptionListItemSchema,
} from '../../../lists/common/schemas';
import { Language, Query } from './schemas/common/schemas';
@ -182,7 +181,7 @@ export const buildExceptionItemEntries = ({
}): string => {
const and = getLanguageBooleanOperator({ language, value: 'and' });
const exceptionItem = lists
.filter((t) => !entriesList.is(t))
.filter(({ type }) => type !== 'list')
.reduce<string[]>((accum, listItem) => {
const exceptionSegment = evaluateValues({ item: listItem, language });
return [...accum, exceptionSegment];
@ -200,7 +199,7 @@ export const buildQueryExceptions = ({
language: Language;
lists: ExceptionListItemSchema[] | undefined;
}): DataQuery[] => {
if (lists && lists !== null) {
if (lists != null) {
const exceptions = lists.map((exceptionItem) =>
buildExceptionItemEntries({ lists: exceptionItem.entries, language })
);

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { EntriesArray, namespaceType } from '../../../lists/common/schemas';
export { EntriesArray, exceptionListType, namespaceType } from '../../../lists/common/schemas';

View file

@ -1447,10 +1447,12 @@ describe('add prepackaged rules schema', () => {
{
id: 'some_uuid',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
namespace_type: 'agnostic',
type: 'endpoint',
},
],
};
@ -1533,6 +1535,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,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);
expect(message.schema).toEqual({});

View file

@ -1514,10 +1514,12 @@ describe('create rules schema', () => {
{
id: 'some_uuid',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
namespace_type: 'agnostic',
type: 'endpoint',
},
],
};
@ -1598,6 +1600,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,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);
expect(message.schema).toEqual({});

View file

@ -1643,10 +1643,12 @@ describe('import rules schema', () => {
{
id: 'some_uuid',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
namespace_type: 'agnostic',
type: 'endpoint',
},
],
};
@ -1728,6 +1730,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,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);
expect(message.schema).toEqual({});

View file

@ -1177,10 +1177,12 @@ describe('patch_rules_schema', () => {
{
id: 'some_uuid',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
namespace_type: 'agnostic',
type: 'endpoint',
},
],
};
@ -1249,6 +1251,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,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

@ -1449,10 +1449,12 @@ describe('update rules schema', () => {
{
id: 'some_uuid',
namespace_type: 'single',
type: 'detection',
},
{
id: 'some_uuid',
namespace_type: 'agnostic',
type: 'endpoint',
},
],
};
@ -1532,6 +1534,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,type"',
'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"',
]);
expect(message.schema).toEqual({});

View file

@ -8,11 +8,13 @@ import { List, ListArray } from './lists';
export const getListMock = (): List => ({
id: 'some_uuid',
namespace_type: 'single',
type: 'detection',
});
export const getListAgnosticMock = (): List => ({
id: 'some_uuid',
namespace_type: 'agnostic',
type: 'endpoint',
});
export const getListArrayMock = (): ListArray => [getListMock(), getListAgnosticMock()];

View file

@ -30,7 +30,7 @@ describe('Lists', () => {
expect(message.schema).toEqual(payload);
});
test('it should validate a list with "namespace_type" of"agnostic"', () => {
test('it should validate a list with "namespace_type" of "agnostic"', () => {
const payload = getListAgnosticMock();
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, namespace_type: "agnostic" | "single" |}>"',
'Invalid value "1" supplied to "Array<{| id: string, 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, namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "[1]" supplied to "(Array<{| id: string, 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: string, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"',
]);
expect(message.schema).toEqual({});
});

View file

@ -6,11 +6,12 @@
import * as t from 'io-ts';
import { namespaceType } from '../../lists_common_deps';
import { exceptionListType, namespaceType } from '../../lists_common_deps';
export const list = t.exact(
t.type({
id: t.string,
type: exceptionListType,
namespace_type: namespaceType,
})
);

View file

@ -18,6 +18,10 @@ import {
timestamp_override,
} from '../../../../../common/detection_engine/schemas/common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
import {
listArray,
listArrayOrUndefined,
} from '../../../../../common/detection_engine/schemas/types';
/**
* Params is an "record", since it is a type of AlertActionParams which is action templates.
@ -64,6 +68,7 @@ export const NewRuleSchema = t.intersection([
to: t.string,
updated_by: t.string,
note: t.string,
exceptions_list: listArrayOrUndefined,
}),
]);
@ -135,6 +140,7 @@ export const RuleSchema = t.intersection([
timeline_title: t.string,
timestamp_override,
note: t.string,
exceptions_list: listArray,
version: t.number,
}),
]);

View file

@ -72,7 +72,7 @@ import { SecurityPageName } from '../../../../../app/types';
import { LinkButton } from '../../../../../common/components/links';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer';
import { ExceptionListType } from '../../../../../common/components/exceptions/types';
import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../lists_plugin_deps';
enum RuleDetailTabs {
alerts = 'alerts',
@ -254,6 +254,34 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
const { indicesExist, indexPattern } = useWithSource('default', indexToAdd);
const exceptionLists = useMemo((): {
lists: ExceptionIdentifiers[];
allowedExceptionListTypes: ExceptionListTypeEnum[];
} => {
if (rule != null && rule.exceptions_list != null) {
return rule.exceptions_list.reduce<{
lists: ExceptionIdentifiers[];
allowedExceptionListTypes: ExceptionListTypeEnum[];
}>(
(acc, { 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 }],
allowedExceptionListTypes: shouldAddEndpoint
? [...allowedExceptionListTypes, ExceptionListTypeEnum.ENDPOINT]
: allowedExceptionListTypes,
};
},
{ lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] }
);
} else {
return { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] };
}
}, [rule]);
if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) {
history.replace(getDetectionEngineUrl());
return null;
@ -411,12 +439,9 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
{ruleDetailTab === RuleDetailTabs.exceptions && (
<ExceptionsViewer
ruleId={ruleId ?? ''}
availableListTypes={[
ExceptionListType.DETECTION_ENGINE,
ExceptionListType.ENDPOINT,
]}
availableListTypes={exceptionLists.allowedExceptionListTypes}
commentsAccordionId={'ruleDetailsTabExceptions'}
exceptionListsMeta={[]}
exceptionListsMeta={exceptionLists.lists}
/>
)}
{ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />}

View file

@ -11,6 +11,7 @@ import {
Entry,
ExceptionListItemSchema,
CreateExceptionListItemSchema,
NamespaceType,
OperatorTypeEnum,
OperatorEnum,
} from '../../../lists_plugin_deps';
@ -27,9 +28,9 @@ export interface DescriptionListItem {
description: NonNullable<ReactNode>;
}
export enum ExceptionListType {
DETECTION_ENGINE = 'detection',
ENDPOINT = 'endpoint',
export interface ExceptionListItemIdentifiers {
id: string;
namespaceType: NamespaceType;
}
export interface FilterOptions {

View file

@ -48,6 +48,10 @@ const MyAndOrBadgeContainer = styled(EuiFlexItem)`
padding-bottom: ${({ theme }) => theme.eui.euiSizeS};
`;
const MyActionButton = styled(EuiFlexItem)`
align-self: flex-end;
`;
interface ExceptionEntriesComponentProps {
entries: FormattedEntry[];
disableDelete: boolean;
@ -126,7 +130,7 @@ const ExceptionEntriesComponent = ({
return (
<MyEntriesDetails grow={5}>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="none">
{entries.length > 1 && (
<EuiHideFor sizes={['xs', 's']}>
@ -150,9 +154,9 @@ const ExceptionEntriesComponent = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={1}>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<MyActionButton grow={false}>
<MyEditButton
size="s"
color="primary"
@ -162,8 +166,8 @@ const ExceptionEntriesComponent = ({
>
{i18n.EDIT}
</MyEditButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
</MyActionButton>
<MyActionButton grow={false}>
<MyRemoveButton
size="s"
color="danger"
@ -173,7 +177,7 @@ const ExceptionEntriesComponent = ({
>
{i18n.REMOVE}
</MyRemoveButton>
</EuiFlexItem>
</MyActionButton>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -18,11 +18,8 @@ import styled from 'styled-components';
import { ExceptionDetails } from './exception_details';
import { ExceptionEntries } from './exception_entries';
import { getFormattedEntries, getFormattedComments } from '../../helpers';
import { FormattedEntry } from '../../types';
import {
ExceptionIdentifiers,
ExceptionListItemSchema,
} from '../../../../../../public/lists_plugin_deps';
import { FormattedEntry, ExceptionListItemIdentifiers } from '../../types';
import { ExceptionListItemSchema } from '../../../../../../public/lists_plugin_deps';
const MyFlexItem = styled(EuiFlexItem)`
&.comments--show {
@ -32,10 +29,10 @@ const MyFlexItem = styled(EuiFlexItem)`
`;
interface ExceptionItemProps {
loadingItemIds: ExceptionIdentifiers[];
loadingItemIds: ExceptionListItemIdentifiers[];
exceptionItem: ExceptionListItemSchema;
commentsAccordionId: string;
onDeleteException: (arg: ExceptionIdentifiers) => void;
onDeleteException: (arg: ExceptionListItemIdentifiers) => void;
onEditException: (item: ExceptionListItemSchema) => void;
}
@ -55,8 +52,11 @@ const ExceptionItemComponent = ({
}, [exceptionItem.entries]);
const handleDelete = useCallback((): void => {
onDeleteException({ id: exceptionItem.id, namespaceType: exceptionItem.namespace_type });
}, [onDeleteException, exceptionItem]);
onDeleteException({
id: exceptionItem.id,
namespaceType: exceptionItem.namespace_type,
});
}, [onDeleteException, exceptionItem.id, exceptionItem.namespace_type]);
const handleEdit = useCallback((): void => {
onEditException(exceptionItem);
@ -68,10 +68,10 @@ const ExceptionItemComponent = ({
const formattedComments = useMemo((): EuiCommentProps[] => {
return getFormattedComments(exceptionItem.comments);
}, [exceptionItem]);
}, [exceptionItem.comments]);
const disableDelete = useMemo((): boolean => {
const foundItems = loadingItemIds.filter((t) => t.id === exceptionItem.id);
const foundItems = loadingItemIds.filter(({ id }) => id === exceptionItem.id);
return foundItems.length > 0;
}, [loadingItemIds, exceptionItem.id]);

View file

@ -10,7 +10,7 @@ import { ThemeProvider } from 'styled-components';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionsViewerHeader } from './exceptions_viewer_header';
import { ExceptionListType } from '../types';
import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps';
addDecorator((storyFn) => (
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
@ -23,7 +23,7 @@ storiesOf('Components|ExceptionsViewerHeader', module)
isInitLoading={true}
detectionsListItems={5}
endpointListItems={2000}
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
onFilterChange={action('onClick')}
onAddExceptionClick={action('onClick')}
/>
@ -35,7 +35,7 @@ storiesOf('Components|ExceptionsViewerHeader', module)
isInitLoading={false}
detectionsListItems={5}
endpointListItems={2000}
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
onFilterChange={action('onClick')}
onAddExceptionClick={action('onClick')}
/>
@ -47,7 +47,7 @@ storiesOf('Components|ExceptionsViewerHeader', module)
isInitLoading={false}
detectionsListItems={0}
endpointListItems={2000}
supportedListTypes={[ExceptionListType.DETECTION_ENGINE]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION]}
onFilterChange={action('onClick')}
onAddExceptionClick={action('onClick')}
/>
@ -59,7 +59,7 @@ storiesOf('Components|ExceptionsViewerHeader', module)
isInitLoading={false}
detectionsListItems={5}
endpointListItems={0}
supportedListTypes={[ExceptionListType.DETECTION_ENGINE]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION]}
onFilterChange={action('onClick')}
onAddExceptionClick={action('onClick')}
/>

View file

@ -10,14 +10,14 @@ import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionsViewerHeader } from './exceptions_viewer_header';
import { ExceptionListType } from '../types';
import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps';
describe('ExceptionsViewerHeader', () => {
it('it renders all disabled if "isInitLoading" is true', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={true}
detectionsListItems={0}
endpointListItems={0}
@ -48,7 +48,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -68,7 +68,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -88,7 +88,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -108,7 +108,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -148,7 +148,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -188,7 +188,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -208,7 +208,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -228,7 +228,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -251,7 +251,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}
@ -274,7 +274,7 @@ describe('ExceptionsViewerHeader', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewerHeader
supportedListTypes={[ExceptionListType.DETECTION_ENGINE, ExceptionListType.ENDPOINT]}
supportedListTypes={[ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT]}
isInitLoading={false}
detectionsListItems={0}
endpointListItems={0}

View file

@ -18,15 +18,16 @@ import {
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import * as i18n from '../translations';
import { ExceptionListType, Filter } from '../types';
import { Filter } from '../types';
import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps';
interface ExceptionsViewerHeaderProps {
isInitLoading: boolean;
supportedListTypes: ExceptionListType[];
supportedListTypes: ExceptionListTypeEnum[];
detectionsListItems: number;
endpointListItems: number;
onFilterChange: (arg: Filter) => void;
onAddExceptionClick: (type: ExceptionListType) => void;
onAddExceptionClick: (type: ExceptionListTypeEnum) => void;
}
/**
@ -85,7 +86,7 @@ const ExceptionsViewerHeaderComponent = ({
);
const onAddException = useCallback(
(type: ExceptionListType): void => {
(type: ExceptionListTypeEnum): void => {
onAddExceptionClick(type);
setAddExceptionMenuOpen(false);
},
@ -99,12 +100,12 @@ const ExceptionsViewerHeaderComponent = ({
items: [
{
name: i18n.ADD_TO_ENDPOINT_LIST,
onClick: () => onAddException(ExceptionListType.ENDPOINT),
onClick: () => onAddException(ExceptionListTypeEnum.ENDPOINT),
'data-test-subj': 'addEndpointExceptionBtn',
},
{
name: i18n.ADD_TO_DETECTIONS_LIST,
onClick: () => onAddException(ExceptionListType.DETECTION_ENGINE),
onClick: () => onAddException(ExceptionListTypeEnum.DETECTION),
'data-test-subj': 'addDetectionsExceptionBtn',
},
],

View file

@ -11,10 +11,8 @@ import styled from 'styled-components';
import * as i18n from '../translations';
import { ExceptionItem } from './exception_item';
import { AndOrBadge } from '../../and_or_badge';
import {
ExceptionIdentifiers,
ExceptionListItemSchema,
} from '../../../../../public/lists_plugin_deps';
import { ExceptionListItemSchema } from '../../../../../public/lists_plugin_deps';
import { ExceptionListItemIdentifiers } from '../types';
const MyFlexItem = styled(EuiFlexItem)`
margin: ${({ theme }) => `${theme.eui.euiSize} 0`};
@ -37,9 +35,9 @@ interface ExceptionsViewerItemsProps {
showEmpty: boolean;
isInitLoading: boolean;
exceptions: ExceptionListItemSchema[];
loadingItemIds: ExceptionIdentifiers[];
loadingItemIds: ExceptionListItemIdentifiers[];
commentsAccordionId: string;
onDeleteException: (arg: ExceptionIdentifiers) => void;
onDeleteException: (arg: ExceptionListItemIdentifiers) => void;
onEditExceptionItem: (item: ExceptionListItemSchema) => void;
}

View file

@ -10,9 +10,12 @@ import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionsViewer } from './';
import { ExceptionListType } from '../types';
import { useKibana } from '../../../../common/lib/kibana';
import { useExceptionList, useApi } from '../../../../../public/lists_plugin_deps';
import {
ExceptionListTypeEnum,
useExceptionList,
useApi,
} from '../../../../../public/lists_plugin_deps';
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
jest.mock('../../../../common/lib/kibana');
@ -69,7 +72,7 @@ describe('ExceptionsViewer', () => {
namespaceType: 'single',
},
]}
availableListTypes={[ExceptionListType.DETECTION_ENGINE]}
availableListTypes={[ExceptionListTypeEnum.DETECTION]}
commentsAccordionId="commentsAccordion"
/>
</ThemeProvider>
@ -84,7 +87,7 @@ describe('ExceptionsViewer', () => {
<ExceptionsViewer
ruleId={'123'}
exceptionListsMeta={[]}
availableListTypes={[ExceptionListType.DETECTION_ENGINE]}
availableListTypes={[ExceptionListTypeEnum.DETECTION]}
commentsAccordionId="commentsAccordion"
/>
</ThemeProvider>
@ -117,7 +120,7 @@ describe('ExceptionsViewer', () => {
namespaceType: 'single',
},
]}
availableListTypes={[ExceptionListType.DETECTION_ENGINE]}
availableListTypes={[ExceptionListTypeEnum.DETECTION]}
commentsAccordionId="commentsAccordion"
/>
</ThemeProvider>

View file

@ -14,11 +14,12 @@ import { useKibana } from '../../../../common/lib/kibana';
import { Panel } from '../../../../common/components/panel';
import { Loader } from '../../../../common/components/loader';
import { ExceptionsViewerHeader } from './exceptions_viewer_header';
import { ExceptionListType, Filter } from '../types';
import { ExceptionListItemIdentifiers, Filter } from '../types';
import { allExceptionItemsReducer, State } from './reducer';
import {
useExceptionList,
ExceptionIdentifiers,
ExceptionListTypeEnum,
ExceptionListItemSchema,
UseExceptionListSuccess,
useApi,
@ -54,7 +55,7 @@ enum ModalAction {
interface ExceptionsViewerProps {
ruleId: string;
exceptionListsMeta: ExceptionIdentifiers[];
availableListTypes: ExceptionListType[];
availableListTypes: ExceptionListTypeEnum[];
commentsAccordionId: string;
onAssociateList?: (listId: string) => void;
}
@ -159,7 +160,7 @@ const ExceptionsViewerComponent = ({
);
const handleAddException = useCallback(
(type: ExceptionListType): void => {
(type: ExceptionListTypeEnum): void => {
setIsModalOpen(true);
},
[setIsModalOpen]
@ -179,7 +180,7 @@ const ExceptionsViewerComponent = ({
[setIsModalOpen]
);
const onCloseExceptionModal = useCallback(
const handleCloseExceptionModal = useCallback(
({ actionType, listId }): void => {
setIsModalOpen(false);
@ -195,7 +196,7 @@ const ExceptionsViewerComponent = ({
);
const setLoadingItemIds = useCallback(
(items: ExceptionIdentifiers[]): void => {
(items: ExceptionListItemIdentifiers[]): void => {
dispatch({
type: 'updateLoadingItemIds',
items,
@ -205,14 +206,14 @@ const ExceptionsViewerComponent = ({
);
const handleDeleteException = useCallback(
({ id, namespaceType }: ExceptionIdentifiers) => {
setLoadingItemIds([{ id, namespaceType }]);
({ id: itemId, namespaceType }: ExceptionListItemIdentifiers) => {
setLoadingItemIds([{ id: itemId, namespaceType }]);
deleteExceptionItem({
id,
id: itemId,
namespaceType,
onSuccess: () => {
setLoadingItemIds(loadingItemIds.filter((t) => t.id !== id));
setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId));
handleFetchList();
},
onError: () => {
@ -223,7 +224,7 @@ const ExceptionsViewerComponent = ({
});
dispatchToasterError();
setLoadingItemIds(loadingItemIds.filter((t) => t.id !== id));
setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId));
},
});
},
@ -255,7 +256,7 @@ const ExceptionsViewerComponent = ({
<>
{isModalOpen && (
<EuiOverlayMask>
<EuiModal onClose={onCloseExceptionModal}>
<EuiModal onClose={handleCloseExceptionModal}>
<EuiModalBody>
<EuiCodeBlock language="json" fontSize="m" paddingSize="m" overflowHeight={300}>
{`Modal goes here`}

View file

@ -3,7 +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 { FilterOptions, ExceptionsPagination } from '../types';
import { FilterOptions, ExceptionsPagination, ExceptionListItemIdentifiers } from '../types';
import {
ExceptionList,
ExceptionListItemSchema,
@ -20,7 +20,7 @@ export interface State {
exceptions: ExceptionListItemSchema[];
exceptionToEdit: ExceptionListItemSchema | null;
loadingLists: ExceptionIdentifiers[];
loadingItemIds: ExceptionIdentifiers[];
loadingItemIds: ExceptionListItemIdentifiers[];
isInitLoading: boolean;
isModalOpen: boolean;
}
@ -41,7 +41,7 @@ export type Action =
| { type: 'updateIsInitLoading'; loading: boolean }
| { type: 'updateModalOpen'; isOpen: boolean }
| { type: 'updateExceptionToEdit'; exception: ExceptionListItemSchema }
| { type: 'updateLoadingItemIds'; items: ExceptionIdentifiers[] };
| { type: 'updateLoadingItemIds'; items: ExceptionListItemIdentifiers[] };
export const allExceptionItemsReducer = () => (state: State, action: Action): State => {
switch (action.type) {

View file

@ -4749,6 +4749,14 @@
"type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "exceptions_list",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "ToAny", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,

View file

@ -1044,6 +1044,8 @@ export interface RuleField {
version?: Maybe<string[]>;
note?: Maybe<string[]>;
exceptions_list?: Maybe<ToAny>;
}
export interface SuricataEcsFields {
@ -5032,6 +5034,8 @@ export namespace GetTimelineQuery {
filters: Maybe<ToAny>;
note: Maybe<string[]>;
exceptions_list: Maybe<ToAny>;
};
export type Suricata = {

View file

@ -31,6 +31,7 @@ export {
OperatorEnum,
OperatorType,
OperatorTypeEnum,
ExceptionListTypeEnum,
exceptionListItemSchema,
createExceptionListItemSchema,
listSchema,

View file

@ -210,6 +210,7 @@ export const timelineQuery = gql`
to
filters
note
exceptions_list
}
}
suricata {

View file

@ -416,6 +416,7 @@ export const ecsSchema = gql`
updated_by: ToStringArray
version: ToStringArray
note: ToStringArray
exceptions_list: ToAny
}
type SignalField {

View file

@ -1046,6 +1046,8 @@ export interface RuleField {
version?: Maybe<string[] | string>;
note?: Maybe<string[] | string>;
exceptions_list?: Maybe<ToAny>;
}
export interface SuricataEcsFields {
@ -4907,6 +4909,8 @@ export namespace RuleFieldResolvers {
version?: VersionResolver<Maybe<string[] | string>, TypeParent, TContext>;
note?: NoteResolver<Maybe<string[] | string>, TypeParent, TContext>;
exceptions_list?: ExceptionsListResolver<Maybe<ToAny>, TypeParent, TContext>;
}
export type IdResolver<
@ -5064,6 +5068,11 @@ export namespace RuleFieldResolvers {
Parent = RuleField,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type ExceptionsListResolver<
R = Maybe<ToAny>,
Parent = RuleField,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
}
export namespace SuricataEcsFieldsResolvers {

View file

@ -12,6 +12,7 @@ import {
import { alertsClientMock } from '../../../../../alerts/server/mocks';
import { getExportAll } from './get_export_all';
import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../feature_flags';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
describe('getExportAll', () => {
beforeAll(() => {
@ -83,10 +84,7 @@ describe('getExportAll', () => {
throttle: 'no_actions',
note: '# Investigative notes',
version: 1,
exceptions_list: [
{ id: 'some_uuid', namespace_type: 'single' },
{ id: 'some_uuid', namespace_type: 'agnostic' },
],
exceptions_list: getListArrayMock(),
})}\n`,
exportDetails: `${JSON.stringify({
exported_count: 1,

View file

@ -13,6 +13,7 @@ import {
import * as readRules from './read_rules';
import { alertsClientMock } from '../../../../../alerts/server/mocks';
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
describe('get_export_by_object_ids', () => {
beforeAll(() => {
@ -91,10 +92,7 @@ describe('get_export_by_object_ids', () => {
throttle: 'no_actions',
note: '# Investigative notes',
version: 1,
exceptions_list: [
{ id: 'some_uuid', namespace_type: 'single' },
{ id: 'some_uuid', namespace_type: 'agnostic' },
],
exceptions_list: getListArrayMock(),
})}\n`,
exportDetails: `${JSON.stringify({
exported_count: 1,
@ -195,10 +193,7 @@ describe('get_export_by_object_ids', () => {
throttle: 'no_actions',
note: '# Investigative notes',
version: 1,
exceptions_list: [
{ id: 'some_uuid', namespace_type: 'single' },
{ id: 'some_uuid', namespace_type: 'agnostic' },
],
exceptions_list: getListArrayMock(),
},
],
};

View file

@ -1,9 +1,15 @@
{
"rule_id": "query-with-list",
"rule_id": "query-with-exceptions",
"exceptions_list": [
{
"id": "ID_HERE",
"namespace_type": "single",
"type": "endpoint"
},
{
"id": "some_updated_fake_id",
"namespace_type": "single"
"id": "ID_HERE",
"namespace_type": "single",
"type": "detection"
}
]
}

View file

@ -1,10 +1,11 @@
{
"name": "Rule w exceptions",
"description": "Sample rule with exception list",
"rule_id": "query-with-exceptions",
"risk_score": 1,
"severity": "high",
"type": "query",
"query": "host.name: *",
"interval": "30s",
"exceptions_list": [{ "id": "endpoint_list", "namespace_type": "single" }]
"exceptions_list": [{ "id": "ID_HERE", "namespace_type": "single", "type": "endpoint" }]
}

View file

@ -48,10 +48,14 @@ export const filterEventsAgainstList = async ({
const filteredHitsEntries = entries
.filter((t): t is EntryList => entriesList.is(t))
.map(async (entry) => {
const { list, field, operator } = entry;
const { id, type } = list;
// acquire the list values we are checking for.
const valuesOfGivenType = eventSearchResult.hits.hits.reduce(
(acc, searchResultItem) => {
const valueField = get(entry.field, searchResultItem._source);
const valueField = get(field, searchResultItem._source);
if (valueField != null && isStringableType(valueField)) {
acc.add(valueField.toString());
}
@ -63,8 +67,8 @@ export const filterEventsAgainstList = async ({
// 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,
listId: id,
type,
value: [...valuesOfGivenType],
});
@ -76,7 +80,6 @@ export const filterEventsAgainstList = async ({
// 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') {

View file

@ -11,6 +11,9 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mock
import { listMock } from '../../../../../lists/server/mocks';
import { EntriesArray } from '../../../../common/detection_engine/lists_common_deps';
import { buildRuleMessageFactory } from './rule_messages';
import { ExceptionListClient } from '../../../../../lists/server';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import * as featureFlags from '../feature_flags';
@ -24,6 +27,7 @@ import {
getListsClient,
hasLargeValueList,
getSignalTimeTuples,
getExceptions,
} from './utils';
import { BulkResponseErrorAggregation } from './types';
import {
@ -54,6 +58,9 @@ describe('utils', () => {
afterEach(() => {
clock.restore();
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
});
describe('generateId', () => {
@ -553,10 +560,6 @@ describe('utils', () => {
alertServices = alertsMock.createAlertServices();
});
afterEach(() => {
jest.clearAllMocks();
});
test('it successfully returns list and exceptions list client', async () => {
jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(true);
@ -732,4 +735,120 @@ describe('utils', () => {
expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(13);
});
});
describe('#getExceptions', () => {
beforeEach(() => {
jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(true);
});
test('it successfully returns array of exception list items', async () => {
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.getExceptionList).toHaveBeenNthCalledWith(2, {
id: 'some_uuid',
listId: undefined,
namespaceType: 'agnostic',
});
expect(exceptions).toEqual([
getExceptionListItemSchemaMock(),
getExceptionListItemSchemaMock(),
]);
});
test('it returns empty array if lists feature flag not turned on', async () => {
jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(false);
const exceptions = await getExceptions({
client: listMock.getExceptionListClient(),
lists: getListArrayMock(),
});
expect(exceptions).toEqual([]);
});
test('it throws if "client" is undefined', async () => {
await expect(() =>
getExceptions({
client: undefined,
lists: getListArrayMock(),
})
).rejects.toThrowError('lists plugin unavailable during rule execution');
});
test('it returns empty array if no "lists" is undefined', async () => {
const exceptions = await getExceptions({
client: listMock.getExceptionListClient(),
lists: undefined,
});
expect(exceptions).toEqual([]);
});
test('it throws if "getExceptionListClient" fails', async () => {
const err = new Error('error fetching list');
listMock.getExceptionListClient = () =>
(({
getExceptionList: jest.fn().mockRejectedValue(err),
} as unknown) as ExceptionListClient);
await expect(() =>
getExceptions({
client: listMock.getExceptionListClient(),
lists: getListArrayMock(),
})
).rejects.toThrowError('unable to fetch exception list items');
});
test('it throws if "findExceptionListItem" fails', async () => {
const err = new Error('error fetching list');
listMock.getExceptionListClient = () =>
(({
findExceptionListItem: jest.fn().mockRejectedValue(err),
} as unknown) as ExceptionListClient);
await expect(() =>
getExceptions({
client: listMock.getExceptionListClient(),
lists: getListArrayMock(),
})
).rejects.toThrowError('unable to fetch exception list items');
});
test('it returns empty array if "getExceptionList" 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),
} as unknown) as ExceptionListClient);
const exceptions = await getExceptions({
client: listMock.getExceptionListClient(),
lists: undefined,
});
expect(exceptions).toEqual([]);
});
});
});

View file

@ -84,24 +84,44 @@ export const getExceptions = async ({
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 : [];
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: 5000,
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();
} catch {
return [];
throw new Error('unable to fetch exception list items');
}
} else {
return [];
}
} else {
return [];
}
};

View file

@ -322,6 +322,7 @@ export const signalFieldsMap: Readonly<Record<string, string>> = {
'signal.rule.updated_by': 'signal.rule.updated_by',
'signal.rule.version': 'signal.rule.version',
'signal.rule.note': 'signal.rule.note',
'signal.rule.exceptions_list': 'signal.rule.exceptions_list',
};
export const ruleFieldsMap: Readonly<Record<string, string>> = {