Skip to content

Commit

Permalink
[Security solution] Fix grouping query, be ready for arrays! (#157330)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored May 11, 2023
1 parent 951fca5 commit 4371c15
Show file tree
Hide file tree
Showing 18 changed files with 560 additions and 693 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand All @@ -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', () => {
Expand All @@ -82,8 +53,7 @@ describe('grouping accordion panel', () => {
{...testProps}
groupBucket={{
...testProps.groupBucket,
// @ts-expect-error
key: null,
selectedGroup: 'wrong-group',
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 && (
Expand Down Expand Up @@ -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(
Expand All @@ -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}
/>
Expand All @@ -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"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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])
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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={
Expand Down
14 changes: 11 additions & 3 deletions packages/kbn-securitysolution-grouping/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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,
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit 4371c15

Please sign in to comment.