mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Grouped table] allow changing default groups, buttonContent and extraAction via props (for AI4DSOC) (#216572)
## Summary This PR makes changes to the GroupedAlertTable code to support a behavior in the AI for the SOC Alert summary page that the current code cannot. In the new Alert summary page (see [mocks](https://www.figma.com/design/DYs7j4GQdAhg7aWTLI4R69/AI4DSOC?node-id=3284-69401&p=f&m=dev)) there are a few customization that we need to be able to do: - we need a different set of default groups to be shown in the dropdown - we need to be able to customize the title shown in the EuiAccordion in a way that would conflict with the current implementation - we need to also customize the group statistics shown in the EuiAccordion ### Challenge The current implementation within the GroupedAlertTable was not allowing full customization. - while the default groups could be changed, it was done via if/else conditions, using the `tableId` to know where the table was being used. This isn't a clean way to do this. The component shouldn't be aware of where it's being used... - regarding the title and group statistics, these were hardcoded and not customizable. While I could also have added if/else conditions to support the Alert summary page different behavior, this would only have built more tech debt... ### Approach Instead of continuing adding more if/else conditions, the approach in the PR adds 3 new props to the GroupedAlertTable: - `accordionButtonContent` allows to customize how the EuiAccordion `buttonContent` (title) is rendered - `accordionExtraActionGroupStats` allows to customize how the EuiAccordion `extraAction` (statistics) are rendered. This actually consists of 2 sub properties: - `renderer` which will drive the UI - aggregations which will be used to fetch the data - `defaultGroupingOptions` allows to customize the default values in the dropdown ### Notes **_The 3 places where the GroupedAlertTable is used have been updated to use the same default values. Their behavior should be unchanged. A follow up PR will implement the Alert summary variation._** In the new state, any new usage of the alerts table with no default values will provide the following behavior: - the EuiAccordion `buttonContent` will use [the default component](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-grouping/src/components/accordion_panel/index.tsx#L33) from the `kbn-grouping` package - the EuiAccordion `extraAction` will display only the number of alerts within the group - the default options in the `Group alerts by` dropdown will be `None` and `Custom field` https://github.com/user-attachments/assets/57563735-78ee-455f-aab6-806028aec713 https://github.com/user-attachments/assets/0659c74e-b4a0-4051-8fb7-25457424c06b ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Will help https://github.com/elastic/security-team/issues/11973
This commit is contained in:
parent
ef907a32f2
commit
0e633d777a
24 changed files with 737 additions and 338 deletions
|
@ -41264,12 +41264,6 @@
|
|||
"xpack.securitySolution.securityIntegration.cribl.mapsTo": "MAPPE À",
|
||||
"xpack.securitySolution.securityIntegration.cribl.missingPermissionsCalloutDescription": "Pour configurer cette intégration, vous devez disposer des privilèges \"manage_index_templates\" et \"manage_pipeline\" ou \"manage_ingest_pipelines\".",
|
||||
"xpack.securitySolution.securityIntegration.cribl.missingPermissionsCalloutTitle": "Assurez-vous de disposer des privilèges nécessaires",
|
||||
"xpack.securitySolution.selector.grouping.hostName.label": "Nom d'hôte",
|
||||
"xpack.securitySolution.selector.grouping.sourceIP.label": "IP source",
|
||||
"xpack.securitySolution.selector.grouping.userName.label": "Nom d'utilisateur",
|
||||
"xpack.securitySolution.selector.groups.destinationAddress.label": "Adresse de destination",
|
||||
"xpack.securitySolution.selector.groups.ruleName.label": "Nom de règle",
|
||||
"xpack.securitySolution.selector.groups.sourceAddress.label": "Adresse de la source",
|
||||
"xpack.securitySolution.selector.summaryView.eventRendererView.label": "Vue rendue des événements",
|
||||
"xpack.securitySolution.selector.summaryView.gridView.label": "Vue Grille",
|
||||
"xpack.securitySolution.selector.summaryView.options.default.description": "Afficher sous forme de données tabulaires avec la possibilité de regrouper et de trier selon des champs spécifiques",
|
||||
|
|
|
@ -41237,12 +41237,6 @@
|
|||
"xpack.securitySolution.securityIntegration.cribl.mapsTo": "マッピング先",
|
||||
"xpack.securitySolution.securityIntegration.cribl.missingPermissionsCalloutDescription": "この統合を構成するには、manage_index_templates権限と、manage_pipelineまたはmanage_ingest_pipelines権限が必要です。",
|
||||
"xpack.securitySolution.securityIntegration.cribl.missingPermissionsCalloutTitle": "必要な権限があることを確認してください",
|
||||
"xpack.securitySolution.selector.grouping.hostName.label": "ホスト名",
|
||||
"xpack.securitySolution.selector.grouping.sourceIP.label": "ソース IP",
|
||||
"xpack.securitySolution.selector.grouping.userName.label": "ユーザー名",
|
||||
"xpack.securitySolution.selector.groups.destinationAddress.label": "ターゲットアドレス",
|
||||
"xpack.securitySolution.selector.groups.ruleName.label": "ルール名",
|
||||
"xpack.securitySolution.selector.groups.sourceAddress.label": "ソースアドレス",
|
||||
"xpack.securitySolution.selector.summaryView.eventRendererView.label": "イベント表示ビュー",
|
||||
"xpack.securitySolution.selector.summaryView.gridView.label": "グリッドビュー",
|
||||
"xpack.securitySolution.selector.summaryView.options.default.description": "特定のフィールドでグループ化および並べ替えることができるタブ形式のデータとして表示",
|
||||
|
|
|
@ -41302,12 +41302,6 @@
|
|||
"xpack.securitySolution.securityIntegration.cribl.mapsTo": "映射到",
|
||||
"xpack.securitySolution.securityIntegration.cribl.missingPermissionsCalloutDescription": "要配置此集成,您必须具有 `manage_index_templates` 权限和 `manage_pipeline` 或 `manage_ingest_pipelines` 权限。",
|
||||
"xpack.securitySolution.securityIntegration.cribl.missingPermissionsCalloutTitle": "请确保您具有必要权限",
|
||||
"xpack.securitySolution.selector.grouping.hostName.label": "主机名",
|
||||
"xpack.securitySolution.selector.grouping.sourceIP.label": "源 IP",
|
||||
"xpack.securitySolution.selector.grouping.userName.label": "用户名",
|
||||
"xpack.securitySolution.selector.groups.destinationAddress.label": "目标地址",
|
||||
"xpack.securitySolution.selector.groups.ruleName.label": "规则名称",
|
||||
"xpack.securitySolution.selector.groups.sourceAddress.label": "源地址",
|
||||
"xpack.securitySolution.selector.summaryView.eventRendererView.label": "事件渲染视图",
|
||||
"xpack.securitySolution.selector.summaryView.gridView.label": "网格视图",
|
||||
"xpack.securitySolution.selector.summaryView.options.default.description": "以表格数据方式查看,这样可以按特定字段分组和排序",
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import type { TableId } from '@kbn/securitysolution-data-table';
|
||||
import type { GroupOption } from '@kbn/grouping';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/groups');
|
||||
|
||||
export const updateGroups = actionCreator<{
|
||||
activeGroups?: string[];
|
||||
tableId: TableId;
|
||||
options?: Array<{ key: string; label: string }>;
|
||||
options?: GroupOption[];
|
||||
}>('UPDATE_GROUPS');
|
||||
|
|
|
@ -6,19 +6,21 @@
|
|||
*/
|
||||
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
import { getDefaultGroupingOptions } from '../../utils/alerts';
|
||||
import { DEFAULT_GROUPING_OPTIONS } from '../../../detections/components/alerts_table/alerts_grouping';
|
||||
import { updateGroups } from './actions';
|
||||
import type { Groups } from './types';
|
||||
|
||||
export const initialGroupingState: Groups = {};
|
||||
|
||||
const EMPTY_ACTIVE_GROUP: string[] = [];
|
||||
|
||||
export const groupsReducer = reducerWithInitialState(initialGroupingState).case(
|
||||
updateGroups,
|
||||
(state, { tableId, ...rest }) => ({
|
||||
...state,
|
||||
[tableId]: {
|
||||
activeGroups: [],
|
||||
options: getDefaultGroupingOptions(tableId),
|
||||
activeGroups: EMPTY_ACTIVE_GROUP,
|
||||
options: DEFAULT_GROUPING_OPTIONS,
|
||||
...(state[tableId] ? state[tableId] : {}),
|
||||
...rest,
|
||||
},
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
import { merge } from '@kbn/std';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import type { Ecs } from '@kbn/cases-plugin/common';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import type { GroupOption } from '@kbn/grouping';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const buildAlertsQuery = (alertIds: string[]) => {
|
||||
if (alertIds.length === 0) {
|
||||
|
@ -121,47 +118,3 @@ export interface Alert {
|
|||
signal: Signal;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// generates default grouping option for alerts table
|
||||
export const getDefaultGroupingOptions = (tableId: TableId): GroupOption[] => {
|
||||
if (tableId === TableId.alertsOnAlertsPage || tableId === TableId.alertsRiskInputs) {
|
||||
return [
|
||||
{
|
||||
label: i18n.ruleName,
|
||||
key: 'kibana.alert.rule.name',
|
||||
},
|
||||
{
|
||||
label: i18n.userName,
|
||||
key: 'user.name',
|
||||
},
|
||||
{
|
||||
label: i18n.hostName,
|
||||
key: 'host.name',
|
||||
},
|
||||
{
|
||||
label: i18n.sourceIP,
|
||||
key: 'source.ip',
|
||||
},
|
||||
];
|
||||
} else if (tableId === TableId.alertsOnRuleDetailsPage) {
|
||||
return [
|
||||
{
|
||||
label: i18n.sourceAddress,
|
||||
key: 'source.address',
|
||||
},
|
||||
{
|
||||
label: i18n.userName,
|
||||
key: 'user.name',
|
||||
},
|
||||
{
|
||||
label: i18n.hostName,
|
||||
key: 'host.name',
|
||||
},
|
||||
{
|
||||
label: i18n.destinationAddress,
|
||||
key: 'destination.address,',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
|
|
@ -1,34 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ruleName = i18n.translate('xpack.securitySolution.selector.groups.ruleName.label', {
|
||||
defaultMessage: 'Rule name',
|
||||
});
|
||||
export const userName = i18n.translate('xpack.securitySolution.selector.grouping.userName.label', {
|
||||
defaultMessage: 'User name',
|
||||
});
|
||||
export const hostName = i18n.translate('xpack.securitySolution.selector.grouping.hostName.label', {
|
||||
defaultMessage: 'Host name',
|
||||
});
|
||||
export const sourceIP = i18n.translate('xpack.securitySolution.selector.grouping.sourceIP.label', {
|
||||
defaultMessage: 'Source IP',
|
||||
});
|
||||
export const sourceAddress = i18n.translate(
|
||||
'xpack.securitySolution.selector.groups.sourceAddress.label',
|
||||
{
|
||||
defaultMessage: 'Source address',
|
||||
}
|
||||
);
|
||||
|
||||
export const destinationAddress = i18n.translate(
|
||||
'xpack.securitySolution.selector.groups.destinationAddress.label',
|
||||
{
|
||||
defaultMessage: 'Destination address',
|
||||
}
|
||||
);
|
|
@ -39,6 +39,11 @@ import {
|
|||
TableId,
|
||||
} from '@kbn/securitysolution-data-table';
|
||||
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import {
|
||||
defaultGroupStatsAggregations,
|
||||
defaultGroupStatsRenderer,
|
||||
defaultGroupTitleRenderers,
|
||||
} from '../../../../detections/components/alerts_table/grouping_settings';
|
||||
import { EndpointExceptionsViewer } from '../../../endpoint_exceptions/endpoint_exceptions_viewer';
|
||||
import { DetectionEngineAlertsTable } from '../../../../detections/components/alerts_table';
|
||||
import { GroupedAlertsTable } from '../../../../detections/components/alerts_table/alerts_grouping';
|
||||
|
@ -179,6 +184,25 @@ const RuleFieldsSectionWrapper = styled.div`
|
|||
overflow-wrap: anywhere;
|
||||
`;
|
||||
|
||||
const defaultGroupingOptions = [
|
||||
{
|
||||
label: i18n.SOURCE_ADDRESS,
|
||||
key: 'source.address',
|
||||
},
|
||||
{
|
||||
label: i18n.USER_NAME,
|
||||
key: 'user.name',
|
||||
},
|
||||
{
|
||||
label: i18n.HOST_NAME,
|
||||
key: 'host.name',
|
||||
},
|
||||
{
|
||||
label: i18n.DESTINATION_ADDRESS,
|
||||
key: 'destination.address',
|
||||
},
|
||||
];
|
||||
|
||||
type DetectionEngineComponentProps = PropsFromRedux;
|
||||
|
||||
const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
||||
|
@ -535,6 +559,14 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
confirmManualRuleRun,
|
||||
} = useManualRuleRunConfirmation();
|
||||
|
||||
const accordionExtraActionGroupStats = useMemo(
|
||||
() => ({
|
||||
aggregations: defaultGroupStatsAggregations,
|
||||
renderer: defaultGroupStatsRenderer,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
if (
|
||||
redirectToDetections(
|
||||
isSignalIndexExists,
|
||||
|
@ -762,8 +794,11 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
</Display>
|
||||
{ruleId != null && (
|
||||
<GroupedAlertsTable
|
||||
accordionButtonContent={defaultGroupTitleRenderers}
|
||||
accordionExtraActionGroupStats={accordionExtraActionGroupStats}
|
||||
currentAlertStatusFilterValue={currentAlertStatusFilterValue}
|
||||
defaultFilters={alertMergedFilters}
|
||||
defaultGroupingOptions={defaultGroupingOptions}
|
||||
from={from}
|
||||
globalFilters={filters}
|
||||
globalQuery={query}
|
||||
|
|
|
@ -73,3 +73,31 @@ export const DELETE_CONFIRMATION_BODY = i18n.translate(
|
|||
defaultMessage: 'This action will delete the rule. Click "Delete" to continue.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_ADDRESS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetails.groups.sourceAddress',
|
||||
{
|
||||
defaultMessage: 'Source address',
|
||||
}
|
||||
);
|
||||
|
||||
export const USER_NAME = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetails.groups.userName',
|
||||
{
|
||||
defaultMessage: 'User name',
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST_NAME = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetails.groups.hostName',
|
||||
{
|
||||
defaultMessage: 'Host name',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESTINATION_ADDRESS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetails.groups.destinationAddress',
|
||||
{
|
||||
defaultMessage: 'Destination address',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -21,6 +21,12 @@ import { createTelemetryServiceMock } from '../../../common/lib/telemetry/teleme
|
|||
import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query';
|
||||
import { getQuery, groupingSearchResponse } from './grouping_settings/mock';
|
||||
import { AlertsEventTypes } from '../../../common/lib/telemetry';
|
||||
import {
|
||||
defaultGroupingOptions,
|
||||
defaultGroupStatsAggregations,
|
||||
defaultGroupStatsRenderer,
|
||||
defaultGroupTitleRenderers,
|
||||
} from './grouping_settings';
|
||||
|
||||
jest.mock('../../containers/detection_engine/alerts/use_query');
|
||||
jest.mock('../../../sourcerer/containers');
|
||||
|
@ -45,10 +51,10 @@ jest.mock('../../../common/containers/use_global_time', () => {
|
|||
});
|
||||
|
||||
const mockOptions = [
|
||||
{ label: 'ruleName', key: 'kibana.alert.rule.name' },
|
||||
{ label: 'userName', key: 'user.name' },
|
||||
{ label: 'hostName', key: 'host.name' },
|
||||
{ label: 'sourceIP', key: 'source.ip' },
|
||||
{ label: 'Rule name', key: 'kibana.alert.rule.name' },
|
||||
{ label: 'User name', key: 'user.name' },
|
||||
{ label: 'Host name', key: 'host.name' },
|
||||
{ label: 'Source IP', key: 'source.ip' },
|
||||
];
|
||||
|
||||
jest.mock('../../../common/utils/alerts', () => {
|
||||
|
@ -113,7 +119,13 @@ const renderChildComponent = (groupingFilters: Filter[]) => <p data-test-subj="a
|
|||
|
||||
const testProps: AlertsTableComponentProps = {
|
||||
...mockDate,
|
||||
accordionButtonContent: defaultGroupTitleRenderers,
|
||||
accordionExtraActionGroupStats: {
|
||||
aggregations: defaultGroupStatsAggregations,
|
||||
renderer: defaultGroupStatsRenderer,
|
||||
},
|
||||
defaultFilters: [],
|
||||
defaultGroupingOptions,
|
||||
globalFilters: [],
|
||||
globalQuery: {
|
||||
query: 'query',
|
||||
|
@ -191,6 +203,7 @@ describe('GroupedAlertsTable', () => {
|
|||
});
|
||||
expect(mockDispatch.mock.calls[1][0].payload).toEqual({
|
||||
activeGroups: ['none'],
|
||||
options: mockOptions,
|
||||
tableId: testProps.tableId,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,28 +8,61 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { isNoneGroup, useGrouping } from '@kbn/grouping';
|
||||
import {
|
||||
type GroupOption,
|
||||
type GroupStatsItem,
|
||||
isNoneGroup,
|
||||
type NamedAggregation,
|
||||
type RawBucket,
|
||||
useGrouping,
|
||||
} from '@kbn/grouping';
|
||||
import { isEmpty, isEqual } from 'lodash/fp';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { TableIdLiteral } from '@kbn/securitysolution-data-table';
|
||||
import type { GroupingArgs } from '@kbn/grouping/src';
|
||||
import type { GetGroupStats, GroupingArgs, GroupPanelRenderer } from '@kbn/grouping/src';
|
||||
import type { AlertsGroupingAggregation } from './grouping_settings/types';
|
||||
import { groupIdSelector } from '../../../common/store/grouping/selectors';
|
||||
import { getDefaultGroupingOptions } from '../../../common/utils/alerts';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { updateGroups } from '../../../common/store/grouping/actions';
|
||||
import type { Status } from '../../../../common/api/detection_engine';
|
||||
import { defaultUnit } from '../../../common/components/toolbar/unit';
|
||||
import { useSourcererDataView } from '../../../sourcerer/containers';
|
||||
import { SourcererScopeName } from '../../../sourcerer/store/model';
|
||||
import type { RunTimeMappings } from '../../../sourcerer/store/model';
|
||||
import { renderGroupPanel, getStats } from './grouping_settings';
|
||||
import { SourcererScopeName } from '../../../sourcerer/store/model';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { GroupedSubLevel } from './alerts_sub_grouping';
|
||||
import { AlertsEventTypes, track } from '../../../common/lib/telemetry';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface AlertsTableComponentProps {
|
||||
/**
|
||||
* Allows to customize the `buttonContent` props of the EuiAccordion.
|
||||
* It basically renders the text next to the chevron, used to expand/collapse the accordion.
|
||||
* If none provided, the DefaultGroupPanelRenderer will be used (see kbn-grouping package).
|
||||
*/
|
||||
accordionButtonContent?: GroupPanelRenderer<AlertsGroupingAggregation>;
|
||||
/**
|
||||
* Allow to partially customize the `extraAction` props of the EuiAccordion.
|
||||
* It basically renders the statistics to right side of the title and the left side of the Take actions button.
|
||||
* If none provided, we display the number of alerts for the group.
|
||||
*/
|
||||
accordionExtraActionGroupStats?: {
|
||||
/**
|
||||
* Responsible to fetch the aggregation data to populate the UI values
|
||||
*/
|
||||
aggregations: (field: string) => NamedAggregation[];
|
||||
/**
|
||||
* Responsible for rendering the aggregation data
|
||||
*/
|
||||
renderer: GetGroupStats<AlertsGroupingAggregation>;
|
||||
};
|
||||
currentAlertStatusFilterValue?: Status[];
|
||||
defaultFilters?: Filter[];
|
||||
/**
|
||||
* Default values to display in the group selection dropdown.
|
||||
* If none are provided, the only options there will None (default) and be Custom field.
|
||||
*/
|
||||
defaultGroupingOptions?: GroupOption[];
|
||||
from: string;
|
||||
globalFilters: Filter[];
|
||||
globalQuery: Query;
|
||||
|
@ -46,6 +79,39 @@ export interface AlertsTableComponentProps {
|
|||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const DEFAULT_PAGE_INDEX = 0;
|
||||
const MAX_GROUPING_LEVELS = 3;
|
||||
export const DEFAULT_GROUPING_OPTIONS: GroupOption[] = [];
|
||||
|
||||
/**
|
||||
* This is used as default behavior if no group renderer is passed via props.
|
||||
* This will render the number of alerts.
|
||||
* It's paired with the DEFAULT_GROUP_STATS_AGGREGATION which retrieves the aggregation data.
|
||||
*/
|
||||
export const DEFAULT_GROUP_STATS_RENDERER: GetGroupStats<AlertsGroupingAggregation> = (
|
||||
_: string,
|
||||
bucket: RawBucket<AlertsGroupingAggregation>
|
||||
): GroupStatsItem[] => [
|
||||
{
|
||||
title: i18n.STATS_GROUP_ALERTS,
|
||||
badge: {
|
||||
value: bucket.doc_count,
|
||||
width: 50,
|
||||
color: '#a83632',
|
||||
},
|
||||
},
|
||||
];
|
||||
/**
|
||||
* This is used as default behavior if no group aggregations is passed via props.
|
||||
* This will render retrieve the values to render the DEFAULT_GROUP_STATS_RENDERER above.
|
||||
*/
|
||||
export const DEFAULT_GROUP_STATS_AGGREGATION: (field: string) => NamedAggregation[] = () => [
|
||||
{
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const useStorage = (storage: Storage, tableId: string) =>
|
||||
useMemo(
|
||||
|
@ -110,14 +176,29 @@ const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> = (props)
|
|||
|
||||
const fields = useMemo(() => Object.values(sourcererDataView.fields || {}), [sourcererDataView]);
|
||||
|
||||
const groupingOptions = useMemo(
|
||||
() => props.defaultGroupingOptions || DEFAULT_GROUPING_OPTIONS,
|
||||
[props.defaultGroupingOptions]
|
||||
);
|
||||
|
||||
const groupStatsRenderer = useMemo(
|
||||
() => props.accordionExtraActionGroupStats?.renderer || DEFAULT_GROUP_STATS_RENDERER,
|
||||
[props.accordionExtraActionGroupStats?.renderer]
|
||||
);
|
||||
|
||||
const groupStatusAggregations = useMemo(
|
||||
() => props.accordionExtraActionGroupStats?.aggregations || DEFAULT_GROUP_STATS_AGGREGATION,
|
||||
[props.accordionExtraActionGroupStats?.aggregations]
|
||||
);
|
||||
|
||||
const { getGrouping, selectedGroups, setSelectedGroups } = useGrouping({
|
||||
componentProps: {
|
||||
groupPanelRenderer: renderGroupPanel,
|
||||
getGroupStats: getStats,
|
||||
groupPanelRenderer: props.accordionButtonContent,
|
||||
getGroupStats: groupStatsRenderer,
|
||||
onGroupToggle,
|
||||
unit: defaultUnit,
|
||||
},
|
||||
defaultGroupingOptions: getDefaultGroupingOptions(props.tableId),
|
||||
defaultGroupingOptions: groupingOptions,
|
||||
fields,
|
||||
groupingId: props.tableId,
|
||||
maxGroupingLevels: MAX_GROUPING_LEVELS,
|
||||
|
@ -134,11 +215,12 @@ const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> = (props)
|
|||
dispatch(
|
||||
updateGroups({
|
||||
activeGroups: selectedGroups,
|
||||
options: groupingOptions,
|
||||
tableId: props.tableId,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [dispatch, props.tableId, selectedGroups]);
|
||||
}, [groupingOptions, dispatch, props.tableId, selectedGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupInRedux != null && !isNoneGroup(groupInRedux.activeGroups)) {
|
||||
|
@ -245,6 +327,7 @@ const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> = (props)
|
|||
{...props}
|
||||
getGrouping={getGrouping}
|
||||
groupingLevel={level}
|
||||
groupStatsAggregations={groupStatusAggregations}
|
||||
onGroupClose={() => resetGroupChildrenPagination(level)}
|
||||
pageIndex={pageIndex[level] ?? DEFAULT_PAGE_INDEX}
|
||||
pageSize={pageSize[level] ?? DEFAULT_PAGE_SIZE}
|
||||
|
@ -256,7 +339,7 @@ const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> = (props)
|
|||
/>
|
||||
);
|
||||
},
|
||||
[getGrouping, pageIndex, pageSize, props, selectedGroups, setPageVar]
|
||||
[getGrouping, groupStatusAggregations, pageIndex, pageSize, props, selectedGroups, setPageVar]
|
||||
);
|
||||
|
||||
if (isEmpty(selectedPatterns)) {
|
||||
|
|
|
@ -9,15 +9,15 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import type { GroupingAggregation } from '@kbn/grouping';
|
||||
import type { GroupingAggregation, NamedAggregation } from '@kbn/grouping';
|
||||
import { isNoneGroup } from '@kbn/grouping';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import type { DynamicGroupingProps } from '@kbn/grouping/src';
|
||||
import { parseGroupingQuery } from '@kbn/grouping/src';
|
||||
import type { TableIdLiteral } from '@kbn/securitysolution-data-table';
|
||||
import type { RunTimeMappings } from '../../../sourcerer/store/model';
|
||||
import { combineQueries } from '../../../common/lib/kuery';
|
||||
import { SourcererScopeName } from '../../../sourcerer/store/model';
|
||||
import { combineQueries } from '../../../common/lib/kuery';
|
||||
import type { AlertsGroupingAggregation } from './grouping_settings/types';
|
||||
import type { Status } from '../../../../common/api/detection_engine';
|
||||
import { InspectButton } from '../../../common/components/inspect';
|
||||
|
@ -46,6 +46,11 @@ interface OwnProps {
|
|||
globalFilters: Filter[];
|
||||
globalQuery: Query;
|
||||
groupingLevel?: number;
|
||||
/**
|
||||
* Function that returns the group aggregations by field.
|
||||
* This is then used to render values in the EuiAccordion `extraAction` section.
|
||||
*/
|
||||
groupStatsAggregations: (field: string) => NamedAggregation[];
|
||||
hasIndexMaintenance: boolean;
|
||||
hasIndexWrite: boolean;
|
||||
loading: boolean;
|
||||
|
@ -73,6 +78,7 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
globalFilters,
|
||||
globalQuery,
|
||||
groupingLevel,
|
||||
groupStatsAggregations,
|
||||
hasIndexMaintenance,
|
||||
hasIndexWrite,
|
||||
loading,
|
||||
|
@ -147,6 +153,7 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
|
||||
const queryGroups = useMemo(() => {
|
||||
return getAlertsGroupingQuery({
|
||||
groupStatsAggregations,
|
||||
additionalFilters,
|
||||
selectedGroup,
|
||||
uniqueValue,
|
||||
|
@ -159,6 +166,7 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
}, [
|
||||
additionalFilters,
|
||||
from,
|
||||
groupStatsAggregations,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
runtimeMappings,
|
||||
|
@ -251,6 +259,13 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
[defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems]
|
||||
);
|
||||
|
||||
const onChangeGroupsItemsPerPage = useCallback(
|
||||
(size: number) => setPageSize(size),
|
||||
[setPageSize]
|
||||
);
|
||||
|
||||
const onChangeGroupsPage = useCallback((index: number) => setPageIndex(index), [setPageIndex]);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
getGrouping({
|
||||
|
@ -260,8 +275,8 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
inspectButton: inspect,
|
||||
isLoading: loading || isLoadingGroups,
|
||||
itemsPerPage: pageSize,
|
||||
onChangeGroupsItemsPerPage: (size: number) => setPageSize(size),
|
||||
onChangeGroupsPage: (index) => setPageIndex(index),
|
||||
onChangeGroupsItemsPerPage,
|
||||
onChangeGroupsPage,
|
||||
onGroupClose,
|
||||
renderChildComponent,
|
||||
selectedGroup,
|
||||
|
@ -275,13 +290,13 @@ export const GroupedSubLevelComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
inspect,
|
||||
isLoadingGroups,
|
||||
loading,
|
||||
onChangeGroupsItemsPerPage,
|
||||
onChangeGroupsPage,
|
||||
onGroupClose,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
renderChildComponent,
|
||||
selectedGroup,
|
||||
setPageIndex,
|
||||
setPageSize,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { defaultGroupStatsAggregations } from '.';
|
||||
|
||||
describe('defaultGroupStatsAggregations', () => {
|
||||
it('should return the default values if the field is not supported', () => {
|
||||
const aggregations = defaultGroupStatsAggregations('unknown');
|
||||
|
||||
expect(aggregations).toEqual([
|
||||
{
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return values depending on the input field', () => {
|
||||
const ruleAggregations = defaultGroupStatsAggregations('kibana.alert.rule.name');
|
||||
expect(ruleAggregations).toEqual([
|
||||
{
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.description',
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ruleTags: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const hostAggregations = defaultGroupStatsAggregations('host.name');
|
||||
expect(hostAggregations).toEqual([
|
||||
{
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const userAggregations = defaultGroupStatsAggregations('user.name');
|
||||
expect(userAggregations).toEqual([
|
||||
{
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const sourceAggregations = defaultGroupStatsAggregations('source.ip');
|
||||
expect(sourceAggregations).toEqual([
|
||||
{
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { NamedAggregation } from '@kbn/grouping';
|
||||
import { DEFAULT_GROUP_STATS_AGGREGATION } from '../alerts_grouping';
|
||||
|
||||
/**
|
||||
* Returns aggregations to be used to calculate the statistics to be used in the `extraAction` property of the EUiAccordion component.
|
||||
* It handles custom renders for the following fields:
|
||||
* - kibana.alert.rule.name
|
||||
* - host.name
|
||||
* - user.name
|
||||
* - source.ip
|
||||
* And returns a default set of aggregation for all the other fields.
|
||||
*
|
||||
* This go hand in hand with defaultGroupingOptions, defaultGroupTitleRenderers and defaultGroupStatsRenderer.
|
||||
*/
|
||||
export const defaultGroupStatsAggregations = (field: string): NamedAggregation[] => {
|
||||
const aggMetrics: NamedAggregation[] = DEFAULT_GROUP_STATS_AGGREGATION('');
|
||||
|
||||
switch (field) {
|
||||
case 'kibana.alert.rule.name':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
description: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.description',
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ruleTags: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'host.name':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'user.name':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'source.ip':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
default:
|
||||
aggMetrics.push({
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return aggMetrics;
|
||||
};
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getStats } from '.';
|
||||
import { defaultGroupStatsRenderer } from '.';
|
||||
|
||||
describe('getStats', () => {
|
||||
it('returns array of badges which corresponds to the field name', () => {
|
||||
const badgesRuleName = getStats('kibana.alert.rule.name', {
|
||||
const badgesRuleName = defaultGroupStatsRenderer('kibana.alert.rule.name', {
|
||||
key: [],
|
||||
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
|
||||
countSeveritySubAggregation: { value: 1 },
|
||||
|
@ -52,7 +52,7 @@ describe('getStats', () => {
|
|||
)
|
||||
).toBeTruthy();
|
||||
|
||||
const badgesHostName = getStats('host.name', {
|
||||
const badgesHostName = defaultGroupStatsRenderer('host.name', {
|
||||
key: 'Host',
|
||||
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
|
||||
countSeveritySubAggregation: { value: 1 },
|
||||
|
@ -95,7 +95,7 @@ describe('getStats', () => {
|
|||
)
|
||||
).toBeTruthy();
|
||||
|
||||
const badgesUserName = getStats('user.name', {
|
||||
const badgesUserName = defaultGroupStatsRenderer('user.name', {
|
||||
key: 'User test',
|
||||
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
|
||||
countSeveritySubAggregation: { value: 1 },
|
||||
|
@ -138,7 +138,7 @@ describe('getStats', () => {
|
|||
)
|
||||
).toBeTruthy();
|
||||
|
||||
const badgesSourceIp = getStats('source.ip', {
|
||||
const badgesSourceIp = defaultGroupStatsRenderer('source.ip', {
|
||||
key: 'User test',
|
||||
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
|
||||
countSeveritySubAggregation: { value: 1 },
|
||||
|
@ -183,7 +183,7 @@ describe('getStats', () => {
|
|||
});
|
||||
|
||||
it('should return default badges if the field specific does not exist', () => {
|
||||
const badges = getStats('process.name', {
|
||||
const badges = defaultGroupStatsRenderer('process.name', {
|
||||
key: 'process',
|
||||
severitiesSubAggregation: { buckets: [{ key: 'medium', doc_count: 10 }] },
|
||||
countSeveritySubAggregation: { value: 1 },
|
|
@ -8,6 +8,7 @@
|
|||
import { EuiIcon } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import type { GroupStatsItem, RawBucket } from '@kbn/grouping';
|
||||
import { DEFAULT_GROUP_STATS_RENDERER } from '../alerts_grouping';
|
||||
import type { AlertsGroupingAggregation } from './types';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
|
@ -64,7 +65,18 @@ const multiSeverity = (
|
|||
</>
|
||||
);
|
||||
|
||||
export const getStats = (
|
||||
/**
|
||||
* Returns statistics to be used in the`extraAction` property of the EuiAccordion component used within the kbn-grouping package.
|
||||
* It handles custom renders for the following fields:
|
||||
* - kibana.alert.rule.name
|
||||
* - host.name
|
||||
* - user.name
|
||||
* - source.ip
|
||||
* And returns a default view for all the other fields.
|
||||
*
|
||||
* This go hand in hand with defaultGroupingOptions, defaultGroupTitleRenderers and defaultGroupStatsAggregations.
|
||||
*/
|
||||
export const defaultGroupStatsRenderer = (
|
||||
selectedGroup: string,
|
||||
bucket: RawBucket<AlertsGroupingAggregation>
|
||||
): GroupStatsItem[] => {
|
||||
|
@ -86,16 +98,7 @@ export const getStats = (
|
|||
},
|
||||
];
|
||||
|
||||
const defaultBadges: GroupStatsItem[] = [
|
||||
{
|
||||
title: i18n.STATS_GROUP_ALERTS,
|
||||
badge: {
|
||||
value: bucket.doc_count,
|
||||
width: 50,
|
||||
color: '#a83632',
|
||||
},
|
||||
},
|
||||
];
|
||||
const defaultBadges: GroupStatsItem[] = DEFAULT_GROUP_STATS_RENDERER(selectedGroup, bucket);
|
||||
|
||||
switch (selectedGroup) {
|
||||
case 'kibana.alert.rule.name':
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderGroupPanel } from '.';
|
||||
import { defaultGroupTitleRenderers } from '.';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
describe('renderGroupPanel', () => {
|
||||
describe('defaultGroupTitleRenderers', () => {
|
||||
it('renders correctly when the field renderer exists', () => {
|
||||
let { getByTestId } = render(
|
||||
renderGroupPanel(
|
||||
defaultGroupTitleRenderers(
|
||||
'kibana.alert.rule.name',
|
||||
{
|
||||
key: ['Rule name test', 'Some description'],
|
||||
|
@ -23,7 +23,7 @@ describe('renderGroupPanel', () => {
|
|||
|
||||
expect(getByTestId('rule-name-group-renderer')).toBeInTheDocument();
|
||||
const result1 = render(
|
||||
renderGroupPanel(
|
||||
defaultGroupTitleRenderers(
|
||||
'host.name',
|
||||
{
|
||||
key: 'Host',
|
||||
|
@ -37,7 +37,7 @@ describe('renderGroupPanel', () => {
|
|||
expect(getByTestId('host-name-group-renderer')).toBeInTheDocument();
|
||||
|
||||
const result2 = render(
|
||||
renderGroupPanel(
|
||||
defaultGroupTitleRenderers(
|
||||
'user.name',
|
||||
{
|
||||
key: 'User test',
|
||||
|
@ -50,7 +50,7 @@ describe('renderGroupPanel', () => {
|
|||
|
||||
expect(getByTestId('host-name-group-renderer')).toBeInTheDocument();
|
||||
const result3 = render(
|
||||
renderGroupPanel(
|
||||
defaultGroupTitleRenderers(
|
||||
'source.ip',
|
||||
{
|
||||
key: 'sourceIp',
|
||||
|
@ -65,7 +65,7 @@ describe('renderGroupPanel', () => {
|
|||
});
|
||||
|
||||
it('returns undefined when the renderer does not exist', () => {
|
||||
const wrapper = renderGroupPanel(
|
||||
const wrapper = defaultGroupTitleRenderers(
|
||||
'process.name',
|
||||
{
|
||||
key: 'process',
|
|
@ -24,7 +24,18 @@ import type { GenericBuckets } from '../../../../../common/search_strategy';
|
|||
import { PopoverItems } from '../../../../common/components/popover_items';
|
||||
import { COLUMN_TAGS } from '../../../../detection_engine/common/translations';
|
||||
|
||||
export const renderGroupPanel: GroupPanelRenderer<AlertsGroupingAggregation> = (
|
||||
/**
|
||||
* Returns renderers to be used in the `buttonContent` property of the EuiAccordion component used within the kbn-grouping package.
|
||||
* It handles custom renders for the following fields:
|
||||
* - kibana.alert.rule.name
|
||||
* - host.name
|
||||
* - user.name
|
||||
* - source.ip
|
||||
* For all the other fields the default renderer managed within the kbn-grouping package will be used.
|
||||
*
|
||||
* This go hand in hand with defaultGroupingOptions and defaultGroupStatsRenderer and defaultGroupStatsAggregations.
|
||||
*/
|
||||
export const defaultGroupTitleRenderers: GroupPanelRenderer<AlertsGroupingAggregation> = (
|
||||
selectedGroup,
|
||||
bucket,
|
||||
nullGroupMessage
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { GroupOption } from '@kbn/grouping/src';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const RULE_NAME = i18n.translate('xpack.securitySolution.alertsTable.groups.ruleName', {
|
||||
defaultMessage: 'Rule name',
|
||||
});
|
||||
|
||||
const USER_NAME = i18n.translate('xpack.securitySolution.alertsTable.groups.userName', {
|
||||
defaultMessage: 'User name',
|
||||
});
|
||||
|
||||
const HOST_NAME = i18n.translate('xpack.securitySolution.alertsTable.groups.hostName', {
|
||||
defaultMessage: 'Host name',
|
||||
});
|
||||
|
||||
const SOURCE_IP = i18n.translate('xpack.securitySolution.alertsTable.groups.sourceIP', {
|
||||
defaultMessage: 'Source IP',
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a list of fields for the default grouping options. These are displayed in the `Group alerts by` dropdown button.
|
||||
*
|
||||
* These go hand in hand with defaultGroupTitleRenderers, defaultGroupStats and defaultGroupStatsAggregations.
|
||||
*/
|
||||
export const defaultGroupingOptions: GroupOption[] = [
|
||||
{
|
||||
label: RULE_NAME,
|
||||
key: 'kibana.alert.rule.name',
|
||||
},
|
||||
{
|
||||
label: USER_NAME,
|
||||
key: 'user.name',
|
||||
},
|
||||
{
|
||||
label: HOST_NAME,
|
||||
key: 'host.name',
|
||||
},
|
||||
{
|
||||
label: SOURCE_IP,
|
||||
key: 'source.ip',
|
||||
},
|
||||
];
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './group_stats';
|
||||
export * from './group_panel_renderers';
|
||||
export * from './default_grouping_options';
|
||||
export * from './default_group_stats_aggregations';
|
||||
export * from './default_group_stats_renderers';
|
||||
export * from './default_group_title_renderers';
|
||||
export * from './group_take_action_items';
|
||||
export * from './query_builder';
|
||||
|
|
|
@ -5,18 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getAlertsGroupingQuery } from '.';
|
||||
import type { AlertsGroupingQueryParams } from '.';
|
||||
import { defaultGroupStatsAggregations, getAlertsGroupingQuery } from '.';
|
||||
import { getQuery } from './mock';
|
||||
|
||||
let sampleData = {
|
||||
let sampleData: AlertsGroupingQueryParams = {
|
||||
additionalFilters: [{ bool: { filter: [], must: [], must_not: [], should: [] } }],
|
||||
from: '2022-12-29T22:57:34.029Z',
|
||||
to: '2023-01-28T22:57:29.029Z',
|
||||
groupStatsAggregations: defaultGroupStatsAggregations,
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
runtimeMappings: {},
|
||||
uniqueValue: 'aSuperUniqueValue',
|
||||
selectedGroup: 'kibana.alert.rule.name',
|
||||
additionalFilters: [{ bool: { filter: [], must: [], must_not: [], should: [] } }],
|
||||
uniqueValue: 'aSuperUniqueValue',
|
||||
to: '2023-01-28T22:57:29.029Z',
|
||||
};
|
||||
|
||||
describe('getAlertsGroupingQuery', () => {
|
||||
|
|
|
@ -6,15 +6,19 @@
|
|||
*/
|
||||
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import type { NamedAggregation } from '@kbn/grouping';
|
||||
import { isNoneGroup, getGroupingQuery } from '@kbn/grouping';
|
||||
import { getGroupingQuery, isNoneGroup, type NamedAggregation } from '@kbn/grouping';
|
||||
import type { RunTimeMappings } from '../../../../sourcerer/store/model';
|
||||
|
||||
interface AlertsGroupingQueryParams {
|
||||
export interface AlertsGroupingQueryParams {
|
||||
additionalFilters: Array<{
|
||||
bool: BoolQuery;
|
||||
}>;
|
||||
from: string;
|
||||
/**
|
||||
* Function that returns the group aggregations by field.
|
||||
* This is then used to render values in the EuiAccordion `extraAction` section.
|
||||
*/
|
||||
groupStatsAggregations: (field: string) => NamedAggregation[];
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
runtimeMappings: RunTimeMappings;
|
||||
|
@ -26,6 +30,7 @@ interface AlertsGroupingQueryParams {
|
|||
export const getAlertsGroupingQuery = ({
|
||||
additionalFilters,
|
||||
from,
|
||||
groupStatsAggregations,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
runtimeMappings,
|
||||
|
@ -40,186 +45,10 @@ export const getAlertsGroupingQuery = ({
|
|||
to,
|
||||
},
|
||||
groupByField: selectedGroup,
|
||||
statsAggregations: !isNoneGroup([selectedGroup])
|
||||
? getAggregationsByGroupField(selectedGroup)
|
||||
: [],
|
||||
statsAggregations: !isNoneGroup([selectedGroup]) ? groupStatsAggregations(selectedGroup) : [],
|
||||
pageNumber: pageIndex * pageSize,
|
||||
runtimeMappings,
|
||||
uniqueValue,
|
||||
size: pageSize,
|
||||
sort: [{ unitsCount: { order: 'desc' } }],
|
||||
});
|
||||
|
||||
const getAggregationsByGroupField = (field: string): NamedAggregation[] => {
|
||||
const aggMetrics: NamedAggregation[] = [
|
||||
{
|
||||
unitsCount: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
switch (field) {
|
||||
case 'kibana.alert.rule.name':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
description: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.description',
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ruleTags: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'host.name':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
usersCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'user.name':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'source.ip':
|
||||
aggMetrics.push(
|
||||
...[
|
||||
{
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
countSeveritySubAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
severitiesSubAggregation: {
|
||||
terms: {
|
||||
field: 'kibana.alert.severity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hostsCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
break;
|
||||
default:
|
||||
aggMetrics.push({
|
||||
rulesCountAggregation: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.rule_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return aggMetrics;
|
||||
};
|
||||
|
|
|
@ -34,6 +34,12 @@ import {
|
|||
import { isEqual } from 'lodash';
|
||||
import type { FilterGroupHandler } from '@kbn/alerts-ui-shared';
|
||||
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import {
|
||||
defaultGroupingOptions,
|
||||
defaultGroupStatsAggregations,
|
||||
defaultGroupStatsRenderer,
|
||||
defaultGroupTitleRenderers,
|
||||
} from '../../components/alerts_table/grouping_settings';
|
||||
import { DetectionEngineFilters } from '../../components/detection_engine_filters/detection_engine_filters';
|
||||
import { FilterByAssigneesPopover } from '../../../common/components/filter_by_assignees_popover/filter_by_assignees_popover';
|
||||
import type { AssigneesIdsSelection } from '../../../common/components/assignees/types';
|
||||
|
@ -321,6 +327,14 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ()
|
|||
[alertsTableDefaultFilters, isAlertTableLoading]
|
||||
);
|
||||
|
||||
const accordionExtraActionGroupStats = useMemo(
|
||||
() => ({
|
||||
aggregations: defaultGroupStatsAggregations,
|
||||
renderer: defaultGroupStatsRenderer,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SecuritySolutionPageWrapper>
|
||||
|
@ -420,8 +434,11 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ()
|
|||
<EuiSpacer size="l" />
|
||||
</Display>
|
||||
<GroupedAlertsTable
|
||||
accordionButtonContent={defaultGroupTitleRenderers}
|
||||
accordionExtraActionGroupStats={accordionExtraActionGroupStats}
|
||||
currentAlertStatusFilterValue={statusFilter}
|
||||
defaultFilters={alertsTableDefaultFilters}
|
||||
defaultGroupingOptions={defaultGroupingOptions}
|
||||
from={from}
|
||||
globalFilters={filters}
|
||||
globalQuery={query}
|
||||
|
|
|
@ -9,10 +9,14 @@ import React, { useCallback, useMemo } from 'react';
|
|||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
|
||||
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import {
|
||||
defaultGroupingOptions,
|
||||
defaultGroupStatsAggregations,
|
||||
defaultGroupStatsRenderer,
|
||||
defaultGroupTitleRenderers,
|
||||
} from '../../../detections/components/alerts_table/grouping_settings';
|
||||
import { HeaderSection } from '../../../common/components/header_section';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { RiskInputs } from '../../../../common/entity_analytics/risk_engine';
|
||||
import type { EntityRiskScore } from '../../../../common/search_strategy';
|
||||
|
@ -95,6 +99,16 @@ export const TopRiskScoreContributorsAlerts = <T extends EntityType>({
|
|||
[inputFilters, filters]
|
||||
);
|
||||
|
||||
const defaultFilters = useMemo(() => [...inputFilters, ...filters], [filters, inputFilters]);
|
||||
|
||||
const accordionExtraActionGroupStats = useMemo(
|
||||
() => ({
|
||||
aggregations: defaultGroupStatsAggregations,
|
||||
renderer: defaultGroupStatsRenderer,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder data-test-subj="topRiskScoreContributorsAlerts">
|
||||
<EuiFlexGroup gutterSize={'none'}>
|
||||
|
@ -117,7 +131,10 @@ export const TopRiskScoreContributorsAlerts = <T extends EntityType>({
|
|||
>
|
||||
<EuiFlexItem grow={1}>
|
||||
<GroupedAlertsTable
|
||||
defaultFilters={[...inputFilters, ...filters]}
|
||||
accordionButtonContent={defaultGroupTitleRenderers}
|
||||
accordionExtraActionGroupStats={accordionExtraActionGroupStats}
|
||||
defaultFilters={defaultFilters}
|
||||
defaultGroupingOptions={defaultGroupingOptions}
|
||||
from={from}
|
||||
globalFilters={filters}
|
||||
globalQuery={query}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue