Skip to content

Commit

Permalink
[8.x] [Security Solution] Update alert kpi to exclude closed alerts i…
Browse files Browse the repository at this point in the history
…n document details flyout (#200268) (#200642)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Update alert kpi to exclude closed alerts in
document details flyout
(#200268)](#200268)

<!--- Backport version: 9.4.3 -->

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

<!--BACKPORT
[{"author":{"name":"christineweng","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-11-18T19:42:34Z","message":"[Security
Solution] Update alert kpi to exclude closed alerts in document details
flyout (#200268)\n\n## Summary\r\n\r\nThis PR made some changes to the
alert count API in document details\r\nflyout. Closed alerts are now
removed when showing total count and\r\ndistributions. Data fetching
logic is updated to match the one used in\r\nhost flyout
(https://github.com/elastic/kibana/pull/197102).\r\n\r\nNotable
changes:\r\n- Closed alerts are now excluded\r\n- Number of alerts in
alerts flyout should match the ones in host flyout\r\n- Clicking the
number will open timeline with the specific entity and\r\n`NOT
kibana.alert.workflow_status: closed` filters\r\n- If a host/user does
not have active alerts (all closed), no\r\ndistribution bar is
shown\r\n\r\n\r\nhttps://github.com/user-attachments/assets/3a1d6e36-527e-4b62-816b-e1f4de7314ca\r\n\r\n\r\n\r\n###
Checklist\r\n\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":"a37a02efcff84282aafb1b94778fd09f0f2025f0","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","backport","v9.0.0","Team:Threat
Hunting","Team:Threat
Hunting:Investigations","v8.17.0"],"title":"[Security Solution] Update
alert kpi to exclude closed alerts in document details
flyout","number":200268,"url":"https://github.com/elastic/kibana/pull/200268","mergeCommit":{"message":"[Security
Solution] Update alert kpi to exclude closed alerts in document details
flyout (#200268)\n\n## Summary\r\n\r\nThis PR made some changes to the
alert count API in document details\r\nflyout. Closed alerts are now
removed when showing total count and\r\ndistributions. Data fetching
logic is updated to match the one used in\r\nhost flyout
(https://github.com/elastic/kibana/pull/197102).\r\n\r\nNotable
changes:\r\n- Closed alerts are now excluded\r\n- Number of alerts in
alerts flyout should match the ones in host flyout\r\n- Clicking the
number will open timeline with the specific entity and\r\n`NOT
kibana.alert.workflow_status: closed` filters\r\n- If a host/user does
not have active alerts (all closed), no\r\ndistribution bar is
shown\r\n\r\n\r\nhttps://github.com/user-attachments/assets/3a1d6e36-527e-4b62-816b-e1f4de7314ca\r\n\r\n\r\n\r\n###
Checklist\r\n\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":"a37a02efcff84282aafb1b94778fd09f0f2025f0"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/200268","number":200268,"mergeCommit":{"message":"[Security
Solution] Update alert kpi to exclude closed alerts in document details
flyout (#200268)\n\n## Summary\r\n\r\nThis PR made some changes to the
alert count API in document details\r\nflyout. Closed alerts are now
removed when showing total count and\r\ndistributions. Data fetching
logic is updated to match the one used in\r\nhost flyout
(https://github.com/elastic/kibana/pull/197102).\r\n\r\nNotable
changes:\r\n- Closed alerts are now excluded\r\n- Number of alerts in
alerts flyout should match the ones in host flyout\r\n- Clicking the
number will open timeline with the specific entity and\r\n`NOT
kibana.alert.workflow_status: closed` filters\r\n- If a host/user does
not have active alerts (all closed), no\r\ndistribution bar is
shown\r\n\r\n\r\nhttps://github.com/user-attachments/assets/3a1d6e36-527e-4b62-816b-e1f4de7314ca\r\n\r\n\r\n\r\n###
Checklist\r\n\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":"a37a02efcff84282aafb1b94778fd09f0f2025f0"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: christineweng <[email protected]>
  • Loading branch information
kibanamachine and christineweng authored Nov 18, 2024
1 parent 6b57073 commit f41ddf0
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 @@ -113,8 +113,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 @@ -172,7 +181,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 @@ -321,9 +330,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 @@ -107,8 +107,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 @@ -165,7 +174,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 @@ -298,9 +307,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 f41ddf0

Please sign in to comment.