Skip to content

Commit

Permalink
[Security Solution] Update alert kpi to exclude closed alerts in docu…
Browse files Browse the repository at this point in the history
…ment details flyout (#200268)

## Summary

This PR made some changes to the alert count API in document details
flyout. Closed alerts are now removed when showing total count and
distributions. Data fetching logic is updated to match the one used in
host flyout (#197102).

Notable changes:
- Closed alerts are now excluded
- Number of alerts in alerts flyout should match the ones in host flyout
- Clicking the number will open timeline with the specific entity and
`NOT kibana.alert.workflow_status: closed` filters
- If a host/user does not have active alerts (all closed), no
distribution bar is shown


https://github.com/user-attachments/assets/3a1d6e36-527e-4b62-816b-e1f4de7314ca



### Checklist

- [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
  • Loading branch information
christineweng authored Nov 18, 2024
1 parent e9881a7 commit a37a02e
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';

jest.mock('@kbn/expandable-flyout');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
Expand Down Expand Up @@ -115,8 +115,17 @@ jest.mock('../../../../entity_analytics/api/hooks/use_risk_score');
const mockUseRiskScore = useRiskScore as jest.Mock;

jest.mock(
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
'../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
const mockAlertData = {
open: {
total: 2,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
],
},
};

const timestamp = '2022-07-25T08:20:18.966Z';

Expand Down Expand Up @@ -174,7 +183,7 @@ describe('<HostDetails />', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
(useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});

it('should render host details correctly', () => {
Expand Down Expand Up @@ -323,9 +332,9 @@ describe('<HostDetails />', () => {
});

it('should render alert count when data is available', () => {
(useSummaryChartData as jest.Mock).mockReturnValue({
(useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
items: [{ key: 'high', value: 78, label: 'High' }],
items: mockAlertData,
});

const { getByTestId } = renderHostDetails(mockContextValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';

jest.mock('@kbn/expandable-flyout');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
Expand Down Expand Up @@ -109,8 +109,17 @@ jest.mock('../../../../entity_analytics/api/hooks/use_risk_score');
const mockUseRiskScore = useRiskScore as jest.Mock;

jest.mock(
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
'../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
const mockAlertData = {
open: {
total: 2,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
],
},
};

const timestamp = '2022-07-25T08:20:18.966Z';

Expand Down Expand Up @@ -167,7 +176,7 @@ describe('<UserDetails />', () => {
mockUseUsersRelatedHosts.mockReturnValue(mockRelatedHostsResponse);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
(useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});

it('should render user details correctly', () => {
Expand Down Expand Up @@ -300,9 +309,9 @@ describe('<UserDetails />', () => {
});

it('should render alert count when data is available', () => {
(useSummaryChartData as jest.Mock).mockReturnValue({
(useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
items: [{ key: 'high', value: 78, label: 'High' }],
items: mockAlertData,
});

const { getByTestId } = renderUserDetails(mockContextValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';

const hostName = 'host';
const osFamily = 'Windows';
Expand All @@ -61,8 +61,17 @@ jest.mock('react-router-dom', () => {
});

jest.mock(
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
'../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
const mockAlertData = {
open: {
total: 2,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
],
},
};

const mockedTelemetry = createTelemetryServiceMock();
jest.mock('../../../../common/lib/kibana', () => {
Expand Down Expand Up @@ -118,7 +127,7 @@ describe('<HostEntityContent />', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
(useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});

describe('license is valid', () => {
Expand Down Expand Up @@ -248,9 +257,9 @@ describe('<HostEntityContent />', () => {
});

it('should render alert count when data is available', () => {
(useSummaryChartData as jest.Mock).mockReturnValue({
(useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
items: [{ key: 'high', value: 78, label: 'High' }],
items: mockAlertData,
});

const { getByTestId } = renderHostEntityContent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';

const userName = 'user';
const domain = 'n54bg2lfc7';
Expand Down Expand Up @@ -59,8 +59,17 @@ jest.mock('react-router-dom', () => {
});

jest.mock(
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
'../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
const mockAlertData = {
open: {
total: 2,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
],
},
};

jest.mock('../../../../common/hooks/use_experimental_features');
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
Expand Down Expand Up @@ -102,7 +111,7 @@ describe('<UserEntityOverview />', () => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
(useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});

describe('license is valid', () => {
Expand Down Expand Up @@ -245,9 +254,9 @@ describe('<UserEntityOverview />', () => {
});

it('should render alert count when data is available', () => {
(useSummaryChartData as jest.Mock).mockReturnValue({
(useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
items: [{ key: 'high', value: 78, label: 'High' }],
items: mockAlertData,
});

const { getByTestId } = renderUserEntityOverview();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../../common/mock';
import { AlertCountInsight } from './alert_count_insight';
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
import { AlertCountInsight, getFormattedAlertStats } from './alert_count_insight';
import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
import type { ParsedAlertsData } from '../../../../overview/components/detection_response/alerts_by_status/types';
import { SEVERITY_COLOR } from '../../../../overview/components/detection_response/utils';

jest.mock('../../../../common/lib/kibana');

Expand All @@ -19,12 +21,41 @@ jest.mock('react-router-dom', () => {
});
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
jest.mock(
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
'../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);

const fieldName = 'host.name';
const name = 'test host';
const testId = 'test';
const mockAlertData: ParsedAlertsData = {
open: {
total: 4,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
{ key: 'medium', value: 1, label: 'Medium' },
{ key: 'critical', value: 1, label: 'Critical' },
],
},
acknowledged: {
total: 4,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
{ key: 'medium', value: 1, label: 'Medium' },
{ key: 'critical', value: 1, label: 'Critical' },
],
},
closed: {
total: 6,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
{ key: 'medium', value: 2, label: 'Medium' },
{ key: 'critical', value: 2, label: 'Critical' },
],
},
};

const renderAlertCountInsight = () => {
return render(
Expand All @@ -36,30 +67,69 @@ const renderAlertCountInsight = () => {

describe('AlertCountInsight', () => {
it('renders', () => {
(useSummaryChartData as jest.Mock).mockReturnValue({
(useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
items: [
{ key: 'high', value: 78, label: 'High' },
{ key: 'low', value: 46, label: 'Low' },
{ key: 'medium', value: 32, label: 'Medium' },
{ key: 'critical', value: 21, label: 'Critical' },
],
items: mockAlertData,
});
const { getByTestId } = renderAlertCountInsight();
expect(getByTestId(testId)).toBeInTheDocument();
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
expect(getByTestId(`${testId}-count`)).toHaveTextContent('177');
expect(getByTestId(`${testId}-count`)).toHaveTextContent('8');
});

it('renders loading spinner if data is being fetched', () => {
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: true, items: [] });
(useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: true, items: {} });
const { getByTestId } = renderAlertCountInsight();
expect(getByTestId(`${testId}-loading-spinner`)).toBeInTheDocument();
});

it('renders null if no misconfiguration data found', () => {
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
it('renders null if no alert data found', () => {
(useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
const { container } = renderAlertCountInsight();
expect(container).toBeEmptyDOMElement();
});

it('renders null if no non-closed alert data found', () => {
(useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
items: {
closed: {
total: 6,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
{ key: 'medium', value: 2, label: 'Medium' },
{ key: 'critical', value: 2, label: 'Critical' },
],
},
},
});
const { container } = renderAlertCountInsight();
expect(container).toBeEmptyDOMElement();
});
});

describe('getFormattedAlertStats', () => {
it('should return alert stats', () => {
const alertStats = getFormattedAlertStats(mockAlertData);
expect(alertStats).toEqual([
{ key: 'High', count: 2, color: SEVERITY_COLOR.high },
{ key: 'Low', count: 2, color: SEVERITY_COLOR.low },
{ key: 'Medium', count: 2, color: SEVERITY_COLOR.medium },
{ key: 'Critical', count: 2, color: SEVERITY_COLOR.critical },
]);
});

it('should return empty array if no active alerts are available', () => {
const alertStats = getFormattedAlertStats({
closed: {
total: 2,
severities: [
{ key: 'high', value: 1, label: 'High' },
{ key: 'low', value: 1, label: 'Low' },
],
},
});
expect(alertStats).toEqual([]);
});
});
Loading

0 comments on commit a37a02e

Please sign in to comment.