[Security Solution] Improve grouping interfaces naming (#153124)

Change/ cleaned up grouping package interfaces
<img width="488" alt="Screenshot 2023-03-09 at 10 07 06 PM"
src="https://user-images.githubusercontent.com/55110838/224393885-44e61967-8b45-4b8d-bb68-d5e152a3ca14.png">

---------

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
Yuliia Naumenko 2023-03-11 08:57:49 -08:00 committed by GitHub
parent f4e6ef75be
commit 238ff29c8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 122 additions and 189 deletions

View file

@ -24,10 +24,10 @@ const rule2Desc = 'Rule 2 description';
const testProps = {
data: {
groupCount0: {
groupsCount: {
value: 2,
},
stackByMultipleFields0: {
groupByFields: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
@ -75,7 +75,7 @@ const testProps = {
sum_other_doc_count: 0,
buckets: [],
},
unitCount0: {
unitsCount: {
value: 1,
},
severitiesSubAggregation: {
@ -97,7 +97,7 @@ const testProps = {
},
],
},
unitCount0: {
unitsCount: {
value: 2,
},
},
@ -120,7 +120,7 @@ describe('grouping container', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('Renders group counts when groupCount0 > 0', () => {
it('Renders groups count when groupsCount > 0', () => {
const { getByTestId, getAllByTestId, queryByTestId } = render(
<I18nProvider>
<Grouping {...testProps} />
@ -132,17 +132,17 @@ describe('grouping container', () => {
expect(queryByTestId('empty-results-panel')).not.toBeInTheDocument();
});
it('Does not render group counts when groupCount0 = 0', () => {
it('Does not render group counts when groupsCount = 0', () => {
const data = {
groupCount0: {
groupsCount: {
value: 0,
},
stackByMultipleFields0: {
groupByFields: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [],
},
unitCount0: {
unitsCount: {
value: 0,
},
};

View file

@ -81,12 +81,12 @@ const GroupingComponent = <T,>({
Record<string, { state: 'open' | 'closed' | undefined; selectedBucket: RawBucket<T> }>
>({});
const unitCount = data?.unitCount0?.value ?? 0;
const unitCount = data?.unitsCount?.value ?? 0;
const unitCountText = useMemo(() => {
return `${unitCount.toLocaleString()} ${unit && unit(unitCount)}`;
}, [unitCount, unit]);
const groupCount = data?.groupCount0?.value ?? 0;
const groupCount = data?.groupsCount?.value ?? 0;
const groupCountText = useMemo(
() => `${groupCount.toLocaleString()} ${GROUPS_UNIT(groupCount)}`,
[groupCount]
@ -94,7 +94,7 @@ const GroupingComponent = <T,>({
const groupPanels = useMemo(
() =>
data?.stackByMultipleFields0?.buckets?.map((groupBucket, groupNumber) => {
data?.groupByFields?.buckets?.map((groupBucket, groupNumber) => {
const group = firstNonNullValue(groupBucket.key);
const groupKey = `group-${groupNumber}-${group}`;
@ -145,7 +145,7 @@ const GroupingComponent = <T,>({
[
badgeMetricStats,
customMetricStats,
data?.stackByMultipleFields0?.buckets,
data?.groupByFields?.buckets,
groupPanelRenderer,
groupingId,
isLoading,

View file

@ -20,13 +20,13 @@ export type RawBucket<T> = GenericBuckets & T;
/** Defines the shape of the aggregation returned by Elasticsearch */
// TODO: write developer docs for these fields
export interface GroupingAggregation<T> {
stackByMultipleFields0?: {
groupByFields?: {
buckets?: Array<RawBucket<T>>;
};
groupCount0?: {
groupsCount?: {
value?: number | null;
};
unitCount0?: {
unitsCount?: {
value?: number | null;
};
}

View file

@ -11,8 +11,9 @@ import { getGroupingQuery, MAX_QUERY_SIZE } from '.';
const testProps: GroupingQueryArgs = {
additionalFilters: [],
additionalAggregationsRoot: [],
additionalStatsAggregationsFields0: [
from: '2022-12-28T15:35:32.871Z',
groupByFields: ['host.name'],
metricsAggregations: [
{
alertsCount: {
cardinality: {
@ -49,14 +50,10 @@ const testProps: GroupingQueryArgs = {
},
},
],
additionalStatsAggregationsFields1: [],
from: '2022-12-28T15:35:32.871Z',
pageNumber: 0,
rootAggregations: [],
runtimeMappings: {},
stackByMultipleFields0: ['host.name'],
stackByMultipleFields0Size: 25,
stackByMultipleFields0From: 0,
stackByMultipleFields1: [],
stackByMultipleFields1Size: 10,
size: 25,
to: '2023-02-23T06:59:59.999Z',
};
describe('group selector', () => {
@ -65,12 +62,12 @@ describe('group selector', () => {
});
it('Sets terms query when single stackBy field requested', () => {
const result = getGroupingQuery(testProps);
expect(result.aggs.stackByMultipleFields0.multi_terms).toBeUndefined();
expect(result.aggs.stackByMultipleFields0.terms).toEqual({
expect(result.aggs.groupByFields.multi_terms).toBeUndefined();
expect(result.aggs.groupByFields.terms).toEqual({
field: 'host.name',
size: MAX_QUERY_SIZE,
});
expect(result.aggs.stackByMultipleFields0.aggs).toEqual({
expect(result.aggs.groupByFields.aggs).toEqual({
bucket_truncate: { bucket_sort: { from: 0, size: 25 } },
alertsCount: { cardinality: { field: 'kibana.alert.uuid' } },
rulesCountAggregation: { cardinality: { field: 'kibana.alert.rule.rule_id' } },
@ -85,7 +82,7 @@ describe('group selector', () => {
it('Sets terms query when single stackBy field requested additionalAggregationsRoot', () => {
const result = getGroupingQuery({
...testProps,
additionalAggregationsRoot: [
rootAggregations: [
{
alertsCount: {
terms: {
@ -111,10 +108,10 @@ describe('group selector', () => {
it('Sets terms query when multiple stackBy fields requested', () => {
const result = getGroupingQuery({
...testProps,
stackByMultipleFields0: ['kibana.alert.rule.name', 'kibana.alert.rule.description'],
groupByFields: ['kibana.alert.rule.name', 'kibana.alert.rule.description'],
});
expect(result.aggs.stackByMultipleFields0.terms).toBeUndefined();
expect(result.aggs.stackByMultipleFields0.multi_terms).toEqual({
expect(result.aggs.groupByFields.terms).toBeUndefined();
expect(result.aggs.groupByFields.multi_terms).toEqual({
terms: [{ field: 'kibana.alert.rule.name' }, { field: 'kibana.alert.rule.description' }],
size: MAX_QUERY_SIZE,
});

View file

@ -6,111 +6,58 @@
* Side Public License, v 1.
*/
import { isEmpty } from 'lodash/fp';
import type { GroupingQueryArgs, GroupingQuery, NamedAggregation } from './types';
/** The maximum number of items to render */
export const DEFAULT_STACK_BY_FIELD0_SIZE = 10;
export const DEFAULT_STACK_BY_FIELD1_SIZE = 10;
const getOptionalSubAggregation = ({
stackByMultipleFields1,
stackByMultipleFields1Size,
stackByMultipleFields1From = 0,
stackByMultipleFields1Sort,
additionalStatsAggregationsFields1,
}: {
stackByMultipleFields1: string[] | undefined;
stackByMultipleFields1Size: number;
stackByMultipleFields1From?: number;
stackByMultipleFields1Sort?: Array<{ [category: string]: { order: 'asc' | 'desc' } }>;
additionalStatsAggregationsFields1: NamedAggregation[];
}): NamedAggregation | {} =>
stackByMultipleFields1 != null && !isEmpty(stackByMultipleFields1)
? {
stackByMultipleFields1: {
multi_terms: {
terms: stackByMultipleFields1.map((stackByMultipleField1) => ({
field: stackByMultipleField1,
})),
},
aggs: {
bucket_truncate: {
bucket_sort: {
sort: stackByMultipleFields1Sort,
from: stackByMultipleFields1From,
size: stackByMultipleFields1Size,
},
},
...additionalStatsAggregationsFields1.reduce(
(aggObj, subAgg) => Object.assign(aggObj, subAgg),
{}
),
},
},
}
: {};
import type { GroupingQueryArgs, GroupingQuery } from './types';
/** The maximum number of groups to render */
export const DEFAULT_GROUP_BY_FIELD_SIZE = 10;
// our pagination will be broken if the stackBy field cardinality exceeds 10,000
// https://github.com/elastic/kibana/issues/151913
export const MAX_QUERY_SIZE = 10000;
export const getGroupingQuery = ({
additionalFilters = [],
additionalAggregationsRoot,
additionalStatsAggregationsFields0,
additionalStatsAggregationsFields1,
from,
groupByFields,
metricsAggregations,
pageNumber,
rootAggregations,
runtimeMappings,
stackByMultipleFields0,
stackByMultipleFields0Size = DEFAULT_STACK_BY_FIELD0_SIZE,
stackByMultipleFields0From,
stackByMultipleFields0Sort,
stackByMultipleFields1,
stackByMultipleFields1Size = DEFAULT_STACK_BY_FIELD1_SIZE,
stackByMultipleFields1From,
stackByMultipleFields1Sort,
size = DEFAULT_GROUP_BY_FIELD_SIZE,
sort,
to,
}: GroupingQueryArgs): GroupingQuery => ({
size: 0,
aggs: {
stackByMultipleFields0: {
...(stackByMultipleFields0.length > 1
groupByFields: {
...(groupByFields.length > 1
? {
multi_terms: {
terms: stackByMultipleFields0.map((stackByMultipleField0) => ({
field: stackByMultipleField0,
terms: groupByFields.map((groupByField) => ({
field: groupByField,
})),
size: MAX_QUERY_SIZE,
},
}
: {
terms: {
field: stackByMultipleFields0[0],
field: groupByFields[0],
size: MAX_QUERY_SIZE,
},
}),
aggs: {
...getOptionalSubAggregation({
stackByMultipleFields1,
stackByMultipleFields1Size,
stackByMultipleFields1From,
stackByMultipleFields1Sort,
additionalStatsAggregationsFields1,
}),
bucket_truncate: {
bucket_sort: {
sort: stackByMultipleFields0Sort,
from: stackByMultipleFields0From,
size: stackByMultipleFields0Size,
sort,
from: pageNumber,
size,
},
},
...additionalStatsAggregationsFields0.reduce(
(aggObj, subAgg) => Object.assign(aggObj, subAgg),
{}
),
...(metricsAggregations
? metricsAggregations.reduce((aggObj, subAgg) => Object.assign(aggObj, subAgg), {})
: {}),
},
},
...(additionalAggregationsRoot
? additionalAggregationsRoot.reduce((aggObj, subAgg) => Object.assign(aggObj, subAgg), {})
...(rootAggregations
? rootAggregations.reduce((aggObj, subAgg) => Object.assign(aggObj, subAgg), {})
: {}),
},
query: {

View file

@ -23,37 +23,32 @@ export type NamedAggregation = Record<string, estypes.AggregationsAggregationCon
export interface GroupingQueryArgs {
additionalFilters: BoolAgg[];
from: string;
groupByFields: string[];
metricsAggregations?: NamedAggregation[];
pageNumber?: number;
rootAggregations?: NamedAggregation[];
runtimeMappings?: MappingRuntimeFields;
additionalAggregationsRoot?: NamedAggregation[];
stackByMultipleFields0: string[];
stackByMultipleFields0Size?: number;
stackByMultipleFields0From?: number;
stackByMultipleFields0Sort?: Array<{ [category: string]: { order: 'asc' | 'desc' } }>;
additionalStatsAggregationsFields0: NamedAggregation[];
stackByMultipleFields1: string[] | undefined;
stackByMultipleFields1Size?: number;
stackByMultipleFields1From?: number;
stackByMultipleFields1Sort?: Array<{ [category: string]: { order: estypes.SortOrder } }>;
additionalStatsAggregationsFields1: NamedAggregation[];
size?: number;
sort?: Array<{ [category: string]: { order: 'asc' | 'desc' } }>;
to: string;
}
export interface MainAggregation extends NamedAggregation {
stackByMultipleFields0: {
terms?: estypes.AggregationsAggregationContainer['terms'];
multi_terms?: estypes.AggregationsAggregationContainer['multi_terms'];
groupByFields: {
aggs: NamedAggregation;
multi_terms?: estypes.AggregationsAggregationContainer['multi_terms'];
terms?: estypes.AggregationsAggregationContainer['terms'];
};
}
export interface GroupingQuery extends estypes.QueryDslQueryContainer {
size: number;
runtime_mappings: MappingRuntimeFields | undefined;
aggs: MainAggregation;
query: {
bool: {
filter: Array<BoolAgg | RangeAgg>;
};
};
runtime_mappings: MappingRuntimeFields | undefined;
size: number;
_source: boolean;
aggs: MainAggregation;
}

View file

@ -39,9 +39,9 @@ import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/con
import {
getAlertsGroupingQuery,
getDefaultGroupingOptions,
getSelectedGroupBadgeMetrics,
getSelectedGroupButtonContent,
getSelectedGroupCustomMetrics,
getBadgeMetrics,
renderGroupPanel,
getCustomMetrics,
useGroupTakeActionsItems,
} from './grouping_settings';
import { updateGroupSelector, updateSelectedGroup } from '../../../common/store/grouping/actions';
@ -250,13 +250,13 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
? renderChildComponent([])
: getGrouping({
badgeMetricStats: (fieldBucket: RawBucket<AlertsGroupingAggregation>) =>
getSelectedGroupBadgeMetrics(selectedGroup, fieldBucket),
getBadgeMetrics(selectedGroup, fieldBucket),
customMetricStats: (fieldBucket: RawBucket<AlertsGroupingAggregation>) =>
getSelectedGroupCustomMetrics(selectedGroup, fieldBucket),
getCustomMetrics(selectedGroup, fieldBucket),
data: alertsGroupsData?.aggregations,
groupingId: tableId,
groupPanelRenderer: (fieldBucket: RawBucket<AlertsGroupingAggregation>) =>
getSelectedGroupButtonContent(selectedGroup, fieldBucket),
renderGroupPanel(selectedGroup, fieldBucket),
inspectButton: inspect,
isLoading: loading || isLoadingGroups,
onToggleCallback: (param) => {

View file

@ -7,12 +7,12 @@
import { shallow } from 'enzyme';
import { getSelectedGroupButtonContent } from '.';
import { renderGroupPanel } from '.';
describe('getSelectedGroupButtonContent', () => {
describe('renderGroupPanel', () => {
it('renders correctly when the field renderer exists', () => {
const wrapperRuleName = shallow(
getSelectedGroupButtonContent('kibana.alert.rule.name', {
renderGroupPanel('kibana.alert.rule.name', {
key: ['Rule name test', 'Some description'],
doc_count: 10,
})!
@ -20,7 +20,7 @@ describe('getSelectedGroupButtonContent', () => {
expect(wrapperRuleName.find('[data-test-subj="rule-name-group-renderer"]')).toBeTruthy();
const wrapperHostName = shallow(
getSelectedGroupButtonContent('host.name', {
renderGroupPanel('host.name', {
key: 'Host',
doc_count: 2,
})!
@ -28,7 +28,7 @@ describe('getSelectedGroupButtonContent', () => {
expect(wrapperHostName.find('[data-test-subj="host-name-group-renderer"]')).toBeTruthy();
const wrapperUserName = shallow(
getSelectedGroupButtonContent('user.name', {
renderGroupPanel('user.name', {
key: 'User test',
doc_count: 1,
})!
@ -36,7 +36,7 @@ describe('getSelectedGroupButtonContent', () => {
expect(wrapperUserName.find('[data-test-subj="host-name-group-renderer"]')).toBeTruthy();
const wrapperSourceIp = shallow(
getSelectedGroupButtonContent('source.ip', {
renderGroupPanel('source.ip', {
key: 'sourceIp',
doc_count: 23,
})!
@ -46,7 +46,7 @@ describe('getSelectedGroupButtonContent', () => {
});
it('returns undefined when the renderer does not exist', () => {
const wrapper = getSelectedGroupButtonContent('process.name', {
const wrapper = renderGroupPanel('process.name', {
key: 'process',
doc_count: 10,
});

View file

@ -25,7 +25,7 @@ import type { GenericBuckets } from '../../../../../common/search_strategy';
import { PopoverItems } from '../../../../common/components/popover_items';
import { COLUMN_TAGS } from '../../../pages/detection_engine/rules/translations';
export const getSelectedGroupButtonContent = (
export const renderGroupPanel = (
selectedGroup: string,
bucket: RawBucket<AlertsGroupingAggregation>
) => {

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { getSelectedGroupBadgeMetrics } from '.';
import { getBadgeMetrics } from '.';
describe('getSelectedGroupBadgeMetrics', () => {
describe('getBadgeMetrics', () => {
it('returns array of badges which roccespondes to the field name', () => {
const badgesRuleName = getSelectedGroupBadgeMetrics('kibana.alert.rule.name', {
const badgesRuleName = getBadgeMetrics('kibana.alert.rule.name', {
key: ['Rule name test', 'Some description'],
usersCountAggregation: {
value: 10,
@ -24,7 +24,7 @@ describe('getSelectedGroupBadgeMetrics', () => {
badgesRuleName.find((badge) => badge.title === 'Alerts:' && badge.value === 10)
).toBeTruthy();
const badgesHostName = getSelectedGroupBadgeMetrics('host.name', {
const badgesHostName = getBadgeMetrics('host.name', {
key: 'Host',
rulesCountAggregation: {
value: 3,
@ -36,7 +36,7 @@ describe('getSelectedGroupBadgeMetrics', () => {
badgesHostName.find((badge) => badge.title === 'Rules:' && badge.value === 3)
).toBeTruthy();
const badgesUserName = getSelectedGroupBadgeMetrics('user.name', {
const badgesUserName = getBadgeMetrics('user.name', {
key: 'User test',
hostsCountAggregation: {
value: 1,
@ -49,7 +49,7 @@ describe('getSelectedGroupBadgeMetrics', () => {
});
it('returns default badges if the field specific does not exist', () => {
const badges = getSelectedGroupBadgeMetrics('process.name', {
const badges = getBadgeMetrics('process.name', {
key: 'process',
rulesCountAggregation: {
value: 3,

View file

@ -11,7 +11,7 @@ import type { RawBucket } from '@kbn/securitysolution-grouping';
import type { AlertsGroupingAggregation } from './types';
import * as i18n from '../translations';
const getSingleGroupSeverity = (severity?: string) => {
const getSeverity = (severity?: string) => {
switch (severity) {
case 'low':
return (
@ -64,7 +64,7 @@ const multiSeverity = (
</>
);
export const getSelectedGroupBadgeMetrics = (
export const getBadgeMetrics = (
selectedGroup: string,
bucket: RawBucket<AlertsGroupingAggregation>
) => {
@ -135,13 +135,13 @@ export const getSelectedGroupBadgeMetrics = (
];
};
export const getSelectedGroupCustomMetrics = (
export const getCustomMetrics = (
selectedGroup: string,
bucket: RawBucket<AlertsGroupingAggregation>
) => {
const singleSeverityComponent =
bucket.severitiesSubAggregation?.buckets && bucket.severitiesSubAggregation?.buckets?.length
? getSingleGroupSeverity(bucket.severitiesSubAggregation?.buckets[0].key.toString())
? getSeverity(bucket.severitiesSubAggregation?.buckets[0].key.toString())
: null;
const severityComponent =
bucket.countSeveritySubAggregation?.value && bucket.countSeveritySubAggregation?.value > 1

View file

@ -43,19 +43,19 @@ describe('getAlertsGroupingQuery', () => {
expect(groupingQuery).toStrictEqual({
_source: false,
aggs: {
unitCount0: {
unitsCount: {
value_count: {
field: 'kibana.alert.rule.name',
},
},
groupCount0: {
groupsCount: {
cardinality: {
field: 'kibana.alert.rule.name',
},
},
stackByMultipleFields0: {
groupByFields: {
aggs: {
unitCount0: {
unitsCount: {
cardinality: {
field: 'kibana.alert.uuid',
},
@ -180,19 +180,19 @@ describe('getAlertsGroupingQuery', () => {
expect(groupingQuery).toStrictEqual({
_source: false,
aggs: {
unitCount0: {
unitsCount: {
value_count: {
field: 'process.name',
},
},
groupCount0: {
groupsCount: {
cardinality: {
field: 'process.name',
},
},
stackByMultipleFields0: {
groupByFields: {
aggs: {
unitCount0: {
unitsCount: {
cardinality: {
field: 'kibana.alert.uuid',
},

View file

@ -8,7 +8,7 @@
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import type { BoolQuery } from '@kbn/es-query';
import type { NamedAggregation } from '@kbn/securitysolution-grouping';
import { getGroupingQuery } from '@kbn/securitysolution-grouping';
import { isNoneGroup, getGroupingQuery } from '@kbn/securitysolution-grouping';
const getGroupFields = (groupValue: string) => {
if (groupValue === 'kibana.alert.rule.name') {
@ -19,60 +19,51 @@ const getGroupFields = (groupValue: string) => {
};
interface AlertsGroupingQueryParams {
from: string;
to: string;
additionalFilters: Array<{
bool: BoolQuery;
}>;
selectedGroup: string;
runtimeMappings: MappingRuntimeFields;
pageSize: number;
from: string;
pageIndex: number;
pageSize: number;
runtimeMappings: MappingRuntimeFields;
selectedGroup: string;
to: string;
}
export const getAlertsGroupingQuery = ({
from,
to,
additionalFilters,
selectedGroup,
runtimeMappings,
pageSize,
from,
pageIndex,
pageSize,
runtimeMappings,
selectedGroup,
to,
}: AlertsGroupingQueryParams) =>
getGroupingQuery({
additionalFilters,
additionalAggregationsRoot: [
from,
groupByFields: !isNoneGroup(selectedGroup) ? getGroupFields(selectedGroup) : [],
metricsAggregations: !isNoneGroup(selectedGroup)
? getAggregationsByGroupField(selectedGroup)
: [],
pageNumber: pageIndex * pageSize,
rootAggregations: [
{
unitCount0: { value_count: { field: selectedGroup } },
unitsCount: { value_count: { field: selectedGroup } },
},
...(selectedGroup !== 'none'
? [
{
groupCount0: {
cardinality: {
field: selectedGroup,
},
},
},
]
...(!isNoneGroup(selectedGroup)
? [{ groupsCount: { cardinality: { field: selectedGroup } } }]
: []),
],
from,
runtimeMappings,
stackByMultipleFields0: selectedGroup !== 'none' ? getGroupFields(selectedGroup) : [],
size: pageSize,
to,
additionalStatsAggregationsFields0:
selectedGroup !== 'none' ? getAggregationsByGroupField(selectedGroup) : [],
stackByMultipleFields0Size: pageSize,
stackByMultipleFields0From: pageIndex * pageSize,
additionalStatsAggregationsFields1: [],
stackByMultipleFields1: [],
});
const getAggregationsByGroupField = (field: string): NamedAggregation[] => {
const aggMetrics: NamedAggregation[] = [
{
unitCount0: {
unitsCount: {
cardinality: {
field: 'kibana.alert.uuid',
},

View file

@ -9,7 +9,7 @@ import type { GenericBuckets } from '@kbn/securitysolution-grouping/src';
// Elasticsearch returns `null` when a sub-aggregation cannot be computed
type NumberOrNull = number | null;
export interface AlertsGroupingAggregation {
unitCount0?: {
unitsCount?: {
value?: NumberOrNull;
};
severitiesSubAggregation?: {
@ -24,6 +24,9 @@ export interface AlertsGroupingAggregation {
hostsCountAggregation?: {
value?: NumberOrNull;
};
ipsCountAggregation?: {
value?: NumberOrNull;
};
rulesCountAggregation?: {
value?: NumberOrNull;
};