Skip to content

Commit cd7391e

Browse files
[AI4DSOC] Alert summary table section
1 parent 837059b commit cd7391e

21 files changed

Lines changed: 1699 additions & 0 deletions

x-pack/solutions/security/packages/data-table/common/types/data_table/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as runtimeTypes from 'io-ts';
1111
export { Direction };
1212

1313
export type SortDirectionTable = 'none' | 'asc' | 'desc' | Direction;
14+
1415
export interface SortColumnTable {
1516
columnId: string;
1617
columnType: string;
@@ -25,6 +26,7 @@ export enum TableId {
2526
hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked.
2627
alertsOnRuleDetailsPage = 'alerts-rules-details-page',
2728
alertsOnAlertsPage = 'alerts-page',
29+
alertsOnAlertSummaryPage = 'alert-summary-page',
2830
test = 'table-test', // Reserved for testing purposes
2931
alternateTest = 'alternateTest',
3032
rulePreview = 'rule-preview',
@@ -43,6 +45,7 @@ export enum TableEntityType {
4345

4446
export const tableEntity: Record<TableId, TableEntityType> = {
4547
[TableId.alertsOnAlertsPage]: TableEntityType.alert,
48+
[TableId.alertsOnAlertSummaryPage]: TableEntityType.alert,
4649
[TableId.alertsOnCasePage]: TableEntityType.alert,
4750
[TableId.alertsOnRuleDetailsPage]: TableEntityType.alert,
4851
[TableId.hostsPageEvents]: TableEntityType.event,
@@ -64,6 +67,7 @@ const TableIdLiteralRt = runtimeTypes.union([
6467
runtimeTypes.literal(TableId.hostsPageSessions),
6568
runtimeTypes.literal(TableId.alertsOnRuleDetailsPage),
6669
runtimeTypes.literal(TableId.alertsOnAlertsPage),
70+
runtimeTypes.literal(TableId.alertsOnAlertSummaryPage),
6771
runtimeTypes.literal(TableId.test),
6872
runtimeTypes.literal(TableId.rulePreview),
6973
runtimeTypes.literal(TableId.kubernetesPageSessions),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { AdditionalToolbarControls } from './additional_toolbar_controls';
10+
import { TableId } from '@kbn/securitysolution-data-table';
11+
import { fireEvent, render, screen } from '@testing-library/react';
12+
import { createMockStore, mockGlobalState, TestProviders } from '../../../../common/mock';
13+
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
14+
import type { DataView } from '@kbn/data-views-plugin/common';
15+
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';
16+
17+
const mockDispatch = jest.fn();
18+
jest.mock('react-redux', () => {
19+
const original = jest.requireActual('react-redux');
20+
return {
21+
...original,
22+
useDispatch: () => mockDispatch,
23+
};
24+
});
25+
jest.mock('../../../../common/hooks/use_selector');
26+
27+
const dataView: DataView = createStubDataView({ spec: {} });
28+
const mockOptions = [
29+
{ label: 'ruleName', key: 'kibana.alert.rule.name' },
30+
{ label: 'userName', key: 'user.name' },
31+
{ label: 'hostName', key: 'host.name' },
32+
{ label: 'sourceIP', key: 'source.ip' },
33+
];
34+
const tableId = TableId.alertsOnAlertSummaryPage;
35+
36+
const groups = {
37+
[tableId]: { options: mockOptions, activeGroups: ['kibana.alert.rule.name'] },
38+
};
39+
40+
describe('AdditionalToolbarControls', () => {
41+
beforeEach(() => {
42+
(useDeepEqualSelector as jest.Mock).mockImplementation(() => groups[tableId]);
43+
});
44+
45+
test('should render the group selector component and allow the user to select a grouping field', () => {
46+
const store = createMockStore({
47+
...mockGlobalState,
48+
groups,
49+
});
50+
render(
51+
<TestProviders store={store}>
52+
<AdditionalToolbarControls dataView={dataView} />
53+
</TestProviders>
54+
);
55+
56+
fireEvent.click(screen.getByTestId('group-selector-dropdown'));
57+
fireEvent.click(screen.getByTestId('panel-user.name'));
58+
expect(mockDispatch.mock.calls[0][0].payload).toEqual({
59+
activeGroups: ['user.name'],
60+
tableId,
61+
});
62+
});
63+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { memo, useCallback, useMemo } from 'react';
9+
import type { DataView } from '@kbn/data-views-plugin/common';
10+
import { TableId } from '@kbn/securitysolution-data-table';
11+
import { useGetGroupSelectorStateless } from '@kbn/grouping/src/hooks/use_get_group_selector';
12+
import { useDispatch } from 'react-redux';
13+
import { groupIdSelector } from '../../../../common/store/grouping/selectors';
14+
import { updateGroups } from '../../../../common/store/grouping/actions';
15+
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
16+
17+
const TABLE_ID = TableId.alertsOnAlertSummaryPage;
18+
const MAX_GROUPING_LEVELS = 3;
19+
const NO_OPTIONS = { options: [] };
20+
21+
export interface RenderAdditionalToolbarControlsProps {
22+
/**
23+
* DataView created for the alert summary page
24+
*/
25+
dataView: DataView;
26+
}
27+
28+
/**
29+
* Renders a button that when clicked shows a dropdown to allow selecting a group for the GroupedAlertTable.
30+
* Handles further communication with the kbn-grouping package via redux.
31+
*/
32+
export const AdditionalToolbarControls = memo(
33+
({ dataView }: RenderAdditionalToolbarControlsProps) => {
34+
const dispatch = useDispatch();
35+
36+
const onGroupChange = useCallback(
37+
(selectedGroups: string[]) =>
38+
dispatch(updateGroups({ activeGroups: selectedGroups, tableId: TABLE_ID })),
39+
[dispatch]
40+
);
41+
42+
const groupId = useMemo(() => groupIdSelector(), []);
43+
const { options: defaultGroupingOptions } =
44+
useDeepEqualSelector((state) => groupId(state, TABLE_ID)) ?? NO_OPTIONS;
45+
46+
const groupSelector = useGetGroupSelectorStateless({
47+
groupingId: TABLE_ID,
48+
onGroupChange,
49+
fields: dataView.fields,
50+
defaultGroupingOptions,
51+
maxGroupingLevels: MAX_GROUPING_LEVELS,
52+
});
53+
54+
return <>{groupSelector}</>;
55+
}
56+
);
57+
58+
AdditionalToolbarControls.displayName = 'AdditionalToolbarControls';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { groupStatsAggregations } from './group_stats_aggregations';
9+
10+
describe('groupStatsAggregations', () => {
11+
it('should return values depending for signal.rule.id input field', () => {
12+
const aggregations = groupStatsAggregations('signal.rule.id');
13+
expect(aggregations).toEqual([
14+
{
15+
unitsCount: {
16+
cardinality: {
17+
field: 'kibana.alert.uuid',
18+
},
19+
},
20+
},
21+
{
22+
severitiesSubAggregation: {
23+
terms: {
24+
field: 'kibana.alert.severity',
25+
},
26+
},
27+
},
28+
{
29+
rulesCountAggregation: {
30+
cardinality: {
31+
field: 'kibana.alert.rule.rule_id',
32+
},
33+
},
34+
},
35+
]);
36+
});
37+
38+
it('should return values depending for kibana.alert.severity input field', () => {
39+
const aggregations = groupStatsAggregations('kibana.alert.severity');
40+
expect(aggregations).toEqual([
41+
{
42+
unitsCount: {
43+
cardinality: {
44+
field: 'kibana.alert.uuid',
45+
},
46+
},
47+
},
48+
{
49+
signalRuleIdSubAggregation: {
50+
terms: {
51+
field: 'signal.rule.id',
52+
},
53+
},
54+
},
55+
{
56+
rulesCountAggregation: {
57+
cardinality: {
58+
field: 'kibana.alert.rule.rule_id',
59+
},
60+
},
61+
},
62+
]);
63+
});
64+
65+
it('should return values depending for kibana.alert.rule.name input field', () => {
66+
const aggregations = groupStatsAggregations('kibana.alert.rule.name');
67+
expect(aggregations).toEqual([
68+
{
69+
unitsCount: {
70+
cardinality: {
71+
field: 'kibana.alert.uuid',
72+
},
73+
},
74+
},
75+
{
76+
signalRuleIdSubAggregation: {
77+
terms: {
78+
field: 'signal.rule.id',
79+
},
80+
},
81+
},
82+
{
83+
severitiesSubAggregation: {
84+
terms: {
85+
field: 'kibana.alert.severity',
86+
},
87+
},
88+
},
89+
]);
90+
});
91+
92+
it('should return the default values if the field is not supported', () => {
93+
const aggregations = groupStatsAggregations('unknown');
94+
expect(aggregations).toEqual([
95+
{
96+
unitsCount: {
97+
cardinality: {
98+
field: 'kibana.alert.uuid',
99+
},
100+
},
101+
},
102+
{
103+
signalRuleIdSubAggregation: {
104+
terms: {
105+
field: 'signal.rule.id',
106+
},
107+
},
108+
},
109+
{
110+
severitiesSubAggregation: {
111+
terms: {
112+
field: 'kibana.alert.severity',
113+
},
114+
},
115+
},
116+
{
117+
rulesCountAggregation: {
118+
cardinality: {
119+
field: 'kibana.alert.rule.rule_id',
120+
},
121+
},
122+
},
123+
]);
124+
});
125+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { NamedAggregation } from '@kbn/grouping';
9+
import { DEFAULT_GROUP_STATS_AGGREGATION } from '../../alerts_table/alerts_grouping';
10+
import {
11+
RULE_COUNT_AGGREGATION,
12+
SEVERITY_SUB_AGGREGATION,
13+
} from '../../alerts_table/grouping_settings';
14+
15+
const RULE_SIGNAL_ID_SUB_AGGREGATION = {
16+
signalRuleIdSubAggregation: {
17+
terms: {
18+
field: 'signal.rule.id',
19+
},
20+
},
21+
};
22+
23+
/**
24+
* Returns aggregations to be used to calculate the statistics to be used in the`extraAction` property of the EuiAccordion component.
25+
* It handles custom renders for the following fields:
26+
* - signal.rule.id
27+
* - kibana.alert.severity
28+
* - kibana.alert.rule.name
29+
* And returns a default set of aggregation for all the other fields.
30+
*
31+
* These go hand in hand with groupingOptions and groupPanelRenderers.
32+
*/
33+
export const groupStatsAggregations = (field: string): NamedAggregation[] => {
34+
const aggMetrics: NamedAggregation[] = DEFAULT_GROUP_STATS_AGGREGATION('');
35+
36+
switch (field) {
37+
case 'signal.rule.id':
38+
aggMetrics.push(...[SEVERITY_SUB_AGGREGATION, RULE_COUNT_AGGREGATION]);
39+
break;
40+
case 'kibana.alert.severity':
41+
aggMetrics.push(...[RULE_SIGNAL_ID_SUB_AGGREGATION, RULE_COUNT_AGGREGATION]);
42+
break;
43+
case 'kibana.alert.rule.name':
44+
aggMetrics.push(...[RULE_SIGNAL_ID_SUB_AGGREGATION, SEVERITY_SUB_AGGREGATION]);
45+
break;
46+
default:
47+
aggMetrics.push(
48+
...[RULE_SIGNAL_ID_SUB_AGGREGATION, SEVERITY_SUB_AGGREGATION, RULE_COUNT_AGGREGATION]
49+
);
50+
}
51+
return aggMetrics;
52+
};

0 commit comments

Comments
 (0)