mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
### Summary This PR addresses the remaining query preview bugs. - it adds index, and request information to eql inspect - it seems that for some reason the eql search strategy response returns `null` for the `params.body` in complete responses, but not in partial responses and does not include index info. As a workaround, I set the inspect info on partial responses and manually add index info - added to-dos pointing this out in the code - updated eql sequence queries preview to use the last event timestamp of a sequence to display the hits within a histogram - it checks buckets length to determine noise warning for threshold rules, as opposed to total hit count - remove unused i18n text - fixes bug where threshold is being passed in for all rule types as it's always defined in the creation step, added a check to only pass through to `useMatrixHistogram` hook when rule type is threshold
This commit is contained in:
parent
b034db9ed7
commit
a92a3d1af7
19 changed files with 1186 additions and 590 deletions
|
@ -0,0 +1,178 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
|
||||||
|
import { Source } from './types';
|
||||||
|
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||||
|
import { Connection } from '@elastic/elasticsearch';
|
||||||
|
|
||||||
|
export const getMockEqlResponse = (): EqlSearchStrategyResponse<EqlSearchResponse<Source>> => ({
|
||||||
|
id: 'some-id',
|
||||||
|
rawResponse: {
|
||||||
|
body: {
|
||||||
|
hits: {
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '1',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:16:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '2',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:50:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '3',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:06:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '4',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:15:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: {
|
||||||
|
value: 4,
|
||||||
|
relation: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
is_partial: false,
|
||||||
|
is_running: false,
|
||||||
|
took: 300,
|
||||||
|
timed_out: false,
|
||||||
|
},
|
||||||
|
headers: {},
|
||||||
|
warnings: [],
|
||||||
|
meta: {
|
||||||
|
aborted: false,
|
||||||
|
attempts: 0,
|
||||||
|
context: null,
|
||||||
|
name: 'elasticsearch-js',
|
||||||
|
connection: {} as Connection,
|
||||||
|
request: {
|
||||||
|
params: {
|
||||||
|
body: JSON.stringify({
|
||||||
|
filter: {
|
||||||
|
range: {
|
||||||
|
'@timestamp': {
|
||||||
|
gte: '2020-10-07T00:46:12.414Z',
|
||||||
|
lte: '2020-10-07T01:46:12.414Z',
|
||||||
|
format: 'strict_date_optional_time',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
method: 'GET',
|
||||||
|
path: '/_eql/search/',
|
||||||
|
querystring: 'some query string',
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
id: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statusCode: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getMockEqlSequenceResponse = (): EqlSearchStrategyResponse<
|
||||||
|
EqlSearchResponse<Source>
|
||||||
|
> => ({
|
||||||
|
id: 'some-id',
|
||||||
|
rawResponse: {
|
||||||
|
body: {
|
||||||
|
hits: {
|
||||||
|
sequences: [
|
||||||
|
{
|
||||||
|
join_keys: [],
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '1',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:16:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '2',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:50:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
join_keys: [],
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '3',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:06:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_index: 'index',
|
||||||
|
_id: '4',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2020-10-04T15:15:54.368707900Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: {
|
||||||
|
value: 4,
|
||||||
|
relation: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
is_partial: false,
|
||||||
|
is_running: false,
|
||||||
|
took: 300,
|
||||||
|
timed_out: false,
|
||||||
|
},
|
||||||
|
headers: {},
|
||||||
|
warnings: [],
|
||||||
|
meta: {
|
||||||
|
aborted: false,
|
||||||
|
attempts: 0,
|
||||||
|
context: null,
|
||||||
|
name: 'elasticsearch-js',
|
||||||
|
connection: {} as Connection,
|
||||||
|
request: {
|
||||||
|
params: {
|
||||||
|
body: JSON.stringify({
|
||||||
|
filter: {
|
||||||
|
range: {
|
||||||
|
'@timestamp': {
|
||||||
|
gte: '2020-10-07T00:46:12.414Z',
|
||||||
|
lte: '2020-10-07T01:46:12.414Z',
|
||||||
|
format: 'strict_date_optional_time',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
method: 'GET',
|
||||||
|
path: '/_eql/search/',
|
||||||
|
querystring: 'some query string',
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
id: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statusCode: 200,
|
||||||
|
},
|
||||||
|
});
|
File diff suppressed because it is too large
Load diff
|
@ -10,55 +10,105 @@ import { inputsModel } from '../../../common/store';
|
||||||
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
|
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
|
||||||
import { InspectResponse } from '../../../types';
|
import { InspectResponse } from '../../../types';
|
||||||
import { EqlPreviewResponse, Source } from './types';
|
import { EqlPreviewResponse, Source } from './types';
|
||||||
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
import { BaseHit, EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||||
|
|
||||||
type EqlAggBuckets = Record<string, { timestamp: string; total: number }>;
|
type EqlAggBuckets = Record<string, { timestamp: string; total: number }>;
|
||||||
|
|
||||||
export const EQL_QUERY_EVENT_SIZE = 100;
|
export const EQL_QUERY_EVENT_SIZE = 100;
|
||||||
|
|
||||||
// Calculates which 2 min bucket segment, event should be
|
/**
|
||||||
// sorted into
|
* Calculates which 2 min bucket segment, event should be sorted into
|
||||||
|
* @param eventTimestamp The event to be bucketed timestamp
|
||||||
|
* @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred
|
||||||
|
*/
|
||||||
export const calculateBucketForHour = (eventTimestamp: number, relativeNow: number): number => {
|
export const calculateBucketForHour = (eventTimestamp: number, relativeNow: number): number => {
|
||||||
const diff: number = relativeNow - eventTimestamp;
|
const diff = Math.abs(relativeNow - eventTimestamp);
|
||||||
const minutes: number = Math.floor(diff / 60000);
|
const minutes = Math.floor(diff / 60000);
|
||||||
return Math.ceil(minutes / 2) * 2;
|
return Math.ceil(minutes / 2) * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculates which 1 hour bucket segment, event should be
|
/**
|
||||||
// sorted into
|
* Calculates which 1 hour bucket segment, event should be sorted into
|
||||||
|
* @param eventTimestamp The event to be bucketed timestamp
|
||||||
|
* @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred
|
||||||
|
*/
|
||||||
export const calculateBucketForDay = (eventTimestamp: number, relativeNow: number): number => {
|
export const calculateBucketForDay = (eventTimestamp: number, relativeNow: number): number => {
|
||||||
const diff: number = relativeNow - eventTimestamp;
|
const diff = Math.abs(relativeNow - eventTimestamp);
|
||||||
const minutes: number = Math.floor(diff / 60000);
|
const minutes = Math.floor(diff / 60000);
|
||||||
return Math.ceil(minutes / 60);
|
return Math.ceil(minutes / 60);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the response for the UI inspect modal
|
||||||
|
* @param response The query search response
|
||||||
|
* @param indices The indices the query searched
|
||||||
|
* TODO: Update eql search strategy to return index in it's meta
|
||||||
|
* params info, currently not being returned, but expected for
|
||||||
|
* inspect modal display
|
||||||
|
*/
|
||||||
export const formatInspect = (
|
export const formatInspect = (
|
||||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>
|
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>,
|
||||||
|
indices: string[]
|
||||||
): InspectResponse => {
|
): InspectResponse => {
|
||||||
const body = response.rawResponse.meta.request.params.body;
|
const body = response.rawResponse.meta.request.params.body;
|
||||||
const bodyParse = typeof body === 'string' ? JSON.parse(body) : body;
|
const bodyParse: Record<string, unknown> | undefined =
|
||||||
|
typeof body === 'string' ? JSON.parse(body) : body;
|
||||||
return {
|
return {
|
||||||
dsl: [
|
dsl: [
|
||||||
JSON.stringify({ ...response.rawResponse.meta.request.params, body: bodyParse }, null, 2),
|
JSON.stringify(
|
||||||
|
{ ...response.rawResponse.meta.request.params, index: indices, body: bodyParse },
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
],
|
],
|
||||||
response: [JSON.stringify(response.rawResponse.body, null, 2)],
|
response: [JSON.stringify(response.rawResponse.body, null, 2)],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: Eql does not support aggregations, this is an in-memory
|
/**
|
||||||
// hand-spun aggregation for the events to give the user a visual
|
* Gets the events out of the response based on type of query
|
||||||
// representation of their query results
|
* @param isSequence Is the eql query a sequence query
|
||||||
|
* @param response The query search response
|
||||||
|
*/
|
||||||
|
export const getEventsToBucket = (
|
||||||
|
isSequence: boolean,
|
||||||
|
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>
|
||||||
|
): Array<BaseHit<Source>> => {
|
||||||
|
const hits = response.rawResponse.body.hits ?? [];
|
||||||
|
if (isSequence) {
|
||||||
|
return (
|
||||||
|
hits.sequences?.map((seq) => {
|
||||||
|
return seq.events[seq.events.length - 1];
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return hits.events ?? [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eql does not support aggregations, this is an in-memory
|
||||||
|
* hand-spun aggregation for the events to give the user a visual
|
||||||
|
* representation of their query results
|
||||||
|
* @param response The query search response
|
||||||
|
* @param range User chosen timeframe (last hour, day)
|
||||||
|
* @param to Based on range chosen
|
||||||
|
* @param refetch Callback used in inspect button, ref just passed through
|
||||||
|
* @param indices Indices searched by query
|
||||||
|
* @param isSequence Is the eql query a sequence query
|
||||||
|
*/
|
||||||
export const getEqlAggsData = (
|
export const getEqlAggsData = (
|
||||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>,
|
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>,
|
||||||
range: Unit,
|
range: Unit,
|
||||||
to: string,
|
to: string,
|
||||||
refetch: inputsModel.Refetch
|
refetch: inputsModel.Refetch,
|
||||||
|
indices: string[],
|
||||||
|
isSequence: boolean
|
||||||
): EqlPreviewResponse => {
|
): EqlPreviewResponse => {
|
||||||
const { dsl, response: inspectResponse } = formatInspect(response);
|
const { dsl, response: inspectResponse } = formatInspect(response, indices);
|
||||||
// The upper bound of the timestamps
|
|
||||||
const relativeNow = Date.parse(to);
|
const relativeNow = Date.parse(to);
|
||||||
const accumulator = getInterval(range, relativeNow);
|
const accumulator = getInterval(range, relativeNow);
|
||||||
const events = response.rawResponse.body.hits.events ?? [];
|
const events = getEventsToBucket(isSequence, response);
|
||||||
const totalCount = response.rawResponse.body.hits.total.value;
|
const totalCount = response.rawResponse.body.hits.total.value;
|
||||||
|
|
||||||
const buckets = events.reduce<EqlAggBuckets>((acc, hit) => {
|
const buckets = events.reduce<EqlAggBuckets>((acc, hit) => {
|
||||||
|
@ -94,12 +144,23 @@ export const getEqlAggsData = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createIntervalArray = (start: number, end: number, multiplier: number) => {
|
/**
|
||||||
|
* Helper method to create an array to be used for calculating bucket intervals
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @param multiplier
|
||||||
|
*/
|
||||||
|
export const createIntervalArray = (start: number, end: number, multiplier: number): number[] => {
|
||||||
return Array(end - start + 1)
|
return Array(end - start + 1)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, idx) => start + idx * multiplier);
|
.map((_, idx) => start + idx * multiplier);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an array to be used for calculating bucket intervals
|
||||||
|
* @param range User chosen timeframe (last hour, day)
|
||||||
|
* @param relativeNow Based on range chosen
|
||||||
|
*/
|
||||||
export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets => {
|
export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets => {
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case 'h':
|
case 'h':
|
||||||
|
@ -117,38 +178,6 @@ export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets =>
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
default:
|
default:
|
||||||
throw new Error('Invalid time range selected');
|
throw new RangeError('Invalid time range selected. Must be "Last hour" or "Last day".');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSequenceAggs = (
|
|
||||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>,
|
|
||||||
refetch: inputsModel.Refetch
|
|
||||||
): EqlPreviewResponse => {
|
|
||||||
const { dsl, response: inspectResponse } = formatInspect(response);
|
|
||||||
const sequences = response.rawResponse.body.hits.sequences ?? [];
|
|
||||||
const totalCount = response.rawResponse.body.hits.total.value;
|
|
||||||
|
|
||||||
const data = sequences.map((sequence, i) => {
|
|
||||||
return sequence.events.map((seqEvent) => {
|
|
||||||
if (seqEvent._source['@timestamp'] == null) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
x: seqEvent._source['@timestamp'],
|
|
||||||
y: 1,
|
|
||||||
g: `Seq. ${i + 1}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: data.flat(),
|
|
||||||
totalCount,
|
|
||||||
inspect: {
|
|
||||||
dsl,
|
|
||||||
response: inspectResponse,
|
|
||||||
},
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { Source } from './types';
|
||||||
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||||
import { useKibana } from '../../../common/lib/kibana';
|
import { useKibana } from '../../../common/lib/kibana';
|
||||||
import { useEqlPreview } from '.';
|
import { useEqlPreview } from '.';
|
||||||
import { getMockResponse } from './helpers.test';
|
import { getMockEqlResponse } from './eql_search_response.mock';
|
||||||
|
|
||||||
jest.mock('../../../common/lib/kibana');
|
jest.mock('../../../common/lib/kibana');
|
||||||
|
|
||||||
|
@ -32,7 +32,9 @@ describe('useEqlPreview', () => {
|
||||||
|
|
||||||
useKibana().services.notifications.toasts.addWarning = jest.fn();
|
useKibana().services.notifications.toasts.addWarning = jest.fn();
|
||||||
|
|
||||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse()));
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of(getMockEqlResponse())
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initiate hook', async () => {
|
it('should initiate hook', async () => {
|
||||||
|
@ -96,7 +98,7 @@ describe('useEqlPreview', () => {
|
||||||
it('should not resolve values after search is invoked if component unmounted', async () => {
|
it('should not resolve values after search is invoked if component unmounted', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
of(getMockResponse()).pipe(delay(5000))
|
of(getMockEqlResponse()).pipe(delay(5000))
|
||||||
);
|
);
|
||||||
const { result, waitForNextUpdate, unmount } = renderHook(() => useEqlPreview());
|
const { result, waitForNextUpdate, unmount } = renderHook(() => useEqlPreview());
|
||||||
|
|
||||||
|
@ -117,9 +119,11 @@ describe('useEqlPreview', () => {
|
||||||
it('should not resolve new values on search if response is error response', async () => {
|
it('should not resolve new values on search if response is error response', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
of({ isRunning: false, isPartial: true } as EqlSearchStrategyResponse<
|
of<EqlSearchStrategyResponse<EqlSearchResponse<Source>>>({
|
||||||
EqlSearchResponse<Source>
|
...getMockEqlResponse(),
|
||||||
>)
|
isRunning: false,
|
||||||
|
isPartial: true,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useEqlPreview());
|
const { result, waitForNextUpdate } = renderHook(() => useEqlPreview());
|
||||||
|
@ -136,6 +140,30 @@ describe('useEqlPreview', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Determine why eql search strategy returns null for meta.params.body
|
||||||
|
// in complete responses, but not in partial responses
|
||||||
|
it('should update inspect information on partial response', async () => {
|
||||||
|
const mockResponse = getMockEqlResponse();
|
||||||
|
await act(async () => {
|
||||||
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of<EqlSearchStrategyResponse<EqlSearchResponse<Source>>>({
|
||||||
|
isRunning: true,
|
||||||
|
isPartial: true,
|
||||||
|
rawResponse: mockResponse.rawResponse,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useEqlPreview());
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
result.current[1](params);
|
||||||
|
|
||||||
|
expect(result.current[2].inspect.dsl.length).toEqual(1);
|
||||||
|
expect(result.current[2].inspect.response.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should add danger toast if search throws', async () => {
|
it('should add danger toast if search throws', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
|
|
@ -14,12 +14,13 @@ import {
|
||||||
AbortError,
|
AbortError,
|
||||||
isCompleteResponse,
|
isCompleteResponse,
|
||||||
isErrorResponse,
|
isErrorResponse,
|
||||||
|
isPartialResponse,
|
||||||
} from '../../../../../../../src/plugins/data/common';
|
} from '../../../../../../../src/plugins/data/common';
|
||||||
import {
|
import {
|
||||||
EqlSearchStrategyRequest,
|
EqlSearchStrategyRequest,
|
||||||
EqlSearchStrategyResponse,
|
EqlSearchStrategyResponse,
|
||||||
} from '../../../../../data_enhanced/common';
|
} from '../../../../../data_enhanced/common';
|
||||||
import { getEqlAggsData, getSequenceAggs } from './helpers';
|
import { formatInspect, getEqlAggsData } from './helpers';
|
||||||
import { EqlPreviewResponse, EqlPreviewRequest, Source } from './types';
|
import { EqlPreviewResponse, EqlPreviewRequest, Source } from './types';
|
||||||
import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils';
|
import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils';
|
||||||
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||||
|
@ -106,13 +107,37 @@ export const useEqlPreview = (): [
|
||||||
if (isCompleteResponse(res)) {
|
if (isCompleteResponse(res)) {
|
||||||
if (!didCancel.current) {
|
if (!didCancel.current) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (hasEqlSequenceQuery(query)) {
|
|
||||||
setResponse(getSequenceAggs(res, refetch.current));
|
setResponse((prev) => {
|
||||||
} else {
|
const { inspect, ...rest } = getEqlAggsData(
|
||||||
setResponse(getEqlAggsData(res, interval, to, refetch.current));
|
res,
|
||||||
}
|
interval,
|
||||||
|
to,
|
||||||
|
refetch.current,
|
||||||
|
index,
|
||||||
|
hasEqlSequenceQuery(query)
|
||||||
|
);
|
||||||
|
const inspectDsl = prev.inspect.dsl[0] ? prev.inspect.dsl : inspect.dsl;
|
||||||
|
const inspectResp = prev.inspect.response[0]
|
||||||
|
? prev.inspect.response
|
||||||
|
: inspect.response;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
...rest,
|
||||||
|
inspect: {
|
||||||
|
dsl: inspectDsl,
|
||||||
|
response: inspectResp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribeStream.current.next();
|
unsubscribeStream.current.next();
|
||||||
|
} else if (isPartialResponse(res)) {
|
||||||
|
// TODO: Eql search strategy partial responses return a value under meta.params.body
|
||||||
|
// but the final/complete response does not, that's why the inspect values are set here
|
||||||
|
setResponse((prev) => ({ ...prev, inspect: formatInspect(res, index) }));
|
||||||
} else if (isErrorResponse(res)) {
|
} else if (isErrorResponse(res)) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
notifications.toasts.addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE);
|
notifications.toasts.addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE);
|
||||||
|
|
|
@ -52,7 +52,7 @@ describe('PreviewCustomQueryHistogram', () => {
|
||||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle')
|
wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle')
|
||||||
).toEqual(i18n.PREVIEW_SUBTITLE_LOADING);
|
).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it configures data and subtitle', () => {
|
test('it configures data and subtitle', () => {
|
||||||
|
|
|
@ -54,7 +54,7 @@ export const PreviewCustomQueryHistogram = ({
|
||||||
|
|
||||||
const subtitle = useMemo(
|
const subtitle = useMemo(
|
||||||
(): string =>
|
(): string =>
|
||||||
isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||||
[isLoading, totalCount]
|
[isLoading, totalCount]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ export const PreviewCustomQueryHistogram = ({
|
||||||
barConfig={barConfig}
|
barConfig={barConfig}
|
||||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER}
|
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
data-test-subj="queryPreviewCustomHistogram"
|
data-test-subj="queryPreviewCustomHistogram"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -39,7 +39,6 @@ describe('PreviewEqlQueryHistogram', () => {
|
||||||
<PreviewEqlQueryHistogram
|
<PreviewEqlQueryHistogram
|
||||||
to="2020-07-08T08:20:18.966Z"
|
to="2020-07-08T08:20:18.966Z"
|
||||||
from="2020-07-07T08:20:18.966Z"
|
from="2020-07-07T08:20:18.966Z"
|
||||||
query="file where true"
|
|
||||||
data={[]}
|
data={[]}
|
||||||
totalCount={0}
|
totalCount={0}
|
||||||
inspect={{ dsl: [], response: [] }}
|
inspect={{ dsl: [], response: [] }}
|
||||||
|
@ -53,7 +52,7 @@ describe('PreviewEqlQueryHistogram', () => {
|
||||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle')
|
wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle')
|
||||||
).toEqual(i18n.PREVIEW_SUBTITLE_LOADING);
|
).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it configures data and subtitle', () => {
|
test('it configures data and subtitle', () => {
|
||||||
|
@ -63,7 +62,6 @@ describe('PreviewEqlQueryHistogram', () => {
|
||||||
<PreviewEqlQueryHistogram
|
<PreviewEqlQueryHistogram
|
||||||
to="2020-07-08T08:20:18.966Z"
|
to="2020-07-08T08:20:18.966Z"
|
||||||
from="2020-07-07T08:20:18.966Z"
|
from="2020-07-07T08:20:18.966Z"
|
||||||
query="file where true"
|
|
||||||
data={[
|
data={[
|
||||||
{ x: 1602247050000, y: 2314, g: 'All others' },
|
{ x: 1602247050000, y: 2314, g: 'All others' },
|
||||||
{ x: 1602247162500, y: 3471, g: 'All others' },
|
{ x: 1602247162500, y: 3471, g: 'All others' },
|
||||||
|
@ -115,7 +113,6 @@ describe('PreviewEqlQueryHistogram', () => {
|
||||||
<PreviewEqlQueryHistogram
|
<PreviewEqlQueryHistogram
|
||||||
to="2020-07-08T08:20:18.966Z"
|
to="2020-07-08T08:20:18.966Z"
|
||||||
from="2020-07-07T08:20:18.966Z"
|
from="2020-07-07T08:20:18.966Z"
|
||||||
query="file where true"
|
|
||||||
data={[]}
|
data={[]}
|
||||||
totalCount={0}
|
totalCount={0}
|
||||||
inspect={{ dsl: ['some dsl'], response: ['query response'] }}
|
inspect={{ dsl: ['some dsl'], response: ['query response'] }}
|
||||||
|
@ -133,4 +130,32 @@ describe('PreviewEqlQueryHistogram', () => {
|
||||||
refetch: mockRefetch,
|
refetch: mockRefetch,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it displays histogram', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
<TestProviders>
|
||||||
|
<PreviewEqlQueryHistogram
|
||||||
|
to="2020-07-08T08:20:18.966Z"
|
||||||
|
from="2020-07-07T08:20:18.966Z"
|
||||||
|
data={[
|
||||||
|
{ x: 1602247050000, y: 2314, g: 'All others' },
|
||||||
|
{ x: 1602247162500, y: 3471, g: 'All others' },
|
||||||
|
{ x: 1602247275000, y: 3369, g: 'All others' },
|
||||||
|
]}
|
||||||
|
totalCount={9154}
|
||||||
|
inspect={{ dsl: [], response: [] }}
|
||||||
|
refetch={jest.fn()}
|
||||||
|
isLoading={false}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
wrapper.find('[data-test-subj="sharedPreviewQueryNoHistogramAvailable"]').exists()
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
} from '../../../../common/components/charts/common';
|
} from '../../../../common/components/charts/common';
|
||||||
import { InspectQuery } from '../../../../common/store/inputs/model';
|
import { InspectQuery } from '../../../../common/store/inputs/model';
|
||||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||||
import { hasEqlSequenceQuery } from '../../../../../common/detection_engine/utils';
|
|
||||||
import { inputsModel } from '../../../../common/store';
|
import { inputsModel } from '../../../../common/store';
|
||||||
import { PreviewHistogram } from './histogram';
|
import { PreviewHistogram } from './histogram';
|
||||||
|
|
||||||
|
@ -26,7 +25,6 @@ interface PreviewEqlQueryHistogramProps {
|
||||||
from: string;
|
from: string;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
query: string;
|
|
||||||
data: ChartData[];
|
data: ChartData[];
|
||||||
inspect: InspectQuery;
|
inspect: InspectQuery;
|
||||||
refetch: inputsModel.Refetch;
|
refetch: inputsModel.Refetch;
|
||||||
|
@ -36,7 +34,6 @@ export const PreviewEqlQueryHistogram = ({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
totalCount,
|
totalCount,
|
||||||
query,
|
|
||||||
data,
|
data,
|
||||||
inspect,
|
inspect,
|
||||||
refetch,
|
refetch,
|
||||||
|
@ -50,14 +47,11 @@ export const PreviewEqlQueryHistogram = ({
|
||||||
}
|
}
|
||||||
}, [setQuery, inspect, isInitializing, refetch]);
|
}, [setQuery, inspect, isInitializing, refetch]);
|
||||||
|
|
||||||
const barConfig = useMemo(
|
const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]);
|
||||||
(): ChartSeriesConfigs => getHistogramConfig(to, from, hasEqlSequenceQuery(query)),
|
|
||||||
[from, to, query]
|
|
||||||
);
|
|
||||||
|
|
||||||
const subtitle = useMemo(
|
const subtitle = useMemo(
|
||||||
(): string =>
|
(): string =>
|
||||||
isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||||
[isLoading, totalCount]
|
[isLoading, totalCount]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -70,7 +64,7 @@ export const PreviewEqlQueryHistogram = ({
|
||||||
barConfig={barConfig}
|
barConfig={barConfig}
|
||||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER_EQL}
|
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER_EQL}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
data-test-subj="queryPreviewEqlHistogram"
|
data-test-subj="queryPreviewEqlHistogram"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,13 +16,13 @@ import { FieldValueQueryBar } from '../query_bar';
|
||||||
import { ESQuery } from '../../../../../common/typed_json';
|
import { ESQuery } from '../../../../../common/typed_json';
|
||||||
import { Filter } from '../../../../../../../../src/plugins/data/common/es_query';
|
import { Filter } from '../../../../../../../../src/plugins/data/common/es_query';
|
||||||
|
|
||||||
export const HITS_THRESHOLD: Record<string, number> = {
|
/**
|
||||||
h: 1,
|
* Determines whether or not to display noise warning.
|
||||||
d: 24,
|
* Is considered noisy if alerts/hour rate > 1
|
||||||
M: 730,
|
* @param hits Total query search hits
|
||||||
};
|
* @param timeframe Range selected by user (last hour, day...)
|
||||||
|
*/
|
||||||
export const isNoisy = (hits: number, timeframe: Unit) => {
|
export const isNoisy = (hits: number, timeframe: Unit): boolean => {
|
||||||
if (timeframe === 'h') {
|
if (timeframe === 'h') {
|
||||||
return hits > 1;
|
return hits > 1;
|
||||||
} else if (timeframe === 'd') {
|
} else if (timeframe === 'd') {
|
||||||
|
@ -34,6 +34,12 @@ export const isNoisy = (hits: number, timeframe: Unit) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines what timerange options to show.
|
||||||
|
* Eql sequence queries tend to be slower, so decided
|
||||||
|
* not to include the last month option.
|
||||||
|
* @param ruleType
|
||||||
|
*/
|
||||||
export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
|
export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
|
||||||
if (ruleType === 'eql') {
|
if (ruleType === 'eql') {
|
||||||
return [
|
return [
|
||||||
|
@ -49,6 +55,13 @@ export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick little helper to extract the query info from the
|
||||||
|
* queryBar object.
|
||||||
|
* @param queryBar Object containing all query info
|
||||||
|
* @param index Indices searched
|
||||||
|
* @param ruleType
|
||||||
|
*/
|
||||||
export const getInfoFromQueryBar = (
|
export const getInfoFromQueryBar = (
|
||||||
queryBar: FieldValueQueryBar,
|
queryBar: FieldValueQueryBar,
|
||||||
index: string[],
|
index: string[],
|
||||||
|
@ -88,10 +101,15 @@ export const getInfoFromQueryBar = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config passed into elastic-charts settings.
|
||||||
|
* @param to
|
||||||
|
* @param from
|
||||||
|
*/
|
||||||
export const getHistogramConfig = (
|
export const getHistogramConfig = (
|
||||||
to: string,
|
to: string,
|
||||||
from: string,
|
from: string,
|
||||||
showLegend: boolean = false
|
showLegend = false
|
||||||
): ChartSeriesConfigs => {
|
): ChartSeriesConfigs => {
|
||||||
return {
|
return {
|
||||||
series: {
|
series: {
|
||||||
|
@ -131,7 +149,11 @@ export const getHistogramConfig = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getThresholdHistogramConfig = (height: number | undefined): ChartSeriesConfigs => {
|
/**
|
||||||
|
* Threshold histogram is displayed a bit differently,
|
||||||
|
* x-axis is not time based, but ordinal.
|
||||||
|
*/
|
||||||
|
export const getThresholdHistogramConfig = (): ChartSeriesConfigs => {
|
||||||
return {
|
return {
|
||||||
series: {
|
series: {
|
||||||
xScaleType: ScaleType.Ordinal,
|
xScaleType: ScaleType.Ordinal,
|
||||||
|
@ -165,6 +187,6 @@ export const getThresholdHistogramConfig = (height: number | undefined): ChartSe
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
customHeight: height ?? 200,
|
customHeight: 200,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,9 +13,13 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
import { TestProviders } from '../../../../common/mock';
|
import { TestProviders } from '../../../../common/mock';
|
||||||
import { useKibana } from '../../../../common/lib/kibana';
|
import { useKibana } from '../../../../common/lib/kibana';
|
||||||
import { PreviewQuery } from './';
|
import { PreviewQuery } from './';
|
||||||
import { getMockResponse } from '../../../../common/hooks/eql/helpers.test';
|
import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_response.mock';
|
||||||
|
import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram';
|
||||||
|
import { useEqlPreview } from '../../../../common/hooks/eql/';
|
||||||
|
|
||||||
jest.mock('../../../../common/lib/kibana');
|
jest.mock('../../../../common/lib/kibana');
|
||||||
|
jest.mock('../../../../common/containers/matrix_histogram');
|
||||||
|
jest.mock('../../../../common/hooks/eql/');
|
||||||
|
|
||||||
describe('PreviewQuery', () => {
|
describe('PreviewQuery', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -23,7 +27,33 @@ describe('PreviewQuery', () => {
|
||||||
|
|
||||||
useKibana().services.notifications.toasts.addWarning = jest.fn();
|
useKibana().services.notifications.toasts.addWarning = jest.fn();
|
||||||
|
|
||||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse()));
|
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
inspect: { dsl: [], response: [] },
|
||||||
|
totalCount: 1,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
data: [],
|
||||||
|
buckets: [],
|
||||||
|
},
|
||||||
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of(getMockEqlResponse())
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
(useEqlPreview as jest.Mock).mockReturnValue([
|
||||||
|
false,
|
||||||
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of(getMockEqlResponse())
|
||||||
|
),
|
||||||
|
{
|
||||||
|
inspect: { dsl: [], response: [] },
|
||||||
|
totalCount: 1,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
data: [],
|
||||||
|
buckets: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -121,6 +151,42 @@ describe('PreviewQuery', () => {
|
||||||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
<TestProviders>
|
||||||
|
<PreviewQuery
|
||||||
|
ruleType="query"
|
||||||
|
dataTestSubj="queryPreviewSelect"
|
||||||
|
idAria="queryPreview"
|
||||||
|
query={{ query: { query: 'host.name:*', language: 'kuery' }, filters: [] }}
|
||||||
|
index={['foo-*']}
|
||||||
|
threshold={undefined}
|
||||||
|
isDisabled={false}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
inspect: { dsl: [], response: [] },
|
||||||
|
totalCount: 2,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
data: [],
|
||||||
|
buckets: [],
|
||||||
|
},
|
||||||
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of(getMockEqlResponse())
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
test('it renders query histogram when rule type is saved_query and preview button clicked', () => {
|
test('it renders query histogram when rule type is saved_query and preview button clicked', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
@ -175,6 +241,42 @@ describe('PreviewQuery', () => {
|
||||||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeTruthy();
|
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
<TestProviders>
|
||||||
|
<PreviewQuery
|
||||||
|
ruleType="eql"
|
||||||
|
dataTestSubj="queryPreviewSelect"
|
||||||
|
idAria="queryPreview"
|
||||||
|
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||||
|
index={['foo-*']}
|
||||||
|
threshold={undefined}
|
||||||
|
isDisabled={false}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
(useEqlPreview as jest.Mock).mockReturnValue([
|
||||||
|
false,
|
||||||
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of(getMockEqlResponse())
|
||||||
|
),
|
||||||
|
{
|
||||||
|
inspect: { dsl: [], response: [] },
|
||||||
|
totalCount: 2,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
data: [],
|
||||||
|
buckets: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => {
|
test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
@ -192,16 +294,70 @@ describe('PreviewQuery', () => {
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
inspect: { dsl: [], response: [] },
|
||||||
|
totalCount: 500,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
data: [],
|
||||||
|
buckets: [{ key: 'siem-kibana', doc_count: 500 }],
|
||||||
|
},
|
||||||
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of(getMockEqlResponse())
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||||
|
|
||||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||||
|
|
||||||
expect(mockCalls.length).toEqual(1);
|
expect(mockCalls.length).toEqual(1);
|
||||||
|
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy();
|
||||||
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy();
|
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||||
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeTruthy();
|
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeTruthy();
|
||||||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
<TestProviders>
|
||||||
|
<PreviewQuery
|
||||||
|
ruleType="query"
|
||||||
|
dataTestSubj="queryPreviewSelect"
|
||||||
|
idAria="queryPreview"
|
||||||
|
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||||
|
index={['foo-*']}
|
||||||
|
threshold={{ field: 'agent.hostname', value: 200 }}
|
||||||
|
isDisabled={false}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
inspect: { dsl: [], response: [] },
|
||||||
|
totalCount: 500,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
data: [],
|
||||||
|
buckets: [
|
||||||
|
{ key: 'siem-kibana', doc_count: 200 },
|
||||||
|
{ key: 'siem-windows', doc_count: 300 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||||
|
of(getMockEqlResponse())
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => {
|
test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
@ -255,4 +411,33 @@ describe('PreviewQuery', () => {
|
||||||
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy();
|
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy();
|
||||||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it hides histogram when timeframe changes', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
|
<TestProviders>
|
||||||
|
<PreviewQuery
|
||||||
|
ruleType="threshold"
|
||||||
|
dataTestSubj="queryPreviewSelect"
|
||||||
|
idAria="queryPreview"
|
||||||
|
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||||
|
index={['foo-*']}
|
||||||
|
threshold={undefined}
|
||||||
|
isDisabled={false}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy();
|
||||||
|
|
||||||
|
wrapper
|
||||||
|
.find('[data-test-subj="queryPreviewTimeframeSelect"] select')
|
||||||
|
.at(0)
|
||||||
|
.simulate('change', { target: { value: 'd' } });
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import React, { Fragment, useCallback, useEffect, useReducer } from 'react';
|
import React, { Fragment, useCallback, useEffect, useReducer, useRef } from 'react';
|
||||||
import { Unit } from '@elastic/datemath';
|
import { Unit } from '@elastic/datemath';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
|
@ -110,29 +110,31 @@ export const PreviewQuery = ({
|
||||||
{ inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets },
|
{ inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets },
|
||||||
startNonEql,
|
startNonEql,
|
||||||
] = useMatrixHistogram({
|
] = useMatrixHistogram({
|
||||||
errorMessage: i18n.PREVIEW_QUERY_ERROR,
|
errorMessage: i18n.QUERY_PREVIEW_ERROR,
|
||||||
endDate: fromTime,
|
endDate: fromTime,
|
||||||
startDate: toTime,
|
startDate: toTime,
|
||||||
filterQuery: queryFilter,
|
filterQuery: queryFilter,
|
||||||
indexNames: index,
|
indexNames: index,
|
||||||
histogramType: MatrixHistogramType.events,
|
histogramType: MatrixHistogramType.events,
|
||||||
stackByField: 'event.category',
|
stackByField: 'event.category',
|
||||||
threshold,
|
threshold: ruleType === 'threshold' ? threshold : undefined,
|
||||||
skip: true,
|
skip: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setQueryInfo = useCallback(
|
const setQueryInfo = useCallback(
|
||||||
(queryBar: FieldValueQueryBar | undefined): void => {
|
(queryBar: FieldValueQueryBar | undefined, indices: string[], type: Type): void => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'setQueryInfo',
|
type: 'setQueryInfo',
|
||||||
queryBar,
|
queryBar,
|
||||||
index,
|
index: indices,
|
||||||
ruleType,
|
ruleType: type,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[dispatch, index, ruleType]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const debouncedSetQueryInfo = useRef(debounce(500, setQueryInfo));
|
||||||
|
|
||||||
const setTimeframeSelect = useCallback(
|
const setTimeframeSelect = useCallback(
|
||||||
(selection: Unit): void => {
|
(selection: Unit): void => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -190,11 +192,9 @@ export const PreviewQuery = ({
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect((): void => {
|
useEffect(() => {
|
||||||
const debounced = debounce(1000, setQueryInfo);
|
debouncedSetQueryInfo.current(query, index, ruleType);
|
||||||
|
}, [index, query, ruleType]);
|
||||||
debounced(query);
|
|
||||||
}, [setQueryInfo, query]);
|
|
||||||
|
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
setThresholdValues(threshold, ruleType);
|
setThresholdValues(threshold, ruleType);
|
||||||
|
@ -205,12 +205,32 @@ export const PreviewQuery = ({
|
||||||
}, [ruleType, setRuleTypeChange]);
|
}, [ruleType, setRuleTypeChange]);
|
||||||
|
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
const totalHits = ruleType === 'eql' ? eqlQueryTotal : matrixHistTotal;
|
switch (ruleType) {
|
||||||
|
case 'eql':
|
||||||
if (isNoisy(totalHits, timeframe)) {
|
if (isNoisy(eqlQueryTotal, timeframe)) {
|
||||||
setNoiseWarning();
|
setNoiseWarning();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'threshold':
|
||||||
|
const totalHits = thresholdFieldExists ? buckets.length : matrixHistTotal;
|
||||||
|
if (isNoisy(totalHits, timeframe)) {
|
||||||
|
setNoiseWarning();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (isNoisy(matrixHistTotal, timeframe)) {
|
||||||
|
setNoiseWarning();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [timeframe, matrixHistTotal, eqlQueryTotal, ruleType, setNoiseWarning]);
|
}, [
|
||||||
|
timeframe,
|
||||||
|
matrixHistTotal,
|
||||||
|
eqlQueryTotal,
|
||||||
|
ruleType,
|
||||||
|
setNoiseWarning,
|
||||||
|
thresholdFieldExists,
|
||||||
|
buckets.length,
|
||||||
|
]);
|
||||||
|
|
||||||
const handlePreviewEqlQuery = useCallback(
|
const handlePreviewEqlQuery = useCallback(
|
||||||
(to: string, from: string): void => {
|
(to: string, from: string): void => {
|
||||||
|
@ -263,8 +283,9 @@ export const PreviewQuery = ({
|
||||||
options={timeframeOptions}
|
options={timeframeOptions}
|
||||||
value={timeframe}
|
value={timeframe}
|
||||||
onChange={handleSelectPreviewTimeframe}
|
onChange={handleSelectPreviewTimeframe}
|
||||||
aria-label={i18n.PREVIEW_SELECT_ARIA}
|
aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
data-test-subj="queryPreviewTimeframeSelect"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
|
@ -276,7 +297,7 @@ export const PreviewQuery = ({
|
||||||
onClick={handlePreviewClicked}
|
onClick={handlePreviewClicked}
|
||||||
data-test-subj="queryPreviewButton"
|
data-test-subj="queryPreviewButton"
|
||||||
>
|
>
|
||||||
{i18n.PREVIEW_LABEL}
|
{i18n.QUERY_PREVIEW_BUTTON}
|
||||||
</PreviewButton>
|
</PreviewButton>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
@ -307,7 +328,6 @@ export const PreviewQuery = ({
|
||||||
<PreviewEqlQueryHistogram
|
<PreviewEqlQueryHistogram
|
||||||
to={toTime}
|
to={toTime}
|
||||||
from={fromTime}
|
from={fromTime}
|
||||||
query={queryString}
|
|
||||||
totalCount={eqlQueryTotal}
|
totalCount={eqlQueryTotal}
|
||||||
data={eqlQueryData}
|
data={eqlQueryData}
|
||||||
inspect={eqlQueryInspect}
|
inspect={eqlQueryInspect}
|
||||||
|
|
|
@ -33,9 +33,9 @@ describe('queryPreviewReducer', () => {
|
||||||
expect(update).toEqual(initialState);
|
expect(update).toEqual(initialState);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reset showHistogram and warnings if queryBar undefined', () => {
|
test('should reset showHistogram if queryBar undefined', () => {
|
||||||
const update = reducer(
|
const update = reducer(
|
||||||
{ ...initialState, showHistogram: true, warnings: ['uh oh'] },
|
{ ...initialState, showHistogram: true },
|
||||||
{
|
{
|
||||||
type: 'setQueryInfo',
|
type: 'setQueryInfo',
|
||||||
queryBar: undefined,
|
queryBar: undefined,
|
||||||
|
@ -44,11 +44,10 @@ describe('queryPreviewReducer', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(update.warnings).toEqual([]);
|
|
||||||
expect(update.showHistogram).toBeFalsy();
|
expect(update.showHistogram).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reset showHistogram and warnings if queryBar defined', () => {
|
test('should reset showHistogram if queryBar defined', () => {
|
||||||
const update = reducer(
|
const update = reducer(
|
||||||
{ ...initialState, showHistogram: true, warnings: ['uh oh'] },
|
{ ...initialState, showHistogram: true, warnings: ['uh oh'] },
|
||||||
{
|
{
|
||||||
|
@ -62,7 +61,6 @@ describe('queryPreviewReducer', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(update.warnings).toEqual([]);
|
|
||||||
expect(update.showHistogram).toBeFalsy();
|
expect(update.showHistogram).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -82,13 +82,11 @@ export const queryPreviewReducer = () => (state: State, action: Action): State =
|
||||||
filters,
|
filters,
|
||||||
queryFilter,
|
queryFilter,
|
||||||
showHistogram: false,
|
showHistogram: false,
|
||||||
warnings: [],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
warnings: [],
|
|
||||||
showHistogram: false,
|
showHistogram: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,12 +56,12 @@ export const PreviewThresholdQueryHistogram = ({
|
||||||
};
|
};
|
||||||
}, [buckets]);
|
}, [buckets]);
|
||||||
|
|
||||||
const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(200), []);
|
const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(), []);
|
||||||
|
|
||||||
const subtitle = useMemo(
|
const subtitle = useMemo(
|
||||||
(): string =>
|
(): string =>
|
||||||
isLoading
|
isLoading
|
||||||
? i18n.PREVIEW_SUBTITLE_LOADING
|
? i18n.QUERY_PREVIEW_SUBTITLE_LOADING
|
||||||
: i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(totalCount),
|
: i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(totalCount),
|
||||||
[isLoading, totalCount]
|
[isLoading, totalCount]
|
||||||
);
|
);
|
||||||
|
@ -73,7 +73,7 @@ export const PreviewThresholdQueryHistogram = ({
|
||||||
barConfig={barConfig}
|
barConfig={barConfig}
|
||||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER}
|
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
data-test-subj="thresholdQueryPreviewHistogram"
|
data-test-subj="thresholdQueryPreviewHistogram"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
export const PREVIEW_LABEL = i18n.translate(
|
export const QUERY_PREVIEW_BUTTON = i18n.translate(
|
||||||
'xpack.securitySolution.stepDefineRule.previewQueryLabel',
|
'xpack.securitySolution.stepDefineRule.previewQueryButton',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Preview results',
|
defaultMessage: 'Preview results',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PREVIEW_SELECT_ARIA = i18n.translate(
|
export const QUERY_PREVIEW_SELECT_ARIA = i18n.translate(
|
||||||
'xpack.securitySolution.stepDefineRule.previewQueryAriaLabel',
|
'xpack.securitySolution.stepDefineRule.previewQueryAriaLabel',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Query preview timeframe select',
|
defaultMessage: 'Query preview timeframe select',
|
||||||
|
@ -85,14 +85,14 @@ export const QUERY_PREVIEW_NO_HITS = i18n.translate(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PREVIEW_QUERY_ERROR = i18n.translate(
|
export const QUERY_PREVIEW_ERROR = i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError',
|
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Error fetching preview',
|
defaultMessage: 'Error fetching preview',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PREVIEW_QUERY_DISCLAIMER = i18n.translate(
|
export const QUERY_PREVIEW_DISCLAIMER = i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer',
|
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer',
|
||||||
{
|
{
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
|
@ -100,7 +100,7 @@ export const PREVIEW_QUERY_DISCLAIMER = i18n.translate(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PREVIEW_QUERY_DISCLAIMER_EQL = i18n.translate(
|
export const QUERY_PREVIEW_DISCLAIMER_EQL = i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql',
|
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql',
|
||||||
{
|
{
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
|
@ -108,26 +108,24 @@ export const PREVIEW_QUERY_DISCLAIMER_EQL = i18n.translate(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PREVIEW_WARNING_CAP_HIT = (cap: number) =>
|
export const QUERY_PREVIEW_SUBTITLE_LOADING = i18n.translate(
|
||||||
i18n.translate(
|
|
||||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning',
|
|
||||||
{
|
|
||||||
values: { cap },
|
|
||||||
defaultMessage:
|
|
||||||
'Hit query cap size of {cap}. This query could produce more hits than the {cap} shown.',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const PREVIEW_WARNING_TIMESTAMP = i18n.translate(
|
|
||||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Unable to find "@timestamp" field on events.',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const PREVIEW_SUBTITLE_LOADING = i18n.translate(
|
|
||||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading',
|
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading',
|
||||||
{
|
{
|
||||||
defaultMessage: '...loading',
|
defaultMessage: '...loading',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const QUERY_PREVIEW_EQL_SEQUENCE_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewEqlSequenceTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'No histogram available',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const QUERY_PREVIEW_EQL_SEQUENCE_DESCRIPTION = i18n.translate(
|
||||||
|
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewEqlSequenceDescription',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'No histogram is available at this time for EQL sequence queries. You can use the inspect in the top right corner to view query details.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui';
|
import { EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||||
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
|
import React, { FC, memo, useCallback, useState, useEffect, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
// Prefer importing entire lodash library, e.g. import { get } from "lodash"
|
// Prefer importing entire lodash library, e.g. import { get } from "lodash"
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
@ -53,7 +53,7 @@ import {
|
||||||
import { EqlQueryBar } from '../eql_query_bar';
|
import { EqlQueryBar } from '../eql_query_bar';
|
||||||
import { ThreatMatchInput } from '../threatmatch_input';
|
import { ThreatMatchInput } from '../threatmatch_input';
|
||||||
import { useFetchIndex } from '../../../../common/containers/source';
|
import { useFetchIndex } from '../../../../common/containers/source';
|
||||||
import { PreviewQuery } from '../query_preview';
|
import { PreviewQuery, Threshold } from '../query_preview';
|
||||||
|
|
||||||
const CommonUseField = getUseField({ component: Field });
|
const CommonUseField = getUseField({ component: Field });
|
||||||
|
|
||||||
|
@ -210,6 +210,12 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
||||||
setOpenTimelineSearch(false);
|
setOpenTimelineSearch(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const thresholdFormValue = useMemo((): Threshold | undefined => {
|
||||||
|
return formThresholdValue != null && formThresholdField != null
|
||||||
|
? { value: formThresholdValue, field: formThresholdField[0] }
|
||||||
|
: undefined;
|
||||||
|
}, [formThresholdField, formThresholdValue]);
|
||||||
|
|
||||||
const ThresholdInputChildren = useCallback(
|
const ThresholdInputChildren = useCallback(
|
||||||
({ thresholdField, thresholdValue }) => (
|
({ thresholdField, thresholdValue }) => (
|
||||||
<ThresholdInput
|
<ThresholdInput
|
||||||
|
@ -403,11 +409,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
||||||
index={index}
|
index={index}
|
||||||
query={formQuery}
|
query={formQuery}
|
||||||
isDisabled={queryBarQuery.trim() === '' || !isQueryBarValid || index.length === 0}
|
isDisabled={queryBarQuery.trim() === '' || !isQueryBarValid || index.length === 0}
|
||||||
threshold={
|
threshold={thresholdFormValue}
|
||||||
formThresholdValue != null && formThresholdField != null
|
|
||||||
? { value: formThresholdValue, field: formThresholdField[0] }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -17356,9 +17356,7 @@
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "クエリプレビュー",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "クエリプレビュー",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "ノイズ警告:このルールではノイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "ノイズ警告:このルールではノイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "ヒットが見つかりませんでした。",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "ヒットが見つかりませんでした。",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "クエリの上限サイズ{cap}に達しました。このクエリは表示されている{cap}を超えるヒットを生成できませんでした。",
|
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} {buckets, plural, =1 {固有のヒット} other {固有のヒット}}",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} {buckets, plural, =1 {固有のヒット} other {固有のヒット}}",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "イベントで「@timestamp」フィールドが見つかりません",
|
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} {hits, plural, =1 {ヒット} other {ヒット}}",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} {hits, plural, =1 {ヒット} other {ヒット}}",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "クイッククエリプレビュー",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "クイッククエリプレビュー",
|
||||||
|
@ -18356,7 +18354,6 @@
|
||||||
"xpack.securitySolution.security.title": "セキュリティ",
|
"xpack.securitySolution.security.title": "セキュリティ",
|
||||||
"xpack.securitySolution.source.destination.packetsLabel": "パケット",
|
"xpack.securitySolution.source.destination.packetsLabel": "パケット",
|
||||||
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "クエリプレビュータイムフレーム選択",
|
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "クエリプレビュータイムフレーム選択",
|
||||||
"xpack.securitySolution.stepDefineRule.previewQueryLabel": "結果を表示",
|
|
||||||
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "次の手段で接続を受け付けました。",
|
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "次の手段で接続を受け付けました。",
|
||||||
"xpack.securitySolution.system.acceptedDescription": "以下を経由してユーザーを受け入れました。",
|
"xpack.securitySolution.system.acceptedDescription": "以下を経由してユーザーを受け入れました。",
|
||||||
"xpack.securitySolution.system.attemptedLoginDescription": "以下を経由してログインを試行しました:",
|
"xpack.securitySolution.system.attemptedLoginDescription": "以下を経由してログインを試行しました:",
|
||||||
|
|
|
@ -17374,9 +17374,7 @@
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "查询预览",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "查询预览",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "找不到任何命中。",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "找不到任何命中。",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "命中查询上限大小为 {cap}。此查询生成的命中数可能大于显示的 {cap}。",
|
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} 个{buckets, plural, =1 {唯一命中} other {唯一命中}}",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} 个{buckets, plural, =1 {唯一命中} other {唯一命中}}",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "在事件中找不到“@timestamp”字段。",
|
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} 个{hits, plural, =1 {命中} other {命中}}",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} 个{hits, plural, =1 {命中} other {命中}}",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果",
|
||||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "快速查询预览",
|
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "快速查询预览",
|
||||||
|
@ -18375,7 +18373,6 @@
|
||||||
"xpack.securitySolution.security.title": "安全",
|
"xpack.securitySolution.security.title": "安全",
|
||||||
"xpack.securitySolution.source.destination.packetsLabel": "pkts",
|
"xpack.securitySolution.source.destination.packetsLabel": "pkts",
|
||||||
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "查询预览时间范围选择",
|
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "查询预览时间范围选择",
|
||||||
"xpack.securitySolution.stepDefineRule.previewQueryLabel": "预览结果",
|
|
||||||
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "已接受连接 - 通过",
|
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "已接受连接 - 通过",
|
||||||
"xpack.securitySolution.system.acceptedDescription": "已接受该用户 - 通过",
|
"xpack.securitySolution.system.acceptedDescription": "已接受该用户 - 通过",
|
||||||
"xpack.securitySolution.system.attemptedLoginDescription": "已尝试登录 - 通过",
|
"xpack.securitySolution.system.attemptedLoginDescription": "已尝试登录 - 通过",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue