From cd217c072fc786cb76ee47d885501688507c2dde Mon Sep 17 00:00:00 2001
From: christineweng <18648970+christineweng@users.noreply.github.com>
Date: Thu, 10 Oct 2024 15:46:51 -0500
Subject: [PATCH] [Security Solution] Add alert and cloud insights to document
flyout (#195509)
## Summary
This PR adds alert count, misconfiguration and vulnerabilities insights
to alert/event flyout. If data is not available, the insights are
hidden.
[Mocks](https://www.figma.com/design/ubvhBGHee58diJNvSiy0GZ/%5B8.%2B%5D-%5BAlerts%5D-Expandable-Event-Flyout?node-id=8017-179782&node-type=canvas&t=0YjHfPi9zOUFUScc-0)
![image](https://github.com/user-attachments/assets/ba706ab8-448a-4286-8229-c4c398136638)
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../src/distribution_bar.stories.tsx | 8 ++
.../src/distribution_bar.test.tsx | 62 ++++++++++++
.../distribution_bar/src/distribution_bar.tsx | 9 +-
.../misconfiguration_preview.tsx | 2 +-
.../left/components/host_details.test.tsx | 52 ++++++++++
.../left/components/host_details.tsx | 30 ++++++
.../left/components/test_ids.ts | 8 ++
.../left/components/user_details.test.tsx | 38 +++++++
.../left/components/user_details.tsx | 22 +++++
.../components/host_entity_overview.test.tsx | 65 ++++++++++++
.../right/components/host_entity_overview.tsx | 24 ++++-
.../right/components/test_ids.ts | 10 ++
.../components/user_entity_overview.test.tsx | 52 ++++++++++
.../right/components/user_entity_overview.tsx | 18 +++-
.../components/alert_count_insight.test.tsx | 64 ++++++++++++
.../shared/components/alert_count_insight.tsx | 99 +++++++++++++++++++
.../insight_distribution_bar.test.tsx | 41 ++++++++
.../components/insight_distribution_bar.tsx | 88 +++++++++++++++++
.../misconfiguration_insight.test.tsx | 43 ++++++++
.../components/misconfiguration_insight.tsx | 80 +++++++++++++++
.../shared/components/test_ids.ts | 3 +
.../vulnerabilities_insight.test.tsx | 44 +++++++++
.../components/vulnerabilities_insight.tsx | 91 +++++++++++++++++
23 files changed, 946 insertions(+), 7 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx
diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx
index 90b6887636c8a..c1b292c3f08cc 100644
--- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx
+++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx
@@ -70,6 +70,14 @@ export const DistributionBar = () => {
,
+
+
+ {'Hide last tooltip'}
+
+
+
+
+ ,
{'Empty state'}
diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx
index d4bdf4c20f133..e83b66e5e01e7 100644
--- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx
+++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx
@@ -79,5 +79,67 @@ describe('DistributionBar', () => {
});
});
+ it('should render last tooltip by default', () => {
+ const stats = [
+ {
+ key: 'low',
+ count: 9,
+ color: 'green',
+ },
+ {
+ key: 'medium',
+ count: 90,
+ color: 'red',
+ },
+ {
+ key: 'high',
+ count: 900,
+ color: 'red',
+ },
+ ];
+
+ const { container } = render(
+
+ );
+ expect(container).toBeInTheDocument();
+ const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`);
+ parts.forEach((part, index) => {
+ if (index < parts.length - 1) {
+ expect(part).toHaveStyle({ opacity: 0 });
+ } else {
+ expect(part).toHaveStyle({ opacity: 1 });
+ }
+ });
+ });
+
+ it('should not render last tooltip when hideLastTooltip is true', () => {
+ const stats = [
+ {
+ key: 'low',
+ count: 9,
+ color: 'green',
+ },
+ {
+ key: 'medium',
+ count: 90,
+ color: 'red',
+ },
+ {
+ key: 'high',
+ count: 900,
+ color: 'red',
+ },
+ ];
+
+ const { container } = render(
+
+ );
+ expect(container).toBeInTheDocument();
+ const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`);
+ parts.forEach((part) => {
+ expect(part).toHaveStyle({ opacity: 0 });
+ });
+ });
+
// todo: test tooltip visibility logic
});
diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx
index 28d8ca4a8a148..5b06292813ccd 100644
--- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx
+++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx
@@ -13,6 +13,8 @@ import { css } from '@emotion/react';
export interface DistributionBarProps {
/** distribution data points */
stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>;
+ /** hide the label above the bar at first render */
+ hideLastTooltip?: boolean;
/** data-test-subj used for querying the component in tests */
['data-test-subj']?: string;
}
@@ -136,18 +138,21 @@ export const DistributionBar: React.FC = React.memo(functi
props
) {
const styles = useStyles();
- const { stats, 'data-test-subj': dataTestSubj } = props;
+ const { stats, 'data-test-subj': dataTestSubj, hideLastTooltip } = props;
const parts = stats.map((stat) => {
const partStyle = [
styles.part.base,
styles.part.tick,
styles.part.hover,
- styles.part.lastTooltip,
css`
background-color: ${stat.color};
flex: ${stat.count};
`,
];
+ if (!hideLastTooltip) {
+ partStyle.push(styles.part.lastTooltip);
+ }
+
const prettyNumber = numeral(stat.count).format('0,0a');
return (
diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx
index a13a77a3562ff..a372ca4755fd8 100644
--- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx
+++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx
@@ -35,7 +35,7 @@ const FIRST_RECORD_PAGINATION = {
querySize: 1,
};
-const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
+export const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
if (passedFindingsStats === 0 && failedFindingsStats === 0) return [];
return [
{
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 46288434f48bb..23f6969c36778 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
@@ -7,6 +7,8 @@
import React from 'react';
import { render } from '@testing-library/react';
+import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
+import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import type { Anomalies } from '../../../../common/components/ml/types';
import { DocumentDetailsContext } from '../../shared/context';
import { TestProviders } from '../../../../common/mock';
@@ -24,6 +26,9 @@ import {
HOST_DETAILS_LINK_TEST_ID,
HOST_DETAILS_RELATED_USERS_LINK_TEST_ID,
HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID,
+ HOST_DETAILS_MISCONFIGURATIONS_TEST_ID,
+ HOST_DETAILS_VULNERABILITIES_TEST_ID,
+ HOST_DETAILS_ALERT_COUNT_TEST_ID,
} from './test_ids';
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common';
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
@@ -35,8 +40,11 @@ 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';
jest.mock('@kbn/expandable-flyout');
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
jest.mock('../../../../common/hooks/use_experimental_features');
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
@@ -104,6 +112,10 @@ const mockUseHostsRelatedUsers = useHostRelatedUsers as jest.Mock;
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'
+);
+
const timestamp = '2022-07-25T08:20:18.966Z';
const defaultProps = {
@@ -158,6 +170,9 @@ describe('', () => {
mockUseRiskScore.mockReturnValue(mockRiskScoreResponse);
mockUseHostsRelatedUsers.mockReturnValue(mockRelatedUsersResponse);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({});
+ (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
+ (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
});
it('should render host details correctly', () => {
@@ -296,4 +311,41 @@ describe('', () => {
});
});
});
+
+ describe('distribution bar insights', () => {
+ it('should not render if no data is available', () => {
+ const { queryByTestId } = renderHostDetails(mockContextValue);
+ expect(queryByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should render alert count when data is available', () => {
+ (useSummaryChartData as jest.Mock).mockReturnValue({
+ isLoading: false,
+ items: [{ key: 'high', value: 78, label: 'High' }],
+ });
+
+ const { getByTestId } = renderHostDetails(mockContextValue);
+ expect(getByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render misconfiguration when data is available', () => {
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({
+ data: { count: { passed: 1, failed: 2 } },
+ });
+
+ const { getByTestId } = renderHostDetails(mockContextValue);
+ expect(getByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render vulnerabilities when data is available', () => {
+ (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
+ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
+ });
+
+ const { getByTestId } = renderHostDetails(mockContextValue);
+ expect(getByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).toBeInTheDocument();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx
index 33b8bb22fce53..122caa657b039 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx
@@ -18,6 +18,8 @@ import {
EuiToolTip,
EuiIcon,
EuiPanel,
+ EuiHorizontalRule,
+ EuiFlexGrid,
} from '@elastic/eui';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -51,6 +53,9 @@ import {
HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID,
HOST_DETAILS_RELATED_USERS_LINK_TEST_ID,
HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID,
+ HOST_DETAILS_ALERT_COUNT_TEST_ID,
+ HOST_DETAILS_MISCONFIGURATIONS_TEST_ID,
+ HOST_DETAILS_VULNERABILITIES_TEST_ID,
} from './test_ids';
import {
USER_NAME_FIELD_NAME,
@@ -63,6 +68,9 @@ import { PreviewLink } from '../../../shared/components/preview_link';
import { HostPreviewPanelKey } from '../../../entity_details/host_right';
import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview';
import type { NarrowDateRange } from '../../../../common/components/ml/types';
+import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
+import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight';
+import { AlertCountInsight } from '../../shared/components/alert_count_insight';
const HOST_DETAILS_ID = 'entities-hosts-details';
const RELATED_USERS_ID = 'entities-hosts-related-users';
@@ -337,6 +345,28 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s
)}
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts
index 0779f3c135b2d..8669b504f6861 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts
@@ -43,6 +43,9 @@ export const PREVALENCE_DETAILS_TABLE_UPSELL_CELL_TEST_ID =
export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}EntitiesDetails` as const;
export const USER_DETAILS_TEST_ID = `${PREFIX}UsersDetails` as const;
export const USER_DETAILS_LINK_TEST_ID = `${USER_DETAILS_TEST_ID}TitleLink` as const;
+export const USER_DETAILS_ALERT_COUNT_TEST_ID = `${USER_DETAILS_TEST_ID}AlertCount` as const;
+export const USER_DETAILS_MISCONFIGURATIONS_TEST_ID =
+ `${USER_DETAILS_TEST_ID}Misconfigurations` as const;
export const USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID =
`${USER_DETAILS_TEST_ID}RelatedHostsTable` as const;
export const USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID =
@@ -53,6 +56,11 @@ export const USER_DETAILS_INFO_TEST_ID = 'user-overview' as const;
export const HOST_DETAILS_TEST_ID = `${PREFIX}HostsDetails` as const;
export const HOST_DETAILS_LINK_TEST_ID = `${HOST_DETAILS_TEST_ID}TitleLink` as const;
+export const HOST_DETAILS_ALERT_COUNT_TEST_ID = `${HOST_DETAILS_TEST_ID}AlertCount` as const;
+export const HOST_DETAILS_MISCONFIGURATIONS_TEST_ID =
+ `${HOST_DETAILS_TEST_ID}Misconfigurations` as const;
+export const HOST_DETAILS_VULNERABILITIES_TEST_ID =
+ `${HOST_DETAILS_TEST_ID}Vulnerabilities` as const;
export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID =
`${HOST_DETAILS_TEST_ID}RelatedUsersTable` as const;
export const HOST_DETAILS_RELATED_USERS_LINK_TEST_ID =
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 c1ed881e80a95..a2c53afb8c3f3 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
@@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
+import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import type { Anomalies } from '../../../../common/components/ml/types';
import { TestProviders } from '../../../../common/mock';
import { DocumentDetailsContext } from '../../shared/context';
@@ -24,6 +25,8 @@ import {
USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID,
USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID,
USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID,
+ USER_DETAILS_MISCONFIGURATIONS_TEST_ID,
+ USER_DETAILS_ALERT_COUNT_TEST_ID,
} from './test_ids';
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common';
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
@@ -35,8 +38,10 @@ 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';
jest.mock('@kbn/expandable-flyout');
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
jest.mock('../../../../common/hooks/use_experimental_features');
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
@@ -101,6 +106,10 @@ const mockUseUsersRelatedHosts = useUserRelatedHosts as jest.Mock;
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'
+);
+
const timestamp = '2022-07-25T08:20:18.966Z';
const defaultProps = {
@@ -155,6 +164,8 @@ describe('', () => {
mockUseRiskScore.mockReturnValue(mockRiskScoreResponse);
mockUseUsersRelatedHosts.mockReturnValue(mockRelatedHostsResponse);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({});
+ (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
});
it('should render user details correctly', () => {
@@ -278,4 +289,31 @@ describe('', () => {
});
});
});
+
+ describe('distribution bar insights', () => {
+ it('should not render if no data is available', () => {
+ const { queryByTestId } = renderUserDetails(mockContextValue);
+ expect(queryByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should render alert count when data is available', () => {
+ (useSummaryChartData as jest.Mock).mockReturnValue({
+ isLoading: false,
+ items: [{ key: 'high', value: 78, label: 'High' }],
+ });
+
+ const { getByTestId } = renderUserDetails(mockContextValue);
+ expect(getByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render misconfiguration when data is available', () => {
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({
+ data: { count: { passed: 1, failed: 2 } },
+ });
+
+ const { getByTestId } = renderUserDetails(mockContextValue);
+ expect(getByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx
index 13d3e825053ba..c90d11f4b8bc2 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx
@@ -18,6 +18,8 @@ import {
EuiFlexItem,
EuiToolTip,
EuiPanel,
+ EuiHorizontalRule,
+ EuiFlexGrid,
} from '@elastic/eui';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -51,6 +53,8 @@ import {
USER_DETAILS_TEST_ID,
USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID,
USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID,
+ USER_DETAILS_MISCONFIGURATIONS_TEST_ID,
+ USER_DETAILS_ALERT_COUNT_TEST_ID,
} from './test_ids';
import {
HOST_NAME_FIELD_NAME,
@@ -63,6 +67,8 @@ import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
import { PreviewLink } from '../../../shared/components/preview_link';
import type { NarrowDateRange } from '../../../../common/components/ml/types';
+import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
+import { AlertCountInsight } from '../../shared/components/alert_count_insight';
const USER_DETAILS_ID = 'entities-users-details';
const RELATED_HOSTS_ID = 'entities-users-related-hosts';
@@ -340,6 +346,22 @@ export const UserDetails: React.FC = ({ userName, timestamp, s
)}
+
+
+
+
+
+
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 b710df84e1a13..6ad90adb28997 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
@@ -6,6 +6,8 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
+import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
+import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { TestProviders } from '../../../../common/mock';
import { HostEntityOverview, HOST_PREVIEW_BANNER } from './host_entity_overview';
import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details';
@@ -16,6 +18,9 @@ import {
ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID,
} from './test_ids';
import { DocumentDetailsContext } from '../../shared/context';
import { mockContextValue } from '../../shared/mocks/mock_context';
@@ -29,6 +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';
const hostName = 'host';
const osFamily = 'Windows';
@@ -46,6 +52,17 @@ const panelContextValue = {
};
jest.mock('@kbn/expandable-flyout');
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
+
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
+});
+
+jest.mock(
+ '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
+);
const mockedTelemetry = createTelemetryServiceMock();
jest.mock('../../../../common/lib/kibana', () => {
@@ -99,6 +116,9 @@ describe('', () => {
beforeAll(() => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({});
+ (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
+ (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
});
describe('license is valid', () => {
@@ -150,6 +170,7 @@ describe('', () => {
);
expect(getByTestId(ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument();
});
+
describe('license is not valid', () => {
it('should render os family and last seen', () => {
mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);
@@ -210,4 +231,48 @@ describe('', () => {
});
});
});
+
+ describe('distribution bar insights', () => {
+ beforeEach(() => {
+ mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);
+ mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
+ });
+
+ it('should not render if no data is available', () => {
+ const { queryByTestId } = renderHostEntityContent();
+ expect(
+ queryByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID)
+ ).not.toBeInTheDocument();
+ expect(queryByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should render alert count when data is available', () => {
+ (useSummaryChartData as jest.Mock).mockReturnValue({
+ isLoading: false,
+ items: [{ key: 'high', value: 78, label: 'High' }],
+ });
+
+ const { getByTestId } = renderHostEntityContent();
+ expect(getByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render misconfiguration when data is available', () => {
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({
+ data: { count: { passed: 1, failed: 2 } },
+ });
+
+ const { getByTestId } = renderHostEntityContent();
+ expect(getByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render vulnerabilities when data is available', () => {
+ (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
+ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
+ });
+
+ const { getByTestId } = renderHostEntityContent();
+ expect(getByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).toBeInTheDocument();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx
index ca6a68eb23be8..90405286b004c 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx
@@ -52,11 +52,17 @@ import {
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID,
} from './test_ids';
import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys';
import { LeftPanelInsightsTab } from '../../left';
import { RiskScoreDocTooltip } from '../../../../overview/components/common';
import { PreviewLink } from '../../../shared/components/preview_link';
+import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
+import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight';
+import { AlertCountInsight } from '../../shared/components/alert_count_insight';
const HOST_ICON = 'storage';
@@ -196,12 +202,12 @@ export const HostEntityOverview: React.FC = ({ hostName
return (
-
+
@@ -270,6 +276,20 @@ export const HostEntityOverview: React.FC = ({ hostName
)}
+
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts
index 40670ddc7110a..e0d8bc6db0f5c 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts
@@ -121,6 +121,10 @@ export const ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID =
`${ENTITIES_USER_OVERVIEW_TEST_ID}LastSeen` as const;
export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID =
`${ENTITIES_USER_OVERVIEW_TEST_ID}RiskLevel` as const;
+export const ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID =
+ `${ENTITIES_USER_OVERVIEW_TEST_ID}AlertCount` as const;
+export const ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID =
+ `${ENTITIES_USER_OVERVIEW_TEST_ID}Misconfigurations` as const;
export const ENTITIES_HOST_OVERVIEW_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}HostOverview` as const;
export const ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID =
@@ -132,6 +136,12 @@ export const ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID =
`${ENTITIES_HOST_OVERVIEW_TEST_ID}LastSeen` as const;
export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID =
`${ENTITIES_HOST_OVERVIEW_TEST_ID}RiskLevel` as const;
+export const ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID =
+ `${ENTITIES_HOST_OVERVIEW_TEST_ID}AlertCount` as const;
+export const ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID =
+ `${ENTITIES_HOST_OVERVIEW_TEST_ID}Misconfigurations` as const;
+export const ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID =
+ `${ENTITIES_HOST_OVERVIEW_TEST_ID}Vulnerabilities` as const;
/* Threat intelligence */
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 000da8946ff61..95c399ca4362e 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
@@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../../common/mock';
+import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { UserEntityOverview, USER_PREVIEW_BANNER } from './user_entity_overview';
import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen';
import {
@@ -15,6 +16,8 @@ import {
ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_USER_OVERVIEW_LOADING_TEST_ID,
+ ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
+ ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID,
} from './test_ids';
import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details';
import { mockContextValue } from '../../shared/mocks/mock_context';
@@ -28,6 +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';
const userName = 'user';
const domain = 'n54bg2lfc7';
@@ -45,6 +49,18 @@ const panelContextValue = {
};
jest.mock('@kbn/expandable-flyout');
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
+
+jest.mock('../../../../common/lib/kibana');
+
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
+});
+
+jest.mock(
+ '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
+);
jest.mock('../../../../common/hooks/use_experimental_features');
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
@@ -85,6 +101,8 @@ describe('', () => {
beforeAll(() => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({});
+ (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
});
describe('license is valid', () => {
@@ -211,4 +229,38 @@ describe('', () => {
});
});
});
+
+ describe('distribution bar insights', () => {
+ beforeEach(() => {
+ mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]);
+ mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
+ });
+
+ it('should not render if no data is available', () => {
+ const { queryByTestId } = renderUserEntityOverview();
+ expect(
+ queryByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID)
+ ).not.toBeInTheDocument();
+ expect(queryByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should render alert count when data is available', () => {
+ (useSummaryChartData as jest.Mock).mockReturnValue({
+ isLoading: false,
+ items: [{ key: 'high', value: 78, label: 'High' }],
+ });
+
+ const { getByTestId } = renderUserEntityOverview();
+ expect(getByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render misconfiguration when data is available', () => {
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({
+ data: { count: { passed: 1, failed: 2 } },
+ });
+
+ const { getByTestId } = renderUserEntityOverview();
+ expect(getByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx
index 624b9e816c9e5..0019228d656cd 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx
@@ -53,10 +53,14 @@ import {
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
ENTITIES_USER_OVERVIEW_LOADING_TEST_ID,
+ ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
+ ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID,
} from './test_ids';
import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details';
import { RiskScoreDocTooltip } from '../../../../overview/components/common';
import { PreviewLink } from '../../../shared/components/preview_link';
+import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
+import { AlertCountInsight } from '../../shared/components/alert_count_insight';
const USER_ICON = 'user';
@@ -196,12 +200,12 @@ export const UserEntityOverview: React.FC = ({ userName
return (
-
+
@@ -270,6 +274,16 @@ export const UserEntityOverview: React.FC = ({ userName
)}
+
+
);
};
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
new file mode 100644
index 0000000000000..f0d16a418f2b2
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { 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';
+
+jest.mock('../../../../common/lib/kibana');
+
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
+});
+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'
+);
+
+const fieldName = 'host.name';
+const name = 'test host';
+const testId = 'test';
+
+const renderAlertCountInsight = () => {
+ return render(
+
+
+
+ );
+};
+
+describe('AlertCountInsight', () => {
+ it('renders', () => {
+ (useSummaryChartData 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' },
+ ],
+ });
+ const { getByTestId } = renderAlertCountInsight();
+ expect(getByTestId(testId)).toBeInTheDocument();
+ expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
+ });
+
+ it('renders loading spinner if data is being fetched', () => {
+ (useSummaryChartData 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: [] });
+ const { container } = renderAlertCountInsight();
+ expect(container).toBeEmptyDOMElement();
+ });
+});
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
new file mode 100644
index 0000000000000..566b77b5739a9
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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, { useMemo } from 'react';
+import { v4 as uuid } from 'uuid';
+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';
+
+const ENTITY_ALERT_COUNT_ID = 'entity-alert-count';
+
+interface AlertCountInsightProps {
+ /**
+ * The name of the entity to filter the alerts by.
+ */
+ name: string;
+ /**
+ * The field name to filter the alerts by.
+ */
+ fieldName: 'host.name' | 'user.name';
+ /**
+ * The direction of the flex group.
+ */
+ direction?: EuiFlexGroupProps['direction'];
+ /**
+ * The data-test-subj to use for the component.
+ */
+ ['data-test-subj']?: string;
+}
+
+/*
+ * Displays a distribution bar with the count of critical alerts for a given entity
+ */
+export const AlertCountInsight: React.FC = ({
+ name,
+ fieldName,
+ direction,
+ 'data-test-subj': dataTestSubj,
+}) => {
+ const uniqueQueryId = useMemo(() => `${ENTITY_ALERT_COUNT_ID}-${uuid()}`, []);
+ const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]);
+
+ const { items, isLoading } = useSummaryChartData({
+ aggregations: severityAggregations,
+ entityFilter,
+ uniqueQueryId,
+ signalIndexName: null,
+ });
+
+ 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,
+ [data]
+ );
+
+ if (!isLoading && items.length === 0) return null;
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+ }
+ stats={alertStats}
+ count={count}
+ direction={direction}
+ data-test-subj={`${dataTestSubj}-distribution-bar`}
+ />
+ )}
+
+ );
+};
+
+AlertCountInsight.displayName = 'AlertCountInsight';
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
new file mode 100644
index 0000000000000..a775da8a7f73a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { InsightDistributionBar } from './insight_distribution_bar';
+import { TestProviders } from '../../../../common/mock';
+
+const title = 'test title';
+const count = 10;
+const testId = 'test-id';
+const stats = [
+ {
+ key: 'passed',
+ count: 90,
+ color: 'green',
+ },
+ {
+ key: 'failed',
+ count: 10,
+ color: 'red',
+ },
+];
+
+describe('', () => {
+ it('should render', () => {
+ const { getByTestId, getByText } = render(
+
+
+
+ );
+ expect(getByTestId(testId)).toBeInTheDocument();
+ expect(getByText(title)).toBeInTheDocument();
+ expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`);
+ 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
new file mode 100644
index 0000000000000..006ec8c5dad4f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 { css } from '@emotion/css';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiBadge,
+ useEuiTheme,
+ useEuiFontSize,
+ type EuiFlexGroupProps,
+} from '@elastic/eui';
+import { DistributionBar } from '@kbn/security-solution-distribution-bar';
+import { FormattedCount } from '../../../../common/components/formatted_number';
+
+export interface InsightDistributionBarProps {
+ /**
+ * Title of the insight
+ */
+ title: string | React.ReactNode;
+ /**
+ * Distribution stats to display
+ */
+ stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>;
+ /**
+ * Count to be displayed in the badge
+ */
+ count: number;
+ /**
+ * Flex direction of the component
+ */
+ direction?: EuiFlexGroupProps['direction'];
+ /**
+ * Optional test id
+ */
+ ['data-test-subj']?: string;
+}
+
+// Displays a distribution bar with a count badge
+export const InsightDistributionBar: React.FC = ({
+ title,
+ stats,
+ count,
+ direction = 'row',
+ 'data-test-subj': dataTestSubj,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const xsFontSize = useEuiFontSize('xs').fontSize;
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+InsightDistributionBar.displayName = 'InsightDistributionBar';
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
new file mode 100644
index 0000000000000..296a61f444a17
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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 { TestProviders } from '../../../../common/mock';
+import { MisconfigurationsInsight } from './misconfiguration_insight';
+import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
+
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
+
+const fieldName = 'host.name';
+const name = 'test host';
+const testId = 'test';
+
+const renderMisconfigurationsInsight = () => {
+ return render(
+
+
+
+ );
+};
+
+describe('MisconfigurationsInsight', () => {
+ it('renders', () => {
+ (useMisconfigurationPreview as jest.Mock).mockReturnValue({
+ data: { count: { passed: 1, failed: 2 } },
+ });
+ const { getByTestId } = renderMisconfigurationsInsight();
+ 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();
+ expect(container).toBeEmptyDOMElement();
+ });
+});
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
new file mode 100644
index 0000000000000..552a242c84893
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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, { useMemo } from 'react';
+import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
+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 { InsightDistributionBar } from './insight_distribution_bar';
+import { getFindingsStats } from '../../../../cloud_security_posture/components/misconfiguration/misconfiguration_preview';
+
+interface MisconfigurationsInsightProps {
+ /**
+ * Entity name to retrieve misconfigurations for
+ */
+ name: string;
+ /**
+ * Indicator whether the entity is host or user
+ */
+ fieldName: 'host.name' | 'user.name';
+ /**
+ * The direction of the flex group
+ */
+ direction?: EuiFlexGroupProps['direction'];
+ /**
+ * The data-test-subj to use for the component
+ */
+ ['data-test-subj']?: string;
+}
+
+/*
+ * Displays a distribution bar with the count of failed misconfigurations for a given entity
+ */
+export const MisconfigurationsInsight: React.FC = ({
+ name,
+ fieldName,
+ direction,
+ 'data-test-subj': dataTestSubj,
+}) => {
+ const { data } = useMisconfigurationPreview({
+ query: buildEntityFlyoutPreviewQuery(fieldName, name),
+ sort: [],
+ enabled: true,
+ pageSize: 1,
+ });
+
+ const passedFindings = data?.count.passed || 0;
+ const failedFindings = data?.count.failed || 0;
+ const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
+
+ const misconfigurationsStats = useMemo(
+ () => getFindingsStats(passedFindings, failedFindings),
+ [passedFindings, failedFindings]
+ );
+
+ if (!hasMisconfigurationFindings) return null;
+
+ return (
+
+
+ }
+ stats={misconfigurationsStats}
+ count={failedFindings}
+ direction={direction}
+ data-test-subj={`${dataTestSubj}-distribution-bar`}
+ />
+
+ );
+};
+
+MisconfigurationsInsight.displayName = 'MisconfigurationsInsight';
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts
index 8561df63d7199..7c2ce2ff5870b 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts
@@ -12,3 +12,6 @@ export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const;
export const SESSION_VIEW_UPSELL_TEST_ID = `${PREFIX}SessionViewUpsell` as const;
export const SESSION_VIEW_NO_DATA_TEST_ID = `${PREFIX}SessionViewNoData` as const;
+
+export const MISCONFIGURATIONS_TEST_ID = `${PREFIX}Misconfigurations` as const;
+export const VULNERABILITIES_TEST_ID = `${PREFIX}Vulnerabilities` as const;
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
new file mode 100644
index 0000000000000..77c6737266b89
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { TestProviders } from '../../../../common/mock';
+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';
+
+jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
+
+const hostName = 'test host';
+const testId = 'test';
+
+const renderVulnerabilitiesInsight = () => {
+ return render(
+
+
+
+ );
+};
+
+describe('VulnerabilitiesInsight', () => {
+ it('renders', () => {
+ (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
+ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
+ });
+
+ const { getByTestId } = renderVulnerabilitiesInsight();
+ expect(getByTestId(testId)).toBeInTheDocument();
+ expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
+ });
+
+ it('renders null when data is not available', () => {
+ (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
+
+ const { container } = renderVulnerabilitiesInsight();
+ expect(container).toBeEmptyDOMElement();
+ });
+});
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
new file mode 100644
index 0000000000000..4c581b6db57d0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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, { useMemo } from 'react';
+import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+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';
+
+interface VulnerabilitiesInsightProps {
+ /**
+ * Host name to retrieve vulnerabilities for
+ */
+ hostName: string;
+ /**
+ * The direction of the flex group
+ */
+ direction?: EuiFlexGroupProps['direction'];
+ /**
+ * The data-test-subj to use for the component
+ */
+ ['data-test-subj']?: string;
+}
+
+/*
+ * Displays a distribution bar with the count of critical vulnerabilities for a given host
+ */
+export const VulnerabilitiesInsight: React.FC = ({
+ hostName,
+ direction,
+ 'data-test-subj': dataTestSubj,
+}) => {
+ const { data } = useVulnerabilitiesPreview({
+ query: buildEntityFlyoutPreviewQuery('host.name', hostName),
+ sort: [],
+ enabled: true,
+ pageSize: 1,
+ });
+
+ const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {};
+ const hasVulnerabilitiesFindings = useMemo(
+ () =>
+ hasVulnerabilitiesData({
+ critical: CRITICAL,
+ high: HIGH,
+ medium: MEDIUM,
+ low: LOW,
+ none: NONE,
+ }),
+ [CRITICAL, HIGH, MEDIUM, LOW, NONE]
+ );
+
+ const vulnerabilitiesStats = useMemo(
+ () =>
+ getVulnerabilityStats({
+ critical: CRITICAL,
+ high: HIGH,
+ medium: MEDIUM,
+ low: LOW,
+ none: NONE,
+ }),
+ [CRITICAL, HIGH, MEDIUM, LOW, NONE]
+ );
+
+ if (!hasVulnerabilitiesFindings) return null;
+
+ return (
+
+
+ }
+ stats={vulnerabilitiesStats}
+ count={CRITICAL}
+ direction={direction}
+ data-test-subj={`${dataTestSubj}-distribution-bar`}
+ />
+
+ );
+};
+
+VulnerabilitiesInsight.displayName = 'VulnerabilitiesInsight';