mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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\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\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\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\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\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\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:
parent
84b3ef0104
commit
3b9016625e
16 changed files with 483 additions and 144 deletions
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
};
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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' },
|
||||
];
|
|
@ -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,
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue