From f41ddf0aa1baa9a008e51ee10e180ed9fd6ca9ff Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Nov 2024 08:29:49 +1100
Subject: [PATCH] [8.x] [Security Solution] Update alert kpi to exclude closed
alerts in 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)](https://github.com/elastic/kibana/pull/200268)
### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)
Co-authored-by: christineweng <18648970+christineweng@users.noreply.github.com>
---
.../left/components/host_details.test.tsx | 19 +++-
.../left/components/user_details.test.tsx | 19 +++-
.../components/host_entity_overview.test.tsx | 19 +++-
.../components/user_entity_overview.test.tsx | 19 +++-
.../components/alert_count_insight.test.tsx | 98 ++++++++++++++---
.../shared/components/alert_count_insight.tsx | 104 ++++++++++++------
6 files changed, 211 insertions(+), 67 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
index e998c29d8ab6f..21346628b1f91 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
@@ -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');
@@ -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';
@@ -172,7 +181,7 @@ describe('', () => {
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', () => {
@@ -321,9 +330,9 @@ describe('', () => {
});
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);
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
index 17ec5f052be32..dd92106aa3cea 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
@@ -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');
@@ -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';
@@ -165,7 +174,7 @@ describe('', () => {
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', () => {
@@ -298,9 +307,9 @@ describe('', () => {
});
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);
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
index 6ad90adb28997..4c29a84d431ae 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
@@ -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';
@@ -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', () => {
@@ -118,7 +127,7 @@ describe('', () => {
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', () => {
@@ -248,9 +257,9 @@ describe('', () => {
});
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();
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
index 95c399ca4362e..5df159c2e5a29 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
@@ -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';
@@ -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;
@@ -102,7 +111,7 @@ describe('', () => {
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', () => {
@@ -245,9 +254,9 @@ describe('', () => {
});
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();
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
index 5e4650179291d..d9ae4673b1749 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
@@ -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');
@@ -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(
@@ -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([]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
index 08325584bd8cb..9b5b056311354 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
@@ -6,22 +6,26 @@
*/
import React, { useMemo } from 'react';
-import { v4 as uuid } from 'uuid';
+import { capitalize } from 'lodash';
import { EuiLoadingSpinner, EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { InsightDistributionBar } from './insight_distribution_bar';
-import { severityAggregations } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations';
-import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
-import {
- getIsAlertsBySeverityData,
- getSeverityColor,
-} from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers';
+import { getSeverityColor } from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers';
import { FormattedCount } from '../../../../common/components/formatted_number';
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button';
-import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider';
-
-const ENTITY_ALERT_COUNT_ID = 'entity-alert-count';
-const SEVERITIES = ['unknown', 'low', 'medium', 'high', 'critical'];
+import {
+ getDataProvider,
+ getDataProviderAnd,
+} from '../../../../common/components/event_details/use_action_cell_data_provider';
+import { FILTER_CLOSED, IS_OPERATOR } from '../../../../../common/types';
+import { useGlobalTime } from '../../../../common/containers/use_global_time';
+import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
+import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
+import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../../overview/components/detection_response/alerts_by_status/types';
+import type {
+ AlertsByStatus,
+ ParsedAlertsData,
+} from '../../../../overview/components/detection_response/alerts_by_status/types';
interface AlertCountInsightProps {
/**
@@ -42,6 +46,33 @@ interface AlertCountInsightProps {
['data-test-subj']?: string;
}
+/**
+ * Filters closed alerts and format the alert stats for the distribution bar
+ */
+export const getFormattedAlertStats = (alertsData: ParsedAlertsData) => {
+ const severityMap = new Map();
+
+ const filteredAlertsData: ParsedAlertsData = alertsData
+ ? Object.fromEntries(Object.entries(alertsData).filter(([key]) => key !== FILTER_CLOSED))
+ : {};
+
+ (Object.keys(filteredAlertsData || {}) as AlertsByStatus[]).forEach((status) => {
+ if (filteredAlertsData?.[status]?.severities) {
+ filteredAlertsData?.[status]?.severities.forEach((severity) => {
+ const currentSeverity = severityMap.get(severity.key) || 0;
+ severityMap.set(severity.key, currentSeverity + severity.value);
+ });
+ }
+ });
+
+ const alertStats = Array.from(severityMap, ([key, count]) => ({
+ key: capitalize(key),
+ count,
+ color: getSeverityColor(key),
+ }));
+ return alertStats;
+};
+
/*
* Displays a distribution bar with the total alert count for a given entity
*/
@@ -51,37 +82,44 @@ export const AlertCountInsight: React.FC = ({
direction,
'data-test-subj': dataTestSubj,
}) => {
- const uniqueQueryId = useMemo(() => `${ENTITY_ALERT_COUNT_ID}-${uuid()}`, []);
const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]);
+ const { to, from } = useGlobalTime();
+ const { signalIndexName } = useSignalIndex();
- const { items, isLoading } = useSummaryChartData({
- aggregations: severityAggregations,
+ const { items, isLoading } = useAlertsByStatus({
entityFilter,
- uniqueQueryId,
- signalIndexName: null,
+ signalIndexName,
+ queryId: DETECTION_RESPONSE_ALERTS_BY_STATUS_ID,
+ to,
+ from,
});
- const dataProviders = useMemo(
- () => [getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name)],
- [fieldName, name]
- );
- const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]);
+ const alertStats = useMemo(() => getFormattedAlertStats(items), [items]);
- const alertStats = useMemo(
- () =>
- data
- .map((item) => ({
- key: item.key,
- count: item.value,
- color: getSeverityColor(item.key),
- }))
- .sort((a, b) => SEVERITIES.indexOf(a.key) - SEVERITIES.indexOf(b.key)),
- [data]
+ const totalAlertCount = useMemo(
+ () => alertStats.reduce((acc, item) => acc + item.count, 0),
+ [alertStats]
);
- const totalAlertCount = useMemo(() => data.reduce((acc, item) => acc + item.value, 0), [data]);
+ const dataProviders = useMemo(
+ () => [
+ {
+ ...getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name),
+ and: [
+ getDataProviderAnd(
+ 'kibana.alert.workflow_status',
+ `timeline-indicator-kibana.alert.workflow_status-not-closed}`,
+ FILTER_CLOSED,
+ IS_OPERATOR,
+ true
+ ),
+ ],
+ },
+ ],
+ [fieldName, name]
+ );
- if (!isLoading && items.length === 0) return null;
+ if (!isLoading && totalAlertCount === 0) return null;
return (