mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
# Backport This will backport the following commits from `main` to `8.8`: - [[Security solution] Fix grouping query, be ready for arrays! (#157330)](https://github.com/elastic/kibana/pull/157330) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Steph Milovic","email":"stephanie.milovic@elastic.co"},"sourceCommit":{"committedDate":"2023-05-11T23:03:16Z","message":"[Security solution] Fix grouping query, be ready for arrays! (#157330)","sha":"4371c157b0bd7a084836d5c9928bf1752f2c9d52","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Threat Hunting","Team: SecuritySolution","Team:Threat Hunting:Explore","v8.8.0","Feature:Alerts Grouping","v8.9.0"],"number":157330,"url":"https://github.com/elastic/kibana/pull/157330","mergeCommit":{"message":"[Security solution] Fix grouping query, be ready for arrays! (#157330)","sha":"4371c157b0bd7a084836d5c9928bf1752f2c9d52"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/157330","number":157330,"mergeCommit":{"message":"[Security solution] Fix grouping query, be ready for arrays! (#157330)","sha":"4371c157b0bd7a084836d5c9928bf1752f2c9d52"}}]}] BACKPORT--> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c1a961d80a
commit
263945cdc0
18 changed files with 560 additions and 693 deletions
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const createGroupFilter = (selectedGroup: string, query?: string | null) =>
|
||||
query && selectedGroup
|
||||
? [
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
key: selectedGroup,
|
||||
negate: false,
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
[selectedGroup]: {
|
||||
query,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
export const getNullGroupFilter = (selectedGroup: string) => [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: true,
|
||||
alias: null,
|
||||
key: selectedGroup,
|
||||
field: selectedGroup,
|
||||
value: 'exists',
|
||||
type: 'exists',
|
||||
},
|
||||
query: {
|
||||
exists: {
|
||||
field: selectedGroup,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { GroupPanel } from '.';
|
||||
import { createGroupFilter, getNullGroupFilter } from './helpers';
|
||||
import { createGroupFilter, getNullGroupFilter } from '../../containers/query/helpers';
|
||||
import React from 'react';
|
||||
import { groupingBucket } from '../../mocks';
|
||||
|
||||
const onToggleGroup = jest.fn();
|
||||
const renderChildComponent = jest.fn();
|
||||
|
@ -20,40 +21,10 @@ const testProps = {
|
|||
isLoading: false,
|
||||
isNullGroup: false,
|
||||
groupBucket: {
|
||||
...groupingBucket,
|
||||
selectedGroup,
|
||||
key: [ruleName, ruleName],
|
||||
key_as_string: `${ruleName}|${ruleName}`,
|
||||
doc_count: 98,
|
||||
hostsCountAggregation: {
|
||||
value: 5,
|
||||
},
|
||||
ruleTags: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
alertsCount: {
|
||||
value: 98,
|
||||
},
|
||||
rulesCountAggregation: {
|
||||
value: 1,
|
||||
},
|
||||
severitiesSubAggregation: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'low',
|
||||
doc_count: 98,
|
||||
},
|
||||
],
|
||||
},
|
||||
countSeveritySubAggregation: {
|
||||
value: 1,
|
||||
},
|
||||
usersCountAggregation: {
|
||||
value: 98,
|
||||
},
|
||||
key: [ruleName],
|
||||
key_as_string: `${ruleName}`,
|
||||
},
|
||||
renderChildComponent,
|
||||
selectedGroup,
|
||||
|
@ -68,7 +39,7 @@ describe('grouping accordion panel', () => {
|
|||
const { getByTestId } = render(<GroupPanel {...testProps} />);
|
||||
expect(getByTestId('grouping-accordion')).toBeInTheDocument();
|
||||
expect(renderChildComponent).toHaveBeenCalledWith(
|
||||
createGroupFilter(testProps.selectedGroup, ruleName)
|
||||
createGroupFilter(testProps.selectedGroup, [ruleName])
|
||||
);
|
||||
});
|
||||
it('creates the query for the selectedGroup attribute when the group is null', () => {
|
||||
|
@ -82,8 +53,7 @@ describe('grouping accordion panel', () => {
|
|||
{...testProps}
|
||||
groupBucket={{
|
||||
...testProps.groupBucket,
|
||||
// @ts-expect-error
|
||||
key: null,
|
||||
selectedGroup: 'wrong-group',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -9,9 +9,8 @@
|
|||
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { firstNonNullValue } from '../../helpers';
|
||||
import type { GroupingBucket } from '../types';
|
||||
import { createGroupFilter, getNullGroupFilter } from './helpers';
|
||||
import { createGroupFilter, getNullGroupFilter } from '../../containers/query/helpers';
|
||||
|
||||
interface GroupPanelProps<T> {
|
||||
customAccordionButtonClassName?: string;
|
||||
|
@ -41,9 +40,11 @@ const DefaultGroupPanelRenderer = ({
|
|||
}) => (
|
||||
<div>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={false} className="eui-textTruncate">
|
||||
<EuiTitle size="xs" className="euiAccordionForm__title">
|
||||
<h4 className="eui-textTruncate">{title}</h4>
|
||||
<h4 className="eui-textTruncate" title={title}>
|
||||
{title}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{isNullGroup && nullGroupMessage && (
|
||||
|
@ -81,17 +82,23 @@ const GroupPanelComponent = <T,>({
|
|||
lastForceState.current = 'open';
|
||||
}
|
||||
}, [onGroupClose, forceState, selectedGroup]);
|
||||
const groupFieldValue = useMemo(
|
||||
() => (groupBucket.selectedGroup === selectedGroup ? firstNonNullValue(groupBucket.key) : null),
|
||||
[groupBucket.key, groupBucket.selectedGroup, selectedGroup]
|
||||
const groupFieldValue = useMemo<{ asString: string | null; asArray: string[] | null }>(
|
||||
() =>
|
||||
groupBucket.selectedGroup === selectedGroup
|
||||
? {
|
||||
asString: groupBucket.key_as_string,
|
||||
asArray: groupBucket.key,
|
||||
}
|
||||
: { asString: null, asArray: null },
|
||||
[groupBucket.key, groupBucket.key_as_string, groupBucket.selectedGroup, selectedGroup]
|
||||
);
|
||||
|
||||
const groupFilters = useMemo(
|
||||
() =>
|
||||
isNullGroup
|
||||
? getNullGroupFilter(selectedGroup)
|
||||
: createGroupFilter(selectedGroup, groupFieldValue),
|
||||
[groupFieldValue, isNullGroup, selectedGroup]
|
||||
: createGroupFilter(selectedGroup, groupFieldValue.asArray),
|
||||
[groupFieldValue.asArray, isNullGroup, selectedGroup]
|
||||
);
|
||||
|
||||
const onToggle = useCallback(
|
||||
|
@ -103,14 +110,14 @@ const GroupPanelComponent = <T,>({
|
|||
[groupBucket, onToggleGroup]
|
||||
);
|
||||
|
||||
return !groupFieldValue ? null : (
|
||||
return !groupFieldValue.asString ? null : (
|
||||
<EuiAccordion
|
||||
buttonClassName={customAccordionButtonClassName}
|
||||
buttonContent={
|
||||
<div data-test-subj="group-panel-toggle" className="groupingPanelRenderer">
|
||||
{groupPanelRenderer ?? (
|
||||
<DefaultGroupPanelRenderer
|
||||
title={groupFieldValue}
|
||||
title={groupFieldValue.asString}
|
||||
isNullGroup={isNullGroup}
|
||||
nullGroupMessage={nullGroupMessage}
|
||||
/>
|
||||
|
@ -123,7 +130,7 @@ const GroupPanelComponent = <T,>({
|
|||
extraAction={extraAction}
|
||||
forceState={forceState}
|
||||
isLoading={isLoading}
|
||||
id={`group${groupingLevel}-${groupFieldValue}`}
|
||||
id={`group${groupingLevel}-${groupFieldValue.asString}`}
|
||||
onToggle={onToggle}
|
||||
paddingSize="m"
|
||||
>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { fireEvent, render, within } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { Grouping } from './grouping';
|
||||
import { createGroupFilter, getNullGroupFilter } from './accordion_panel/helpers';
|
||||
import { createGroupFilter, getNullGroupFilter } from '../containers/query/helpers';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { getTelemetryEvent } from '../telemetry/const';
|
||||
|
||||
|
@ -79,12 +79,12 @@ describe('grouping container', () => {
|
|||
fireEvent.click(group1);
|
||||
expect(renderChildComponent).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
createGroupFilter(testProps.selectedGroup, host1Name)
|
||||
createGroupFilter(testProps.selectedGroup, [host1Name])
|
||||
);
|
||||
fireEvent.click(group2);
|
||||
expect(renderChildComponent).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
createGroupFilter(testProps.selectedGroup, host2Name)
|
||||
createGroupFilter(testProps.selectedGroup, [host2Name])
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -17,19 +17,19 @@ import type { Filter } from '@kbn/es-query';
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { defaultUnit, firstNonNullValue } from '../helpers';
|
||||
import { createGroupFilter, getNullGroupFilter } from './accordion_panel/helpers';
|
||||
import { createGroupFilter, getNullGroupFilter } from '../containers/query/helpers';
|
||||
import { GroupPanel } from './accordion_panel';
|
||||
import { GroupStats } from './accordion_panel/group_stats';
|
||||
import { EmptyGroupingComponent } from './empty_results_panel';
|
||||
import { countCss, groupingContainerCss, groupingContainerCssLevel } from './styles';
|
||||
import { GROUPS_UNIT, NULL_GROUP } from './translations';
|
||||
import type { GroupingAggregation, GroupPanelRenderer } from './types';
|
||||
import { GroupStatsRenderer, OnGroupToggle } from './types';
|
||||
import type { ParsedGroupingAggregation, GroupPanelRenderer } from './types';
|
||||
import { GroupingBucket, GroupStatsRenderer, OnGroupToggle } from './types';
|
||||
import { getTelemetryEvent } from '../telemetry/const';
|
||||
|
||||
export interface GroupingProps<T> {
|
||||
activePage: number;
|
||||
data?: GroupingAggregation<T>;
|
||||
data?: ParsedGroupingAggregation<T>;
|
||||
groupPanelRenderer?: GroupPanelRenderer<T>;
|
||||
groupSelector?: JSX.Element;
|
||||
// list of custom UI components which correspond to your custom rendered metrics aggregations
|
||||
|
@ -92,7 +92,7 @@ const GroupingComponent = <T,>({
|
|||
|
||||
const groupPanels = useMemo(
|
||||
() =>
|
||||
data?.groupByFields?.buckets?.map((groupBucket, groupNumber) => {
|
||||
data?.groupByFields?.buckets?.map((groupBucket: GroupingBucket<T>, groupNumber) => {
|
||||
const group = firstNonNullValue(groupBucket.key);
|
||||
const groupKey = `group-${groupNumber}-${group}`;
|
||||
const isNullGroup = groupBucket.isNullGroup ?? false;
|
||||
|
@ -112,7 +112,10 @@ const GroupingComponent = <T,>({
|
|||
groupFilter={
|
||||
isNullGroup
|
||||
? getNullGroupFilter(selectedGroup)
|
||||
: createGroupFilter(selectedGroup, group)
|
||||
: createGroupFilter(
|
||||
selectedGroup,
|
||||
Array.isArray(groupBucket.key) ? groupBucket.key : [groupBucket.key]
|
||||
)
|
||||
}
|
||||
groupNumber={groupNumber}
|
||||
statRenderers={
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
// copied from common/search_strategy/common
|
||||
export interface GenericBuckets {
|
||||
key: string | string[];
|
||||
key_as_string?: string; // contains, for example, formatted dates
|
||||
|
@ -17,15 +16,16 @@ export const NONE_GROUP_KEY = 'none';
|
|||
export type RawBucket<T> = GenericBuckets & T;
|
||||
|
||||
export type GroupingBucket<T> = RawBucket<T> & {
|
||||
key: string[];
|
||||
key_as_string: string;
|
||||
selectedGroup: string;
|
||||
isNullGroup?: boolean;
|
||||
};
|
||||
|
||||
/** Defines the shape of the aggregation returned by Elasticsearch */
|
||||
// TODO: write developer docs for these fields
|
||||
export interface RootAggregation<T> {
|
||||
groupByFields?: {
|
||||
buckets?: Array<GroupingBucket<T>>;
|
||||
buckets?: Array<RawBucket<T>>;
|
||||
};
|
||||
groupsCount?: {
|
||||
value?: number | null;
|
||||
|
@ -38,6 +38,12 @@ export interface RootAggregation<T> {
|
|||
};
|
||||
}
|
||||
|
||||
export type ParsedRootAggregation<T> = RootAggregation<T> & {
|
||||
groupByFields?: {
|
||||
buckets?: Array<GroupingBucket<T>>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GroupingFieldTotalAggregation<T> = Record<
|
||||
string,
|
||||
{
|
||||
|
@ -47,6 +53,8 @@ export type GroupingFieldTotalAggregation<T> = Record<
|
|||
>;
|
||||
|
||||
export type GroupingAggregation<T> = RootAggregation<T> & GroupingFieldTotalAggregation<T>;
|
||||
export type ParsedGroupingAggregation<T> = ParsedRootAggregation<T> &
|
||||
GroupingFieldTotalAggregation<T>;
|
||||
|
||||
export interface BadgeMetric {
|
||||
value: number;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createGroupFilter } from './helpers';
|
||||
|
||||
const selectedGroup = 'host.name';
|
||||
describe('createGroupFilter', () => {
|
||||
it('returns an array of Filter objects with correct meta and query properties when values and selectedGroup are truthy', () => {
|
||||
const values = ['host1', 'host2'];
|
||||
const result = createGroupFilter(selectedGroup, values);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].meta.key).toBe(selectedGroup);
|
||||
expect(result[0].query.script.script.params.field).toBe(selectedGroup);
|
||||
expect(result[0].query.script.script.params.size).toBe(values.length);
|
||||
expect(result[1].meta.key).toBe(selectedGroup);
|
||||
expect(result[1].query.match_phrase[selectedGroup].query).toBe(values[0]);
|
||||
expect(result[2].meta.key).toBe(selectedGroup);
|
||||
expect(result[2].query.match_phrase[selectedGroup].query).toBe(values[1]);
|
||||
});
|
||||
|
||||
it('returns an empty array when values is an empty array and selectedGroup is truthy', () => {
|
||||
const result = createGroupFilter(selectedGroup, []);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns an empty array when values is null and selectedGroup is truthy', () => {
|
||||
const result = createGroupFilter(selectedGroup, null);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
|
@ -6,32 +6,82 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
/**
|
||||
* Returns a tuple of values according to the `esType` param, these values are meant to be applied in the _missing_
|
||||
* property of the query aggregation of the grouping, to look up for missing values in the response buckets.
|
||||
* These values do not need to be anything in particular, the only requirement is they have to be 2 different values that validate against the field type.
|
||||
*/
|
||||
export function getFieldTypeMissingValues(esType: string[]): [number, number] | [string, string] {
|
||||
const knownType: ES_FIELD_TYPES = esType[0] as ES_FIELD_TYPES;
|
||||
switch (knownType) {
|
||||
case ES_FIELD_TYPES.BYTE:
|
||||
case ES_FIELD_TYPES.DOUBLE:
|
||||
case ES_FIELD_TYPES.INTEGER:
|
||||
case ES_FIELD_TYPES.LONG:
|
||||
case ES_FIELD_TYPES.FLOAT:
|
||||
case ES_FIELD_TYPES.HALF_FLOAT:
|
||||
case ES_FIELD_TYPES.SCALED_FLOAT:
|
||||
case ES_FIELD_TYPES.SHORT:
|
||||
case ES_FIELD_TYPES.UNSIGNED_LONG:
|
||||
case ES_FIELD_TYPES.DATE:
|
||||
case ES_FIELD_TYPES.DATE_NANOS:
|
||||
return [0, 1];
|
||||
case ES_FIELD_TYPES.IP:
|
||||
return ['0.0.0.0', '::'];
|
||||
default:
|
||||
return ['-', '--'];
|
||||
}
|
||||
}
|
||||
|
||||
import { Filter, FILTERS } from '@kbn/es-query';
|
||||
export const getEmptyValue = () => '—';
|
||||
|
||||
type StrictFilter = Filter & {
|
||||
query: Record<string, any>;
|
||||
};
|
||||
|
||||
export const createGroupFilter = (
|
||||
selectedGroup: string,
|
||||
values?: string[] | null
|
||||
): StrictFilter[] =>
|
||||
values != null && values.length > 0
|
||||
? values.reduce(
|
||||
(acc: StrictFilter[], query) => [
|
||||
...acc,
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
key: selectedGroup,
|
||||
negate: false,
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
[selectedGroup]: {
|
||||
query,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
type: FILTERS.CUSTOM,
|
||||
negate: false,
|
||||
key: selectedGroup,
|
||||
},
|
||||
query: {
|
||||
script: {
|
||||
script: {
|
||||
// this will give us an exact match for events with multiple values on the group field
|
||||
// for example, when values === ['a'], we match events with ['a'], but not ['a', 'b', 'c']
|
||||
source: "doc[params['field']].size()==params['size']",
|
||||
params: {
|
||||
field: selectedGroup,
|
||||
size: values.length,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
: [];
|
||||
|
||||
export const getNullGroupFilter = (selectedGroup: string): StrictFilter[] => [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: true,
|
||||
alias: null,
|
||||
key: selectedGroup,
|
||||
value: 'exists',
|
||||
type: 'exists',
|
||||
},
|
||||
query: {
|
||||
exists: {
|
||||
field: selectedGroup,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { GroupingQueryArgs } from './types';
|
|||
import { getGroupingQuery, parseGroupingQuery } from '.';
|
||||
import { getEmptyValue } from './helpers';
|
||||
import { GroupingAggregation } from '../../..';
|
||||
import { groupingBucket } from '../../mocks';
|
||||
|
||||
const testProps: GroupingQueryArgs = {
|
||||
additionalFilters: [],
|
||||
|
@ -55,7 +56,7 @@ const testProps: GroupingQueryArgs = {
|
|||
pageNumber: 0,
|
||||
rootAggregations: [],
|
||||
runtimeMappings: {},
|
||||
selectedGroupEsTypes: ['keyword'],
|
||||
uniqueValue: 'whatAGreatAndUniqueValue',
|
||||
size: 25,
|
||||
to: '2023-02-23T06:59:59.999Z',
|
||||
};
|
||||
|
@ -63,14 +64,11 @@ describe('group selector', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Sets multi terms query with missing argument for 2 default values', () => {
|
||||
it('Sets runtime field and terms query', () => {
|
||||
const result = getGroupingQuery(testProps);
|
||||
result.aggs.groupByFields?.multi_terms?.terms.forEach((term, i) => {
|
||||
expect(term).toEqual({
|
||||
field: 'host.name',
|
||||
missing: i === 0 ? '-' : '--',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.runtime_mappings.groupByField.script.params.selectedGroup).toEqual('host.name');
|
||||
|
||||
expect(result.aggs.groupByFields.aggs).toEqual({
|
||||
bucket_truncate: { bucket_sort: { from: 0, size: 25 } },
|
||||
alertsCount: { cardinality: { field: 'kibana.alert.uuid' } },
|
||||
|
@ -137,100 +135,120 @@ describe('group selector', () => {
|
|||
});
|
||||
expect(result.query.bool.filter.length).toEqual(2);
|
||||
});
|
||||
it('Uses 0/1 for number fields', () => {
|
||||
const result = getGroupingQuery({ ...testProps, selectedGroupEsTypes: ['long'] });
|
||||
result.aggs.groupByFields?.multi_terms?.terms.forEach((term, i) => {
|
||||
expect(term).toEqual({
|
||||
field: 'host.name',
|
||||
missing: i === 0 ? 0 : 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
it('Uses 0.0.0.0/:: for ip fields', () => {
|
||||
const result = getGroupingQuery({ ...testProps, selectedGroupEsTypes: ['ip'] });
|
||||
result.aggs.groupByFields?.multi_terms?.terms.forEach((term, i) => {
|
||||
expect(term).toEqual({
|
||||
field: 'host.name',
|
||||
missing: i === 0 ? '0.0.0.0' : '::',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const groupingAggs = {
|
||||
groupByFields: {
|
||||
buckets: [
|
||||
{
|
||||
key: ['20.80.64.28', '20.80.64.28'],
|
||||
key_as_string: '20.80.64.28|20.80.64.28',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
...groupingBucket,
|
||||
key: '20.80.64.28',
|
||||
},
|
||||
{
|
||||
key: ['0.0.0.0', '0.0.0.0'],
|
||||
key_as_string: '0.0.0.0|0.0.0.0',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
...groupingBucket,
|
||||
key: '0.0.0.0',
|
||||
},
|
||||
{
|
||||
key: ['0.0.0.0', '::'],
|
||||
key_as_string: '0.0.0.0|::',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
...groupingBucket,
|
||||
key: testProps.uniqueValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
unitsCount: {
|
||||
value: 100,
|
||||
},
|
||||
unitsCountWithoutNull: {
|
||||
value: 100,
|
||||
},
|
||||
groupsCount: {
|
||||
value: 20,
|
||||
},
|
||||
};
|
||||
it('parseGroupingQuery finds and flags the null group', () => {
|
||||
const result = parseGroupingQuery('source.ip', groupingAggs);
|
||||
const result = parseGroupingQuery('source.ip', testProps.uniqueValue, groupingAggs);
|
||||
expect(result).toEqual({
|
||||
groupByFields: {
|
||||
buckets: [
|
||||
{
|
||||
...groupingBucket,
|
||||
key: ['20.80.64.28'],
|
||||
key_as_string: '20.80.64.28',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
},
|
||||
{
|
||||
...groupingBucket,
|
||||
key: ['0.0.0.0'],
|
||||
key_as_string: '0.0.0.0',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
},
|
||||
{
|
||||
...groupingBucket,
|
||||
key: [getEmptyValue()],
|
||||
key_as_string: getEmptyValue(),
|
||||
selectedGroup: 'source.ip',
|
||||
isNullGroup: true,
|
||||
doc_count: 75,
|
||||
},
|
||||
],
|
||||
},
|
||||
unitsCount: {
|
||||
value: 100,
|
||||
},
|
||||
unitsCountWithoutNull: {
|
||||
value: 100,
|
||||
},
|
||||
groupsCount: {
|
||||
value: 20,
|
||||
},
|
||||
});
|
||||
});
|
||||
it('parseGroupingQuery adjust group count when null field group is present', () => {
|
||||
const result: GroupingAggregation<{}> = parseGroupingQuery('source.ip', {
|
||||
it('parseGroupingQuery parses and formats fields witih multiple values', () => {
|
||||
const multiValuedAggs = {
|
||||
...groupingAggs,
|
||||
unitsCountWithoutNull: { value: 99 },
|
||||
});
|
||||
groupByFields: {
|
||||
buckets: [
|
||||
{
|
||||
...groupingBucket,
|
||||
key: `20.80.64.28${testProps.uniqueValue}0.0.0.0${testProps.uniqueValue}1.1.1.1`,
|
||||
},
|
||||
{
|
||||
...groupingBucket,
|
||||
key: `0.0.0.0`,
|
||||
},
|
||||
{
|
||||
...groupingBucket,
|
||||
key: `ip.with,comma${testProps.uniqueValue}ip.without.comma`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result: GroupingAggregation<{}> = parseGroupingQuery(
|
||||
'source.ip',
|
||||
testProps.uniqueValue,
|
||||
multiValuedAggs
|
||||
);
|
||||
|
||||
expect(result.groupsCount?.value).toEqual(21);
|
||||
expect(result).toEqual({
|
||||
groupByFields: {
|
||||
buckets: [
|
||||
{
|
||||
...groupingBucket,
|
||||
key: ['20.80.64.28', '0.0.0.0', '1.1.1.1'],
|
||||
key_as_string: '20.80.64.28, 0.0.0.0, 1.1.1.1',
|
||||
selectedGroup: 'source.ip',
|
||||
},
|
||||
{
|
||||
...groupingBucket,
|
||||
key: ['0.0.0.0'],
|
||||
key_as_string: '0.0.0.0',
|
||||
selectedGroup: 'source.ip',
|
||||
},
|
||||
{
|
||||
...groupingBucket,
|
||||
key: ['ip.with,comma', 'ip.without.comma'],
|
||||
key_as_string: 'ip.with,comma, ip.without.comma',
|
||||
selectedGroup: 'source.ip',
|
||||
},
|
||||
],
|
||||
},
|
||||
unitsCount: {
|
||||
value: 100,
|
||||
},
|
||||
groupsCount: {
|
||||
value: 20,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getEmptyValue, getFieldTypeMissingValues } from './helpers';
|
||||
import { GroupingAggregation } from '../..';
|
||||
import { getEmptyValue } from './helpers';
|
||||
import { GroupingAggregation, ParsedGroupingAggregation } from '../..';
|
||||
import type { GroupingQueryArgs, GroupingQuery } from './types';
|
||||
/** The maximum number of groups to render */
|
||||
export const DEFAULT_GROUP_BY_FIELD_SIZE = 10;
|
||||
|
@ -26,11 +26,11 @@ export const MAX_QUERY_SIZE = 10000;
|
|||
* @param rootAggregations Top level aggregations to get the groups number or overall groups metrics.
|
||||
* Array of {@link NamedAggregation}
|
||||
* @param runtimeMappings mappings of runtime fields [see runtimeMappings]{@link GroupingQueryArgs.runtimeMappings}
|
||||
* @param selectedGroupEsTypes array of selected group types
|
||||
* @param size number of grouping results per page
|
||||
* @param sort add one or more sorts on specific fields
|
||||
* @param statsAggregations group level aggregations which correspond to {@link GroupStatsRenderer} configuration
|
||||
* @param to ending timestamp
|
||||
* @param uniqueValue unique value to use for crazy query magic
|
||||
*
|
||||
* @returns query dsl {@link GroupingQuery}
|
||||
*/
|
||||
|
@ -42,32 +42,40 @@ export const getGroupingQuery = ({
|
|||
pageNumber,
|
||||
rootAggregations,
|
||||
runtimeMappings,
|
||||
selectedGroupEsTypes,
|
||||
size = DEFAULT_GROUP_BY_FIELD_SIZE,
|
||||
sort,
|
||||
statsAggregations,
|
||||
to,
|
||||
uniqueValue,
|
||||
}: GroupingQueryArgs): GroupingQuery => ({
|
||||
size: 0,
|
||||
runtime_mappings: {
|
||||
...runtimeMappings,
|
||||
groupByField: {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source:
|
||||
// when size()==0, emits a uniqueValue as the value to represent this group else join by uniqueValue.
|
||||
"if (doc[params['selectedGroup']].size()==0) { emit(params['uniqueValue']) }" +
|
||||
// Else, join the values with uniqueValue. We cannot simply emit the value like doc[params['selectedGroup']].value,
|
||||
// the runtime field will only return the first value in an array.
|
||||
// The docs advise that if the field has multiple values, "Scripts can call the emit method multiple times to emit multiple values."
|
||||
// However, this gives us a group for each value instead of combining the values like we're aiming for.
|
||||
// Instead of .value, we can retrieve all values with .join().
|
||||
// Instead of joining with a "," we should join with a unique value to avoid splitting a value that happens to contain a ",".
|
||||
// We will format into a proper array in parseGroupingQuery .
|
||||
" else { emit(doc[params['selectedGroup']].join(params['uniqueValue']))}",
|
||||
params: {
|
||||
selectedGroup: groupByField,
|
||||
uniqueValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
groupByFields: {
|
||||
multi_terms: {
|
||||
terms: [
|
||||
// by looking up multiple missing values, we can ensure we're not overwriting an existing group with the default value
|
||||
{
|
||||
field: groupByField,
|
||||
// the AggregationsMultiTermLookup type is wrong in the elasticsearch node package
|
||||
// when this issues is resolved, we can remove these ts expect errors
|
||||
// https://github.com/elastic/elasticsearch/issues/95628
|
||||
// @ts-expect-error
|
||||
missing: getFieldTypeMissingValues(selectedGroupEsTypes)[0],
|
||||
},
|
||||
{
|
||||
field: groupByField,
|
||||
// @ts-expect-error
|
||||
missing: getFieldTypeMissingValues(selectedGroupEsTypes)[1],
|
||||
},
|
||||
],
|
||||
terms: {
|
||||
field: 'groupByField',
|
||||
size: MAX_QUERY_SIZE,
|
||||
},
|
||||
aggs: {
|
||||
|
@ -84,14 +92,8 @@ export const getGroupingQuery = ({
|
|||
},
|
||||
},
|
||||
|
||||
unitsCountWithoutNull: { value_count: { field: groupByField } },
|
||||
unitsCount: {
|
||||
value_count: {
|
||||
field: groupByField,
|
||||
missing: getFieldTypeMissingValues(selectedGroupEsTypes)[0],
|
||||
},
|
||||
},
|
||||
groupsCount: { cardinality: { field: groupByField } },
|
||||
unitsCount: { value_count: { field: 'groupByField' } },
|
||||
groupsCount: { cardinality: { field: 'groupByField' } },
|
||||
|
||||
...(rootAggregations
|
||||
? rootAggregations.reduce((aggObj, subAgg) => Object.assign(aggObj, subAgg), {})
|
||||
|
@ -112,7 +114,6 @@ export const getGroupingQuery = ({
|
|||
],
|
||||
},
|
||||
},
|
||||
runtime_mappings: runtimeMappings,
|
||||
_source: false,
|
||||
});
|
||||
|
||||
|
@ -120,46 +121,45 @@ export const getGroupingQuery = ({
|
|||
* Parses the grouping query response to add the isNullGroup
|
||||
* flag to the buckets and to format the bucket keys
|
||||
* @param selectedGroup from the grouping query
|
||||
* @param aggs aggs returned from the grouping query
|
||||
* @param uniqueValue from the grouping query
|
||||
* @param aggs aggregation response from the grouping query
|
||||
*/
|
||||
export const parseGroupingQuery = <T>(
|
||||
selectedGroup: string,
|
||||
uniqueValue: string,
|
||||
aggs?: GroupingAggregation<T>
|
||||
): GroupingAggregation<T> | {} => {
|
||||
): ParsedGroupingAggregation<T> | {} => {
|
||||
if (!aggs) {
|
||||
return {};
|
||||
}
|
||||
const groupByFields = aggs?.groupByFields?.buckets?.map((group) => {
|
||||
if (!Array.isArray(group.key)) {
|
||||
return group;
|
||||
}
|
||||
const emptyValue = getEmptyValue();
|
||||
// If the keys are different means that the `missing` values of the multi_terms aggregation have been applied, we use the default empty string.
|
||||
// If the keys are equal means the `missing` values have not been applied, they are stored values.
|
||||
return group.key[0] === group.key[1]
|
||||
? {
|
||||
...group,
|
||||
key: [group.key[0]],
|
||||
selectedGroup,
|
||||
key_as_string: group.key[0],
|
||||
}
|
||||
: {
|
||||
...group,
|
||||
key: [emptyValue],
|
||||
selectedGroup,
|
||||
key_as_string: emptyValue,
|
||||
isNullGroup: true,
|
||||
};
|
||||
if (group.key === uniqueValue) {
|
||||
return {
|
||||
...group,
|
||||
key: [emptyValue],
|
||||
selectedGroup,
|
||||
key_as_string: emptyValue,
|
||||
isNullGroup: true,
|
||||
};
|
||||
}
|
||||
// doing isArray check for TS
|
||||
// the key won't be an array, runtime fields cannot be multivalued
|
||||
const groupKey = Array.isArray(group.key) ? group.key[0] : group.key;
|
||||
const valueAsArray = groupKey.split(uniqueValue);
|
||||
return {
|
||||
...group,
|
||||
key: valueAsArray,
|
||||
selectedGroup,
|
||||
key_as_string: valueAsArray.join(', '),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...aggs,
|
||||
groupByFields: { buckets: groupByFields },
|
||||
groupsCount: {
|
||||
value:
|
||||
(aggs.unitsCount?.value !== aggs.unitsCountWithoutNull?.value
|
||||
? (aggs.groupsCount?.value ?? 0) + 1
|
||||
: aggs.groupsCount?.value) ?? 0,
|
||||
value: aggs.groupsCount?.value ?? 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type {
|
||||
InlineScript,
|
||||
MappingRuntimeField,
|
||||
MappingRuntimeFields,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
|
@ -28,7 +32,7 @@ export interface GroupingQueryArgs {
|
|||
runtimeMappings?: MappingRuntimeFields;
|
||||
additionalAggregationsRoot?: NamedAggregation[];
|
||||
pageNumber?: number;
|
||||
selectedGroupEsTypes: string[];
|
||||
uniqueValue: string;
|
||||
size?: number;
|
||||
sort?: Array<{ [category: string]: { order: 'asc' | 'desc' } }>;
|
||||
statsAggregations?: NamedAggregation[];
|
||||
|
@ -38,10 +42,18 @@ export interface GroupingQueryArgs {
|
|||
export interface MainAggregation extends NamedAggregation {
|
||||
groupByFields: {
|
||||
aggs: NamedAggregation;
|
||||
multi_terms: estypes.AggregationsAggregationContainer['multi_terms'];
|
||||
terms: estypes.AggregationsAggregationContainer['terms'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupingRuntimeField extends MappingRuntimeField {
|
||||
script: InlineScript & {
|
||||
params: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
type GroupingMappingRuntimeFields = Record<'groupByField', GroupingRuntimeField>;
|
||||
|
||||
export interface GroupingQuery extends estypes.QueryDslQueryContainer {
|
||||
aggs: MainAggregation;
|
||||
query: {
|
||||
|
@ -49,7 +61,7 @@ export interface GroupingQuery extends estypes.QueryDslQueryContainer {
|
|||
filter: Array<BoolAgg | RangeAgg>;
|
||||
};
|
||||
};
|
||||
runtime_mappings: MappingRuntimeFields | undefined;
|
||||
runtime_mappings: MappingRuntimeFields & GroupingMappingRuntimeFields;
|
||||
size: number;
|
||||
_source: boolean;
|
||||
}
|
||||
|
|
26
packages/kbn-securitysolution-grouping/src/mocks.ts
Normal file
26
packages/kbn-securitysolution-grouping/src/mocks.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const groupingBucket = {
|
||||
key: '192.168.0.4',
|
||||
doc_count: 75,
|
||||
hostsCountAggregation: { value: 1 },
|
||||
rulesCountAggregation: { value: 32 },
|
||||
unitsCount: { value: 1920 },
|
||||
severitiesSubAggregation: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{ key: 'critical', doc_count: 480 },
|
||||
{ key: 'high', doc_count: 480 },
|
||||
{ key: 'low', doc_count: 480 },
|
||||
{ key: 'medium', doc_count: 480 },
|
||||
],
|
||||
},
|
||||
countSeveritySubAggregation: { value: 4 },
|
||||
};
|
|
@ -26,6 +26,5 @@
|
|||
"@kbn/shared-svg",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/analytics",
|
||||
"@kbn/field-types"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -27,20 +27,30 @@ import { createStore } from '../../../common/store';
|
|||
import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__';
|
||||
import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
|
||||
import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query';
|
||||
import { groupingSearchResponse } from './grouping_settings/mock';
|
||||
import { getQuery, groupingSearchResponse } from './grouping_settings/mock';
|
||||
|
||||
jest.mock('../../containers/detection_engine/alerts/use_query');
|
||||
jest.mock('../../../common/containers/sourcerer');
|
||||
jest.mock('../../../common/utils/normalize_time_range');
|
||||
jest.mock('../../../common/containers/use_global_time', () => ({
|
||||
useGlobalTime: jest.fn().mockReturnValue({
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
isInitializing: false,
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
setQuery: jest.fn(),
|
||||
}),
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('test-uuid'),
|
||||
}));
|
||||
|
||||
const mockDate = {
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
};
|
||||
|
||||
const mockUseGlobalTime = jest
|
||||
.fn()
|
||||
.mockReturnValue({ ...mockDate, setQuery: jest.fn(), deleteQuery: jest.fn() });
|
||||
|
||||
jest.mock('../../../common/containers/use_global_time', () => {
|
||||
return {
|
||||
useGlobalTime: (...props: unknown[]) => mockUseGlobalTime(...props),
|
||||
};
|
||||
});
|
||||
|
||||
const mockOptions = [
|
||||
{ label: 'ruleName', key: 'kibana.alert.rule.name' },
|
||||
{ label: 'userName', key: 'user.name' },
|
||||
|
@ -105,8 +115,8 @@ const sourcererDataView = {
|
|||
const renderChildComponent = (groupingFilters: Filter[]) => <p data-test-subj="alerts-table" />;
|
||||
|
||||
const testProps: AlertsTableComponentProps = {
|
||||
...mockDate,
|
||||
defaultFilters: [],
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
globalFilters: [],
|
||||
globalQuery: {
|
||||
query: 'query',
|
||||
|
@ -119,7 +129,6 @@ const testProps: AlertsTableComponentProps = {
|
|||
runtimeMappings: {},
|
||||
signalIndexName: 'test',
|
||||
tableId: TableId.test,
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
};
|
||||
|
||||
const mockUseQueryAlerts = useQueryAlerts as jest.Mock;
|
||||
|
@ -220,61 +229,7 @@ describe('GroupedAlertsTable', () => {
|
|||
);
|
||||
expect(mockUseQueryAlerts).toHaveBeenLastCalledWith({
|
||||
indexName: 'test',
|
||||
query: {
|
||||
_source: false,
|
||||
aggs: {
|
||||
groupByFields: {
|
||||
aggs: {
|
||||
bucket_truncate: {
|
||||
bucket_sort: { from: 0, size: 25, sort: [{ unitsCount: { order: 'desc' } }] },
|
||||
},
|
||||
countSeveritySubAggregation: { cardinality: { field: 'kibana.alert.severity' } },
|
||||
hostsCountAggregation: { cardinality: { field: 'host.name' } },
|
||||
description: { terms: { field: 'kibana.alert.rule.description', size: 1 } },
|
||||
ruleTags: { terms: { field: 'kibana.alert.rule.tags' } },
|
||||
severitiesSubAggregation: { terms: { field: 'kibana.alert.severity' } },
|
||||
unitsCount: { cardinality: { field: 'kibana.alert.uuid' } },
|
||||
usersCountAggregation: { cardinality: { field: 'user.name' } },
|
||||
},
|
||||
multi_terms: {
|
||||
size: 10000,
|
||||
terms: [
|
||||
{ field: 'kibana.alert.rule.name', missing: '-' },
|
||||
{ field: 'kibana.alert.rule.name', missing: '--' },
|
||||
],
|
||||
},
|
||||
},
|
||||
groupsCount: { cardinality: { field: 'kibana.alert.rule.name' } },
|
||||
unitsCount: {
|
||||
value_count: {
|
||||
field: 'kibana.alert.rule.name',
|
||||
missing: '-',
|
||||
},
|
||||
},
|
||||
unitsCountWithoutNull: {
|
||||
value_count: {
|
||||
field: 'kibana.alert.rule.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ bool: { filter: [], must: [], must_not: [], should: [] } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2020-07-07T08:20:18.966Z',
|
||||
lte: '2020-07-08T08:20:18.966Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
runtime_mappings: {},
|
||||
size: 0,
|
||||
},
|
||||
query: getQuery('kibana.alert.rule.name', 'SuperUniqueValue-test-uuid', mockDate),
|
||||
queryName: 'securitySolutionUI fetchAlerts grouping',
|
||||
skip: false,
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import type { DynamicGroupingProps } from '@kbn/securitysolution-grouping/src';
|
|||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { TableIdLiteral } from '@kbn/securitysolution-data-table';
|
||||
import { parseGroupingQuery } from '@kbn/securitysolution-grouping/src';
|
||||
import { combineQueries, getFieldEsTypes } from '../../../common/lib/kuery';
|
||||
import { combineQueries } from '../../../common/lib/kuery';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import type { AlertsGroupingAggregation } from './grouping_settings/types';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common';
|
||||
|
@ -141,16 +141,14 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
}
|
||||
}, [defaultFilters, globalFilters, globalQuery, parentGroupingFilter]);
|
||||
|
||||
const selectedGroupEsTypes = useMemo(
|
||||
() => getFieldEsTypes(selectedGroup, browserFields),
|
||||
[selectedGroup, browserFields]
|
||||
);
|
||||
// create a unique, but stable (across re-renders) value
|
||||
const uniqueValue = useMemo(() => `SuperUniqueValue-${uuidv4()}`, []);
|
||||
|
||||
const queryGroups = useMemo(() => {
|
||||
return getAlertsGroupingQuery({
|
||||
additionalFilters,
|
||||
selectedGroup,
|
||||
selectedGroupEsTypes,
|
||||
uniqueValue,
|
||||
from,
|
||||
runtimeMappings,
|
||||
to,
|
||||
|
@ -164,8 +162,8 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
pageSize,
|
||||
runtimeMappings,
|
||||
selectedGroup,
|
||||
selectedGroupEsTypes,
|
||||
to,
|
||||
uniqueValue,
|
||||
]);
|
||||
|
||||
const emptyGlobalQuery = useMemo(() => getGlobalQuery([]), [getGlobalQuery]);
|
||||
|
@ -201,14 +199,16 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
parseGroupingQuery(
|
||||
// fallback to selectedGroup if queriedGroup.current is null, this happens in tests
|
||||
queriedGroup.current === null ? selectedGroup : queriedGroup.current,
|
||||
uniqueValue,
|
||||
alertsGroupsData?.aggregations
|
||||
),
|
||||
[alertsGroupsData?.aggregations, selectedGroup]
|
||||
[alertsGroupsData?.aggregations, selectedGroup, uniqueValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNoneGroup([selectedGroup])) {
|
||||
queriedGroup.current = queryGroups?.aggs?.groupsCount?.cardinality?.field ?? '';
|
||||
queriedGroup.current =
|
||||
queryGroups?.runtime_mappings?.groupByField?.script?.params?.selectedGroup ?? '';
|
||||
setAlertsQuery(queryGroups);
|
||||
}
|
||||
}, [queryGroups, selectedGroup, setAlertsQuery]);
|
||||
|
@ -255,37 +255,33 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
() =>
|
||||
getGrouping({
|
||||
activePage: pageIndex,
|
||||
data: {
|
||||
...alertsGroupsData?.aggregations,
|
||||
...aggs,
|
||||
},
|
||||
data: aggs,
|
||||
groupingLevel,
|
||||
inspectButton: inspect,
|
||||
isLoading: loading || isLoadingGroups,
|
||||
itemsPerPage: pageSize,
|
||||
onChangeGroupsItemsPerPage: (size: number) => setPageSize(size),
|
||||
onChangeGroupsPage: (index) => setPageIndex(index),
|
||||
renderChildComponent,
|
||||
onGroupClose,
|
||||
renderChildComponent,
|
||||
selectedGroup,
|
||||
takeActionItems: getTakeActionItems,
|
||||
}),
|
||||
[
|
||||
getGrouping,
|
||||
pageIndex,
|
||||
alertsGroupsData,
|
||||
aggs,
|
||||
getGrouping,
|
||||
getTakeActionItems,
|
||||
groupingLevel,
|
||||
inspect,
|
||||
loading,
|
||||
isLoadingGroups,
|
||||
loading,
|
||||
onGroupClose,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
renderChildComponent,
|
||||
onGroupClose,
|
||||
selectedGroup,
|
||||
getTakeActionItems,
|
||||
setPageSize,
|
||||
setPageIndex,
|
||||
setPageSize,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,112 @@
|
|||
|
||||
import { mockAlertSearchResponse } from '../../../../common/components/alerts_treemap/lib/mocks/mock_alert_search_response';
|
||||
|
||||
export const getQuery = (
|
||||
selectedGroup: string,
|
||||
uniqueValue: string,
|
||||
timeRange: { from: string; to: string }
|
||||
) => ({
|
||||
_source: false,
|
||||
aggs: {
|
||||
unitsCount: {
|
||||
value_count: {
|
||||
field: 'groupByField',
|
||||
},
|
||||
},
|
||||
groupsCount: {
|
||||
cardinality: {
|
||||
field: 'groupByField',
|
||||
},
|
||||
},
|
||||
groupByFields: {
|
||||
aggs: {
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.description',
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
bucket_truncate: {
|
||||
bucket_sort: {
|
||||
from: 0,
|
||||
size: 25,
|
||||
sort: [
|
||||
{
|
||||
unitsCount: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
ruleTags: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.tags',
|
||||
},
|
||||
},
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
field: 'groupByField',
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ bool: { filter: [], must: [], must_not: [], should: [] } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: timeRange.from,
|
||||
lte: timeRange.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
runtime_mappings: {
|
||||
groupByField: {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source:
|
||||
"if (doc[params['selectedGroup']].size()==0) { emit(params['uniqueValue']) } else { emit(doc[params['selectedGroup']].join(params['uniqueValue']))}",
|
||||
params: {
|
||||
selectedGroup,
|
||||
uniqueValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
});
|
||||
|
||||
export const groupingSearchResponse = {
|
||||
...mockAlertSearchResponse,
|
||||
hits: {
|
||||
|
@ -26,8 +132,8 @@ export const groupingSearchResponse = {
|
|||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: ['critical hosts [Duplicate]', 'critical hosts [Duplicate]'],
|
||||
key_as_string: 'critical hosts [Duplicate]|critical hosts [Duplicate]',
|
||||
key: ['critical hosts [Duplicate]'],
|
||||
key_as_string: 'critical hosts [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -77,9 +183,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['critical hosts [Duplicate] [Duplicate]', 'critical hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string:
|
||||
'critical hosts [Duplicate] [Duplicate]|critical hosts [Duplicate] [Duplicate]',
|
||||
key: ['critical hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'critical hosts [Duplicate] [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -129,8 +234,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['high hosts [Duplicate]', 'high hosts [Duplicate]'],
|
||||
key_as_string: 'high hosts [Duplicate]|high hosts [Duplicate]',
|
||||
key: ['high hosts [Duplicate]'],
|
||||
key_as_string: 'high hosts [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -180,8 +285,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['high hosts [Duplicate] [Duplicate]', 'high hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'high hosts [Duplicate] [Duplicate]|high hosts [Duplicate] [Duplicate]',
|
||||
key: ['high hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'high hosts [Duplicate] [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -231,8 +336,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['low hosts [Duplicate]', 'low hosts [Duplicate]'],
|
||||
key_as_string: 'low hosts [Duplicate]|low hosts [Duplicate]',
|
||||
key: ['low hosts [Duplicate]'],
|
||||
key_as_string: 'low hosts [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -282,8 +387,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['low hosts [Duplicate] [Duplicate]', 'low hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'low hosts [Duplicate] [Duplicate]|low hosts [Duplicate] [Duplicate]',
|
||||
key: ['low hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'low hosts [Duplicate] [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -333,8 +438,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['medium hosts [Duplicate]', 'medium hosts [Duplicate]'],
|
||||
key_as_string: 'medium hosts [Duplicate]|medium hosts [Duplicate]',
|
||||
key: ['medium hosts [Duplicate]'],
|
||||
key_as_string: 'medium hosts [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -384,9 +489,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['medium hosts [Duplicate] [Duplicate]', 'medium hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string:
|
||||
'medium hosts [Duplicate] [Duplicate]|medium hosts [Duplicate] [Duplicate]',
|
||||
key: ['medium hosts [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'medium hosts [Duplicate] [Duplicate]',
|
||||
doc_count: 300,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -436,8 +540,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['critical users [Duplicate]', 'critical users [Duplicate]'],
|
||||
key_as_string: 'critical users [Duplicate]|critical users [Duplicate]',
|
||||
key: ['critical users [Duplicate]'],
|
||||
key_as_string: 'critical users [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -487,12 +591,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: [
|
||||
'critical users [Duplicate] [Duplicate]',
|
||||
'critical users [Duplicate] [Duplicate]',
|
||||
],
|
||||
key_as_string:
|
||||
'critical users [Duplicate] [Duplicate]|critical users [Duplicate] [Duplicate]',
|
||||
key: ['critical users [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'critical users [Duplicate] [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -542,8 +642,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['high users [Duplicate]', 'high users [Duplicate]'],
|
||||
key_as_string: 'high users [Duplicate]|high users [Duplicate]',
|
||||
key: ['high users [Duplicate]'],
|
||||
key_as_string: 'high users [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -593,8 +693,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['high users [Duplicate] [Duplicate]', 'high users [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'high users [Duplicate] [Duplicate]|high users [Duplicate] [Duplicate]',
|
||||
key: ['high users [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'high users [Duplicate] [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -644,8 +744,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['low users [Duplicate]', 'low users [Duplicate]'],
|
||||
key_as_string: 'low users [Duplicate]|low users [Duplicate]',
|
||||
key: ['low users [Duplicate]'],
|
||||
key_as_string: 'low users [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -695,8 +795,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['low users [Duplicate] [Duplicate]', 'low users [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'low users [Duplicate] [Duplicate]|low users [Duplicate] [Duplicate]',
|
||||
key: ['low users [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'low users [Duplicate] [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -746,8 +846,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['medium users [Duplicate]', 'medium users [Duplicate]'],
|
||||
key_as_string: 'medium users [Duplicate]|medium users [Duplicate]',
|
||||
key: ['medium users [Duplicate]'],
|
||||
key_as_string: 'medium users [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -797,9 +897,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['medium users [Duplicate] [Duplicate]', 'medium users [Duplicate] [Duplicate]'],
|
||||
key_as_string:
|
||||
'medium users [Duplicate] [Duplicate]|medium users [Duplicate] [Duplicate]',
|
||||
key: ['medium users [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'medium users [Duplicate] [Duplicate]',
|
||||
doc_count: 273,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
@ -849,8 +948,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['critical hosts', 'critical hosts'],
|
||||
key_as_string: 'critical hosts|critical hosts',
|
||||
key: ['critical hosts'],
|
||||
key_as_string: 'critical hosts',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -900,12 +999,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: [
|
||||
'critical hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
'critical hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
],
|
||||
key_as_string:
|
||||
'critical hosts [Duplicate] [Duplicate] [Duplicate]|critical hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
key: ['critical hosts [Duplicate] [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'critical hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -955,8 +1050,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['high hosts', 'high hosts'],
|
||||
key_as_string: 'high hosts|high hosts',
|
||||
key: ['high hosts'],
|
||||
key_as_string: 'high hosts',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -1006,12 +1101,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: [
|
||||
'high hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
'high hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
],
|
||||
key_as_string:
|
||||
'high hosts [Duplicate] [Duplicate] [Duplicate]|high hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
key: ['high hosts [Duplicate] [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'high hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -1061,8 +1152,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['low hosts ', 'low hosts '],
|
||||
key_as_string: 'low hosts |low hosts ',
|
||||
key: ['low hosts '],
|
||||
key_as_string: 'low hosts ',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -1112,12 +1203,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: [
|
||||
'low hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
'low hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
],
|
||||
key_as_string:
|
||||
'low hosts [Duplicate] [Duplicate] [Duplicate]|low hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
key: ['low hosts [Duplicate] [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'low hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -1167,8 +1254,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: ['medium hosts', 'medium hosts'],
|
||||
key_as_string: 'medium hosts|medium hosts',
|
||||
key: ['medium hosts'],
|
||||
key_as_string: 'medium hosts',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -1218,12 +1305,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: [
|
||||
'medium hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
'medium hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
],
|
||||
key_as_string:
|
||||
'medium hosts [Duplicate] [Duplicate] [Duplicate]|medium hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
key: ['medium hosts [Duplicate] [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'medium hosts [Duplicate] [Duplicate] [Duplicate]',
|
||||
doc_count: 100,
|
||||
hostsCountAggregation: {
|
||||
value: 30,
|
||||
|
@ -1273,12 +1356,8 @@ export const groupingSearchResponse = {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: [
|
||||
'critical users [Duplicate] [Duplicate] [Duplicate]',
|
||||
'critical users [Duplicate] [Duplicate] [Duplicate]',
|
||||
],
|
||||
key_as_string:
|
||||
'critical users [Duplicate] [Duplicate] [Duplicate]|critical users [Duplicate] [Duplicate] [Duplicate]',
|
||||
key: ['critical users [Duplicate] [Duplicate] [Duplicate]'],
|
||||
key_as_string: 'critical users [Duplicate] [Duplicate] [Duplicate]',
|
||||
doc_count: 91,
|
||||
hostsCountAggregation: {
|
||||
value: 10,
|
||||
|
|
|
@ -6,295 +6,55 @@
|
|||
*/
|
||||
|
||||
import { getAlertsGroupingQuery } from '.';
|
||||
import { getQuery } from './mock';
|
||||
|
||||
let sampleData = {
|
||||
from: '2022-12-29T22:57:34.029Z',
|
||||
to: '2023-01-28T22:57:29.029Z',
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
runtimeMappings: {},
|
||||
uniqueValue: 'aSuperUniqueValue',
|
||||
selectedGroup: 'kibana.alert.rule.name',
|
||||
additionalFilters: [{ bool: { filter: [], must: [], must_not: [], should: [] } }],
|
||||
};
|
||||
|
||||
describe('getAlertsGroupingQuery', () => {
|
||||
it('returns query with aggregations for kibana.alert.rule.name', () => {
|
||||
const groupingQuery = getAlertsGroupingQuery({
|
||||
from: '2022-12-29T22:57:34.029Z',
|
||||
to: '2023-01-28T22:57:29.029Z',
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
runtimeMappings: {},
|
||||
selectedGroupEsTypes: ['keyword'],
|
||||
selectedGroup: 'kibana.alert.rule.name',
|
||||
additionalFilters: [
|
||||
{
|
||||
bool: {
|
||||
must: [],
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
'kibana.alert.workflow_status': 'acknowledged',
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'kibana.alert.building_block_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(groupingQuery).toStrictEqual({
|
||||
_source: false,
|
||||
aggs: {
|
||||
unitsCount: {
|
||||
value_count: {
|
||||
field: 'kibana.alert.rule.name',
|
||||
missing: '-',
|
||||
},
|
||||
},
|
||||
unitsCountWithoutNull: {
|
||||
value_count: {
|
||||
field: 'kibana.alert.rule.name',
|
||||
},
|
||||
},
|
||||
groupsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.name',
|
||||
},
|
||||
},
|
||||
groupByFields: {
|
||||
aggs: {
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.description',
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
bucket_truncate: {
|
||||
bucket_sort: {
|
||||
from: 0,
|
||||
size: 25,
|
||||
sort: [
|
||||
{
|
||||
unitsCount: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
ruleTags: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.tags',
|
||||
},
|
||||
},
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
multi_terms: {
|
||||
size: 10000,
|
||||
terms: [
|
||||
{
|
||||
field: 'kibana.alert.rule.name',
|
||||
missing: '-',
|
||||
},
|
||||
{
|
||||
field: 'kibana.alert.rule.name',
|
||||
missing: '--',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
'kibana.alert.workflow_status': 'acknowledged',
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'kibana.alert.building_block_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2022-12-29T22:57:34.029Z',
|
||||
lte: '2023-01-28T22:57:29.029Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
runtime_mappings: {},
|
||||
size: 0,
|
||||
});
|
||||
const groupingQuery = getAlertsGroupingQuery(sampleData);
|
||||
expect(groupingQuery).toStrictEqual(
|
||||
getQuery(sampleData.selectedGroup, sampleData.uniqueValue, {
|
||||
from: sampleData.from,
|
||||
to: sampleData.to,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns default query with aggregations if the field specific metrics was not defined', () => {
|
||||
const groupingQuery = getAlertsGroupingQuery({
|
||||
from: '2022-12-29T22:57:34.029Z',
|
||||
to: '2023-01-28T22:57:29.029Z',
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
runtimeMappings: {},
|
||||
selectedGroupEsTypes: ['keyword'],
|
||||
sampleData = {
|
||||
...sampleData,
|
||||
selectedGroup: 'process.name',
|
||||
additionalFilters: [
|
||||
{
|
||||
bool: {
|
||||
must: [],
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
'kibana.alert.workflow_status': 'acknowledged',
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'kibana.alert.building_block_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const groupingQuery = getAlertsGroupingQuery(sampleData);
|
||||
const expectedResult = getQuery(sampleData.selectedGroup, sampleData.uniqueValue, {
|
||||
from: sampleData.from,
|
||||
to: sampleData.to,
|
||||
});
|
||||
|
||||
const { unitsCount, bucket_truncate: bucketTruncate } = expectedResult.aggs.groupByFields.aggs;
|
||||
|
||||
expect(groupingQuery).toStrictEqual({
|
||||
_source: false,
|
||||
...expectedResult,
|
||||
aggs: {
|
||||
unitsCount: {
|
||||
value_count: {
|
||||
field: 'process.name',
|
||||
missing: '-',
|
||||
},
|
||||
},
|
||||
unitsCountWithoutNull: {
|
||||
value_count: {
|
||||
field: 'process.name',
|
||||
},
|
||||
},
|
||||
groupsCount: {
|
||||
cardinality: {
|
||||
field: 'process.name',
|
||||
},
|
||||
},
|
||||
...expectedResult.aggs,
|
||||
groupByFields: {
|
||||
...expectedResult.aggs.groupByFields,
|
||||
aggs: {
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
bucket_truncate: {
|
||||
bucket_sort: {
|
||||
from: 0,
|
||||
size: 25,
|
||||
sort: [
|
||||
{
|
||||
unitsCount: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
multi_terms: {
|
||||
size: 10000,
|
||||
terms: [
|
||||
{
|
||||
field: 'process.name',
|
||||
missing: '-',
|
||||
},
|
||||
{
|
||||
field: 'process.name',
|
||||
missing: '--',
|
||||
},
|
||||
],
|
||||
bucket_truncate: bucketTruncate,
|
||||
unitsCount,
|
||||
rulesCountAggregation: { cardinality: { field: 'kibana.alert.rule.rule_id' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
'kibana.alert.workflow_status': 'acknowledged',
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'kibana.alert.building_block_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2022-12-29T22:57:34.029Z',
|
||||
lte: '2023-01-28T22:57:29.029Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
runtime_mappings: {},
|
||||
size: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ interface AlertsGroupingQueryParams {
|
|||
pageSize: number;
|
||||
runtimeMappings: MappingRuntimeFields;
|
||||
selectedGroup: string;
|
||||
selectedGroupEsTypes: string[];
|
||||
uniqueValue: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const getAlertsGroupingQuery = ({
|
|||
pageSize,
|
||||
runtimeMappings,
|
||||
selectedGroup,
|
||||
selectedGroupEsTypes,
|
||||
uniqueValue,
|
||||
to,
|
||||
}: AlertsGroupingQueryParams) =>
|
||||
getGroupingQuery({
|
||||
|
@ -42,7 +42,7 @@ export const getAlertsGroupingQuery = ({
|
|||
: [],
|
||||
pageNumber: pageIndex * pageSize,
|
||||
runtimeMappings,
|
||||
selectedGroupEsTypes,
|
||||
uniqueValue,
|
||||
size: pageSize,
|
||||
sort: [{ unitsCount: { order: 'desc' } }],
|
||||
to,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue