diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx index f496ecc89b90b..e1e3bac6d9c18 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx @@ -8,7 +8,7 @@ import type { FC, PropsWithChildren } from 'react'; import React, { useCallback } from 'react'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; -import type { IconType } from '@elastic/eui'; +import type { IconType, EuiButtonEmptyProps } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; import { useDispatch, useSelector } from 'react-redux'; @@ -34,6 +34,7 @@ export interface InvestigateInTimelineButtonProps { isDisabled?: boolean; iconType?: IconType; children?: React.ReactNode; + flush?: EuiButtonEmptyProps['flush']; } export const InvestigateInTimelineButton: FC< @@ -46,6 +47,7 @@ export const InvestigateInTimelineButton: FC< timeRange, keepDataView, iconType, + flush, ...rest }) => { const dispatch = useDispatch(); @@ -118,7 +120,7 @@ export const InvestigateInTimelineButton: FC< 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 f0d16a418f2b2..5e4650179291d 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 @@ -48,6 +48,7 @@ describe('AlertCountInsight', () => { const { getByTestId } = renderAlertCountInsight(); expect(getByTestId(testId)).toBeInTheDocument(); expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('177'); }); it('renders loading spinner if data is being fetched', () => { 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 566b77b5739a9..08325584bd8cb 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 @@ -16,8 +16,12 @@ import { getIsAlertsBySeverityData, 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']; interface AlertCountInsightProps { /** @@ -39,7 +43,7 @@ interface AlertCountInsightProps { } /* - * Displays a distribution bar with the count of critical alerts for a given entity + * Displays a distribution bar with the total alert count for a given entity */ export const AlertCountInsight: React.FC = ({ name, @@ -56,22 +60,27 @@ export const AlertCountInsight: React.FC = ({ uniqueQueryId, signalIndexName: null, }); + const dataProviders = useMemo( + () => [getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name)], + [fieldName, name] + ); const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]); - const alertStats = useMemo(() => { - return data.map((item) => ({ - key: item.key, - count: item.value, - color: getSeverityColor(item.key), - })); - }, [data]); - - const count = useMemo( - () => data.filter((item) => item.key === 'critical')[0]?.value ?? 0, + 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(() => data.reduce((acc, item) => acc + item.value, 0), [data]); + if (!isLoading && items.length === 0) return null; return ( @@ -87,7 +96,17 @@ export const AlertCountInsight: React.FC = ({ /> } stats={alertStats} - count={count} + count={ +
+ + + +
+ } direction={direction} data-test-subj={`${dataTestSubj}-distribution-bar`} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx index a775da8a7f73a..405c0528a9b2c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx @@ -11,7 +11,7 @@ import { InsightDistributionBar } from './insight_distribution_bar'; import { TestProviders } from '../../../../common/mock'; const title = 'test title'; -const count = 10; +const count =
{'100'}
; const testId = 'test-id'; const stats = [ { @@ -35,7 +35,7 @@ describe('', () => { ); expect(getByTestId(testId)).toBeInTheDocument(); expect(getByText(title)).toBeInTheDocument(); - expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`); + expect(getByTestId('test-count')).toBeInTheDocument(); expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx index 006ec8c5dad4f..083738e6766bc 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { css } from '@emotion/css'; import { EuiFlexGroup, @@ -17,7 +17,6 @@ import { type EuiFlexGroupProps, } from '@elastic/eui'; import { DistributionBar } from '@kbn/security-solution-distribution-bar'; -import { FormattedCount } from '../../../../common/components/formatted_number'; export interface InsightDistributionBarProps { /** @@ -31,7 +30,7 @@ export interface InsightDistributionBarProps { /** * Count to be displayed in the badge */ - count: number; + count: React.ReactNode; /** * Flex direction of the component */ @@ -53,34 +52,53 @@ export const InsightDistributionBar: React.FC = ({ const { euiTheme } = useEuiTheme(); const xsFontSize = useEuiFontSize('xs').fontSize; + const barComponent = useMemo( + () => ( + + + + + + {count} + + + ), + [stats, count, dataTestSubj] + ); + return ( - - + + {title} - - - - - - - - - - - - + {direction === 'column' ? ( + + {barComponent} + + ) : ( + {barComponent} + )} ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx index 296a61f444a17..8976a01eedbc4 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx @@ -10,34 +10,87 @@ import { render } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { MisconfigurationsInsight } from './misconfiguration_insight'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { DocumentDetailsContext } from '../context'; +import { mockFlyoutApi } from '../mocks/mock_flyout_context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { mockContextValue } from '../mocks/mock_context'; +import { HostPreviewPanelKey } from '../../../entity_details/host_right'; +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'; +jest.mock('@kbn/expandable-flyout'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); -const fieldName = 'host.name'; -const name = 'test host'; +const hostName = 'test host'; +const userName = 'test user'; const testId = 'test'; -const renderMisconfigurationsInsight = () => { +const renderMisconfigurationsInsight = (fieldName: 'host.name' | 'user.name', value: string) => { return render( - + + + ); }; describe('MisconfigurationsInsight', () => { + beforeEach(() => { + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + it('renders', () => { (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 2 } }, }); - const { getByTestId } = renderMisconfigurationsInsight(); + const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName); expect(getByTestId(testId)).toBeInTheDocument(); expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); }); it('renders null if no misconfiguration data found', () => { (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); - const { container } = renderMisconfigurationsInsight(); + const { container } = renderMisconfigurationsInsight('host.name', hostName); expect(container).toBeEmptyDOMElement(); }); + + describe('should open entity flyout when clicking on badge', () => { + it('should open host entity flyout when clicking on host badge', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('3'); + + getByTestId(`${testId}-count`).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: HostPreviewPanelKey, + params: { + hostName, + banner: HOST_PREVIEW_BANNER, + scopeId: mockContextValue.scopeId, + }, + }); + }); + + it('should open user entity flyout when clicking on user badge', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 2, failed: 3 } }, + }); + const { getByTestId } = renderMisconfigurationsInsight('user.name', userName); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('5'); + + getByTestId(`${testId}-count`).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: UserPreviewPanelKey, + params: { + userName, + banner: USER_PREVIEW_BANNER, + scopeId: mockContextValue.scopeId, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx index 552a242c84893..961fa1d5f3a45 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx @@ -6,12 +6,16 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/css'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { InsightDistributionBar } from './insight_distribution_bar'; import { getFindingsStats } from '../../../../cloud_security_posture/components/misconfiguration/misconfiguration_preview'; +import { FormattedCount } from '../../../../common/components/formatted_number'; +import { PreviewLink } from '../../../shared/components/preview_link'; +import { useDocumentDetailsContext } from '../context'; interface MisconfigurationsInsightProps { /** @@ -33,7 +37,7 @@ interface MisconfigurationsInsightProps { } /* - * Displays a distribution bar with the count of failed misconfigurations for a given entity + * Displays a distribution bar with the count of total misconfigurations for a given entity */ export const MisconfigurationsInsight: React.FC = ({ name, @@ -41,6 +45,8 @@ export const MisconfigurationsInsight: React.FC = direction, 'data-test-subj': dataTestSubj, }) => { + const { scopeId, isPreview } = useDocumentDetailsContext(); + const { euiTheme } = useEuiTheme(); const { data } = useMisconfigurationPreview({ query: buildEntityFlyoutPreviewQuery(fieldName, name), sort: [], @@ -50,13 +56,39 @@ export const MisconfigurationsInsight: React.FC = const passedFindings = data?.count.passed || 0; const failedFindings = data?.count.failed || 0; - const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + const totalFindings = useMemo( + () => passedFindings + failedFindings, + [passedFindings, failedFindings] + ); + const hasMisconfigurationFindings = totalFindings > 0; const misconfigurationsStats = useMemo( () => getFindingsStats(passedFindings, failedFindings), [passedFindings, failedFindings] ); + const count = useMemo( + () => ( +
+ + + +
+ ), + [totalFindings, fieldName, name, scopeId, isPreview, dataTestSubj, euiTheme.size] + ); + if (!hasMisconfigurationFindings) return null; return ( @@ -69,7 +101,7 @@ export const MisconfigurationsInsight: React.FC = /> } stats={misconfigurationsStats} - count={failedFindings} + count={count} direction={direction} data-test-subj={`${dataTestSubj}-distribution-bar`} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx index 77c6737266b89..cfac8703fbc89 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx @@ -10,7 +10,14 @@ import { render } from '@testing-library/react'; import React from 'react'; import { VulnerabilitiesInsight } from './vulnerabilities_insight'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { DocumentDetailsContext } from '../context'; +import { mockFlyoutApi } from '../mocks/mock_flyout_context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { mockContextValue } from '../mocks/mock_context'; +import { HostPreviewPanelKey } from '../../../entity_details/host_right'; +import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; +jest.mock('@kbn/expandable-flyout'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); const hostName = 'test host'; @@ -19,15 +26,21 @@ const testId = 'test'; const renderVulnerabilitiesInsight = () => { return render( - + + + ); }; describe('VulnerabilitiesInsight', () => { + beforeEach(() => { + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + it('renders', () => { (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ - data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, NONE: 0 } }, }); const { getByTestId } = renderVulnerabilitiesInsight(); @@ -35,6 +48,24 @@ describe('VulnerabilitiesInsight', () => { expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); }); + it('opens host preview when click on count badge', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 1, HIGH: 2, MEDIUM: 1, LOW: 2, NONE: 2 } }, + }); + const { getByTestId } = renderVulnerabilitiesInsight(); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('8'); + + getByTestId(`${testId}-count`).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: HostPreviewPanelKey, + params: { + hostName, + banner: HOST_PREVIEW_BANNER, + scopeId: mockContextValue.scopeId, + }, + }); + }); + it('renders null when data is not available', () => { (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx index 4c581b6db57d0..c675c0a0e079b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx @@ -6,12 +6,16 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/css'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; import { InsightDistributionBar } from './insight_distribution_bar'; +import { FormattedCount } from '../../../../common/components/formatted_number'; +import { PreviewLink } from '../../../shared/components/preview_link'; +import { useDocumentDetailsContext } from '../context'; interface VulnerabilitiesInsightProps { /** @@ -29,13 +33,15 @@ interface VulnerabilitiesInsightProps { } /* - * Displays a distribution bar with the count of critical vulnerabilities for a given host + * Displays a distribution bar and the total vulnerabilities count for a given host */ export const VulnerabilitiesInsight: React.FC = ({ hostName, direction, 'data-test-subj': dataTestSubj, }) => { + const { scopeId, isPreview } = useDocumentDetailsContext(); + const { euiTheme } = useEuiTheme(); const { data } = useVulnerabilitiesPreview({ query: buildEntityFlyoutPreviewQuery('host.name', hostName), sort: [], @@ -44,6 +50,11 @@ export const VulnerabilitiesInsight: React.FC = ({ }); const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {}; + const totalVulnerabilities = useMemo( + () => CRITICAL + HIGH + MEDIUM + LOW + NONE, + [CRITICAL, HIGH, MEDIUM, LOW, NONE] + ); + const hasVulnerabilitiesFindings = useMemo( () => hasVulnerabilitiesData({ @@ -68,6 +79,28 @@ export const VulnerabilitiesInsight: React.FC = ({ [CRITICAL, HIGH, MEDIUM, LOW, NONE] ); + const count = useMemo( + () => ( +
+ + + +
+ ), + [totalVulnerabilities, hostName, scopeId, isPreview, dataTestSubj, euiTheme.size] + ); + if (!hasVulnerabilitiesFindings) return null; return ( @@ -80,7 +113,7 @@ export const VulnerabilitiesInsight: React.FC = ({ /> } stats={vulnerabilitiesStats} - count={CRITICAL} + count={count} direction={direction} data-test-subj={`${dataTestSubj}-distribution-bar`} />