mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] Consider exceptions when loading threshold alert timelines (#128495)
* Add exceptions to threshold timeline * Tests and error handling * Fix unit tests * Add alias for exceptions filter * Fix tests * Type fixes Co-authored-by: Marshall Main <marshall.main@elastic.co>
This commit is contained in:
parent
dbaf630535
commit
11bba0a04b
15 changed files with 467 additions and 202 deletions
|
@ -47,6 +47,7 @@ const ALERT_RULE_CREATED_AT = `${ALERT_RULE_NAMESPACE}.created_at` as const;
|
|||
const ALERT_RULE_CREATED_BY = `${ALERT_RULE_NAMESPACE}.created_by` as const;
|
||||
const ALERT_RULE_DESCRIPTION = `${ALERT_RULE_NAMESPACE}.description` as const;
|
||||
const ALERT_RULE_ENABLED = `${ALERT_RULE_NAMESPACE}.enabled` as const;
|
||||
const ALERT_RULE_EXCEPTIONS_LIST = `${ALERT_RULE_NAMESPACE}.exceptions_list` as const;
|
||||
const ALERT_RULE_EXECUTION_UUID = `${ALERT_RULE_NAMESPACE}.execution.uuid` as const;
|
||||
const ALERT_RULE_FROM = `${ALERT_RULE_NAMESPACE}.from` as const;
|
||||
const ALERT_RULE_INTERVAL = `${ALERT_RULE_NAMESPACE}.interval` as const;
|
||||
|
@ -104,6 +105,7 @@ const fields = {
|
|||
ALERT_RULE_CREATED_BY,
|
||||
ALERT_RULE_DESCRIPTION,
|
||||
ALERT_RULE_ENABLED,
|
||||
ALERT_RULE_EXCEPTIONS_LIST,
|
||||
ALERT_RULE_EXECUTION_UUID,
|
||||
ALERT_RULE_FROM,
|
||||
ALERT_RULE_INTERVAL,
|
||||
|
@ -158,6 +160,7 @@ export {
|
|||
ALERT_RULE_CREATED_BY,
|
||||
ALERT_RULE_DESCRIPTION,
|
||||
ALERT_RULE_ENABLED,
|
||||
ALERT_RULE_EXCEPTIONS_LIST,
|
||||
ALERT_RULE_EXECUTION_UUID,
|
||||
ALERT_RULE_FROM,
|
||||
ALERT_RULE_INTERVAL,
|
||||
|
|
|
@ -27,7 +27,7 @@ export type ReturnExceptionListAndItems = [
|
|||
];
|
||||
|
||||
/**
|
||||
* Hook for using to get an ExceptionList and it's ExceptionListItems
|
||||
* Hook for using to get an ExceptionList and its ExceptionListItems
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param lists array of ExceptionListIdentifiers for all lists to fetch
|
||||
|
|
|
@ -141,10 +141,12 @@ export const buildExceptionFilter = ({
|
|||
lists,
|
||||
excludeExceptions,
|
||||
chunkSize,
|
||||
alias = null,
|
||||
}: {
|
||||
lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>;
|
||||
excludeExceptions: boolean;
|
||||
chunkSize: number;
|
||||
alias: string | null;
|
||||
}): Filter | undefined => {
|
||||
// Remove exception items with large value lists. These are evaluated
|
||||
// elsewhere for the moment being.
|
||||
|
@ -154,7 +156,7 @@ export const buildExceptionFilter = ({
|
|||
|
||||
const exceptionFilter: Filter = {
|
||||
meta: {
|
||||
alias: null,
|
||||
alias,
|
||||
disabled: false,
|
||||
negate: excludeExceptions,
|
||||
},
|
||||
|
@ -195,7 +197,7 @@ export const buildExceptionFilter = ({
|
|||
|
||||
return {
|
||||
meta: {
|
||||
alias: null,
|
||||
alias,
|
||||
disabled: false,
|
||||
negate: excludeExceptions,
|
||||
},
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('build_exceptions_filter', () => {
|
|||
describe('buildExceptionFilter', () => {
|
||||
test('it should return undefined if no exception items', () => {
|
||||
const booleanFilter = buildExceptionFilter({
|
||||
alias: null,
|
||||
chunkSize: 1,
|
||||
excludeExceptions: false,
|
||||
lists: [],
|
||||
|
@ -54,6 +55,7 @@ describe('build_exceptions_filter', () => {
|
|||
|
||||
test('it should build a filter given an exception list', () => {
|
||||
const booleanFilter = buildExceptionFilter({
|
||||
alias: null,
|
||||
chunkSize: 1,
|
||||
excludeExceptions: false,
|
||||
lists: [getExceptionListItemSchemaMock()],
|
||||
|
@ -109,6 +111,7 @@ describe('build_exceptions_filter', () => {
|
|||
entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }],
|
||||
};
|
||||
const exceptionFilter = buildExceptionFilter({
|
||||
alias: null,
|
||||
chunkSize: 2,
|
||||
excludeExceptions: true,
|
||||
lists: [exceptionItem1, exceptionItem2],
|
||||
|
@ -187,6 +190,7 @@ describe('build_exceptions_filter', () => {
|
|||
entries: [{ field: 'file.path', operator: 'included', type: 'match', value: '/safe/path' }],
|
||||
};
|
||||
const exceptionFilter = buildExceptionFilter({
|
||||
alias: null,
|
||||
chunkSize: 2,
|
||||
excludeExceptions: true,
|
||||
lists: [exceptionItem1, exceptionItem2, exceptionItem3],
|
||||
|
@ -284,6 +288,7 @@ describe('build_exceptions_filter', () => {
|
|||
];
|
||||
|
||||
const booleanFilter = buildExceptionFilter({
|
||||
alias: null,
|
||||
chunkSize: 1,
|
||||
excludeExceptions: true,
|
||||
lists: exceptions,
|
||||
|
|
|
@ -43,6 +43,7 @@ export const getQueryFilter = (
|
|||
lists,
|
||||
excludeExceptions,
|
||||
chunkSize: 1024,
|
||||
alias: null,
|
||||
});
|
||||
const initialQuery = { query, language };
|
||||
const allFilters = getAllFilters(filters as Filter[], exceptionFilter);
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import sinon from 'sinon';
|
||||
import moment from 'moment';
|
||||
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { sendAlertToTimelineAction, determineToAndFrom } from './actions';
|
||||
import {
|
||||
defaultTimelineProps,
|
||||
|
@ -34,6 +36,18 @@ import {
|
|||
DEFAULT_FROM_MOMENT,
|
||||
DEFAULT_TO_MOMENT,
|
||||
} from '../../../common/utils/default_date_settings';
|
||||
import {
|
||||
COMMENTS,
|
||||
DATE_NOW,
|
||||
DESCRIPTION,
|
||||
ENTRIES,
|
||||
ITEM_TYPE,
|
||||
META,
|
||||
NAME,
|
||||
NAMESPACE_TYPE,
|
||||
TIE_BREAKER,
|
||||
USER,
|
||||
} from '../../../../../lists/common/constants.mock';
|
||||
|
||||
jest.mock('../../../timelines/containers/api', () => ({
|
||||
getTimelineTemplate: jest.fn(),
|
||||
|
@ -41,6 +55,30 @@ jest.mock('../../../timelines/containers/api', () => ({
|
|||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
export const getExceptionListItemSchemaMock = (
|
||||
overrides?: Partial<ExceptionListItemSchema>
|
||||
): ExceptionListItemSchema => ({
|
||||
_version: undefined,
|
||||
comments: COMMENTS,
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
description: DESCRIPTION,
|
||||
entries: ENTRIES,
|
||||
id: '1',
|
||||
item_id: 'endpoint_list_item',
|
||||
list_id: 'endpoint_list_id',
|
||||
meta: META,
|
||||
name: NAME,
|
||||
namespace_type: NAMESPACE_TYPE,
|
||||
os_types: [],
|
||||
tags: ['user added string for a tag', 'malware'],
|
||||
tie_breaker_id: TIE_BREAKER,
|
||||
type: ITEM_TYPE,
|
||||
updated_at: DATE_NOW,
|
||||
updated_by: USER,
|
||||
...(overrides || {}),
|
||||
});
|
||||
|
||||
describe('alert actions', () => {
|
||||
const anchor = '2020-03-01T17:59:46.349Z';
|
||||
const unix = moment(anchor).valueOf();
|
||||
|
@ -49,9 +87,51 @@ describe('alert actions', () => {
|
|||
let searchStrategyClient: jest.Mocked<ISearchStart>;
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
let mockKibanaServices: jest.Mock;
|
||||
let mockGetExceptions: jest.Mock;
|
||||
let fetchMock: jest.Mock;
|
||||
let toastMock: jest.Mock;
|
||||
|
||||
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
|
||||
...mockAADEcsDataWithAlert,
|
||||
kibana: {
|
||||
alert: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert,
|
||||
rule: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
|
||||
parameters: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
|
||||
threshold: {
|
||||
field: ['destination.ip'],
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
name: ['mock threshold rule'],
|
||||
saved_id: [],
|
||||
type: ['threshold'],
|
||||
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
timeline_id: undefined,
|
||||
timeline_title: undefined,
|
||||
},
|
||||
threshold_result: {
|
||||
count: 99,
|
||||
from: '2021-01-10T21:11:45.839Z',
|
||||
cardinality: [
|
||||
{
|
||||
field: 'source.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
{
|
||||
field: 'destination.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// jest carries state between mocked implementations when using
|
||||
// spyOn. So now we're doing all three of these.
|
||||
|
@ -59,6 +139,7 @@ describe('alert actions', () => {
|
|||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
mockGetExceptions = jest.fn().mockResolvedValue([]);
|
||||
|
||||
createTimeline = jest.fn() as jest.Mocked<CreateTimeline>;
|
||||
updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>;
|
||||
|
@ -98,8 +179,10 @@ describe('alert actions', () => {
|
|||
ecsData: mockEcsDataWithAlert,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1);
|
||||
expect(updateTimelineIsLoading).toHaveBeenCalledWith({
|
||||
id: TimelineId.active,
|
||||
|
@ -113,6 +196,7 @@ describe('alert actions', () => {
|
|||
ecsData: mockEcsDataWithAlert,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
const expected = {
|
||||
from: '2018-11-05T18:58:25.937Z',
|
||||
|
@ -248,6 +332,7 @@ describe('alert actions', () => {
|
|||
ruleNote: '# this is some markdown documentation',
|
||||
};
|
||||
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
|
@ -269,9 +354,11 @@ describe('alert actions', () => {
|
|||
ecsData: mockEcsDataWithAlert,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0];
|
||||
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery');
|
||||
});
|
||||
|
@ -286,6 +373,7 @@ describe('alert actions', () => {
|
|||
ecsData: mockEcsDataWithAlert,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
const defaultTimelinePropsWithoutNote = { ...defaultTimelineProps };
|
||||
|
||||
|
@ -299,6 +387,7 @@ describe('alert actions', () => {
|
|||
id: TimelineId.active,
|
||||
isLoading: false,
|
||||
});
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith({
|
||||
...defaultTimelinePropsWithoutNote,
|
||||
|
@ -331,9 +420,11 @@ describe('alert actions', () => {
|
|||
ecsData: ecsDataMock,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
|
||||
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps);
|
||||
});
|
||||
|
@ -356,9 +447,11 @@ describe('alert actions', () => {
|
|||
ecsData: ecsDataMock,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
|
||||
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps);
|
||||
});
|
||||
|
@ -385,9 +478,11 @@ describe('alert actions', () => {
|
|||
ecsData: ecsDataMock,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
|
||||
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith({
|
||||
...defaultTimelineProps,
|
||||
|
@ -426,215 +521,260 @@ describe('alert actions', () => {
|
|||
ecsData: ecsDataMock,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
|
||||
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
|
||||
expect(mockGetExceptions).not.toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineToAndFrom', () => {
|
||||
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
|
||||
...mockAADEcsDataWithAlert,
|
||||
kibana: {
|
||||
alert: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert,
|
||||
rule: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
|
||||
parameters: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
|
||||
threshold: {
|
||||
field: ['destination.ip'],
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
name: ['mock threshold rule'],
|
||||
saved_id: [],
|
||||
type: ['threshold'],
|
||||
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
timeline_id: undefined,
|
||||
timeline_title: undefined,
|
||||
},
|
||||
threshold_result: {
|
||||
count: 99,
|
||||
from: '2021-01-10T21:11:45.839Z',
|
||||
cardinality: [
|
||||
describe('Threshold', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
field: 'source.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
{
|
||||
field: 'destination.ip',
|
||||
value: 1,
|
||||
_id: ecsDataMockWithNoTemplateTimeline[0]._id,
|
||||
_index: 'mock',
|
||||
_source: ecsDataMockWithNoTemplateTimeline[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: ecsDataMockWithNoTemplateTimeline[0]._id,
|
||||
_index: 'mock',
|
||||
_source: ecsDataMockWithNoTemplateTimeline[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
test('it uses ecs.Data.timestamp if one is provided', () => {
|
||||
const ecsDataMock: Ecs = {
|
||||
...mockEcsDataWithAlert,
|
||||
timestamp: '2020-03-20T17:59:46.349Z',
|
||||
};
|
||||
const result = determineToAndFrom({ ecs: ecsDataMock });
|
||||
|
||||
expect(result.from).toEqual('2020-03-20T17:54:46.349Z');
|
||||
expect(result.to).toEqual('2020-03-20T17:59:46.349Z');
|
||||
});
|
||||
test('Exceptions are included', async () => {
|
||||
mockGetExceptions.mockResolvedValue([getExceptionListItemSchemaMock()]);
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsDataMockWithNoTemplateTimeline,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
|
||||
test('it uses current time timestamp if ecsData.timestamp is not provided', () => {
|
||||
const { timestamp, ...ecsDataMock } = mockEcsDataWithAlert;
|
||||
const result = determineToAndFrom({ ecs: ecsDataMock });
|
||||
const expectedFrom = '2021-01-10T21:11:45.839Z';
|
||||
const expectedTo = '2021-01-10T21:12:45.839Z';
|
||||
|
||||
expect(result.from).toEqual('2020-03-01T17:54:46.349Z');
|
||||
expect(result.to).toEqual('2020-03-01T17:59:46.349Z');
|
||||
});
|
||||
|
||||
test('it uses original_time and threshold_result.from for threshold alerts', async () => {
|
||||
const expectedFrom = '2021-01-10T21:11:45.839Z';
|
||||
const expectedTo = '2021-01-10T21:12:45.839Z';
|
||||
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsDataMockWithNoTemplateTimeline,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
});
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith({
|
||||
...defaultTimelineProps,
|
||||
timeline: {
|
||||
...defaultTimelineProps.timeline,
|
||||
dataProviders: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-destination-ip-1',
|
||||
kqlQuery: '',
|
||||
name: 'destination.ip',
|
||||
queryMatch: { field: 'destination.ip', operator: ':', value: 1 },
|
||||
},
|
||||
],
|
||||
dateRange: {
|
||||
start: expectedFrom,
|
||||
end: expectedTo,
|
||||
},
|
||||
description: '_id: 1',
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
expression: ['user.id:1'],
|
||||
kind: ['kuery'],
|
||||
},
|
||||
serializedQuery: ['user.id:1'],
|
||||
},
|
||||
},
|
||||
resolveTimelineConfig: undefined,
|
||||
},
|
||||
from: expectedFrom,
|
||||
to: expectedTo,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('show toasts when data is malformed', () => {
|
||||
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
|
||||
...mockAADEcsDataWithAlert,
|
||||
kibana: {
|
||||
alert: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert,
|
||||
rule: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
|
||||
parameters: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
|
||||
threshold: {
|
||||
field: ['destination.ip'],
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
name: ['mock threshold rule'],
|
||||
saved_id: [],
|
||||
type: ['threshold'],
|
||||
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
timeline_id: undefined,
|
||||
timeline_title: undefined,
|
||||
},
|
||||
threshold_result: {
|
||||
count: 99,
|
||||
from: '2021-01-10T21:11:45.839Z',
|
||||
cardinality: [
|
||||
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
|
||||
expect(mockGetExceptions).toHaveBeenCalled();
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith({
|
||||
...defaultTimelineProps,
|
||||
timeline: {
|
||||
...defaultTimelineProps.timeline,
|
||||
dataProviders: [
|
||||
{
|
||||
field: 'source.ip',
|
||||
value: 1,
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-destination-ip-1',
|
||||
kqlQuery: '',
|
||||
name: 'destination.ip',
|
||||
queryMatch: { field: 'destination.ip', operator: ':', value: 1 },
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
dateRange: {
|
||||
start: expectedFrom,
|
||||
end: expectedTo,
|
||||
},
|
||||
description: '_id: 1',
|
||||
filters: [
|
||||
{
|
||||
field: 'destination.ip',
|
||||
value: 1,
|
||||
meta: {
|
||||
alias: 'Exceptions',
|
||||
disabled: false,
|
||||
negate: true,
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
nested: {
|
||||
path: 'some.parentField',
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match_phrase: {
|
||||
'some.parentField.nested.field': 'some value',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
score_mode: 'none',
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match_phrase: {
|
||||
'some.not.nested.field': 'some value',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
expression: ['user.id:1'],
|
||||
kind: ['kuery'],
|
||||
},
|
||||
serializedQuery: ['user.id:1'],
|
||||
},
|
||||
},
|
||||
resolveTimelineConfig: undefined,
|
||||
},
|
||||
from: expectedFrom,
|
||||
to: expectedTo,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineToAndFrom', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: ecsDataMockWithNoTemplateTimeline[0]._id,
|
||||
_index: 'mock',
|
||||
_source: ecsDataMockWithNoTemplateTimeline[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue({
|
||||
hits: 'not correctly formed doc',
|
||||
});
|
||||
});
|
||||
|
||||
test('it uses ecs.Data.timestamp if one is provided', () => {
|
||||
const ecsDataMock: Ecs = {
|
||||
...mockEcsDataWithAlert,
|
||||
timestamp: '2020-03-20T17:59:46.349Z',
|
||||
};
|
||||
const result = determineToAndFrom({ ecs: ecsDataMock });
|
||||
|
||||
expect(result.from).toEqual('2020-03-20T17:54:46.349Z');
|
||||
expect(result.to).toEqual('2020-03-20T17:59:46.349Z');
|
||||
});
|
||||
|
||||
test('it uses current time timestamp if ecsData.timestamp is not provided', () => {
|
||||
const { timestamp, ...ecsDataMock } = mockEcsDataWithAlert;
|
||||
const result = determineToAndFrom({ ecs: ecsDataMock });
|
||||
|
||||
expect(result.from).toEqual('2020-03-01T17:54:46.349Z');
|
||||
expect(result.to).toEqual('2020-03-01T17:59:46.349Z');
|
||||
});
|
||||
|
||||
test('it uses original_time and threshold_result.from for threshold alerts', async () => {
|
||||
const expectedFrom = '2021-01-10T21:11:45.839Z';
|
||||
const expectedTo = '2021-01-10T21:12:45.839Z';
|
||||
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsDataMockWithNoTemplateTimeline,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith({
|
||||
...defaultTimelineProps,
|
||||
timeline: {
|
||||
...defaultTimelineProps.timeline,
|
||||
dataProviders: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-destination-ip-1',
|
||||
kqlQuery: '',
|
||||
name: 'destination.ip',
|
||||
queryMatch: { field: 'destination.ip', operator: ':', value: 1 },
|
||||
},
|
||||
],
|
||||
dateRange: {
|
||||
start: expectedFrom,
|
||||
end: expectedTo,
|
||||
},
|
||||
description: '_id: 1',
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
expression: ['user.id:1'],
|
||||
kind: ['kuery'],
|
||||
},
|
||||
serializedQuery: ['user.id:1'],
|
||||
},
|
||||
},
|
||||
resolveTimelineConfig: undefined,
|
||||
},
|
||||
from: expectedFrom,
|
||||
to: expectedTo,
|
||||
});
|
||||
});
|
||||
});
|
||||
test('renders a toast and calls create timeline with basic defaults', async () => {
|
||||
const expectedFrom = DEFAULT_FROM_MOMENT.toISOString();
|
||||
const expectedTo = DEFAULT_TO_MOMENT.toISOString();
|
||||
const timelineProps = {
|
||||
...defaultTimelineProps,
|
||||
timeline: {
|
||||
...defaultTimelineProps.timeline,
|
||||
dataProviders: [],
|
||||
dateRange: {
|
||||
start: expectedFrom,
|
||||
end: expectedTo,
|
||||
},
|
||||
description: '',
|
||||
kqlQuery: {
|
||||
filterQuery: null,
|
||||
},
|
||||
resolveTimelineConfig: undefined,
|
||||
},
|
||||
from: expectedFrom,
|
||||
to: expectedTo,
|
||||
};
|
||||
|
||||
delete timelineProps.ruleNote;
|
||||
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsDataMockWithNoTemplateTimeline,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
describe('show toasts when data is malformed', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue({
|
||||
hits: 'not correctly formed doc',
|
||||
});
|
||||
});
|
||||
|
||||
test('renders a toast and calls create timeline with basic defaults', async () => {
|
||||
const expectedFrom = DEFAULT_FROM_MOMENT.toISOString();
|
||||
const expectedTo = DEFAULT_TO_MOMENT.toISOString();
|
||||
const timelineProps = {
|
||||
...defaultTimelineProps,
|
||||
timeline: {
|
||||
...defaultTimelineProps.timeline,
|
||||
dataProviders: [],
|
||||
dateRange: {
|
||||
start: expectedFrom,
|
||||
end: expectedTo,
|
||||
},
|
||||
description: '',
|
||||
kqlQuery: {
|
||||
filterQuery: null,
|
||||
},
|
||||
resolveTimelineConfig: undefined,
|
||||
},
|
||||
from: expectedFrom,
|
||||
to: expectedTo,
|
||||
};
|
||||
|
||||
delete timelineProps.ruleNote;
|
||||
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsDataMockWithNoTemplateTimeline,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions: mockGetExceptions,
|
||||
});
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith(timelineProps);
|
||||
expect(toastMock).toHaveBeenCalled();
|
||||
});
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith(timelineProps);
|
||||
expect(toastMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,9 +11,11 @@ import { getOr, isEmpty } from 'lodash/fp';
|
|||
import moment from 'moment';
|
||||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import { FilterStateStore, Filter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
ALERT_RULE_FROM,
|
||||
ALERT_RULE_TYPE,
|
||||
|
@ -21,7 +23,9 @@ import {
|
|||
ALERT_RULE_PARAMETERS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { buildExceptionFilter } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import {
|
||||
ALERT_ORIGINAL_TIME,
|
||||
ALERT_GROUP_ID,
|
||||
|
@ -265,14 +269,14 @@ export const getThresholdAggregationData = (ecsData: Ecs | Ecs[]): ThresholdAggr
|
|||
);
|
||||
};
|
||||
|
||||
export const isEqlRuleWithGroupId = (ecsData: Ecs): boolean => {
|
||||
export const isEqlAlertWithGroupId = (ecsData: Ecs): boolean => {
|
||||
const ruleType = getField(ecsData, ALERT_RULE_TYPE);
|
||||
const groupId = getField(ecsData, ALERT_GROUP_ID);
|
||||
const isEql = ruleType === 'eql' || (Array.isArray(ruleType) && ruleType[0] === 'eql');
|
||||
return isEql && groupId?.length > 0;
|
||||
};
|
||||
|
||||
export const isThresholdRule = (ecsData: Ecs): boolean => {
|
||||
export const isThresholdAlert = (ecsData: Ecs): boolean => {
|
||||
const ruleType = getField(ecsData, ALERT_RULE_TYPE);
|
||||
return (
|
||||
ruleType === 'threshold' ||
|
||||
|
@ -396,7 +400,8 @@ const createThresholdTimeline = async (
|
|||
ecsData: Ecs,
|
||||
createTimeline: ({ from, timeline, to }: CreateTimelineProps) => void,
|
||||
noteContent: string,
|
||||
templateValues: { filters?: Filter[]; query?: string; dataProviders?: DataProvider[] }
|
||||
templateValues: { filters?: Filter[]; query?: string; dataProviders?: DataProvider[] },
|
||||
getExceptions: (ecs: Ecs) => Promise<ExceptionListItemSchema[]>
|
||||
) => {
|
||||
try {
|
||||
const alertResponse = await KibanaServices.get().http.fetch<
|
||||
|
@ -417,6 +422,7 @@ const createThresholdTimeline = async (
|
|||
},
|
||||
];
|
||||
}, []) ?? [];
|
||||
|
||||
const alertDoc = formattedAlertData[0];
|
||||
const params = getField(alertDoc, ALERT_RULE_PARAMETERS);
|
||||
const filters = getFiltersFromRule(params.filters ?? alertDoc.signal?.rule?.filters) ?? [];
|
||||
|
@ -425,13 +431,23 @@ const createThresholdTimeline = async (
|
|||
const indexNames = params.index ?? alertDoc.signal?.rule?.index ?? [];
|
||||
|
||||
const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(alertDoc);
|
||||
const exceptions = await getExceptions(ecsData);
|
||||
const exceptionsFilter =
|
||||
buildExceptionFilter({
|
||||
lists: exceptions,
|
||||
excludeExceptions: true,
|
||||
chunkSize: 10000,
|
||||
alias: 'Exceptions',
|
||||
}) ?? [];
|
||||
const allFilters = (templateValues.filters ?? filters).concat(exceptionsFilter);
|
||||
|
||||
return createTimeline({
|
||||
from: thresholdFrom,
|
||||
notes: null,
|
||||
timeline: {
|
||||
...timelineDefaults,
|
||||
description: `_id: ${alertDoc._id}`,
|
||||
filters: templateValues.filters ?? filters,
|
||||
filters: allFilters,
|
||||
dataProviders: templateValues.dataProviders ?? dataProviders,
|
||||
id: TimelineId.active,
|
||||
indexNames,
|
||||
|
@ -495,6 +511,7 @@ export const sendAlertToTimelineAction = async ({
|
|||
ecsData: ecs,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
getExceptions,
|
||||
}: SendAlertToTimelineActionProps) => {
|
||||
/* FUTURE DEVELOPER
|
||||
* We are making an assumption here that if you have an array of ecs data they are all coming from the same rule
|
||||
|
@ -554,12 +571,18 @@ export const sendAlertToTimelineAction = async ({
|
|||
timeline.timelineType
|
||||
);
|
||||
// threshold with template
|
||||
if (isThresholdRule(ecsData)) {
|
||||
return createThresholdTimeline(ecsData, createTimeline, noteContent, {
|
||||
filters,
|
||||
query,
|
||||
dataProviders,
|
||||
});
|
||||
if (isThresholdAlert(ecsData)) {
|
||||
return createThresholdTimeline(
|
||||
ecsData,
|
||||
createTimeline,
|
||||
noteContent,
|
||||
{
|
||||
filters,
|
||||
query,
|
||||
dataProviders,
|
||||
},
|
||||
getExceptions
|
||||
);
|
||||
} else {
|
||||
return createTimeline({
|
||||
from,
|
||||
|
@ -612,11 +635,11 @@ export const sendAlertToTimelineAction = async ({
|
|||
to,
|
||||
});
|
||||
}
|
||||
} else if (isThresholdRule(ecsData)) {
|
||||
return createThresholdTimeline(ecsData, createTimeline, noteContent, {});
|
||||
} else if (isThresholdAlert(ecsData)) {
|
||||
return createThresholdTimeline(ecsData, createTimeline, noteContent, {}, getExceptions);
|
||||
} else {
|
||||
let { dataProviders, filters } = buildTimelineDataProviderOrFilter(alertIds ?? [], ecsData._id);
|
||||
if (isEqlRuleWithGroupId(ecsData)) {
|
||||
if (isEqlAlertWithGroupId(ecsData)) {
|
||||
const tempEql = buildEqlDataProviderOrFilter(alertIds ?? [], ecs);
|
||||
dataProviders = tempEql.dataProviders;
|
||||
filters = tempEql.filters;
|
||||
|
|
|
@ -6,14 +6,16 @@
|
|||
*/
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
import { Filter, FilterStateStore, KueryNode, fromKueryExpression } from '@kbn/es-query';
|
||||
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
import {
|
||||
DataProvider,
|
||||
DataProviderType,
|
||||
DataProvidersAnd,
|
||||
} from '../../../timelines/components/timeline/data_providers/data_provider';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
|
||||
interface FindValueToChangeInQuery {
|
||||
field: string;
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as actions from '../actions';
|
|||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import type { SendAlertToTimelineActionProps } from '../types';
|
||||
import { InvestigateInTimelineAction } from './investigate_in_timeline_action';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
|
||||
const ecsRowData: Ecs = {
|
||||
_id: '1',
|
||||
|
@ -29,6 +30,7 @@ const ecsRowData: Ecs = {
|
|||
};
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
jest.mock('../actions');
|
||||
|
||||
const props = {
|
||||
|
@ -54,6 +56,9 @@ describe('use investigate in timeline hook', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
(useAppToasts as jest.Mock).mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useInvestigateInTimeline } from './use_investigate_in_timeline';
|
|||
import * as actions from '../actions';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import type { SendAlertToTimelineActionProps } from '../types';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
|
||||
const ecsRowData: Ecs = {
|
||||
_id: '1',
|
||||
|
@ -29,6 +30,7 @@ const ecsRowData: Ecs = {
|
|||
};
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
jest.mock('../actions');
|
||||
|
||||
const props = {
|
||||
|
@ -53,6 +55,9 @@ describe('use investigate in timeline hook', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
(useAppToasts as jest.Mock).mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
|
|
@ -8,8 +8,17 @@ import React, { useCallback, useMemo } from 'react';
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERT_RULE_EXCEPTIONS_LIST } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
ExceptionListIdentifiers,
|
||||
ExceptionListItemSchema,
|
||||
ReadExceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useApi } from '@kbn/securitysolution-list-hooks';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { TimelineId, TimelineType } from '../../../../../common/types/timeline';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline';
|
||||
|
@ -19,6 +28,8 @@ import { useCreateTimeline } from '../../../../timelines/components/timeline/pro
|
|||
import { CreateTimelineProps } from '../types';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { getField } from '../../../../helpers';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
|
||||
interface UseInvestigateInTimelineActionProps {
|
||||
ecsRowData?: Ecs | Ecs[] | null;
|
||||
|
@ -29,11 +40,65 @@ export const useInvestigateInTimeline = ({
|
|||
ecsRowData,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
}: UseInvestigateInTimelineActionProps) => {
|
||||
const { addError } = useAppToasts();
|
||||
const {
|
||||
data: { search: searchStrategyClient, query },
|
||||
} = useKibana().services;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { services } = useKibana();
|
||||
const { getExceptionListsItems } = useApi(services.http);
|
||||
|
||||
const getExceptions = useCallback(
|
||||
async (ecsData: Ecs): Promise<ExceptionListItemSchema[]> => {
|
||||
const exceptionsLists: ReadExceptionListSchema[] = (
|
||||
getField(ecsData, ALERT_RULE_EXCEPTIONS_LIST) ?? []
|
||||
)
|
||||
.map((list: string) => JSON.parse(list))
|
||||
.filter((list: ExceptionListIdentifiers) => list.type === 'detection');
|
||||
|
||||
const allExceptions: ExceptionListItemSchema[] = [];
|
||||
|
||||
if (exceptionsLists.length > 0) {
|
||||
for (const list of exceptionsLists) {
|
||||
if (list.id && list.list_id && list.namespace_type) {
|
||||
await getExceptionListsItems({
|
||||
lists: [
|
||||
{
|
||||
id: list.id,
|
||||
listId: list.list_id,
|
||||
type: 'detection',
|
||||
namespaceType: list.namespace_type,
|
||||
},
|
||||
],
|
||||
filterOptions: [],
|
||||
pagination: {
|
||||
page: 0,
|
||||
perPage: 10000,
|
||||
total: 10000,
|
||||
},
|
||||
showDetectionsListsOnly: true,
|
||||
showEndpointListsOnly: false,
|
||||
onSuccess: ({ exceptions }) => {
|
||||
allExceptions.push(...exceptions);
|
||||
},
|
||||
onError: (err: string[]) => {
|
||||
addError(err, {
|
||||
title: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.alerts.fetchExceptionsFailure',
|
||||
{ defaultMessage: 'Error fetching exceptions.' }
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return allExceptions;
|
||||
},
|
||||
[addError, getExceptionListsItems]
|
||||
);
|
||||
|
||||
const filterManagerBackup = useMemo(() => query.filterManager, [query.filterManager]);
|
||||
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
|
||||
const { filterManager: activeFilterManager } = useDeepEqualSelector((state) =>
|
||||
|
@ -86,6 +151,7 @@ export const useInvestigateInTimeline = ({
|
|||
ecsData: ecsRowData,
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
getExceptions,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
|
@ -94,6 +160,7 @@ export const useInvestigateInTimeline = ({
|
|||
onInvestigateInTimelineAlertClick,
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
getExceptions,
|
||||
]);
|
||||
|
||||
const investigateInTimelineActionItems = useMemo(
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import type { ISearchStart } from '../../../../../../../src/plugins/data/public';
|
||||
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
|
@ -55,6 +57,7 @@ export interface SendAlertToTimelineActionProps {
|
|||
ecsData: Ecs | Ecs[];
|
||||
updateTimelineIsLoading: UpdateTimelineLoading;
|
||||
searchStrategyClient: ISearchStart;
|
||||
getExceptions: GetExceptions;
|
||||
}
|
||||
|
||||
export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void;
|
||||
|
@ -68,6 +71,7 @@ export interface CreateTimelineProps {
|
|||
}
|
||||
|
||||
export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void;
|
||||
export type GetExceptions = (ecsData: Ecs) => Promise<ExceptionListItemSchema[]>;
|
||||
|
||||
export interface ThresholdAggregationData {
|
||||
thresholdFrom: string;
|
||||
|
|
|
@ -35,6 +35,12 @@ jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () =
|
|||
}));
|
||||
jest.mock('../../../cases/components/use_insert_timeline');
|
||||
|
||||
jest.mock('../../../common/hooks/use_app_toasts', () => ({
|
||||
useAppToasts: jest.fn().mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
|
|
@ -193,6 +193,7 @@ export const buildEqlSearchRequest = (
|
|||
lists: exceptionLists,
|
||||
excludeExceptions: true,
|
||||
chunkSize: 1024,
|
||||
alias: null,
|
||||
});
|
||||
|
||||
const rangeFilter = buildTimeRangeFilter({ to, from, timestampOverride });
|
||||
|
|
|
@ -57,6 +57,7 @@ export const getAnomalies = async (
|
|||
lists: params.exceptionItems,
|
||||
excludeExceptions: true,
|
||||
chunkSize: 1024,
|
||||
alias: null,
|
||||
})?.query,
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue