[8.7] [Security Solution][Alert Page] Hide type column in KPI visualization (#152872) (#152919)

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Security Solution][Alert Page] Hide type column in KPI visualization
(#152872)](https://github.com/elastic/kibana/pull/152872)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"christineweng","email":"18648970+christineweng@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-03-08T14:24:49Z","message":"[Security
Solution][Alert Page] Hide type column in KPI visualization
(#152872)\n\n## Summary\r\n\r\nThis PR added a feature flag
`alertTypeEnabled` to enable/disable the\r\ntype column in `Alert by
type` chart on Alerts page.\r\n\r\n**Before (same as
`alertTypeEnabled=true`)**\r\n- Title is `Alert by type`\r\n- `Type`
distribution bar is present\r\n- `Type` column is
present\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/223577917-2f2e9250-692c-4b08-b830-96871ad184ea.png)\r\n\r\n\r\n**After
( default `alertTypeEnabled=false`)**\r\n- Title is renamed to `Alert by
name`\r\n- `Type` column is not present\r\n- `Type` distribution bar is
not
present\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/223578273-323d4c31-ce3e-4105-adc8-be03e5d92806.png)\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"c319ab9480b6944d7f89c8998799a9f3f8f8cf3b","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Threat
Hunting","Team: SecuritySolution","release_note:feature","Team:Threat
Hunting:Investigations","v8.7.0","v8.8.0"],"number":152872,"url":"https://github.com/elastic/kibana/pull/152872","mergeCommit":{"message":"[Security
Solution][Alert Page] Hide type column in KPI visualization
(#152872)\n\n## Summary\r\n\r\nThis PR added a feature flag
`alertTypeEnabled` to enable/disable the\r\ntype column in `Alert by
type` chart on Alerts page.\r\n\r\n**Before (same as
`alertTypeEnabled=true`)**\r\n- Title is `Alert by type`\r\n- `Type`
distribution bar is present\r\n- `Type` column is
present\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/223577917-2f2e9250-692c-4b08-b830-96871ad184ea.png)\r\n\r\n\r\n**After
( default `alertTypeEnabled=false`)**\r\n- Title is renamed to `Alert by
name`\r\n- `Type` column is not present\r\n- `Type` distribution bar is
not
present\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/223578273-323d4c31-ce3e-4105-adc8-be03e5d92806.png)\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"c319ab9480b6944d7f89c8998799a9f3f8f8cf3b"}},"sourceBranch":"main","suggestedTargetBranches":["8.7"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/152872","number":152872,"mergeCommit":{"message":"[Security
Solution][Alert Page] Hide type column in KPI visualization
(#152872)\n\n## Summary\r\n\r\nThis PR added a feature flag
`alertTypeEnabled` to enable/disable the\r\ntype column in `Alert by
type` chart on Alerts page.\r\n\r\n**Before (same as
`alertTypeEnabled=true`)**\r\n- Title is `Alert by type`\r\n- `Type`
distribution bar is present\r\n- `Type` column is
present\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/223577917-2f2e9250-692c-4b08-b830-96871ad184ea.png)\r\n\r\n\r\n**After
( default `alertTypeEnabled=false`)**\r\n- Title is renamed to `Alert by
name`\r\n- `Type` column is not present\r\n- `Type` distribution bar is
not
present\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/223578273-323d4c31-ce3e-4105-adc8-be03e5d92806.png)\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"c319ab9480b6944d7f89c8998799a9f3f8f8cf3b"}}]}]
BACKPORT-->
This commit is contained in:
christineweng 2023-03-08 10:45:35 -06:00 committed by GitHub
parent 84b3ef0104
commit 3b9016625e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 483 additions and 144 deletions

View file

@ -90,6 +90,11 @@ export const allowedExperimentalValues = Object.freeze({
* Enables top charts on Alerts Page
*/
alertsPageChartsEnabled: true,
alertTypeEnabled: false,
/**
* Enables the new security flyout over the current alert details flyout
*/
securityFlyoutEnabled: false,
/**
* Keep DEPRECATED experimental flags that are documented to prevent failed upgrades.

View file

@ -8,7 +8,8 @@ import { act, render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { AlertsByType } from './alerts_by_type';
import { parsedAlerts } from './mock_data';
import { parsedAlerts } from './mock_type_data';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
const display = 'alerts-by-type-palette-display';
@ -19,6 +20,9 @@ jest.mock('react-router-dom', () => {
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
});
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('../../../../common/hooks/use_experimental_features');
describe('Alert by type chart', () => {
const defaultProps = {
data: [],
@ -30,70 +34,139 @@ describe('Alert by type chart', () => {
jest.restoreAllMocks();
});
test('renders health and pallette display correctly without data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType {...defaultProps} />
</TestProviders>
);
expect(container.querySelector(`[data-test-subj="${display}"]`)).toBeInTheDocument();
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Detection:0'
);
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Prevention:0'
);
describe('isAlertTypeEnabled flag is true', () => {
beforeEach(() => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
});
});
test('renders table correctly without data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType {...defaultProps} />
</TestProviders>
);
expect(
container.querySelector('[data-test-subj="alerts-by-type-table"]')
).toBeInTheDocument();
expect(
container.querySelector('[data-test-subj="alerts-by-type-table"] tbody')?.textContent
).toEqual('No items found');
});
});
test('renders health and pallette display correctly with data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType data={parsedAlerts} isLoading={false} />
</TestProviders>
);
expect(container.querySelector(`[data-test-subj="${display}"]`)).toBeInTheDocument();
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Detection:583'
);
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Prevention:6'
);
});
});
test('renders table correctly with data', () => {
act(() => {
const { queryAllByRole } = render(
<TestProviders>
<AlertsByType data={parsedAlerts} isLoading={false} />
</TestProviders>
);
parsedAlerts.forEach((_, i) => {
expect(queryAllByRole('row')[i + 1].textContent).toContain(parsedAlerts[i].rule);
expect(queryAllByRole('row')[i + 1].textContent).toContain(parsedAlerts[i].type);
expect(queryAllByRole('row')[i + 1].textContent).toContain(
parsedAlerts[i].value.toString()
test('renders health and pallette display correctly without data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType {...defaultProps} />
</TestProviders>
);
expect(container.querySelector(`[data-test-subj="${display}"]`)).toBeInTheDocument();
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Detection:0'
);
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Prevention:0'
);
});
});
test('renders table correctly without data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType {...defaultProps} />
</TestProviders>
);
expect(
container.querySelector('[data-test-subj="alerts-by-type-table"]')
).toBeInTheDocument();
expect(
container.querySelector('[data-test-subj="alerts-by-type-table"] tbody')?.textContent
).toEqual('No items found');
});
});
test('renders health and pallette display correctly with data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType data={parsedAlerts} isLoading={false} />
</TestProviders>
);
expect(container.querySelector(`[data-test-subj="${display}"]`)).toBeInTheDocument();
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Detection:583'
);
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain(
'Prevention:6'
);
});
});
test('renders table correctly with data', () => {
act(() => {
const { queryAllByRole } = render(
<TestProviders>
<AlertsByType data={parsedAlerts} isLoading={false} />
</TestProviders>
);
parsedAlerts.forEach((_, i) => {
expect(queryAllByRole('row')[i + 1].textContent).toContain(parsedAlerts[i].rule);
expect(queryAllByRole('row')[i + 1].textContent).toContain(parsedAlerts[i].type);
expect(queryAllByRole('row')[i + 1].textContent).toContain(
parsedAlerts[i].value.toString()
);
});
});
});
});
describe('isAlertTypeEnabled flag is false', () => {
beforeEach(() => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
});
test('do not renders health and pallette display correctly without data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType {...defaultProps} />
</TestProviders>
);
expect(container.querySelector(`[data-test-subj="${display}"]`)).not.toBeInTheDocument();
});
});
test('renders table correctly without data', () => {
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType {...defaultProps} />
</TestProviders>
);
expect(
container.querySelector('[data-test-subj="alerts-by-type-table"]')
).toBeInTheDocument();
expect(
container.querySelector('[data-test-subj="alerts-by-type-table"] tbody')?.textContent
).toEqual('No items found');
});
});
test('do not renders health and pallette display correctly with data', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
act(() => {
const { container } = render(
<TestProviders>
<AlertsByType data={parsedAlerts} isLoading={false} />
</TestProviders>
);
expect(container.querySelector(`[data-test-subj="${display}"]`)).not.toBeInTheDocument();
});
});
test('renders table correctly with data', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
act(() => {
const { queryAllByRole } = render(
<TestProviders>
<AlertsByType data={parsedAlerts} isLoading={false} />
</TestProviders>
);
parsedAlerts.forEach((_, i) => {
expect(queryAllByRole('row')[i + 1].textContent).toContain(parsedAlerts[i].rule);
expect(queryAllByRole('row')[i + 1].textContent).toContain(
parsedAlerts[i].value.toString()
);
});
});
});
});

View file

@ -21,6 +21,7 @@ import type { AlertsTypeData, AlertType } from './types';
import { FormattedCount } from '../../../../common/components/formatted_number';
import { getAlertsTypeTableColumns } from './columns';
import { ALERT_TYPE_COLOR } from './helpers';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
const Wrapper = styled.div`
margin-top: -${({ theme }) => theme.eui.euiSizeM};
@ -43,7 +44,11 @@ export interface AlertsByTypeProps {
}
export const AlertsByType: React.FC<AlertsByTypeProps> = ({ data, isLoading }) => {
const columns = useMemo(() => getAlertsTypeTableColumns(), []);
const isAlertTypeEnabled = useIsExperimentalFeatureEnabled('alertTypeEnabled');
const columns = useMemo(
() => getAlertsTypeTableColumns(isAlertTypeEnabled),
[isAlertTypeEnabled]
);
const subtotals = useMemo(
() =>
@ -92,30 +97,33 @@ export const AlertsByType: React.FC<AlertsByTypeProps> = ({ data, isLoading }) =
return (
<Wrapper data-test-subj="alerts-by-type">
<EuiFlexGroup gutterSize="xs" data-test-subj="alerts-by-type-palette-display">
{(Object.keys(subtotals) as AlertType[]).map((type) => (
<EuiFlexItem key={type} grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiHealth className="eui-alignMiddle" color={ALERT_TYPE_COLOR[type]}>
<EuiText size="xs">
<h4>{`${type}:`}</h4>
</EuiText>
</EuiHealth>
{isAlertTypeEnabled && (
<>
<EuiFlexGroup gutterSize="xs" data-test-subj="alerts-by-type-palette-display">
{(Object.keys(subtotals) as AlertType[]).map((type) => (
<EuiFlexItem key={type} grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiHealth className="eui-alignMiddle" color={ALERT_TYPE_COLOR[type]}>
<EuiText size="xs">
<h4>{`${type}:`}</h4>
</EuiText>
</EuiHealth>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<FormattedCount count={subtotals[type] || 0} />
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<FormattedCount count={subtotals[type] || 0} />
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
))}
<EuiSpacer size="xs" />
</EuiFlexGroup>
<EuiSpacer size="xs" />
<StyledEuiColorPaletteDisplay size="xs" palette={palette} />
))}
<EuiSpacer size="xs" />
</EuiFlexGroup>
<EuiSpacer size="xs" />
<StyledEuiColorPaletteDisplay size="xs" palette={palette} />
</>
)}
<EuiSpacer size="xs" />
<TableWrapper className="eui-yScroll">
<EuiInMemoryTable

View file

@ -18,7 +18,9 @@ import { COUNT_TABLE_TITLE } from '../alerts_count_panel/translations';
import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../../common/constants';
import * as i18n from './translations';
export const getAlertsTypeTableColumns = (): Array<EuiBasicTableColumn<AlertsTypeData>> => [
export const getAlertsTypeTableColumns = (
isAlertTypeEnabled: boolean
): Array<EuiBasicTableColumn<AlertsTypeData>> => [
{
field: 'rule',
name: ALERTS_HEADERS_RULE_NAME,
@ -39,35 +41,39 @@ export const getAlertsTypeTableColumns = (): Array<EuiBasicTableColumn<AlertsTyp
</EuiText>
),
},
{
field: 'type',
name: i18n.ALERTS_TYPE_COLUMN_TITLE,
'data-test-subj': 'detectionsTable-type',
truncateText: true,
render: (type: string) => {
return (
<EuiHealth color={ALERT_TYPE_COLOR[type as AlertType]}>
<EuiText grow={false} size="xs">
<CellActions
mode={CellActionsMode.HOVER}
visibleCellActions={4}
showActionTooltips
triggerId={CELL_ACTIONS_DEFAULT_TRIGGER}
field={{
name: 'event.type',
value: 'denied',
type: 'keyword',
}}
metadata={{ negateFilters: type === 'Detection' }} // Detection: event.type != denied
>
{ALERT_TYPE_LABEL[type as AlertType]}
</CellActions>
</EuiText>
</EuiHealth>
);
},
width: '30%',
},
...(isAlertTypeEnabled
? [
{
field: 'type',
name: i18n.ALERTS_TYPE_COLUMN_TITLE,
'data-test-subj': 'detectionsTable-type',
truncateText: true,
render: (type: string) => {
return (
<EuiHealth color={ALERT_TYPE_COLOR[type as AlertType]}>
<EuiText grow={false} size="xs">
<CellActions
mode={CellActionsMode.HOVER}
visibleCellActions={4}
showActionTooltips
triggerId={CELL_ACTIONS_DEFAULT_TRIGGER}
field={{
name: 'event.type',
value: 'denied',
type: 'keyword',
}}
metadata={{ negateFilters: type === 'Detection' }} // Detection: event.type != denied
>
{ALERT_TYPE_LABEL[type as AlertType]}
</CellActions>
</EuiText>
</EuiHealth>
);
},
width: '30%',
},
]
: []),
{
field: 'value',
name: COUNT_TABLE_TITLE,

View file

@ -4,22 +4,39 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { parseAlertsTypeData } from './helpers';
import * as mock from './mock_data';
import type { AlertsByTypeAgg } from './types';
import { parseAlertsTypeData, parseAlertsRuleData } from './helpers';
import * as mockType from './mock_type_data';
import * as mockRule from './mock_rule_data';
import type { AlertsByTypeAgg, AlertsByRuleAgg } from './types';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
describe('parse alerts by type data', () => {
test('parse alerts with data', () => {
const res = parseAlertsTypeData(
mock.mockAlertsData as AlertSearchResponse<{}, AlertsByTypeAgg>
mockType.mockAlertsData as AlertSearchResponse<{}, AlertsByTypeAgg>
);
expect(res).toEqual(mock.parsedAlerts);
expect(res).toEqual(mockType.parsedAlerts);
});
test('parse alerts without data', () => {
const res = parseAlertsTypeData(
mock.mockAlertsEmptyData as AlertSearchResponse<{}, AlertsByTypeAgg>
mockType.mockAlertsEmptyData as AlertSearchResponse<{}, AlertsByTypeAgg>
);
expect(res).toEqual([]);
});
});
describe('parse alerts by rule data', () => {
test('parse alerts with data', () => {
const res = parseAlertsRuleData(
mockRule.mockAlertsData as AlertSearchResponse<{}, AlertsByRuleAgg>
);
expect(res).toEqual(mockRule.parsedAlerts);
});
test('parse alerts without data', () => {
const res = parseAlertsRuleData(
mockRule.mockAlertsEmptyData as AlertSearchResponse<{}, AlertsByRuleAgg>
);
expect(res).toEqual([]);
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { has } from 'lodash';
import type { AlertType, AlertsByTypeAgg, AlertsTypeData } from './types';
import type { AlertType, AlertsByTypeAgg, AlertsTypeData, AlertsByRuleAgg } from './types';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
import type { SummaryChartsData, SummaryChartsAgg } from '../alerts_summary_charts_panel/types';
import { DETECTION, PREVENTION } from './translations';
@ -19,10 +19,27 @@ export const ALERT_TYPE_LABEL = {
Prevention: PREVENTION,
};
export const parseAlertsRuleData = (
response: AlertSearchResponse<{}, AlertsByRuleAgg>
): AlertsTypeData[] => {
const rulesBuckets = response?.aggregations?.alertsByRule?.buckets ?? [];
return rulesBuckets.length === 0
? []
: rulesBuckets.map((rule) => {
return {
rule: rule.key,
type: 'Detection' as AlertType,
value: rule.doc_count,
color: ALERT_TYPE_COLOR.Detection,
};
});
};
export const parseAlertsTypeData = (
response: AlertSearchResponse<{}, AlertsByTypeAgg>
): AlertsTypeData[] => {
const rulesBuckets = response?.aggregations?.alertsByRule?.buckets ?? [];
const rulesBuckets = response?.aggregations?.alertsByType?.buckets ?? [];
return rulesBuckets.length === 0
? []
: rulesBuckets.flatMap((rule) => {
@ -75,5 +92,11 @@ export const getIsAlertsTypeData = (data: SummaryChartsData[]): data is AlertsTy
export const getIsAlertsByTypeAgg = (
data: AlertSearchResponse<{}, SummaryChartsAgg>
): data is AlertSearchResponse<{}, AlertsByTypeAgg> => {
return has(data, 'aggregations.alertsByType');
};
export const getIsAlertsByRuleAgg = (
data: AlertSearchResponse<{}, SummaryChartsAgg>
): data is AlertSearchResponse<{}, AlertsByRuleAgg> => {
return has(data, 'aggregations.alertsByRule');
};

View file

@ -13,9 +13,13 @@ import { AlertsByType } from './alerts_by_type';
import { HeaderSection } from '../../../../common/components/header_section';
import { InspectButtonContainer } from '../../../../common/components/inspect';
import { useSummaryChartData } from '../alerts_summary_charts_panel/use_summary_chart_data';
import { alertTypeAggregations } from '../alerts_summary_charts_panel/aggregations';
import {
alertTypeAggregations,
alertRuleAggregations,
} from '../alerts_summary_charts_panel/aggregations';
import { getIsAlertsTypeData } from './helpers';
import * as i18n from './translations';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
const ALERTS_BY_TYPE_CHART_ID = 'alerts-summary-alert_by_type';
@ -26,10 +30,11 @@ export const AlertsByTypePanel: React.FC<ChartsPanelProps> = ({
runtimeMappings,
skip,
}) => {
const isAlertTypeEnabled = useIsExperimentalFeatureEnabled('alertTypeEnabled');
const uniqueQueryId = useMemo(() => `${ALERTS_BY_TYPE_CHART_ID}-${uuid()}`, []);
const { items, isLoading } = useSummaryChartData({
aggregations: alertTypeAggregations,
aggregations: isAlertTypeEnabled ? alertTypeAggregations : alertRuleAggregations,
filters,
query,
signalIndexName,
@ -44,9 +49,9 @@ export const AlertsByTypePanel: React.FC<ChartsPanelProps> = ({
<EuiPanel hasBorder hasShadow={false} data-test-subj="alerts-by-type-panel">
<HeaderSection
id={uniqueQueryId}
inspectTitle={i18n.ALERTS_TYPE_TITLE}
inspectTitle={isAlertTypeEnabled ? i18n.ALERTS_TYPE_TITLE : i18n.ALERTS_RULE_TITLE}
outerDirection="row"
title={i18n.ALERTS_TYPE_TITLE}
title={isAlertTypeEnabled ? i18n.ALERTS_TYPE_TITLE : i18n.ALERTS_RULE_TITLE}
titleSize="xs"
hideSubtitle
/>

View file

@ -0,0 +1,109 @@
/*
* 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 { AlertsTypeData } from './types';
const from = '2022-04-05T12:00:00.000Z';
const to = '2022-04-08T12:00:00.000Z';
export const mockAlertsData = {
took: 0,
timeout: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 589,
relation: 'eq',
},
max_score: null,
hits: [],
},
aggregations: {
alertsByRule: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'Test rule 1',
doc_count: 537,
},
{
key: 'Test rule 2',
doc_count: 27,
},
{
key: 'Test rule 3',
doc_count: 25,
},
],
},
},
};
export const mockAlertsEmptyData = {
took: 0,
timeout: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 0,
relation: 'eq',
},
max_score: null,
hits: [],
},
aggregations: {
alertsByRule: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [],
},
},
};
export const query = {
size: 0,
query: {
bool: {
filter: [
{
bool: {
filter: [],
must: [],
must_not: [],
should: [],
},
},
{ range: { '@timestamp': { gte: from, lte: to } } },
],
},
},
aggs: {
alertsByRule: {
terms: {
field: 'kibana.alert.rule.name',
size: 1000,
},
},
},
runtime_mappings: undefined,
};
export const parsedAlerts: AlertsTypeData[] = [
{ rule: 'Test rule 1', type: 'Detection', value: 537, color: '#D36086' },
{ rule: 'Test rule 2', type: 'Detection', value: 27, color: '#D36086' },
{ rule: 'Test rule 3', type: 'Detection', value: 25, color: '#D36086' },
];

View file

@ -27,7 +27,7 @@ export const mockAlertsData = {
hits: [],
},
aggregations: {
alertsByRule: {
alertsByType: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
@ -108,7 +108,7 @@ export const mockAlertsEmptyData = {
hits: [],
},
aggregations: {
alertsByRule: {
alertsByType: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [],
@ -134,7 +134,7 @@ export const query = {
},
},
aggs: {
alertsByRule: {
alertsByType: {
terms: {
field: 'kibana.alert.rule.name',
size: 1000,

View file

@ -13,6 +13,13 @@ export const ALERTS_TYPE_TITLE = i18n.translate(
}
);
export const ALERTS_RULE_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.alertsByType.alertRuleChartTitle',
{
defaultMessage: 'Alerts by name',
}
);
export const ALERTS_TYPE_COLUMN_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.alertsByType.typeColumn',
{

View file

@ -9,13 +9,21 @@ import type { BucketItem } from '../../../../../common/search_strategy/security_
export type AlertType = 'Detection' | 'Prevention';
export interface AlertsByTypeAgg {
alertsByRule: {
alertsByType: {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: RuleBucket[];
};
}
export interface AlertsByRuleAgg {
alertsByRule: {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: BucketItem[];
};
}
interface RuleBucket {
key: string;
doc_count: number;

View file

@ -18,7 +18,7 @@ export const severityAggregations = {
};
export const alertTypeAggregations = {
alertsByRule: {
alertsByType: {
terms: {
field: ALERT_RULE_NAME,
size: DEFAULT_QUERY_SIZE,
@ -34,6 +34,15 @@ export const alertTypeAggregations = {
},
};
export const alertRuleAggregations = {
alertsByRule: {
terms: {
field: ALERT_RULE_NAME,
size: DEFAULT_QUERY_SIZE,
},
},
};
export const alertsGroupingAggregations = (stackByField: GroupBySelection) => {
return {
alertsByGrouping: {

View file

@ -6,7 +6,8 @@
*/
import { parseData } from './helpers';
import * as severityMock from '../severity_level_panel/mock_data';
import * as alertsTypeMock from '../alerts_by_type_panel/mock_data';
import * as alertsTypeMock from '../alerts_by_type_panel/mock_type_data';
import * as alertsRuleMock from '../alerts_by_type_panel/mock_rule_data';
import * as alertsGroupingMock from '../alerts_progress_bar_panel/mock_data';
import type { SummaryChartsAgg } from './types';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
@ -17,14 +18,19 @@ describe('parse data by aggregation type', () => {
expect(res).toEqual(severityMock.parsedAlerts);
});
test('parse detections data', () => {
const res = parseData(
test('parse alert type data', () => {
const resType = parseData(
alertsTypeMock.mockAlertsData as AlertSearchResponse<{}, SummaryChartsAgg>
);
expect(res).toEqual(alertsTypeMock.parsedAlerts);
expect(resType).toEqual(alertsTypeMock.parsedAlerts);
const resRule = parseData(
alertsRuleMock.mockAlertsData as AlertSearchResponse<{}, SummaryChartsAgg>
);
expect(resRule).toEqual(alertsRuleMock.parsedAlerts);
});
test('parse host data', () => {
test('parse alert groupping data', () => {
const res = parseData(
alertsGroupingMock.mockAlertsData as AlertSearchResponse<{}, SummaryChartsAgg>
);

View file

@ -7,7 +7,12 @@
import type { SummaryChartsAgg } from './types';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
import { parseSeverityData, getIsAlertsBySeverityAgg } from '../severity_level_panel/helpers';
import { parseAlertsTypeData, getIsAlertsByTypeAgg } from '../alerts_by_type_panel/helpers';
import {
parseAlertsTypeData,
getIsAlertsByTypeAgg,
parseAlertsRuleData,
getIsAlertsByRuleAgg,
} from '../alerts_by_type_panel/helpers';
import {
parseAlertsGroupingData,
getIsAlertsByGroupingAgg,
@ -24,6 +29,9 @@ export const parseData = (data: AlertSearchResponse<{}, SummaryChartsAgg>) => {
if (getIsAlertsByTypeAgg(data)) {
return parseAlertsTypeData(data);
}
if (getIsAlertsByRuleAgg(data)) {
return parseAlertsRuleData(data);
}
if (getIsAlertsByGroupingAgg(data)) {
return parseAlertsGroupingData(data);
}

View file

@ -8,7 +8,11 @@ import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'
import type { Filter, Query } from '@kbn/es-query';
import type { SeverityBuckets as SeverityData } from '../../../../overview/components/detection_response/alerts_by_status/types';
import type { AlertsBySeverityAgg } from '../severity_level_panel/types';
import type { AlertsByTypeAgg, AlertsTypeData } from '../alerts_by_type_panel/types';
import type {
AlertsByTypeAgg,
AlertsTypeData,
AlertsByRuleAgg,
} from '../alerts_by_type_panel/types';
import type {
AlertsByGroupingAgg,
AlertsProgressBarData,
@ -19,7 +23,7 @@ import type {
} from '../../../pages/detection_engine/chart_panels/chart_collapse/types';
export type SummaryChartsAgg = Partial<
AlertsBySeverityAgg | AlertsByTypeAgg | AlertsByGroupingAgg | ChartCollapseAgg
AlertsBySeverityAgg | AlertsByTypeAgg | AlertsByGroupingAgg | ChartCollapseAgg | AlertsByRuleAgg
>;
export type SummaryChartsData =

View file

@ -12,8 +12,10 @@ import type { UseAlerts, UseAlertsQueryProps } from './use_summary_chart_data';
import { useSummaryChartData, getAlertsQuery } from './use_summary_chart_data';
import * as aggregations from './aggregations';
import * as severityMock from '../severity_level_panel/mock_data';
import * as alertTypeMock from '../alerts_by_type_panel/mock_data';
import * as alertTypeMock from '../alerts_by_type_panel/mock_type_data';
import * as alertRuleMock from '../alerts_by_type_panel/mock_rule_data';
import * as alertsGroupingMock from '../alerts_progress_bar_panel/mock_data';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
const from = '2022-04-05T12:00:00.000Z';
const to = '2022-04-08T12:00:00.000Z';
@ -47,6 +49,9 @@ jest.mock('../../../../common/containers/use_global_time', () => {
};
});
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('../../../../common/hooks/use_experimental_features');
describe('getAlertsQuery', () => {
test('it returns the expected severity query', () => {
expect(
@ -68,6 +73,14 @@ describe('getAlertsQuery', () => {
aggregations: aggregations.alertTypeAggregations,
})
).toEqual(alertTypeMock.query);
expect(
getAlertsQuery({
from,
to,
additionalFilters,
aggregations: aggregations.alertRuleAggregations,
})
).toEqual(alertRuleMock.query);
});
test('it returns the expected alerts by grouping query', () => {
@ -177,8 +190,9 @@ describe('get summary charts data', () => {
jest.clearAllMocks();
mockDateNow.mockReturnValue(dateNow);
mockUseQueryAlerts.mockReturnValue(defaultUseQueryAlertsReturn);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
});
it('should return default values', () => {
it('should return correct default values when alertsTypeChartsEnabled is true', () => {
const { result } = renderUseSummaryChartData({
aggregations: aggregations.alertTypeAggregations,
});
@ -197,7 +211,27 @@ describe('get summary charts data', () => {
});
});
it('should return parsed alerts by type items', () => {
it('should return correct default values when alertsTypeChartsEnabled is false', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
const { result } = renderUseSummaryChartData({
aggregations: aggregations.alertRuleAggregations,
});
expect(result.current).toEqual({
items: [],
isLoading: false,
updatedAt: dateNow,
});
expect(mockUseQueryAlerts).toBeCalledWith({
query: alertRuleMock.query,
indexName: 'signal-alerts',
skip: false,
queryName: ALERTS_QUERY_NAMES.COUNT,
});
});
it('should return parsed alerts by type items when alertsTypeChartsEnabled is true', () => {
mockUseQueryAlerts.mockReturnValue({
...defaultUseQueryAlertsReturn,
data: alertTypeMock.mockAlertsData,
@ -212,6 +246,23 @@ describe('get summary charts data', () => {
updatedAt: dateNow,
});
});
it('should return parsed alerts by type items when alertsTypeChartsEnabled is false', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
mockUseQueryAlerts.mockReturnValue({
...defaultUseQueryAlertsReturn,
data: alertRuleMock.mockAlertsData,
});
const { result } = renderUseSummaryChartData({
aggregations: aggregations.alertRuleAggregations,
});
expect(result.current).toEqual({
items: alertRuleMock.parsedAlerts,
isLoading: false,
updatedAt: dateNow,
});
});
});
describe('get top alerts data', () => {