mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.8`: - [[Security solution] Grouping bug fixes from BC1 (#156619)](https://github.com/elastic/kibana/pull/156619) <!--- 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-04T17:01:51Z","message":"[Security solution] Grouping bug fixes from BC1 (#156619)","sha":"0f2ee85d8b492c654f7e613e8b40a8f2a91ae634","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","v8.9.0"],"number":156619,"url":"https://github.com/elastic/kibana/pull/156619","mergeCommit":{"message":"[Security solution] Grouping bug fixes from BC1 (#156619)","sha":"0f2ee85d8b492c654f7e613e8b40a8f2a91ae634"}},"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/156619","number":156619,"mergeCommit":{"message":"[Security solution] Grouping bug fixes from BC1 (#156619)","sha":"0f2ee85d8b492c654f7e613e8b40a8f2a91ae634"}}]}] BACKPORT--> Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
parent
b8904f0332
commit
15863ca642
11 changed files with 58 additions and 33 deletions
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const createGroupFilter = (selectedGroup: string, query?: string) =>
|
||||
export const createGroupFilter = (selectedGroup: string, query?: string | null) =>
|
||||
query && selectedGroup
|
||||
? [
|
||||
{
|
||||
|
|
|
@ -8,19 +8,21 @@
|
|||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { GroupPanel } from '.';
|
||||
import { createGroupFilter } from './helpers';
|
||||
import { createGroupFilter, getNullGroupFilter } from './helpers';
|
||||
import React from 'react';
|
||||
|
||||
const onToggleGroup = jest.fn();
|
||||
const renderChildComponent = jest.fn();
|
||||
const ruleName = 'Rule name';
|
||||
const ruleDesc = 'Rule description';
|
||||
const selectedGroup = 'kibana.alert.rule.name';
|
||||
|
||||
const testProps = {
|
||||
isLoading: false,
|
||||
isNullGroup: false,
|
||||
groupBucket: {
|
||||
key: [ruleName, ruleDesc],
|
||||
key_as_string: `${ruleName}|${ruleDesc}`,
|
||||
selectedGroup,
|
||||
key: [ruleName, ruleName],
|
||||
key_as_string: `${ruleName}|${ruleName}`,
|
||||
doc_count: 98,
|
||||
hostsCountAggregation: {
|
||||
value: 5,
|
||||
|
@ -54,7 +56,7 @@ const testProps = {
|
|||
},
|
||||
},
|
||||
renderChildComponent,
|
||||
selectedGroup: 'kibana.alert.rule.name',
|
||||
selectedGroup,
|
||||
onGroupClose: () => {},
|
||||
};
|
||||
|
||||
|
@ -69,7 +71,12 @@ describe('grouping accordion panel', () => {
|
|||
createGroupFilter(testProps.selectedGroup, ruleName)
|
||||
);
|
||||
});
|
||||
it('does not create query without a valid groupFieldValue', () => {
|
||||
it('creates the query for the selectedGroup attribute when the group is null', () => {
|
||||
const { getByTestId } = render(<GroupPanel {...testProps} isNullGroup />);
|
||||
expect(getByTestId('grouping-accordion')).toBeInTheDocument();
|
||||
expect(renderChildComponent).toHaveBeenCalledWith(getNullGroupFilter(testProps.selectedGroup));
|
||||
});
|
||||
it('does not render accordion or create query without a valid groupFieldValue', () => {
|
||||
const { queryByTestId } = render(
|
||||
<GroupPanel
|
||||
{...testProps}
|
||||
|
@ -83,6 +90,11 @@ describe('grouping accordion panel', () => {
|
|||
expect(queryByTestId('grouping-accordion')).not.toBeInTheDocument();
|
||||
expect(renderChildComponent).not.toHaveBeenCalled();
|
||||
});
|
||||
it('Does not render accordion or create query when groupBucket.selectedGroup !== selectedGroup', () => {
|
||||
const { queryByTestId } = render(<GroupPanel {...testProps} selectedGroup="source.ip" />);
|
||||
expect(queryByTestId('grouping-accordion')).not.toBeInTheDocument();
|
||||
expect(testProps.renderChildComponent).not.toHaveBeenCalled();
|
||||
});
|
||||
it('When onToggleGroup not defined, does nothing on toggle', () => {
|
||||
const { container } = render(<GroupPanel {...testProps} />);
|
||||
fireEvent.click(container.querySelector('[data-test-subj="grouping-accordion"] button')!);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@
|
|||
import type { Filter } from '@kbn/es-query';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { firstNonNullValue } from '../../helpers';
|
||||
import type { RawBucket } from '../types';
|
||||
import type { GroupingBucket } from '../types';
|
||||
import { createGroupFilter, getNullGroupFilter } from './helpers';
|
||||
|
||||
interface GroupPanelProps<T> {
|
||||
|
@ -18,14 +18,14 @@ interface GroupPanelProps<T> {
|
|||
customAccordionClassName?: string;
|
||||
extraAction?: React.ReactNode;
|
||||
forceState?: 'open' | 'closed';
|
||||
groupBucket: RawBucket<T>;
|
||||
groupBucket: GroupingBucket<T>;
|
||||
groupPanelRenderer?: JSX.Element;
|
||||
groupingLevel?: number;
|
||||
isLoading: boolean;
|
||||
isNullGroup?: boolean;
|
||||
nullGroupMessage?: string;
|
||||
onGroupClose: () => void;
|
||||
onToggleGroup?: (isOpen: boolean, groupBucket: RawBucket<T>) => void;
|
||||
onToggleGroup?: (isOpen: boolean, groupBucket: GroupingBucket<T>) => void;
|
||||
renderChildComponent: (groupFilter: Filter[]) => React.ReactElement;
|
||||
selectedGroup: string;
|
||||
}
|
||||
|
@ -81,8 +81,10 @@ const GroupPanelComponent = <T,>({
|
|||
lastForceState.current = 'open';
|
||||
}
|
||||
}, [onGroupClose, forceState, selectedGroup]);
|
||||
|
||||
const groupFieldValue = useMemo(() => firstNonNullValue(groupBucket.key), [groupBucket.key]);
|
||||
const groupFieldValue = useMemo(
|
||||
() => (groupBucket.selectedGroup === selectedGroup ? firstNonNullValue(groupBucket.key) : null),
|
||||
[groupBucket.key, groupBucket.selectedGroup, selectedGroup]
|
||||
);
|
||||
|
||||
const groupFilters = useMemo(
|
||||
() =>
|
||||
|
|
|
@ -24,6 +24,7 @@ export const mockGroupingProps = {
|
|||
{
|
||||
key: [host1Name],
|
||||
key_as_string: `${host1Name}`,
|
||||
selectedGroup: 'host.name',
|
||||
doc_count: 1,
|
||||
hostsCountAggregation: {
|
||||
value: 1,
|
||||
|
@ -56,6 +57,7 @@ export const mockGroupingProps = {
|
|||
{
|
||||
key: [host2Name],
|
||||
key_as_string: `${host2Name}`,
|
||||
selectedGroup: 'host.name',
|
||||
doc_count: 1,
|
||||
hostsCountAggregation: {
|
||||
value: 1,
|
||||
|
@ -88,6 +90,7 @@ export const mockGroupingProps = {
|
|||
{
|
||||
key: ['-'],
|
||||
key_as_string: `-`,
|
||||
selectedGroup: 'host.name',
|
||||
isNullGroup: true,
|
||||
doc_count: 11,
|
||||
hostsCountAggregation: {
|
||||
|
|
|
@ -16,15 +16,16 @@ export const NONE_GROUP_KEY = 'none';
|
|||
|
||||
export type RawBucket<T> = GenericBuckets & T;
|
||||
|
||||
export interface GroupingBucket {
|
||||
export type GroupingBucket<T> = RawBucket<T> & {
|
||||
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<RawBucket<T> & GroupingBucket>;
|
||||
buckets?: Array<GroupingBucket<T>>;
|
||||
};
|
||||
groupsCount?: {
|
||||
value?: number | null;
|
||||
|
|
|
@ -161,16 +161,19 @@ describe('group selector', () => {
|
|||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
key: ['0.0.0.0', '::'],
|
||||
key_as_string: '0.0.0.0|::',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
},
|
||||
],
|
||||
|
@ -186,23 +189,26 @@ describe('group selector', () => {
|
|||
},
|
||||
};
|
||||
it('parseGroupingQuery finds and flags the null group', () => {
|
||||
const result = parseGroupingQuery(groupingAggs);
|
||||
const result = parseGroupingQuery('source.ip', groupingAggs);
|
||||
expect(result).toEqual({
|
||||
groupByFields: {
|
||||
buckets: [
|
||||
{
|
||||
key: ['20.80.64.28'],
|
||||
key_as_string: '20.80.64.28',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
},
|
||||
{
|
||||
key: ['0.0.0.0'],
|
||||
key_as_string: '0.0.0.0',
|
||||
selectedGroup: 'source.ip',
|
||||
doc_count: 75,
|
||||
},
|
||||
{
|
||||
key: [getEmptyValue()],
|
||||
key_as_string: getEmptyValue(),
|
||||
selectedGroup: 'source.ip',
|
||||
isNullGroup: true,
|
||||
doc_count: 75,
|
||||
},
|
||||
|
@ -220,7 +226,7 @@ describe('group selector', () => {
|
|||
});
|
||||
});
|
||||
it('parseGroupingQuery adjust group count when null field group is present', () => {
|
||||
const result: GroupingAggregation<{}> = parseGroupingQuery({
|
||||
const result: GroupingAggregation<{}> = parseGroupingQuery('source.ip', {
|
||||
...groupingAggs,
|
||||
unitsCountWithoutNull: { value: 99 },
|
||||
});
|
||||
|
|
|
@ -119,9 +119,11 @@ export const getGroupingQuery = ({
|
|||
/**
|
||||
* Parses the grouping query response to add the isNullGroup
|
||||
* flag to the buckets and to format the bucket keys
|
||||
* @param buckets buckets returned from the grouping query
|
||||
* @param selectedGroup from the grouping query
|
||||
* @param aggs aggs returned from the grouping query
|
||||
*/
|
||||
export const parseGroupingQuery = <T>(
|
||||
selectedGroup: string,
|
||||
aggs?: GroupingAggregation<T>
|
||||
): GroupingAggregation<T> | {} => {
|
||||
if (!aggs) {
|
||||
|
@ -138,11 +140,13 @@ export const parseGroupingQuery = <T>(
|
|||
? {
|
||||
...group,
|
||||
key: [group.key[0]],
|
||||
selectedGroup,
|
||||
key_as_string: group.key[0],
|
||||
}
|
||||
: {
|
||||
...group,
|
||||
key: [emptyValue],
|
||||
selectedGroup,
|
||||
key_as_string: emptyValue,
|
||||
isNullGroup: true,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
|
@ -193,13 +193,17 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
skip: isNoneGroup([selectedGroup]),
|
||||
});
|
||||
|
||||
const [queriedGroup, setQueriedGroup] = useState('');
|
||||
|
||||
const aggs = useMemo(
|
||||
() => parseGroupingQuery(alertsGroupsData?.aggregations),
|
||||
[alertsGroupsData]
|
||||
// queriedGroup because `selectedGroup` updates before the query response
|
||||
() => parseGroupingQuery(queriedGroup, alertsGroupsData?.aggregations),
|
||||
[alertsGroupsData?.aggregations, queriedGroup]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNoneGroup([selectedGroup])) {
|
||||
setQueriedGroup(queryGroups?.aggs?.groupsCount?.cardinality?.field ?? '');
|
||||
setAlertsQuery(queryGroups);
|
||||
}
|
||||
}, [queryGroups, selectedGroup, setAlertsQuery]);
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('getStats', () => {
|
|||
});
|
||||
expect(
|
||||
badgesUserName.find(
|
||||
(badge) => badge.badge != null && badge.title === `IP's:` && badge.badge.value === 1
|
||||
(badge) => badge.badge != null && badge.title === `Hosts:` && badge.badge.value === 1
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -136,7 +136,7 @@ export const getStats = (
|
|||
return [
|
||||
...severityStat,
|
||||
{
|
||||
title: i18n.STATS_GROUP_IPS,
|
||||
title: i18n.STATS_GROUP_HOSTS,
|
||||
badge: {
|
||||
value: bucket.hostsCountAggregation?.value ?? 0,
|
||||
},
|
||||
|
@ -153,7 +153,7 @@ export const getStats = (
|
|||
return [
|
||||
...severityStat,
|
||||
{
|
||||
title: i18n.STATS_GROUP_IPS,
|
||||
title: i18n.STATS_GROUP_HOSTS,
|
||||
badge: {
|
||||
value: bucket.hostsCountAggregation?.value ?? 0,
|
||||
},
|
||||
|
|
|
@ -167,19 +167,12 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => {
|
|||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
|
@ -208,7 +201,7 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => {
|
|||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue