From 42bcabf6fff6c3489586227e393e10c2a6bf25af Mon Sep 17 00:00:00 2001 From: christineweng Date: Thu, 14 Nov 2024 16:01:48 -0600 Subject: [PATCH] update alert kpi in flyout --- .../components/alert_count_insight.test.tsx | 98 ++++++++++++++--- .../shared/components/alert_count_insight.tsx | 104 ++++++++++++------ 2 files changed, 155 insertions(+), 47 deletions(-) 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 (