[Security Solution][Detections] - Fix threshold preview (#94224) (#94497)

### Summary

Addresses #92732

7.11+ versions of threshold preview histogram were aggregating by "event.category". This PR updates the preview histogram to take into account threshold field groups and cardinality.

It may need to be called out in documentation or updated to remind users that preview is not an exact guarantee of what signals will be produced as it does not take into account interval and any timestamp_override. Threshold gets a tad bit more confusing because of the multiple aggregations occurring (threshold --> group by field --> histogram).
This commit is contained in:
Yara Tercero 2021-03-11 15:55:30 -08:00 committed by GitHub
parent 312238160e
commit e3c3c7c3f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 588 additions and 107 deletions

View file

@ -38,11 +38,11 @@ export interface MatrixHistogramRequestOptions extends RequestBasicOptions {
stackByField: string;
threshold?:
| {
field: string | string[] | undefined;
value: number;
field: string[];
value: string;
cardinality?: {
field: string[];
value: number;
value: string;
};
}
| undefined;

View file

@ -15,6 +15,7 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security
import { UpdateDateRange } from '../charts/common';
import { GlobalTimeArgs } from '../../containers/use_global_time';
import { DocValueFields } from '../../../../common/search_strategy';
import { Threshold } from '../../../detections/components/rules/query_preview';
export type MatrixHistogramMappingTypes = Record<
string,
@ -74,16 +75,7 @@ export interface MatrixHistogramQueryProps {
stackByField: string;
startDate: string;
histogramType: MatrixHistogramType;
threshold?:
| {
field: string | string[] | undefined;
value: number;
cardinality?: {
field: string[];
value: number;
};
}
| undefined;
threshold?: Threshold;
skip?: boolean;
isPtrIncluded?: boolean;
}

View file

@ -166,7 +166,7 @@ export const getThresholdHistogramConfig = (): ChartSeriesConfigs => {
yTickFormatter: (value: string | number): string => value.toLocaleString(),
tickSize: 8,
},
yAxisTitle: i18n.QUERY_GRAPH_COUNT,
yAxisTitle: i18n.THRESHOLD_QUERY_GRAPH_COUNT,
settings: {
legendPosition: Position.Right,
showLegend: true,

View file

@ -284,11 +284,11 @@ describe('PreviewQuery', () => {
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{
field: 'agent.hostname',
value: 200,
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
}}
isDisabled={false}
@ -331,11 +331,11 @@ describe('PreviewQuery', () => {
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{
field: 'agent.hostname',
value: 200,
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
}}
isDisabled={false}
@ -365,7 +365,7 @@ describe('PreviewQuery', () => {
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 empty array', () => {
const wrapper = mount(
<TestProviders>
<PreviewQuery
@ -375,11 +375,11 @@ describe('PreviewQuery', () => {
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{
field: undefined,
value: 200,
field: [],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
}}
isDisabled={false}
@ -407,11 +407,11 @@ describe('PreviewQuery', () => {
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{
field: ' ',
value: 200,
field: [' '],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
}}
isDisabled={false}

View file

@ -32,6 +32,7 @@ import { formatDate } from '../../../../common/components/super_date_picker';
import { State, queryPreviewReducer } from './reducer';
import { isNoisy } from './helpers';
import { PreviewCustomQueryHistogram } from './custom_histogram';
import { FieldValueThreshold } from '../threshold_input';
const Select = styled(EuiSelect)`
width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth};
@ -56,16 +57,7 @@ export const initialState: State = {
showNonEqlHistogram: false,
};
export type Threshold =
| {
field: string | string[] | undefined;
value: number;
cardinality?: {
field: string[];
value: number;
};
}
| undefined;
export type Threshold = FieldValueThreshold | undefined;
interface PreviewQueryProps {
dataTestSubj: string;

View file

@ -335,11 +335,11 @@ describe('queryPreviewReducer', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: {
field: 'agent.hostname',
value: 200,
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
},
ruleType: 'threshold',
@ -351,15 +351,15 @@ describe('queryPreviewReducer', () => {
expect(update.warnings).toEqual([]);
});
test('should set thresholdFieldExists to false if threshold field is not defined', () => {
test('should set thresholdFieldExists to false if threshold field is empty array', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: {
field: undefined,
value: 200,
field: [],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
},
ruleType: 'threshold',
@ -375,11 +375,11 @@ describe('queryPreviewReducer', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: {
field: ' ',
value: 200,
field: [' '],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
},
ruleType: 'threshold',
@ -395,11 +395,11 @@ describe('queryPreviewReducer', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: {
field: 'agent.hostname',
value: 200,
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
},
ruleType: 'eql',
@ -414,11 +414,11 @@ describe('queryPreviewReducer', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: {
field: 'agent.hostname',
value: 200,
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
},
ruleType: 'query',
@ -433,11 +433,11 @@ describe('queryPreviewReducer', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: {
field: 'agent.hostname',
value: 200,
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: 2,
value: '2',
},
},
ruleType: 'saved_query',

View file

@ -67,7 +67,6 @@ export type Action =
type: 'setToFrom';
};
/* eslint-disable-next-line complexity */
export const queryPreviewReducer = () => (state: State, action: Action): State => {
switch (action.type) {
case 'setQueryInfo': {
@ -132,9 +131,8 @@ export const queryPreviewReducer = () => (state: State, action: Action): State =
const thresholdField =
action.threshold != null &&
action.threshold.field != null &&
((typeof action.threshold.field === 'string' && action.threshold.field.trim() !== '') ||
(Array.isArray(action.threshold.field) &&
action.threshold.field.every((field) => field.trim() !== '')));
action.threshold.field.length > 0 &&
action.threshold.field.every((field) => field.trim() !== '');
const showNonEqlHist =
action.ruleType === 'query' ||
action.ruleType === 'saved_query' ||

View file

@ -42,6 +42,13 @@ export const QUERY_GRAPH_COUNT = i18n.translate(
}
);
export const THRESHOLD_QUERY_GRAPH_COUNT = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryThresholdGraphCountLabel',
{
defaultMessage: 'Cumulative Threshold Count',
}
);
export const QUERY_GRAPH_HITS_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle',
{

View file

@ -6,7 +6,7 @@
*/
import { EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui';
import React, { FC, memo, useCallback, useState, useEffect, useMemo } from 'react';
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
import styled from 'styled-components';
// Prefer importing entire lodash library, e.g. import { get } from "lodash"
// eslint-disable-next-line no-restricted-imports
@ -54,7 +54,7 @@ import {
import { EqlQueryBar } from '../eql_query_bar';
import { ThreatMatchInput } from '../threatmatch_input';
import { BrowserField, BrowserFields, useFetchIndex } from '../../../../common/containers/source';
import { PreviewQuery, Threshold } from '../query_preview';
import { PreviewQuery } from '../query_preview';
const CommonUseField = getUseField({ component: Field });
@ -154,24 +154,15 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
ruleType: formRuleType,
queryBar: formQuery,
threatIndex: formThreatIndex,
'threshold.field': formThresholdField,
'threshold.value': formThresholdValue,
'threshold.cardinality.field': formThresholdCardinalityField,
'threshold.cardinality.value': formThresholdCardinalityValue,
threshold: formThreshold,
},
] = useFormData<
DefineStepRule & {
'threshold.field': string[] | undefined;
'threshold.value': number | undefined;
'threshold.cardinality.field': string[] | undefined;
'threshold.cardinality.value': number | undefined;
}
>({
] = useFormData<DefineStepRule>({
form,
watch: [
'index',
'ruleType',
'queryBar',
'threshold',
'threshold.field',
'threshold.value',
'threshold.cardinality.field',
@ -288,24 +279,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
setOpenTimelineSearch(false);
}, []);
const thresholdFormValue = useMemo((): Threshold | undefined => {
return formThresholdValue != null
? {
field: formThresholdField ?? [],
value: formThresholdValue,
cardinality: {
field: formThresholdCardinalityField ?? [],
value: formThresholdCardinalityValue ?? 0, // FIXME
},
}
: undefined;
}, [
formThresholdField,
formThresholdValue,
formThresholdCardinalityField,
formThresholdCardinalityValue,
]);
const ThresholdInputChildren = useCallback(
({ thresholdField, thresholdValue, thresholdCardinalityField, thresholdCardinalityValue }) => (
<ThresholdInput
@ -507,7 +480,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
index={index}
query={formQuery}
isDisabled={!isQueryBarValid || index.length === 0}
threshold={thresholdFormValue}
threshold={formThreshold}
/>
</>
)}

View file

@ -99,17 +99,19 @@ export const expectedThresholdDsl = {
aggregations: {
eventActionGroup: {
terms: {
field: 'host.name',
script: {
lang: 'painless',
source: "doc['host.name'].value + ':' + doc['agent.name'].value",
},
order: { _count: 'desc' },
size: 10,
min_doc_count: 200,
},
aggs: {
events: {
date_histogram: {
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 0,
min_doc_count: 200,
extended_bounds: { min: 1599581486215, max: 1599667886215 },
},
},
@ -157,14 +159,130 @@ export const expectedThresholdMissingFieldDsl = {
missing: 'All others',
order: { _count: 'desc' },
size: 10,
min_doc_count: 200,
},
aggs: {
events: {
date_histogram: {
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 0,
min_doc_count: 200,
extended_bounds: { min: 1599581486215, max: 1599667886215 },
},
},
},
},
},
query: {
bool: {
filter: [
{ bool: { must: [], filter: [{ match_all: {} }], should: [], must_not: [] } },
{
range: {
'@timestamp': {
gte: '2020-09-08T16:11:26.215Z',
lte: '2020-09-09T16:11:26.215Z',
format: 'strict_date_optional_time',
},
},
},
],
},
},
size: 0,
},
};
export const expectedThresholdWithCardinalityDsl = {
allowNoIndices: true,
body: {
aggregations: {
eventActionGroup: {
aggs: {
cardinality_check: {
bucket_selector: {
buckets_path: { cardinalityCount: 'cardinality_count' },
script: 'params.cardinalityCount >= 10',
},
},
cardinality_count: { cardinality: { field: 'agent.name' } },
events: {
date_histogram: {
extended_bounds: { max: 1599667886215, min: 1599581486215 },
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 200,
},
},
},
terms: {
field: 'event.action',
missing: 'All others',
order: { _count: 'desc' },
size: 10,
},
},
},
query: {
bool: {
filter: [
{ bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } },
{
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2020-09-08T16:11:26.215Z',
lte: '2020-09-09T16:11:26.215Z',
},
},
},
],
},
},
size: 0,
},
ignoreUnavailable: true,
index: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
],
track_total_hits: true,
};
export const expectedThresholdWithGroupFieldsAndCardinalityDsl = {
index: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
],
allowNoIndices: true,
ignoreUnavailable: true,
track_total_hits: true,
body: {
aggregations: {
eventActionGroup: {
terms: {
script: {
lang: 'painless',
source: "doc['host.name'].value + ':' + doc['agent.name'].value",
},
order: { _count: 'desc' },
size: 10,
},
aggs: {
events: {
date_histogram: {
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 200,
extended_bounds: { min: 1599581486215, max: 1599667886215 },
},
},

View file

@ -0,0 +1,174 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { buildThresholdTermsQuery, buildThresholdCardinalityQuery, BaseQuery } from './helpers';
const BASE_QUERY: BaseQuery = {
eventActionGroup: {
terms: {
order: {
_count: 'desc',
},
size: 10,
},
aggs: {
events: {
date_histogram: {
field: '@timestamp',
fixed_interval: '5000ms',
min_doc_count: 0,
extended_bounds: {
min: 1599581486215,
max: 1599667886215,
},
},
},
},
},
};
const STACK_BY_FIELD = 'event.action';
describe('buildEventsHistogramQuery - helpers', () => {
describe('buildThresholdTermsQuery', () => {
test('it builds a terms query using script if threshold field/s exist', () => {
const query = buildThresholdTermsQuery({
query: BASE_QUERY,
fields: ['agent.name', 'host.name'],
stackByField: STACK_BY_FIELD,
missing: {},
});
expect(query).toEqual({
eventActionGroup: {
aggs: {
events: {
date_histogram: {
extended_bounds: { max: 1599667886215, min: 1599581486215 },
field: '@timestamp',
fixed_interval: '5000ms',
min_doc_count: 0,
},
},
},
terms: {
order: { _count: 'desc' },
script: {
lang: 'painless',
source: "doc['agent.name'].value + ':' + doc['host.name'].value",
},
size: 10,
},
},
});
});
test('it builds a terms query using default stackByField if threshold field/s do not exist', () => {
const query = buildThresholdTermsQuery({
query: BASE_QUERY,
fields: [],
stackByField: STACK_BY_FIELD,
missing: { missing: 'All others' },
});
expect(query).toEqual({
eventActionGroup: {
aggs: {
events: {
date_histogram: {
extended_bounds: { max: 1599667886215, min: 1599581486215 },
field: '@timestamp',
fixed_interval: '5000ms',
min_doc_count: 0,
},
},
},
terms: {
field: 'event.action',
missing: 'All others',
order: { _count: 'desc' },
size: 10,
},
},
});
});
});
describe('buildThresholdCardinalityQuery', () => {
const TERMS_QUERY = {
eventActionGroup: {
terms: {
field: 'host.name',
order: { _count: 'desc' },
size: 10,
min_doc_count: 200,
},
aggs: {
events: {
date_histogram: {
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 0,
extended_bounds: { min: 1599581486215, max: 1599667886215 },
},
},
},
},
};
test('it builds query with cardinality', () => {
const query = buildThresholdCardinalityQuery({
query: TERMS_QUERY,
cardinalityField: 'agent.name',
cardinalityValue: '100',
});
expect(query).toEqual({
eventActionGroup: {
aggs: {
cardinality_check: {
bucket_selector: {
buckets_path: { cardinalityCount: 'cardinality_count' },
script: 'params.cardinalityCount >= 100',
},
},
cardinality_count: { cardinality: { field: 'agent.name' } },
events: {
date_histogram: {
extended_bounds: { max: 1599667886215, min: 1599581486215 },
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 0,
},
},
},
terms: { field: 'host.name', min_doc_count: 200, order: { _count: 'desc' }, size: 10 },
},
});
});
test('it builds a terms query using default stackByField if threshold field/s do not exist', () => {
const query = buildThresholdCardinalityQuery({
query: TERMS_QUERY,
cardinalityField: '',
cardinalityValue: '',
});
expect(query).toEqual({
eventActionGroup: {
aggs: {
events: {
date_histogram: {
extended_bounds: { max: 1599667886215, min: 1599581486215 },
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 0,
},
},
},
terms: { field: 'host.name', min_doc_count: 200, order: { _count: 'desc' }, size: 10 },
},
});
});
});
});

View file

@ -0,0 +1,114 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface BaseQuery {
eventActionGroup: {
terms: {
min_doc_count?: number;
order?: {
_count?: string;
};
size?: number;
field?: string | string[];
script?: {
lang: string;
source: string;
};
missing?: string;
};
aggs: {
events?: unknown;
cardinality_count?: {
cardinality?: {
field?: string;
};
};
cardinality_check?: {
bucket_selector?: {
buckets_path?: {
cardinalityCount?: string;
};
script?: string;
};
};
};
};
}
export const buildThresholdTermsQuery = ({
query,
fields,
stackByField,
missing,
}: {
query: BaseQuery;
fields: string[];
stackByField: string;
missing: { missing?: string };
}): BaseQuery => {
if (fields.length > 1) {
return {
eventActionGroup: {
...query.eventActionGroup,
terms: {
...query.eventActionGroup.terms,
script: {
lang: 'painless',
source: fields.map((f) => `doc['${f}'].value`).join(` + ':' + `),
},
},
},
};
} else {
return {
eventActionGroup: {
...query.eventActionGroup,
terms: {
...query.eventActionGroup.terms,
field: fields[0] ?? stackByField,
...missing,
},
},
};
}
};
export const buildThresholdCardinalityQuery = ({
query,
cardinalityField,
cardinalityValue,
}: {
query: BaseQuery;
cardinalityField: string | undefined;
cardinalityValue: string;
}): BaseQuery => {
if (cardinalityField != null && cardinalityField !== '' && cardinalityValue !== '') {
return {
eventActionGroup: {
...query.eventActionGroup,
aggs: {
...query.eventActionGroup.aggs,
cardinality_count: {
cardinality: {
field: cardinalityField,
},
},
cardinality_check: {
bucket_selector: {
buckets_path: {
cardinalityCount: 'cardinality_count',
},
script: `params.cardinalityCount >= ${cardinalityValue}`,
},
},
},
},
};
} else {
return query;
}
};

View file

@ -11,6 +11,7 @@ import {
expectedDsl,
expectedThresholdDsl,
expectedThresholdMissingFieldDsl,
expectedThresholdWithCardinalityDsl,
} from './__mocks__/';
describe('buildEventsHistogramQuery', () => {
@ -18,15 +19,111 @@ describe('buildEventsHistogramQuery', () => {
expect(buildEventsHistogramQuery(mockOptions)).toEqual(expectedDsl);
});
test('builds query with just min doc if "threshold.field" is undefined and "missing" param included', () => {
test('builds query with just min doc if "threshold.field" is empty array and "missing" param included', () => {
expect(
buildEventsHistogramQuery({ ...mockOptions, threshold: { field: undefined, value: 200 } })
buildEventsHistogramQuery({
...mockOptions,
threshold: { field: [], value: '200', cardinality: { field: [], value: '0' } },
})
).toEqual(expectedThresholdMissingFieldDsl);
});
test('builds query with specified threshold field and without "missing" param if "threshold.field" is defined', () => {
test('builds query with specified threshold fields and without "missing" param if "threshold.field" is multi field', () => {
expect(
buildEventsHistogramQuery({ ...mockOptions, threshold: { field: 'host.name', value: 200 } })
buildEventsHistogramQuery({
...mockOptions,
threshold: {
field: ['host.name', 'agent.name'],
value: '200',
},
})
).toEqual(expectedThresholdDsl);
});
test('builds query with specified threshold cardinality if defined', () => {
expect(
buildEventsHistogramQuery({
...mockOptions,
threshold: {
field: [],
value: '200',
cardinality: { field: ['agent.name'], value: '10' },
},
})
).toEqual(expectedThresholdWithCardinalityDsl);
});
test('builds query with specified threshold group fields and cardinality if defined', () => {
expect(
buildEventsHistogramQuery({
...mockOptions,
threshold: {
field: ['host.name', 'agent.name'],
value: '200',
cardinality: { field: ['agent.name'], value: '10' },
},
})
).toEqual({
allowNoIndices: true,
body: {
aggregations: {
eventActionGroup: {
aggs: {
cardinality_check: {
bucket_selector: {
buckets_path: { cardinalityCount: 'cardinality_count' },
script: 'params.cardinalityCount >= 10',
},
},
cardinality_count: { cardinality: { field: 'agent.name' } },
events: {
date_histogram: {
extended_bounds: { max: 1599667886215, min: 1599581486215 },
field: '@timestamp',
fixed_interval: '2700000ms',
min_doc_count: 200,
},
},
},
terms: {
order: { _count: 'desc' },
script: {
lang: 'painless',
source: "doc['host.name'].value + ':' + doc['agent.name'].value",
},
size: 10,
},
},
},
query: {
bool: {
filter: [
{ bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } },
{
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2020-09-08T16:11:26.215Z',
lte: '2020-09-09T16:11:26.215Z',
},
},
},
],
},
},
size: 0,
},
ignoreUnavailable: true,
index: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
],
track_total_hits: true,
});
});
});

View file

@ -14,6 +14,7 @@ import {
} from '../../../../../utils/build_query';
import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram';
import * as i18n from './translations';
import { BaseQuery, buildThresholdCardinalityQuery, buildThresholdTermsQuery } from './helpers';
export const buildEventsHistogramQuery = ({
filterQuery,
@ -42,7 +43,7 @@ export const buildEventsHistogramQuery = ({
date_histogram: {
field: histogramTimestampField,
fixed_interval: interval,
min_doc_count: 0,
min_doc_count: threshold != null ? Number(threshold?.value) : 0,
extended_bounds: {
min: moment(from).valueOf(),
max: moment(to).valueOf(),
@ -58,22 +59,37 @@ export const buildEventsHistogramQuery = ({
: {};
if (threshold != null) {
return {
const query: BaseQuery = {
eventActionGroup: {
terms: {
field: threshold.field ?? stackByField,
...(threshold.field != null ? {} : missing),
order: {
_count: 'desc',
},
size: 10,
min_doc_count: threshold.value,
},
aggs: {
events: dateHistogram,
},
},
};
const baseQuery = buildThresholdTermsQuery({
query,
fields: threshold.field ?? [],
stackByField,
missing,
});
if (threshold.cardinality != null) {
const enrichedQuery = buildThresholdCardinalityQuery({
query: baseQuery,
cardinalityField: threshold.cardinality.field[0],
cardinalityValue: threshold.cardinality.value,
});
return enrichedQuery;
}
return baseQuery;
}
return {