Fix Incorrect alerts are displayed in timeline when navigating from Detection & Responses page (#144319)

* Fix Incorrect alerts are displayed in the timeline when navigating from Detection & Responses page
This commit is contained in:
Pablo Machado 2022-11-02 12:39:03 +01:00 committed by GitHub
parent 3f620428b3
commit 10dc2c6f78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 219 additions and 14 deletions

View file

@ -13,6 +13,7 @@ import { TestProviders } from '../../../../common/mock';
import { parsedVulnerableHostsAlertsResult } from './mock_data';
import type { UseHostAlertsItems } from './use_host_alerts_items';
import { HostAlertsTable } from './host_alerts_table';
import { openAlertsFilter } from '../utils';
const mockGetAppUrl = jest.fn();
jest.mock('../../../../common/lib/kibana/hooks', () => {
@ -25,6 +26,15 @@ jest.mock('../../../../common/lib/kibana/hooks', () => {
};
});
const mockOpenTimelineWithFilters = jest.fn();
jest.mock('../hooks/use_navigate_to_timeline', () => {
return {
useNavigateToTimeline: () => ({
openTimelineWithFilters: mockOpenTimelineWithFilters,
}),
};
});
type UseHostAlertsItemsReturn = ReturnType<UseHostAlertsItems>;
const defaultUseHostAlertsItemsReturn: UseHostAlertsItemsReturn = {
items: [],
@ -124,4 +134,42 @@ describe('HostAlertsTable', () => {
fireEvent.click(page3);
expect(mockSetPage).toHaveBeenCalledWith(2);
});
it('should open timeline with filters when total alerts is clicked', () => {
mockUseHostAlertsItemsReturn({ items: [parsedVulnerableHostsAlertsResult[0]] });
const { getByTestId } = renderComponent();
fireEvent.click(getByTestId('hostSeverityAlertsTable-totalAlertsLink'));
expect(mockOpenTimelineWithFilters).toHaveBeenCalledWith([
[
{
field: 'host.name',
value: 'Host-342m5gl1g2',
},
openAlertsFilter,
],
]);
});
it('should open timeline with filters when critical alert count is clicked', () => {
mockUseHostAlertsItemsReturn({ items: [parsedVulnerableHostsAlertsResult[0]] });
const { getByTestId } = renderComponent();
fireEvent.click(getByTestId('hostSeverityAlertsTable-criticalLink'));
expect(mockOpenTimelineWithFilters).toHaveBeenCalledWith([
[
{
field: 'host.name',
value: 'Host-342m5gl1g2',
},
openAlertsFilter,
{
field: 'kibana.alert.severity',
value: 'critical',
},
],
]);
});
});

View file

@ -28,7 +28,7 @@ import { HostDetailsLink } from '../../../../common/components/links';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { useNavigateToTimeline } from '../hooks/use_navigate_to_timeline';
import * as i18n from '../translations';
import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils';
import { ITEMS_PER_PAGE, openAlertsFilter, SEVERITY_COLOR } from '../utils';
import type { HostAlertsItem } from './use_host_alerts_items';
import { useHostAlertsItems } from './use_host_alerts_items';
@ -53,7 +53,9 @@ export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableP
: undefined;
openTimelineWithFilters(
severityFilter ? [[hostNameFilter, severityFilter]] : [[hostNameFilter]]
severityFilter
? [[hostNameFilter, openAlertsFilter, severityFilter]]
: [[hostNameFilter, openAlertsFilter]]
);
},
[openTimelineWithFilters]
@ -133,7 +135,11 @@ const getTableColumns: GetTableColumns = (handleClick) => [
name: i18n.ALERTS_TEXT,
'data-test-subj': 'hostSeverityAlertsTable-totalAlerts',
render: (totalAlerts: number, { hostName }) => (
<EuiLink disabled={totalAlerts === 0} onClick={() => handleClick({ hostName })}>
<EuiLink
data-test-subj="hostSeverityAlertsTable-totalAlertsLink"
disabled={totalAlerts === 0}
onClick={() => handleClick({ hostName })}
>
<FormattedCount count={totalAlerts} />
</EuiLink>
),
@ -144,6 +150,7 @@ const getTableColumns: GetTableColumns = (handleClick) => [
render: (count: number, { hostName }) => (
<EuiHealth data-test-subj="hostSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
<EuiLink
data-test-subj="hostSeverityAlertsTable-criticalLink"
disabled={count === 0}
onClick={() => handleClick({ hostName, severity: 'critical' })}
>

View file

@ -8,13 +8,14 @@
import moment from 'moment';
import React from 'react';
import { render } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { SecurityPageName } from '../../../../../common/constants';
import { TestProviders } from '../../../../common/mock';
import type { RuleAlertsTableProps } from './rule_alerts_table';
import { RuleAlertsTable } from './rule_alerts_table';
import type { RuleAlertsItem, UseRuleAlertsItems } from './use_rule_alerts_items';
import { openAlertsFilter } from '../utils';
const mockGetAppUrl = jest.fn();
jest.mock('../../../../common/lib/kibana/hooks', () => {
@ -27,6 +28,15 @@ jest.mock('../../../../common/lib/kibana/hooks', () => {
};
});
const mockOpenTimelineWithFilters = jest.fn();
jest.mock('../hooks/use_navigate_to_timeline', () => {
return {
useNavigateToTimeline: () => ({
openTimelineWithFilters: mockOpenTimelineWithFilters,
}),
};
});
type UseRuleAlertsItemsReturn = ReturnType<UseRuleAlertsItems>;
const defaultUseRuleAlertsItemsReturn: UseRuleAlertsItemsReturn = {
items: [],
@ -44,10 +54,11 @@ jest.mock('./use_rule_alerts_items', () => ({
const defaultProps: RuleAlertsTableProps = {
signalIndexName: '',
};
const ruleName = 'ruleName';
const items: RuleAlertsItem[] = [
{
id: 'ruleId',
name: 'ruleName',
name: ruleName,
last_alert_at: moment().subtract(1, 'day').format(),
alert_count: 10,
severity: 'high',
@ -144,4 +155,25 @@ describe('RuleAlertsTable', () => {
expect(result.getByTestId('severityRuleAlertsTable-name')).toHaveAttribute('href', linkUrl);
});
it('should open timeline with filters when total alerts is clicked', () => {
mockUseRuleAlertsItemsReturn({ items });
const { getByTestId } = render(
<TestProviders>
<RuleAlertsTable {...defaultProps} />
</TestProviders>
);
fireEvent.click(getByTestId('severityRuleAlertsTable-alertCountLink'));
expect(mockOpenTimelineWithFilters).toHaveBeenCalledWith([
[
{
field: 'kibana.alert.rule.name',
value: ruleName,
},
openAlertsFilter,
],
]);
});
});

View file

@ -22,7 +22,7 @@ import { FormattedRelative } from '@kbn/i18n-react';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { HeaderSection } from '../../../../common/components/header_section';
import { SEVERITY_COLOR } from '../utils';
import { openAlertsFilter, SEVERITY_COLOR } from '../utils';
import * as i18n from '../translations';
import type { RuleAlertsItem } from './use_rule_alerts_items';
import { useRuleAlertsItems } from './use_rule_alerts_items';
@ -90,7 +90,11 @@ export const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo, openRu
name: i18n.RULE_ALERTS_COLUMN_ALERT_COUNT,
'data-test-subj': 'severityRuleAlertsTable-alertCount',
render: (alertCount: number, { name }) => (
<EuiLink disabled={alertCount === 0} onClick={() => openRuleInTimeline(name)}>
<EuiLink
data-test-subj="severityRuleAlertsTable-alertCountLink"
disabled={alertCount === 0}
onClick={() => openRuleInTimeline(name)}
>
<FormattedCount count={alertCount} />
</EuiLink>
),
@ -118,7 +122,9 @@ export const RuleAlertsTable = React.memo<RuleAlertsTableProps>(({ signalIndexNa
const openRuleInTimeline = useCallback(
(ruleName: string) => {
openTimelineWithFilters([[{ field: 'kibana.alert.rule.name', value: ruleName }]]);
openTimelineWithFilters([
[{ field: 'kibana.alert.rule.name', value: ruleName }, openAlertsFilter],
]);
},
[openTimelineWithFilters]
);

View file

@ -13,7 +13,9 @@ import { TestProviders } from '../../../../common/mock';
import { parsedVulnerableUserAlertsResult } from './mock_data';
import type { UseUserAlertsItems } from './use_user_alerts_items';
import { UserAlertsTable } from './user_alerts_table';
import { openAlertsFilter } from '../utils';
const userName = 'crffn20qcs';
const mockGetAppUrl = jest.fn();
jest.mock('../../../../common/lib/kibana/hooks', () => {
const original = jest.requireActual('../../../../common/lib/kibana/hooks');
@ -25,6 +27,15 @@ jest.mock('../../../../common/lib/kibana/hooks', () => {
};
});
const mockOpenTimelineWithFilters = jest.fn();
jest.mock('../hooks/use_navigate_to_timeline', () => {
return {
useNavigateToTimeline: () => ({
openTimelineWithFilters: mockOpenTimelineWithFilters,
}),
};
});
type UseUserAlertsItemsReturn = ReturnType<UseUserAlertsItems>;
const defaultUseUserAlertsItemsReturn: UseUserAlertsItemsReturn = {
items: [],
@ -98,7 +109,7 @@ describe('UserAlertsTable', () => {
mockUseUserAlertsItemsReturn({ items: [parsedVulnerableUserAlertsResult[0]] });
const { queryByTestId } = renderComponent();
expect(queryByTestId('userSeverityAlertsTable-userName')).toHaveTextContent('crffn20qcs');
expect(queryByTestId('userSeverityAlertsTable-userName')).toHaveTextContent(userName);
expect(queryByTestId('userSeverityAlertsTable-totalAlerts')).toHaveTextContent('4');
expect(queryByTestId('userSeverityAlertsTable-critical')).toHaveTextContent('4');
expect(queryByTestId('userSeverityAlertsTable-high')).toHaveTextContent('1');
@ -124,4 +135,42 @@ describe('UserAlertsTable', () => {
fireEvent.click(page3);
expect(mockSetPage).toHaveBeenCalledWith(2);
});
it('should open timeline with filters when total alerts is clicked', () => {
mockUseUserAlertsItemsReturn({ items: [parsedVulnerableUserAlertsResult[0]] });
const { getByTestId } = renderComponent();
fireEvent.click(getByTestId('userSeverityAlertsTable-totalAlertsLink'));
expect(mockOpenTimelineWithFilters).toHaveBeenCalledWith([
[
{
field: 'user.name',
value: userName,
},
openAlertsFilter,
],
]);
});
it('should open timeline with filters when critical alerts link is clicked', () => {
mockUseUserAlertsItemsReturn({ items: [parsedVulnerableUserAlertsResult[0]] });
const { getByTestId } = renderComponent();
fireEvent.click(getByTestId('userSeverityAlertsTable-criticalLink'));
expect(mockOpenTimelineWithFilters).toHaveBeenCalledWith([
[
{
field: 'user.name',
value: userName,
},
openAlertsFilter,
{
field: 'kibana.alert.severity',
value: 'critical',
},
],
]);
});
});

View file

@ -28,7 +28,7 @@ import { UserDetailsLink } from '../../../../common/components/links';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { useNavigateToTimeline } from '../hooks/use_navigate_to_timeline';
import * as i18n from '../translations';
import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils';
import { ITEMS_PER_PAGE, openAlertsFilter, SEVERITY_COLOR } from '../utils';
import type { UserAlertsItem } from './use_user_alerts_items';
import { useUserAlertsItems } from './use_user_alerts_items';
@ -53,7 +53,9 @@ export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableP
: undefined;
openTimelineWithFilters(
severityFilter ? [[userNameFilter, severityFilter]] : [[userNameFilter]]
severityFilter
? [[userNameFilter, openAlertsFilter, severityFilter]]
: [[userNameFilter, openAlertsFilter]]
);
},
[openTimelineWithFilters]
@ -132,7 +134,11 @@ const getTableColumns: GetTableColumns = (handleClick) => [
name: i18n.ALERTS_TEXT,
'data-test-subj': 'userSeverityAlertsTable-totalAlerts',
render: (totalAlerts: number, { userName }) => (
<EuiLink disabled={totalAlerts === 0} onClick={() => handleClick({ userName })}>
<EuiLink
data-test-subj="userSeverityAlertsTable-totalAlertsLink"
disabled={totalAlerts === 0}
onClick={() => handleClick({ userName })}
>
<FormattedCount count={totalAlerts} />
</EuiLink>
),
@ -143,6 +149,7 @@ const getTableColumns: GetTableColumns = (handleClick) => [
render: (count: number, { userName }) => (
<EuiHealth data-test-subj="userSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
<EuiLink
data-test-subj="userSeverityAlertsTable-criticalLink"
disabled={count === 0}
onClick={() => handleClick({ userName, severity: 'critical' })}
>

View file

@ -21,3 +21,5 @@ const MAX_ALLOWED_RESULTS = 100;
* */
export const getPageCount = (count: number = 0) =>
Math.ceil(Math.min(count || 0, MAX_ALLOWED_RESULTS) / ITEMS_PER_PAGE);
export const openAlertsFilter = { field: 'kibana.alert.workflow_status', value: 'open' };

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { EntityAnalyticsRiskScores } from '.';
@ -13,6 +13,7 @@ import type { UserRiskScore } from '../../../../../common/search_strategy';
import { RiskScoreEntity, RiskSeverity } from '../../../../../common/search_strategy';
import type { SeverityCount } from '../../../../common/components/severity/types';
import { useRiskScore, useRiskScoreKpi } from '../../../../risk_score/containers';
import { openAlertsFilter } from '../../detection_response/utils';
const mockSeverityCount: SeverityCount = {
[RiskSeverity.low]: 1,
@ -42,6 +43,15 @@ const mockUseRiskScore = useRiskScore as jest.Mock;
const mockUseRiskScoreKpi = useRiskScoreKpi as jest.Mock;
jest.mock('../../../../risk_score/containers');
const mockOpenTimelineWithFilters = jest.fn();
jest.mock('../../detection_response/hooks/use_navigate_to_timeline', () => {
return {
useNavigateToTimeline: () => ({
openTimelineWithFilters: mockOpenTimelineWithFilters,
}),
};
});
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
'EntityAnalyticsRiskScores entityType: %s',
(riskEntity) => {
@ -150,5 +160,48 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString());
});
it('navigates to timeline with filters when alerts count is clicked', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseRiskScoreKpi.mockReturnValue({
severityCount: mockSeverityCount,
loading: false,
});
const name = 'testName';
const data = [
{
'@timestamp': '1234567899',
[riskEntity]: {
name,
risk: {
rule_risks: [],
calculated_level: RiskSeverity.high,
calculated_score_norm: 75,
multipliers: [],
},
},
alertsCount: 999,
},
];
mockUseRiskScore.mockReturnValue({ ...defaultProps, data });
const { getByTestId } = render(
<TestProviders>
<EntityAnalyticsRiskScores riskEntity={riskEntity} />
</TestProviders>
);
fireEvent.click(getByTestId('risk-score-alerts'));
expect(mockOpenTimelineWithFilters.mock.calls[0][0]).toEqual([
[
{
field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
value: name,
},
openAlertsFilter,
],
]);
});
}
);

View file

@ -42,6 +42,7 @@ import * as commonI18n from '../common/translations';
import { usersActions } from '../../../../users/store';
import { useNavigateToTimeline } from '../../detection_response/hooks/use_navigate_to_timeline';
import type { TimeRange } from '../../../../common/store/inputs/model';
import { openAlertsFilter } from '../../detection_response/utils';
const HOST_RISK_TABLE_QUERY_ID = 'hostRiskDashboardTable';
const HOST_RISK_KPI_QUERY_ID = 'headerHostRiskScoreKpiQuery';
@ -110,7 +111,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
value: entityName,
};
openTimelineWithFilters([[filter]], timeRange);
openTimelineWithFilters([[filter, openAlertsFilter]], timeRange);
},
[riskEntity, openTimelineWithFilters]
);