From 011d3d8e62e747582b8abe486a58b501b19ecb4e Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Wed, 13 Nov 2024 10:10:11 -0800 Subject: [PATCH] [Cloud Security] Alerts Preview for Host Name (#197102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Screenshot 2024-10-21 at 10 38 49 PM This PR is for Alerts preview component in Contextual Flyout (Host Name) (cherry picked from commit 675b54bee70142f03929391d36c724e0f5223196) --- .../components/alerts/alerts_preview.test.tsx | 76 +++++++++++ .../components/alerts/alerts_preview.tsx | 121 ++++++++++++++++++ .../components/entity_insight.tsx | 45 ++++++- .../vulnerabilities_preview.tsx | 4 +- 4 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx new file mode 100644 index 000000000000..e0199ab40168 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { AlertsPreview } from './alerts_preview'; +import { TestProviders } from '../../../common/mock/test_providers'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import type { ParsedAlertsData } from '../../../overview/components/detection_response/alerts_by_status/types'; + +const mockAlertsData: ParsedAlertsData = { + open: { + total: 3, + severities: [ + { key: 'low', value: 2, label: 'Low' }, + { key: 'medium', value: 1, label: 'Medium' }, + ], + }, + acknowledged: { + total: 2, + severities: [ + { key: 'low', value: 1, label: 'Low' }, + { key: 'high', value: 1, label: 'High' }, + ], + }, +}; + +jest.mock( + '../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); +jest.mock('@kbn/expandable-flyout'); + +describe('AlertsPreview', () => { + const mockOpenLeftPanel = jest.fn(); + + beforeEach(() => { + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleText')).toBeInTheDocument(); + }); + + it('renders correct alerts number', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('securitySolutionFlyoutInsightsAlertsCount').textContent).toEqual('5'); + }); + + it('should render the correct number of distribution bar section based on the number of severities', () => { + const { queryAllByTestId } = render( + + + + ); + + expect(queryAllByTestId('AlertsPreviewDistributionBarTestId__part').length).toEqual(3); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx new file mode 100644 index 000000000000..3f9a0115d9ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { capitalize } from 'lodash'; +import type { EuiThemeComputed } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DistributionBar } from '@kbn/security-solution-distribution-bar'; +import { getAbbreviatedNumber } from '@kbn/cloud-security-posture-common'; +import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; +import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers'; +import type { + AlertsByStatus, + ParsedAlertsData, +} from '../../../overview/components/detection_response/alerts_by_status/types'; + +const AlertsCount = ({ + alertsTotal, + euiTheme, +}: { + alertsTotal: number; + euiTheme: EuiThemeComputed<{}>; +}) => { + return ( + + + + +

+ {getAbbreviatedNumber(alertsTotal)} +

+
+
+ + + + + +
+
+ ); +}; + +export const AlertsPreview = ({ + alertsData, + isPreviewMode, +}: { + alertsData: ParsedAlertsData; + isPreviewMode?: boolean; +}) => { + const { euiTheme } = useEuiTheme(); + + const severityMap = new Map(); + + (Object.keys(alertsData || {}) as AlertsByStatus[]).forEach((status) => { + if (alertsData?.[status]?.severities) { + alertsData?.[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), + })); + + const totalAlertsCount = alertStats.reduce((total, item) => total + item.count, 0); + + return ( + + + + ), + }} + data-test-subj={'securitySolutionFlyoutInsightsAlerts'} + > + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx index eee9af194ca3..a43b56876f1a 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx @@ -7,15 +7,22 @@ import { EuiAccordion, EuiHorizontalRule, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; +import { FILTER_CLOSED } from '../../../common/types'; import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview'; import { VulnerabilitiesPreview } from './vulnerabilities/vulnerabilities_preview'; +import { AlertsPreview } from './alerts/alerts_preview'; +import { useGlobalTime } from '../../common/containers/use_global_time'; +import type { ParsedAlertsData } from '../../overview/components/detection_response/alerts_by_status/types'; +import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../overview/components/detection_response/alerts_by_status/types'; +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'; export const EntityInsight = ({ name, @@ -60,6 +67,39 @@ export const EntityInsight = ({ const isVulnerabilitiesFindingForHost = hasVulnerabilitiesFindings && fieldName === 'host.name'; + const { signalIndexName } = useSignalIndex(); + + const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]); + + const { to, from } = useGlobalTime(); + + const { items: alertsData } = useAlertsByStatus({ + entityFilter, + signalIndexName, + queryId: DETECTION_RESPONSE_ALERTS_BY_STATUS_ID, + to, + from, + }); + + const filteredAlertsData: ParsedAlertsData = alertsData + ? Object.fromEntries(Object.entries(alertsData).filter(([key]) => key !== FILTER_CLOSED)) + : {}; + + const alertsOpenCount = filteredAlertsData?.open?.total || 0; + + const alertsAcknowledgedCount = filteredAlertsData?.acknowledged?.total || 0; + + const alertsCount = alertsOpenCount + alertsAcknowledgedCount; + + if (alertsCount > 0) { + insightContent.push( + <> + + + + ); + } + if (hasMisconfigurationFindings) insightContent.push( <> @@ -76,7 +116,8 @@ export const EntityInsight = ({ ); return ( <> - {(hasMisconfigurationFindings || + {(insightContent.length > 0 || + hasMisconfigurationFindings || (isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings)) && ( <> ), }