.euiPanel {
- padding-bottom: 0;
- }
- `}
- >
+
{!isEntityInStore && (
@@ -148,6 +147,6 @@ export const HostPanelHeader = ({
)}
-
+
);
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host.ts
similarity index 78%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts
rename to x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host.ts
index 93137ae727cd1..710baf6ee38a2 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host.ts
@@ -7,24 +7,24 @@
import { useMemo } from 'react';
import deepmerge from 'deepmerge';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-import type { inputsModel } from '../../../../common/store';
-import { inputsSelectors } from '../../../../common/store';
-import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details';
-import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen';
-import { useGlobalTime } from '../../../../common/containers/use_global_time';
-import type { HostItem } from '../../../../../common/search_strategy';
-import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../common/search_strategy';
+import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
+import type { inputsModel } from '../../../../../common/store';
+import { inputsSelectors } from '../../../../../common/store';
+import { useHostDetails } from '../../../../../explore/hosts/containers/hosts/details';
+import { useFirstLastSeen } from '../../../../../common/containers/use_first_last_seen';
+import { useGlobalTime } from '../../../../../common/containers/use_global_time';
+import type { HostItem } from '../../../../../../common/search_strategy';
+import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../../common/search_strategy';
import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } from '../constants';
-import { useQueryInspector } from '../../../../common/components/page/manage_query';
-import type { InspectResponse } from '../../../../types';
+import { useQueryInspector } from '../../../../../common/components/page/manage_query';
+import type { InspectResponse } from '../../../../../types';
import type {
EntityFromStoreResult,
EntityStoreRecord,
-} from '../../shared/hooks/use_entity_from_store';
-import type { ObservedEntityData } from '../../shared/components/observed_entity/types';
-import { isActiveTimeline } from '../../../../helpers';
-import { useSecurityDefaultPatterns } from '../../../../data_view_manager/hooks/use_security_default_patterns';
+} from '../../../../../flyout/entity_details/shared/hooks/use_entity_from_store';
+import type { ObservedEntityData } from '../../../../../flyout/entity_details/shared/components/observed_entity/types';
+import { isActiveTimeline } from '../../../../../helpers';
+import { useSecurityDefaultPatterns } from '../../../../../data_view_manager/hooks/use_security_default_patterns';
export type ObservedHostResult = Omit, 'anomalies'> & {
entityRecord?: EntityStoreRecord | null;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.test.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host_fields.test.ts
similarity index 96%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.test.ts
rename to x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host_fields.test.ts
index 11cf670ba24a0..14dc3e5aa1d77 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.test.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host_fields.test.ts
@@ -7,8 +7,8 @@
import { renderHook } from '@testing-library/react';
import { useObservedHostFields } from './use_observed_host_fields';
-import { mockObservedHostData } from '../../mocks';
-import { TestProviders } from '../../../../common/mock';
+import { mockObservedHostData } from '../../../../../flyout/entity_details/mocks';
+import { TestProviders } from '../../../../../common/mock';
describe('useObservedHostFields', () => {
it('returns managed host items for Entra host', () => {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host_fields.ts
similarity index 64%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.ts
rename to x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host_fields.ts
index 255bb54c2c58a..2861abbb1def7 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/hooks/use_observed_host_fields.ts
@@ -6,11 +6,11 @@
*/
import { useMemo } from 'react';
-import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
-import type { HostItem } from '../../../../../common/search_strategy';
-import { getAnomaliesFields } from '../../shared/common';
-import type { EntityTableRows } from '../../shared/components/entity_table/types';
-import type { ObservedEntityData } from '../../shared/components/observed_entity/types';
+import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities';
+import type { HostItem } from '../../../../../../common/search_strategy';
+import { getAnomaliesFields } from '../../../../../flyout/entity_details/shared/common';
+import type { EntityTableRows } from '../../../../../flyout/entity_details/shared/components/entity_table/types';
+import type { ObservedEntityData } from '../../../../../flyout/entity_details/shared/components/observed_entity/types';
import { policyFields } from '../fields/endpoint_policy_fields';
import { basicHostFields } from '../fields/basic_host_fields';
import { cloudFields } from '../fields/cloud_fields';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/index.test.tsx
new file mode 100644
index 0000000000000..1b05275990980
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/index.test.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import React from 'react';
+import { TestProviders } from '../../../../common/mock';
+import {
+ mockHostRiskScoreState,
+ mockObservedHostData,
+} from '../../../../flyout/entity_details/mocks';
+import { Host } from '.';
+
+const mockProps = {
+ hostName: 'test',
+ scopeId: 'test-scope-id',
+ contextID: 'test-host-panel',
+};
+
+jest.mock('../../../../common/components/visualization_actions/visualization_embeddable');
+
+const mockedHostRiskScore = jest.fn().mockReturnValue(mockHostRiskScoreState);
+jest.mock('../../../../entity_analytics/api/hooks/use_risk_score', () => ({
+ useRiskScore: () => mockedHostRiskScore(),
+}));
+
+const mockedUseObservedHost = jest.fn().mockReturnValue(mockObservedHostData);
+jest.mock('./hooks/use_observed_host', () => ({
+ useObservedHost: () => mockedUseObservedHost(),
+}));
+
+describe('', () => {
+ beforeEach(() => {
+ mockedHostRiskScore.mockReturnValue(mockHostRiskScoreState);
+ mockedUseObservedHost.mockReturnValue(mockObservedHostData);
+ });
+
+ it('renders header, content, and footer', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('host-panel-header')).toBeInTheDocument();
+ expect(getByTestId('observedEntity-accordion')).toBeInTheDocument();
+ });
+
+ it('does not render an expand-details navigation button (no v2 left panel yet)', () => {
+ const { queryByTestId } = render(
+
+
+
+ );
+
+ expect(
+ queryByTestId('securitySolutionFlyoutNavigationExpandDetailButton')
+ ).not.toBeInTheDocument();
+ });
+
+ it('does not render a preview footer', () => {
+ const { queryByTestId } = render(
+
+
+
+ );
+
+ expect(queryByTestId('host-preview-footer')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/index.tsx
new file mode 100644
index 0000000000000..c8bfc32a88677
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/main/index.tsx
@@ -0,0 +1,398 @@
+/*
+ * 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 type { FC } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
+import { noop } from 'lodash/fp';
+import { useHistory } from 'react-router-dom';
+import { useStore } from 'react-redux';
+import { EuiFlyoutHeader, EuiFlyoutBody, EuiSpacer } from '@elastic/eui';
+import type { DataTableRecord } from '@kbn/discover-utils';
+import { FF_ENABLE_ENTITY_STORE_V2, useEntityStoreEuidApi } from '@kbn/entity-store/public';
+import { DOC_VIEWER_FLYOUT_HISTORY_KEY } from '@kbn/unified-doc-viewer';
+import { useUpdateAssetCriticality } from '../../../../entity_analytics/api/hooks/use_update_asset_criticality';
+import { useRefetchQueryById } from '../../../../entity_analytics/api/hooks/use_refetch_query_by_id';
+import { RISK_INPUTS_TAB_QUERY_ID } from '../../../../entity_analytics/components/flyout_v2/constants';
+import type { Refetch } from '../../../../common/types';
+import { useCalculateEntityRiskScore } from '../../../../entity_analytics/api/hooks/use_calculate_entity_risk_score';
+import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
+import { useQueryInspector } from '../../../../common/components/page/manage_query';
+import { useGlobalTime } from '../../../../common/containers/use_global_time';
+import { buildHostNamesFilter, type RiskSeverity } from '../../../../../common/search_strategy';
+import { useUiSetting, useKibana } from '../../../../common/lib/kibana';
+import { useIsInSecurityApp } from '../../../../common/hooks/is_in_security_app';
+import type { EntityDetailsPath } from '../../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
+import {
+ CspInsightLeftPanelSubTab,
+ EntityDetailsLeftPanelTab,
+} from '../../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
+import { flyoutProviders } from '../../../shared/components/flyout_provider';
+import {
+ defaultToolsFlyoutProperties,
+ useDefaultDocumentFlyoutProperties,
+} from '../../../shared/hooks/use_default_flyout_properties';
+import { documentFlyoutHistoryKey } from '../../../shared/constants/flyout_history';
+import { RiskInputs } from '../tools/risk_inputs';
+import { MisconfigurationInsights } from '../tools/misconfiguration_insights';
+import { VulnerabilityInsights } from '../tools/vulnerability_insights';
+import { AlertsInsights } from '../tools/alerts_insights';
+import { Header } from './header';
+import { Content } from './content';
+import { Footer } from './footer';
+import { useObservedHost } from './hooks/use_observed_host';
+import { EntityType } from '../../../../../common/entity_analytics/types';
+import {
+ buildRiskScoreStateFromEntityRecord,
+ getRiskFromEntityRecord,
+} from '../../../../flyout/entity_details/shared/entity_store_risk_utils';
+import {
+ useEntityFromStore,
+ type EntityStoreRecord,
+} from '../../../../flyout/entity_details/shared/hooks/use_entity_from_store';
+import type { CriticalityLevelWithUnassigned } from '../../../../../common/entity_analytics/asset_criticality/types';
+import { ENABLE_ASSET_INVENTORY_SETTING } from '../../../../../common/constants';
+import {
+ mergeLegacyIdentityWhenStoreEntityMissing,
+ type IdentityFields,
+} from '../../../../flyout/document_details/shared/utils';
+import { HOST_PANEL_RISK_SCORE_QUERY_ID } from './constants';
+import {
+ useEntityPanelTabs,
+ TABLE_TAB_ID,
+} from '../../../../flyout/entity_details/shared/hooks/use_entity_panel_tabs';
+import { EntityPanelHeaderTabs } from '../../../../flyout/entity_details/shared/components/entity_panel_tabs';
+import { EntityStoreTableTab } from '../../../../flyout/entity_details/shared/components/entity_store_table_tab';
+import { EntitySummaryGrid } from '../../../../flyout/entity_details/shared/components/entity_summary_grid';
+
+export interface HostProps {
+ /**
+ * Display name from the source row / document (typically `host.name`).
+ */
+ hostName: string;
+ /**
+ * The source document record. When provided, entityId is computed from the document's
+ * host identity fields using the EUID API. Falls back to the `entityId` prop if the
+ * EUID API returns no value.
+ */
+ hit?: DataTableRecord;
+ /**
+ * Canonical Entity Store v2 id (`entity.id`) when already resolved (e.g. from alerts/events table).
+ * Used directly when `hit` is not provided, or as a fallback when EUID resolution from `hit` yields no value.
+ */
+ entityId?: string;
+ /**
+ * Scope id (timeline id, table id, etc.) — used for downstream containers and queries.
+ */
+ scopeId?: string;
+ /**
+ * Stable identifier for the host panel context (defaults to `scopeId` or a static fallback).
+ */
+ contextID?: string;
+}
+
+const FIRST_RECORD_PAGINATION = {
+ cursorStart: 0,
+ querySize: 1,
+};
+
+/**
+ * Standalone host details flyout content (for use with `overlays.openSystemFlyout`).
+ *
+ * Runs the same data hooks as the v1 `HostPanel`, but without the expandable-flyout
+ * navigation or preview-mode handling. Detail panels (risk inputs, graph view, etc.)
+ * open as separate system flyouts via `overlays.openSystemFlyout`.
+ */
+export const Host: FC = memo(function Host({
+ hostName,
+ hit,
+ entityId: entityIdProp,
+ scopeId = '',
+ contextID,
+}) {
+ const { services } = useKibana();
+ const { uiSettings, overlays } = services;
+ const store = useStore();
+ const history = useHistory();
+ const euidApi = useEntityStoreEuidApi();
+
+ // Compute entityId from hit when provided, otherwise use the prop
+ const entityId = useMemo(
+ () => (hit ? euidApi?.euid?.getEuidFromObject('host', hit.flattened) : entityIdProp),
+ [hit, euidApi, entityIdProp]
+ );
+ const assetInventoryEnabled = uiSettings.get(ENABLE_ASSET_INVENTORY_SETTING, true);
+ const entityStoreV2Enabled = useUiSetting(FF_ENABLE_ENTITY_STORE_V2, false);
+ const isInSecurityApp = useIsInSecurityApp();
+ const historyKey = isInSecurityApp ? documentFlyoutHistoryKey : DOC_VIEWER_FLYOUT_HISTORY_KEY;
+ const defaultDocumentFlyoutProperties = useDefaultDocumentFlyoutProperties();
+
+ const safeContextID = contextID ?? scopeId ?? 'host-panel';
+ const { setQuery, deleteQuery, isInitializing } = useGlobalTime();
+
+ const hostStoreIdentityFields = useMemo(
+ () => (!entityId && hostName ? { 'host.name': hostName } : undefined),
+ [entityId, hostName]
+ );
+
+ const entityFromStoreResult = useEntityFromStore({
+ entityId,
+ identityFields: hostStoreIdentityFields,
+ entityType: 'host',
+ skip: !entityStoreV2Enabled || isInitializing,
+ });
+
+ const documentEntityIdentifiers = useMemo(() => {
+ const legacyFields =
+ hostName != null && hostName !== '' ? { 'host.name': hostName } : ({} as IdentityFields);
+ if (entityStoreV2Enabled) {
+ const fromStore =
+ euidApi?.euid?.getEntityIdentifiersFromDocument(
+ 'host',
+ entityFromStoreResult.entityRecord
+ ) ?? {};
+ return mergeLegacyIdentityWhenStoreEntityMissing(fromStore, legacyFields);
+ }
+ return legacyFields;
+ }, [entityStoreV2Enabled, euidApi?.euid, entityFromStoreResult.entityRecord, hostName]);
+
+ const hostNameFilterQuery = useMemo(
+ () => (hostName ? buildHostNamesFilter([hostName]) : undefined),
+ [hostName]
+ );
+
+ const riskScoreState = useRiskScore({
+ riskEntity: EntityType.host,
+ filterQuery: hostNameFilterQuery,
+ onlyLatest: false,
+ pagination: FIRST_RECORD_PAGINATION,
+ skip: entityStoreV2Enabled,
+ });
+
+ const { inspect: inspectRiskScore, refetch, loading } = riskScoreState;
+
+ const refetchRiskInputsTab = useRefetchQueryById(RISK_INPUTS_TAB_QUERY_ID);
+ const refetchRiskScore = useCallback(() => {
+ refetch();
+ (refetchRiskInputsTab as Refetch | null)?.();
+ }, [refetch, refetchRiskInputsTab]);
+
+ const { isLoading: recalculatingScore, calculateEntityRiskScore } = useCalculateEntityRiskScore(
+ EntityType.host,
+ hostName,
+ { onSuccess: refetchRiskScore }
+ );
+
+ const { updateAssetCriticalityLevel } = useUpdateAssetCriticality('host', {
+ onSuccess: calculateEntityRiskScore,
+ });
+
+ const observedHost = useObservedHost(
+ hostName,
+ scopeId,
+ entityStoreV2Enabled ? entityFromStoreResult : undefined
+ );
+
+ const panelDisplayEntityId = useMemo(
+ () => (entityStoreV2Enabled ? observedHost.entityRecord?.entity?.id : entityId),
+ [entityId, entityStoreV2Enabled, observedHost.entityRecord?.entity?.id]
+ );
+
+ const useEntityStoreInspectForRisk = entityStoreV2Enabled && observedHost.entityRecord != null;
+
+ useQueryInspector({
+ deleteQuery,
+ inspect: useEntityStoreInspectForRisk
+ ? entityFromStoreResult?.inspect ?? null
+ : inspectRiskScore,
+ loading: useEntityStoreInspectForRisk ? entityFromStoreResult?.isLoading ?? false : loading,
+ queryId: HOST_PANEL_RISK_SCORE_QUERY_ID,
+ refetch: useEntityStoreInspectForRisk ? entityFromStoreResult?.refetch ?? noop : refetch,
+ setQuery,
+ });
+
+ const entityFromStore: EntityStoreRecord | undefined = entityStoreV2Enabled
+ ? observedHost.entityRecord ?? undefined
+ : undefined;
+ const riskScoreStateFromStore =
+ entityStoreV2Enabled && observedHost.entityRecord
+ ? buildRiskScoreStateFromEntityRecord(EntityType.host, observedHost.entityRecord, {
+ refetch: observedHost.refetchEntityStore ?? noop,
+ isLoading: observedHost.isLoading,
+ error: null,
+ inspect: entityFromStoreResult?.inspect,
+ })
+ : null;
+
+ const effectiveRiskScoreState = riskScoreStateFromStore ?? riskScoreState;
+
+ const onCriticalitySave =
+ entityFromStoreResult.entityRecord && observedHost.entityRecord
+ ? (level: CriticalityLevelWithUnassigned) =>
+ updateAssetCriticalityLevel(level, observedHost.entityRecord)
+ : undefined;
+
+ const entityStoreEntityId = entityStoreV2Enabled
+ ? observedHost.entityRecord?.entity?.id
+ : undefined;
+
+ const noEntityInStore =
+ entityStoreV2Enabled && !entityFromStoreResult.isLoading && !observedHost.entityRecord;
+
+ const { tabs, selectedTabId, setSelectedTabId } = useEntityPanelTabs({
+ entityRecord: observedHost.entityRecord ?? null,
+ });
+
+ const tabsNode = tabs ? (
+
+ ) : undefined;
+
+ const onOpenHost = useCallback(() => {
+ overlays.openSystemFlyout(
+ flyoutProviders({
+ services,
+ store,
+ history,
+ children: ,
+ }),
+ { ...defaultDocumentFlyoutProperties, title: hostName, historyKey, session: 'inherit' }
+ );
+ }, [
+ overlays,
+ services,
+ store,
+ history,
+ historyKey,
+ hostName,
+ entityId,
+ scopeId,
+ defaultDocumentFlyoutProperties,
+ ]);
+
+ const openDetailsPanel = useCallback(
+ (path: EntityDetailsPath) => {
+ const common = {
+ ...defaultToolsFlyoutProperties,
+ title: hostName,
+ historyKey,
+ session: 'start' as const,
+ };
+ const wrap = (children: React.ReactNode) =>
+ overlays.openSystemFlyout(flyoutProviders({ services, store, history, children }), common);
+
+ switch (path.tab) {
+ case EntityDetailsLeftPanelTab.RISK_INPUTS:
+ return wrap(
+
+ );
+ case EntityDetailsLeftPanelTab.CSP_INSIGHTS:
+ switch (path.subTab) {
+ case CspInsightLeftPanelSubTab.VULNERABILITIES:
+ return wrap(
+
+ );
+ case CspInsightLeftPanelSubTab.ALERTS:
+ return wrap(
+
+ );
+ case CspInsightLeftPanelSubTab.MISCONFIGURATIONS:
+ return wrap(
+
+ );
+ }
+ }
+ },
+ [
+ overlays,
+ services,
+ store,
+ history,
+ historyKey,
+ hostName,
+ panelDisplayEntityId,
+ scopeId,
+ entityStoreEntityId,
+ onOpenHost,
+ ]
+ );
+
+ const riskLevel = observedHost.entityRecord
+ ? ((getRiskFromEntityRecord(observedHost.entityRecord)?.calculated_level ??
+ 'Unknown') as RiskSeverity)
+ : undefined;
+
+ return (
+ <>
+
+
+
+
+ {observedHost.entityRecord && (
+
+ )}
+ {tabsNode}
+ {tabs && }
+ {tabs && selectedTabId === TABLE_TAB_ID && observedHost.entityRecord ? (
+
+ ) : (
+
+ )}
+
+ {assetInventoryEnabled && (
+
+ )}
+ >
+ );
+});
+
+Host.displayName = 'Host';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/index.test.tsx
new file mode 100644
index 0000000000000..6e9a543061940
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/index.test.tsx
@@ -0,0 +1,144 @@
+/*
+ * 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 { AlertsInsights } from '.';
+
+const mockOpenSystemFlyout = jest.fn();
+
+jest.mock('../../../../shared/components/tools_flyout_header', () => ({
+ ToolsFlyoutHeader: ({
+ title,
+ label,
+ iconType,
+ onTitleClick,
+ }: {
+ title: string;
+ label?: string;
+ iconType?: string;
+ onTitleClick?: () => void;
+ }) => (
+
+ ),
+}));
+
+jest.mock(
+ '../../../../../cloud_security_posture/components/flyout_v2/csp_details/alerts_findings_details_table',
+ () => ({
+ AlertsDetailsTable: ({
+ field,
+ value,
+ entityId,
+ entityType,
+ onShowAlert,
+ }: {
+ field: string;
+ value: string;
+ entityId?: string;
+ entityType?: string;
+ onShowAlert?: (eventId: string, indexName: string) => void;
+ }) => (
+
+ ),
+ })
+);
+
+jest.mock('../../../../shared/components/flyout_provider', () => ({
+ flyoutProviders: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+jest.mock('../../../../document/main/document_flyout_wrapper', () => ({
+ DocumentFlyoutWrapper: () => ,
+}));
+
+jest.mock('../../../../shared/components/cell_actions', () => ({
+ cellActionRenderer: jest.fn(),
+}));
+
+jest.mock('../../../../shared/hooks/use_default_flyout_properties', () => ({
+ useDefaultDocumentFlyoutProperties: () => ({ size: 'm' }),
+}));
+
+jest.mock('../../../../../common/hooks/is_in_security_app', () => ({
+ useIsInSecurityApp: () => true,
+}));
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useStore: () => ({}),
+}));
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({ push: jest.fn() }),
+}));
+
+jest.mock('../../../../../common/lib/kibana', () => ({
+ useKibana: () => ({
+ services: {
+ overlays: { openSystemFlyout: mockOpenSystemFlyout },
+ },
+ }),
+}));
+
+describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the header with the title, host label and entity icon', () => {
+ const { getByTestId } = render();
+ const header = getByTestId('mockToolsFlyoutHeader');
+ expect(header).toHaveAttribute('data-title', 'Alerts');
+ expect(header).toHaveAttribute('data-label', 'my-host');
+ expect(header).toHaveAttribute('data-icon-type', 'storage');
+ });
+
+ it('forwards the host name and entity id to the alerts table', () => {
+ const { getByTestId } = render();
+ const table = getByTestId('mockAlertsDetailsTable');
+ expect(table).toHaveAttribute('data-field', 'host.name');
+ expect(table).toHaveAttribute('data-value', 'my-host');
+ expect(table).toHaveAttribute('data-entity-id', 'euid-123');
+ expect(table).toHaveAttribute('data-entity-type', 'host');
+ });
+
+ it('forwards onOpenHost to the header click handler', () => {
+ const onOpenHost = jest.fn();
+ const { getByTestId } = render();
+ getByTestId('mockToolsFlyoutHeader').click();
+ expect(onOpenHost).toHaveBeenCalledTimes(1);
+ });
+
+ it('opens a child system flyout when an alert row is expanded', () => {
+ const { getByTestId } = render();
+ getByTestId('mockAlertsDetailsTable').click();
+ expect(mockOpenSystemFlyout).toHaveBeenCalledTimes(1);
+ expect(mockOpenSystemFlyout).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ session: 'inherit' })
+ );
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/index.tsx
new file mode 100644
index 0000000000000..105fab1e821e9
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/index.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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, { memo, useCallback } from 'react';
+import { EuiFlyoutHeader } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useHistory } from 'react-router-dom';
+import { useStore } from 'react-redux';
+import { noop } from 'lodash/fp';
+import { DOC_VIEWER_FLYOUT_HISTORY_KEY } from '@kbn/unified-doc-viewer';
+import {
+ EntityIdentifierFields,
+ EntityType,
+} from '../../../../../../common/entity_analytics/types';
+import { ToolsFlyoutHeader } from '../../../../shared/components/tools_flyout_header';
+import { AlertsDetailsTable } from '../../../../../cloud_security_posture/components/flyout_v2/csp_details/alerts_findings_details_table';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { flyoutProviders } from '../../../../shared/components/flyout_provider';
+import { useDefaultDocumentFlyoutProperties } from '../../../../shared/hooks/use_default_flyout_properties';
+import { DocumentFlyoutWrapper } from '../../../../document/main/document_flyout_wrapper';
+import { cellActionRenderer } from '../../../../shared/components/cell_actions';
+import { useIsInSecurityApp } from '../../../../../common/hooks/is_in_security_app';
+import { documentFlyoutHistoryKey } from '../../../../shared/constants/flyout_history';
+
+const TITLE = i18n.translate(
+ 'xpack.securitySolution.flyout.entityDetails.host.alertsInsights.title',
+ { defaultMessage: 'Alerts' }
+);
+
+export interface AlertsInsightsProps {
+ /** The host name used to query alerts (`host.name` field value). */
+ value: string;
+ /** Canonical Entity Store v2 id (`entity.id`) when already resolved. */
+ entityId?: string;
+ /** Opens the originating host flyout as a child. */
+ onOpenHost?: () => void;
+}
+
+/**
+ * Tool flyout displaying alert findings for a host entity.
+ */
+export const AlertsInsights = memo(({ value, entityId, onOpenHost }: AlertsInsightsProps) => {
+ const { services } = useKibana();
+ const store = useStore();
+ const history = useHistory();
+ const defaultFlyoutProperties = useDefaultDocumentFlyoutProperties();
+ const isInSecurityApp = useIsInSecurityApp();
+ const historyKey = isInSecurityApp ? documentFlyoutHistoryKey : DOC_VIEWER_FLYOUT_HISTORY_KEY;
+
+ const onShowAlert = useCallback(
+ (eventId: string, indexName: string) => {
+ services.overlays.openSystemFlyout(
+ flyoutProviders({
+ services,
+ store,
+ history,
+ children: (
+
+ ),
+ }),
+ {
+ ...defaultFlyoutProperties,
+ historyKey,
+ session: 'inherit',
+ }
+ );
+ },
+ [services, store, history, defaultFlyoutProperties, historyKey]
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+});
+
+AlertsInsights.displayName = 'AlertsInsights';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/test_ids.ts
new file mode 100644
index 0000000000000..c10dc22fc2325
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/alerts_insights/test_ids.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export const ALERTS_INSIGHTS_TOOL_TEST_ID = 'alertsInsightsTool';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/index.test.tsx
new file mode 100644
index 0000000000000..926f48ddf21e9
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/index.test.tsx
@@ -0,0 +1,144 @@
+/*
+ * 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 { MisconfigurationInsights } from '.';
+
+const mockOpenSystemFlyout = jest.fn();
+
+jest.mock('../../../../shared/components/tools_flyout_header', () => ({
+ ToolsFlyoutHeader: ({
+ title,
+ label,
+ iconType,
+ onTitleClick,
+ }: {
+ title: string;
+ label?: string;
+ iconType?: string;
+ onTitleClick?: () => void;
+ }) => (
+
+ ),
+}));
+
+jest.mock(
+ '../../../../../cloud_security_posture/components/flyout_v2/csp_details/misconfiguration_findings_details_table',
+ () => ({
+ MisconfigurationFindingsDetailsTable: ({
+ field,
+ value,
+ scopeId,
+ entityId,
+ entityType,
+ onShowFinding,
+ }: {
+ field: string;
+ value: string;
+ scopeId: string;
+ entityId?: string;
+ entityType?: string;
+ onShowFinding?: (resourceId: string, ruleId: string) => void;
+ }) => (
+
+ ),
+ })
+);
+
+jest.mock('../../../../csp_details/misconfiguration_panel', () => ({
+ MisconfigurationPanel: () => ,
+}));
+
+jest.mock('../../../../shared/components/flyout_provider', () => ({
+ flyoutProviders: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+jest.mock('../../../../shared/hooks/use_default_flyout_properties', () => ({
+ useDefaultDocumentFlyoutProperties: () => ({ size: 'm' }),
+}));
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useStore: () => ({}),
+}));
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({ push: jest.fn() }),
+}));
+
+jest.mock('../../../../../common/lib/kibana', () => ({
+ useKibana: () => ({
+ services: {
+ overlays: { openSystemFlyout: mockOpenSystemFlyout },
+ },
+ }),
+}));
+
+describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the header with the title, host label and entity icon', () => {
+ const { getByTestId } = render();
+ const header = getByTestId('mockToolsFlyoutHeader');
+ expect(header).toHaveAttribute('data-title', 'Misconfigurations');
+ expect(header).toHaveAttribute('data-label', 'my-host');
+ expect(header).toHaveAttribute('data-icon-type', 'storage');
+ });
+
+ it('forwards the host name, scope and entity id to the findings table', () => {
+ const { getByTestId } = render(
+
+ );
+ const table = getByTestId('mockMisconfigurationFindingsDetailsTable');
+ expect(table).toHaveAttribute('data-field', 'host.name');
+ expect(table).toHaveAttribute('data-value', 'my-host');
+ expect(table).toHaveAttribute('data-scope-id', 'my-scope');
+ expect(table).toHaveAttribute('data-entity-id', 'euid-123');
+ expect(table).toHaveAttribute('data-entity-type', 'host');
+ });
+
+ it('forwards onOpenHost to the header click handler', () => {
+ const onOpenHost = jest.fn();
+ const { getByTestId } = render(
+
+ );
+ getByTestId('mockToolsFlyoutHeader').click();
+ expect(onOpenHost).toHaveBeenCalledTimes(1);
+ });
+
+ it('opens a child system flyout when a finding row is expanded', () => {
+ const { getByTestId } = render();
+ getByTestId('mockMisconfigurationFindingsDetailsTable').click();
+ expect(mockOpenSystemFlyout).toHaveBeenCalledTimes(1);
+ expect(mockOpenSystemFlyout).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ session: 'inherit', title: 'my-host' })
+ );
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/index.tsx
new file mode 100644
index 0000000000000..cff33b09b4f14
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/index.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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, { memo, useCallback } from 'react';
+import { EuiFlyoutHeader } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useHistory } from 'react-router-dom';
+import { useStore } from 'react-redux';
+import {
+ EntityIdentifierFields,
+ EntityType,
+} from '../../../../../../common/entity_analytics/types';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { flyoutProviders } from '../../../../shared/components/flyout_provider';
+import { useDefaultDocumentFlyoutProperties } from '../../../../shared/hooks/use_default_flyout_properties';
+import { MisconfigurationPanel } from '../../../../csp_details/misconfiguration_panel';
+import { ToolsFlyoutHeader } from '../../../../shared/components/tools_flyout_header';
+import { MisconfigurationFindingsDetailsTable } from '../../../../../cloud_security_posture/components/flyout_v2/csp_details/misconfiguration_findings_details_table';
+
+const TITLE = i18n.translate(
+ 'xpack.securitySolution.flyout.entityDetails.host.misconfigurationInsights.title',
+ { defaultMessage: 'Misconfigurations' }
+);
+
+export interface MisconfigurationInsightsProps {
+ /** The host name used to query misconfigurations (`host.name` field value). */
+ value: string;
+ /** Canonical Entity Store v2 id (`entity.id`) when already resolved. */
+ entityId?: string;
+ /** Scope id passed to downstream containers. */
+ scopeId: string;
+ /** Opens the originating host flyout as a child. */
+ onOpenHost?: () => void;
+}
+
+/**
+ * Tool flyout displaying CSP misconfiguration findings for a host entity.
+ */
+export const MisconfigurationInsights = memo(
+ ({ value, entityId, scopeId, onOpenHost }: MisconfigurationInsightsProps) => {
+ const { services } = useKibana();
+ const { overlays } = services;
+ const store = useStore();
+ const history = useHistory();
+ const defaultDocumentFlyoutProperties = useDefaultDocumentFlyoutProperties();
+
+ const onShowFinding = useCallback(
+ (resourceId: string, ruleId: string) => {
+ overlays.openSystemFlyout(
+ flyoutProviders({
+ services,
+ store,
+ history,
+ children: ,
+ }),
+ { ...defaultDocumentFlyoutProperties, title: value, session: 'inherit' }
+ );
+ },
+ [overlays, services, store, history, defaultDocumentFlyoutProperties, value]
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+ }
+);
+
+MisconfigurationInsights.displayName = 'MisconfigurationInsights';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/test_ids.ts
new file mode 100644
index 0000000000000..a1da88e524fbe
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/misconfiguration_insights/test_ids.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export const MISCONFIGURATION_INSIGHTS_TOOL_TEST_ID = 'misconfigurationInsightsTool';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/index.test.tsx
new file mode 100644
index 0000000000000..a1657c1bac183
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/index.test.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 from 'react';
+import { render } from '@testing-library/react';
+import { RiskInputs } from '.';
+import { RISK_INPUTS_TOOL_TEST_ID } from './test_ids';
+
+jest.mock('../../../../shared/components/tools_flyout_header', () => ({
+ ToolsFlyoutHeader: ({
+ title,
+ label,
+ iconType,
+ onTitleClick,
+ }: {
+ title: string;
+ label?: string;
+ iconType?: string;
+ onTitleClick?: () => void;
+ }) => (
+
+ ),
+}));
+
+jest.mock('../../../../../entity_analytics/components/flyout_v2/risk_inputs_tab', () => ({
+ RiskInputsTab: ({
+ entityType,
+ entityName,
+ scopeId,
+ entityId,
+ }: {
+ entityType: string;
+ entityName: string;
+ scopeId: string;
+ entityId?: string;
+ }) => (
+
+ ),
+}));
+
+describe('', () => {
+ it('renders the header with the "Risk score" title and host label', () => {
+ const { getByTestId } = render();
+ const header = getByTestId('mockToolsFlyoutHeader');
+ expect(header).toHaveAttribute('data-title', 'Risk score');
+ expect(header).toHaveAttribute('data-label', 'my-host');
+ expect(header).toHaveAttribute('data-icon-type', 'storage');
+ });
+
+ it('renders the risk inputs body container', () => {
+ const { getByTestId } = render();
+ expect(getByTestId(RISK_INPUTS_TOOL_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('passes entity context to the underlying RiskInputsTab', () => {
+ const { getByTestId } = render(
+
+ );
+ const tab = getByTestId('mockRiskInputsTab');
+ expect(tab).toHaveAttribute('data-entity-type', 'host');
+ expect(tab).toHaveAttribute('data-entity-name', 'my-host');
+ expect(tab).toHaveAttribute('data-scope-id', 'my-scope');
+ expect(tab).toHaveAttribute('data-entity-id', 'euid-123');
+ });
+
+ it('forwards onOpenHost to the header click handler', () => {
+ const onOpenHost = jest.fn();
+ const { getByTestId } = render(
+
+ );
+ getByTestId('mockToolsFlyoutHeader').click();
+ expect(onOpenHost).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/index.tsx
new file mode 100644
index 0000000000000..3cdf987cb4d07
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/index.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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, { memo } from 'react';
+import { EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { EntityType } from '../../../../../../common/entity_analytics/types';
+import { RiskInputsTab } from '../../../../../entity_analytics/components/flyout_v2/risk_inputs_tab';
+import { ToolsFlyoutHeader } from '../../../../shared/components/tools_flyout_header';
+import { RISK_INPUTS_TOOL_TEST_ID } from './test_ids';
+
+const TITLE = i18n.translate('xpack.securitySolution.flyout.entityDetails.host.riskInputs.title', {
+ defaultMessage: 'Risk score',
+});
+
+export interface RiskInputsProps {
+ /** Display name of the host (typically `host.name`). */
+ entityName: string;
+ /** Scope id (timeline id, table id, etc.) passed to downstream containers. */
+ scopeId: string;
+ /** Canonical Entity Store v2 id (`entity.id`) when already resolved. */
+ entityId?: string;
+ /** Opens the originating host flyout as a child. */
+ onOpenHost?: () => void;
+}
+
+export const RiskInputs = memo(({ entityName, scopeId, entityId, onOpenHost }: RiskInputsProps) => {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+});
+
+RiskInputs.displayName = 'RiskInputs';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/test_ids.ts
new file mode 100644
index 0000000000000..bb34b73049bfe
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/risk_inputs/test_ids.ts
@@ -0,0 +1,10 @@
+/*
+ * 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 { PREFIX } from '../../../../../flyout/shared/test_ids';
+
+export const RISK_INPUTS_TOOL_TEST_ID = `${PREFIX}HostRiskInputsTool` as const;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/index.test.tsx
new file mode 100644
index 0000000000000..2693054472ec4
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/index.test.tsx
@@ -0,0 +1,151 @@
+/*
+ * 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 { VulnerabilityInsights } from '.';
+
+const mockOpenSystemFlyout = jest.fn();
+const vulnerabilityRowParams = {
+ vulnerabilityId: 'CVE-1',
+ resourceId: 'resource-1',
+ packageName: 'pkg',
+ packageVersion: '1.0.0',
+ eventId: 'event-1',
+};
+
+jest.mock('../../../../shared/components/tools_flyout_header', () => ({
+ ToolsFlyoutHeader: ({
+ title,
+ label,
+ iconType,
+ onTitleClick,
+ }: {
+ title: string;
+ label?: string;
+ iconType?: string;
+ onTitleClick?: () => void;
+ }) => (
+
+ ),
+}));
+
+jest.mock(
+ '../../../../../cloud_security_posture/components/flyout_v2/csp_details/vulnerabilities_findings_details_table',
+ () => ({
+ VulnerabilitiesFindingsDetailsTable: ({
+ identityField,
+ value,
+ scopeId,
+ entityId,
+ entityType,
+ onShowVulnerability,
+ }: {
+ identityField: string;
+ value: string;
+ scopeId: string;
+ entityId?: string;
+ entityType?: string;
+ onShowVulnerability?: (params: typeof vulnerabilityRowParams) => void;
+ }) => (
+
+ ),
+ })
+);
+
+jest.mock('../../../../csp_details/vulnerability_panel', () => ({
+ VulnerabilityPanel: () => ,
+}));
+
+jest.mock('../../../../shared/components/flyout_provider', () => ({
+ flyoutProviders: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+jest.mock('../../../../shared/hooks/use_default_flyout_properties', () => ({
+ useDefaultDocumentFlyoutProperties: () => ({ size: 'm' }),
+}));
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useStore: () => ({}),
+}));
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({ push: jest.fn() }),
+}));
+
+jest.mock('../../../../../common/lib/kibana', () => ({
+ useKibana: () => ({
+ services: {
+ overlays: { openSystemFlyout: mockOpenSystemFlyout },
+ },
+ }),
+}));
+
+describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the header with the title, host label and entity icon', () => {
+ const { getByTestId } = render();
+ const header = getByTestId('mockToolsFlyoutHeader');
+ expect(header).toHaveAttribute('data-title', 'Vulnerabilities');
+ expect(header).toHaveAttribute('data-label', 'my-host');
+ expect(header).toHaveAttribute('data-icon-type', 'storage');
+ });
+
+ it('forwards the host name, scope and entity id to the findings table', () => {
+ const { getByTestId } = render(
+
+ );
+ const table = getByTestId('mockVulnerabilitiesFindingsDetailsTable');
+ expect(table).toHaveAttribute('data-identity-field', 'host.name');
+ expect(table).toHaveAttribute('data-value', 'my-host');
+ expect(table).toHaveAttribute('data-scope-id', 'my-scope');
+ expect(table).toHaveAttribute('data-entity-id', 'euid-123');
+ expect(table).toHaveAttribute('data-entity-type', 'host');
+ });
+
+ it('forwards onOpenHost to the header click handler', () => {
+ const onOpenHost = jest.fn();
+ const { getByTestId } = render(
+
+ );
+ getByTestId('mockToolsFlyoutHeader').click();
+ expect(onOpenHost).toHaveBeenCalledTimes(1);
+ });
+
+ it('opens a child system flyout when a vulnerability row is expanded', () => {
+ const { getByTestId } = render();
+ getByTestId('mockVulnerabilitiesFindingsDetailsTable').click();
+ expect(mockOpenSystemFlyout).toHaveBeenCalledTimes(1);
+ expect(mockOpenSystemFlyout).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ session: 'inherit', title: 'my-host' })
+ );
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/index.tsx
new file mode 100644
index 0000000000000..f1bf9dce7d550
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/index.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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, { memo, useCallback } from 'react';
+import { EuiFlyoutHeader } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useHistory } from 'react-router-dom';
+import { useStore } from 'react-redux';
+import {
+ EntityIdentifierFields,
+ EntityType,
+} from '../../../../../../common/entity_analytics/types';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { flyoutProviders } from '../../../../shared/components/flyout_provider';
+import { useDefaultDocumentFlyoutProperties } from '../../../../shared/hooks/use_default_flyout_properties';
+import { VulnerabilityPanel } from '../../../../csp_details/vulnerability_panel';
+import { ToolsFlyoutHeader } from '../../../../shared/components/tools_flyout_header';
+import { VulnerabilitiesFindingsDetailsTable } from '../../../../../cloud_security_posture/components/flyout_v2/csp_details/vulnerabilities_findings_details_table';
+
+const TITLE = i18n.translate(
+ 'xpack.securitySolution.flyout.entityDetails.host.vulnerabilityInsights.title',
+ { defaultMessage: 'Vulnerabilities' }
+);
+
+export interface VulnerabilityInsightsProps {
+ /** The host name used to query vulnerabilities (`host.name` field value). */
+ value: string;
+ /** Canonical Entity Store v2 id (`entity.id`) when already resolved. */
+ entityId?: string;
+ /** Scope id passed to downstream containers. */
+ scopeId: string;
+ /** Opens the originating host flyout as a child. */
+ onOpenHost?: () => void;
+}
+
+/**
+ * Tool flyout displaying vulnerability findings for a host entity.
+ */
+export const VulnerabilityInsights = memo(
+ ({ value, entityId, scopeId, onOpenHost }: VulnerabilityInsightsProps) => {
+ const { services } = useKibana();
+ const { overlays } = services;
+ const store = useStore();
+ const history = useHistory();
+ const defaultDocumentFlyoutProperties = useDefaultDocumentFlyoutProperties();
+
+ const onShowVulnerability = useCallback(
+ (params: {
+ vulnerabilityId: string;
+ resourceId: string;
+ packageName: string;
+ packageVersion: string;
+ eventId: string;
+ }) => {
+ overlays.openSystemFlyout(
+ flyoutProviders({
+ services,
+ store,
+ history,
+ children: ,
+ }),
+ { ...defaultDocumentFlyoutProperties, title: value, session: 'inherit' }
+ );
+ },
+ [overlays, services, store, history, defaultDocumentFlyoutProperties, value]
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+ }
+);
+
+VulnerabilityInsights.displayName = 'VulnerabilityInsights';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/test_ids.ts
new file mode 100644
index 0000000000000..1d1d798fff556
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/entity/host/tools/vulnerability_insights/test_ids.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export const VULNERABILITY_INSIGHTS_TOOL_TEST_ID = 'vulnerabilityInsightsTool';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/document_tools_flyout_header.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/document_tools_flyout_header.tsx
new file mode 100644
index 0000000000000..7355b7891629d
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/document_tools_flyout_header.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 type { FC, ReactNode } from 'react';
+import React, { memo } from 'react';
+import type { DataTableRecord } from '@kbn/discover-utils';
+import type { CellActionRenderer } from './cell_actions';
+import { ToolsFlyoutHeader } from './tools_flyout_header';
+import { useDocumentFlyoutTitle } from '../hooks/use_document_flyout_title';
+
+export interface DocumentToolsFlyoutHeaderProps {
+ /**
+ * Title for the tool panel (e.g. "Correlations", "Investigation guide").
+ */
+ title: ReactNode;
+ /**
+ * Source document used to derive the context title button label, icon,
+ * severity badge, timestamp, and the child flyout opened on click.
+ */
+ hit: DataTableRecord;
+ /**
+ * Cell action renderer forwarded to the child document flyout.
+ */
+ renderCellActions?: CellActionRenderer;
+ /**
+ * Callback invoked after alert mutations in the child document flyout.
+ */
+ onAlertUpdated?: () => void;
+}
+
+/**
+ * Wrapper around the ToolsFlyoutHeader that uses the useDocumentFlyoutTitle hook to
+ * compute all relevant values.
+ * */
+export const DocumentToolsFlyoutHeader: FC = memo(
+ ({ title, hit, renderCellActions, onAlertUpdated }) => {
+ const { label, iconType, onTitleClick, badge, timestamp } = useDocumentFlyoutTitle({
+ hit,
+ renderCellActions,
+ onAlertUpdated,
+ });
+
+ return (
+
+ );
+ }
+);
+
+DocumentToolsFlyoutHeader.displayName = 'DocumentToolsFlyoutHeader';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.test.tsx
index 8d4f194f9ece4..321d016af9209 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.test.tsx
@@ -19,6 +19,7 @@ import { flyoutProviders } from './flyout_provider';
jest.mock('../../../common/components/user_privileges/user_privileges_context', () => ({
UserPrivilegesProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
}));
+
jest.mock('../../../common/components/discover_in_timeline/provider', () => ({
DiscoverInTimelineContextProvider: ({ children }: { children: React.ReactNode }) => (
<>{children}>
@@ -36,6 +37,13 @@ const services = {
getTriggerCompatibleActions: jest.fn().mockResolvedValue([]),
},
upselling: new UpsellingService(),
+ data: {
+ query: {
+ timefilter: {
+ timefilter: { getAbsoluteTime: () => ({ from: '2024-01-01', to: '2024-01-02' }) },
+ },
+ },
+ },
application: {
capabilities: {
[SECURITY_FEATURE_ID]: {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.tsx
index 8e8132c1b89b7..dac7c7fa0be47 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/flyout_provider.tsx
@@ -5,24 +5,43 @@
* 2.0.
*/
-import type { ReactElement, ReactNode } from 'react';
-import React from 'react';
+import type { FC, ReactElement, ReactNode } from 'react';
+import React, { useEffect } from 'react';
import type { History } from 'history';
import { Router } from '@kbn/shared-ux-router';
import type { Store } from 'redux';
-import { Provider } from 'react-redux';
+import { Provider, useStore } from 'react-redux';
import { CellActionsProvider } from '@kbn/cell-actions';
import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import { EntityStoreEuidApiProvider } from '@kbn/entity-store/public';
import type { StartServices } from '../../../types';
import { ReactQueryClientProvider } from '../../../common/containers/query_client/query_client_provider';
-import { KibanaContextProvider } from '../../../common/lib/kibana';
+import { KibanaContextProvider, useKibana } from '../../../common/lib/kibana';
import { UserPrivilegesProvider } from '../../../common/components/user_privileges/user_privileges_context';
import { UpsellingProvider } from '../../../common/components/upselling_provider';
import { DiscoverInTimelineContextProvider } from '../../../common/components/discover_in_timeline/provider';
import { AssistantProvider } from '../../../assistant/provider';
import { CaseProvider } from '../../../cases/components/provider/provider';
+import { MlCapabilitiesProvider } from '../../../common/components/ml/permissions/ml_capabilities_provider';
+import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions';
+import { InputsModelId } from '../../../common/store/inputs/constants';
+
+/**
+ * Syncs Kibana's global time filter to the Security Solution Redux store on mount.
+ */
+const TimeRangeSync: FC<{ children: ReactNode }> = ({ children }) => {
+ const { services } = useKibana();
+ const store = useStore();
+
+ useEffect(() => {
+ const tf = services.data.query.timefilter.timefilter;
+ const { from, to } = tf.getAbsoluteTime();
+ store.dispatch(setAbsoluteRangeDatePicker({ id: InputsModelId.global, from, to }));
+ }, [services, store]);
+
+ return <>{children}>;
+};
export const flyoutProviders = ({
services,
@@ -58,7 +77,11 @@ export const flyoutProviders = ({
- {flyoutContent}
+
+
+ {flyoutContent}
+
+
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/child_link.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/open_flyout_link.test.tsx
similarity index 62%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/child_link.test.tsx
rename to x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/open_flyout_link.test.tsx
index 1c23706f66a18..0b3939e3bfca5 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/child_link.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/open_flyout_link.test.tsx
@@ -8,8 +8,8 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
-import { ChildLink } from './child_link';
-import { CHILD_LINK_TEST_ID } from './test_ids';
+import { OpenFlyoutLink } from './open_flyout_link';
+import { OPEN_FLYOUT_LINK_TEST_ID } from './test_ids';
import { buildFlyoutContent } from '../utils/build_flyout_content';
jest.mock('../utils/build_flyout_content');
@@ -40,16 +40,16 @@ jest.mock('../../../common/lib/kibana', () => {
const buildFlyoutContentMock = buildFlyoutContent as jest.Mock;
-const renderChildLink = (props: Partial> = {}) =>
+const renderOpenFlyoutLink = (props: Partial> = {}) =>
render(
-
+
{'fallback'}
-
+
);
-describe('', () => {
+describe('', () => {
beforeEach(() => {
jest.clearAllMocks();
});
@@ -62,32 +62,52 @@ describe('', () => {
it('should render a link with the value as text when no children are provided', () => {
const { getByTestId } = render(
-
+
);
- const link = getByTestId(CHILD_LINK_TEST_ID);
+ const link = getByTestId(OPEN_FLYOUT_LINK_TEST_ID);
expect(link).toBeInTheDocument();
expect(link).toHaveTextContent('10.0.0.1');
});
it('should render children inside the link when children are provided', () => {
- const { getByTestId } = renderChildLink();
+ const { getByTestId } = renderOpenFlyoutLink();
- const link = getByTestId(CHILD_LINK_TEST_ID);
+ const link = getByTestId(OPEN_FLYOUT_LINK_TEST_ID);
expect(link).toBeInTheDocument();
expect(link).toHaveTextContent('fallback');
});
it('should call openSystemFlyout when clicked', () => {
- const { getByTestId } = renderChildLink();
+ const { getByTestId } = renderOpenFlyoutLink();
- getByTestId(CHILD_LINK_TEST_ID).click();
+ getByTestId(OPEN_FLYOUT_LINK_TEST_ID).click();
expect(mockOpenSystemFlyout).toHaveBeenCalled();
});
+ it('should open as child flyout by default', () => {
+ const { getByTestId } = renderOpenFlyoutLink();
+
+ getByTestId(OPEN_FLYOUT_LINK_TEST_ID).click();
+ expect(mockOpenSystemFlyout).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ session: 'inherit', outsideClickCloses: false })
+ );
+ });
+
+ it('should open as standalone flyout when asParent is true', () => {
+ const { getByTestId } = renderOpenFlyoutLink({ asParent: true });
+
+ getByTestId(OPEN_FLYOUT_LINK_TEST_ID).click();
+ expect(mockOpenSystemFlyout).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ session: 'start', outsideClickCloses: true })
+ );
+ });
+
it('should use a custom data-test-subj when provided', () => {
- const { getByTestId } = renderChildLink({ 'data-test-subj': 'customTestId' });
+ const { getByTestId } = renderOpenFlyoutLink({ 'data-test-subj': 'customTestId' });
expect(getByTestId('customTestId')).toBeInTheDocument();
});
@@ -99,14 +119,14 @@ describe('', () => {
});
it('should render children as fallback', () => {
- const { getByTestId, queryByTestId } = renderChildLink();
+ const { getByTestId, queryByTestId } = renderOpenFlyoutLink();
expect(getByTestId('fallbackChild')).toBeInTheDocument();
- expect(queryByTestId(CHILD_LINK_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(OPEN_FLYOUT_LINK_TEST_ID)).not.toBeInTheDocument();
});
it('should not call openSystemFlyout', () => {
- renderChildLink();
+ renderOpenFlyoutLink();
expect(mockOpenSystemFlyout).not.toHaveBeenCalled();
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/child_link.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/open_flyout_link.tsx
similarity index 56%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/child_link.tsx
rename to x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/open_flyout_link.tsx
index 8c9e32092cfd2..3f97a0e7540ac 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/child_link.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/open_flyout_link.tsx
@@ -10,13 +10,20 @@ import React, { useCallback, useMemo } from 'react';
import { EuiLink } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { useStore } from 'react-redux';
+import type { DataTableRecord } from '@kbn/discover-utils';
+import { DOC_VIEWER_FLYOUT_HISTORY_KEY } from '@kbn/unified-doc-viewer';
import { flyoutProviders } from './flyout_provider';
-import { useDefaultDocumentFlyoutProperties } from '../hooks/use_default_flyout_properties';
+import {
+ defaultToolsFlyoutProperties,
+ useDefaultDocumentFlyoutProperties,
+} from '../hooks/use_default_flyout_properties';
import { useKibana } from '../../../common/lib/kibana';
-import { CHILD_LINK_TEST_ID } from './test_ids';
+import { useIsInSecurityApp } from '../../../common/hooks/is_in_security_app';
+import { documentFlyoutHistoryKey } from '../constants/flyout_history';
+import { OPEN_FLYOUT_LINK_TEST_ID } from './test_ids';
import { buildFlyoutContent } from '../utils/build_flyout_content';
-export interface ChildLinkProps {
+export interface OpenFlyoutLinkProps {
/**
* Field name used to determine which flyout to open
*/
@@ -25,6 +32,15 @@ export interface ChildLinkProps {
* Field value
*/
value: string;
+ /**
+ * The source document record. When provided, enables entity resolution for host/user flyouts.
+ */
+ hit?: DataTableRecord;
+ /**
+ * When true, opens as a parent flyout starting a new session.
+ * When false (default), opens as a child flyout inheriting the parent session.
+ */
+ asParent?: boolean;
/**
* Optional data-test-subj value
*/
@@ -37,28 +53,34 @@ export interface ChildLinkProps {
/**
* Renders a clickable link that opens a system flyout for supported field types.
- * Currently supports IP fields (opens the network details flyout).
*
* When the field is supported, the link is rendered with `value` as the link text.
* When the field is not supported, the `children` are rendered as-is (pass-through),
* allowing callers to wrap fallback rendering inside this component.
*/
-export const ChildLink: FC = ({
+export const OpenFlyoutLink: FC = ({
field,
value,
+ hit,
+ asParent = false,
children,
- 'data-test-subj': dataTestSubj = CHILD_LINK_TEST_ID,
+ 'data-test-subj': dataTestSubj = OPEN_FLYOUT_LINK_TEST_ID,
}) => {
const { services } = useKibana();
const { overlays } = services;
const store = useStore();
const history = useHistory();
const defaultDocumentFlyoutProperties = useDefaultDocumentFlyoutProperties();
+ const isInSecurityApp = useIsInSecurityApp();
+ const historyKey = isInSecurityApp ? documentFlyoutHistoryKey : DOC_VIEWER_FLYOUT_HISTORY_KEY;
- const flyoutContent = useMemo(() => buildFlyoutContent(field, value), [field, value]);
+ const flyoutContent = useMemo(() => buildFlyoutContent(field, value, hit), [field, value, hit]);
const onClick = useCallback(() => {
if (flyoutContent) {
+ const baseFlyoutProperties = asParent
+ ? defaultToolsFlyoutProperties
+ : defaultDocumentFlyoutProperties;
overlays.openSystemFlyout(
flyoutProviders({
services,
@@ -67,12 +89,23 @@ export const ChildLink: FC = ({
children: flyoutContent,
}),
{
- ...defaultDocumentFlyoutProperties,
- session: 'inherit',
+ ...baseFlyoutProperties,
+ historyKey,
+ session: asParent ? 'start' : 'inherit',
+ outsideClickCloses: asParent,
}
);
}
- }, [defaultDocumentFlyoutProperties, overlays, services, store, history, flyoutContent]);
+ }, [
+ defaultDocumentFlyoutProperties,
+ overlays,
+ services,
+ store,
+ history,
+ flyoutContent,
+ asParent,
+ historyKey,
+ ]);
if (!flyoutContent) {
return <>{children}>;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/test_ids.ts
index d3ab517f0762b..1cd17752acd93 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/test_ids.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/test_ids.ts
@@ -40,7 +40,7 @@ export const NOTES_LOADING_TEST_ID = `${PREFIX}HeaderNotesLoading` as const;
export const TOOLS_FLYOUT_HEADER_TEST_ID = `${PREFIX}ToolsFlyoutHeader` as const;
export const TOOLS_FLYOUT_HEADER_TITLE_TEST_ID = `${PREFIX}ToolsFlyoutHeaderTitle` as const;
-export const CHILD_LINK_TEST_ID = `${PREFIX}ChildLink` as const;
+export const OPEN_FLYOUT_LINK_TEST_ID = `${PREFIX}OpenFlyoutLink` as const;
export const FLYOUT_LOADING_TEST_ID = `${PREFIX}Loading` as const;
export const FLYOUT_ERROR_TEST_ID = `${PREFIX}Error` as const;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.test.tsx
index 9c850d6d1aa11..cee3feddf009e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.test.tsx
@@ -8,69 +8,63 @@
import React from 'react';
import { render } from '@testing-library/react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
-import type { DataTableRecord } from '@kbn/discover-utils';
import { ToolsFlyoutHeader } from './tools_flyout_header';
import { TOOLS_FLYOUT_HEADER_TEST_ID } from './test_ids';
jest.mock('./tools_flyout_title', () => ({
- ToolsFlyoutTitle: ({ hit }: { hit: DataTableRecord }) => (
- {String(hit.id)}
+ ToolsFlyoutTitle: ({ label }: { label: string }) => (
+ {label}
),
}));
-jest.mock('../../document/main/components/severity', () => ({
- DocumentSeverity: () => ,
-}));
-
-jest.mock('./timestamp', () => ({
- Timestamp: ({ hit }: { hit: { id: string; flattened: Record } }) => (
-
- ),
-}));
-
-const createMockHit = (
- flattened: DataTableRecord['flattened'] = { '@timestamp': '2024-01-15T10:30:00.000Z' }
-): DataTableRecord =>
- ({
- id: 'hit-1',
- raw: {},
- flattened,
- isAnchor: false,
- } as DataTableRecord);
-
-const renderHeader = (props: Partial[0]> = {}) => {
- const hit = createMockHit();
- return render(
+const renderHeader = (props: Partial[0]> = {}) =>
+ render(
- {'Correlations'}} {...props} />
+ {'Correlations'}} {...props} />
);
+
+const sourceProps = {
+ onTitleClick: jest.fn(),
+ label: 'Test Rule',
+ iconType: 'warning',
};
describe('', () => {
- it('should render the header container', () => {
+ it('renders the header container', () => {
const { getByTestId } = renderHeader();
expect(getByTestId(TOOLS_FLYOUT_HEADER_TEST_ID)).toBeInTheDocument();
});
- it('should render the tool title', () => {
+ it('renders the tool title', () => {
const { getByText } = renderHeader({ title: {'Session view'} });
expect(getByText('Session view')).toBeInTheDocument();
});
- it('should render ToolsFlyoutTitle', () => {
- const { getByTestId } = renderHeader();
+ it('renders ToolsFlyoutTitle when onTitleClick, label and iconType are provided', () => {
+ const { getByTestId } = renderHeader(sourceProps);
expect(getByTestId('mockToolsFlyoutTitle')).toBeInTheDocument();
+ expect(getByTestId('mockToolsFlyoutTitle')).toHaveTextContent('Test Rule');
});
- it('should render the document severity', () => {
- const { getByTestId } = renderHeader();
- expect(getByTestId('mockDocumentSeverity')).toBeInTheDocument();
+ it('does not render source context when props are missing', () => {
+ const { queryByTestId } = renderHeader();
+ expect(queryByTestId('mockToolsFlyoutTitle')).not.toBeInTheDocument();
});
- it('should render the document timestamp', () => {
- const { getByTestId } = renderHeader();
+ it('renders badge when provided', () => {
+ const { getByTestId } = renderHeader({
+ ...sourceProps,
+ badge: ,
+ });
+ expect(getByTestId('mockBadge')).toBeInTheDocument();
+ });
+
+ it('renders timestamp when provided', () => {
+ const { getByTestId } = renderHeader({
+ ...sourceProps,
+ timestamp: ,
+ });
expect(getByTestId('mockTimestamp')).toBeInTheDocument();
- expect(getByTestId('mockTimestamp')).toHaveAttribute('data-hit-id', 'hit-1');
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.tsx
index b3d6259ba49c3..4fef0bed4f406 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_header.tsx
@@ -8,41 +8,46 @@
import type { FC, ReactNode } from 'react';
import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
-import { type DataTableRecord } from '@kbn/discover-utils';
-import { Timestamp } from './timestamp';
-import { DocumentSeverity } from '../../document/main/components/severity';
-import type { CellActionRenderer } from './cell_actions';
-import { noopCellActionRenderer } from './cell_actions';
import { ToolsFlyoutTitle } from './tools_flyout_title';
import { TOOLS_FLYOUT_HEADER_TEST_ID } from './test_ids';
-const noop = () => {};
-
export interface ToolsFlyoutHeaderProps {
/**
- * The document to display
+ * Title for the tools flyout (e.g. "Correlations", "Risk score", "Insights").
*/
- hit: DataTableRecord;
+ title: ReactNode;
/**
- * Title for the tools flyout (e.g. "Correlations", "Analyzer", "Session view")
+ * Called when the context title button is clicked. Should open the originating
+ * document or entity flyout as a child via `overlays.openSystemFlyout` with
+ * `session: 'inherit'`.
*/
- title: ReactNode;
+ onTitleClick?: () => void;
+ /**
+ * Label shown in the context title button (e.g. rule name or entity name).
+ */
+ label?: string;
/**
- * Optional cell action renderer passed to the child document flyout.
+ * EUI icon type shown next to the label.
*/
- renderCellActions?: CellActionRenderer;
+ iconType?: string;
/**
- * Optional callback invoked after alert mutations in the child document flyout.
+ * Optional badge rendered alongside the title button (e.g. severity badge for documents).
*/
- onAlertUpdated?: () => void;
+ badge?: ReactNode;
+ /**
+ * Optional metadata rendered below the title row (e.g. timestamp for documents).
+ */
+ timestamp?: ReactNode;
}
/**
- * Shared header for all tools flyouts. Renders the tool title on the left and document
- * context (expand button, rule name, severity, timestamp) on the right.
+ * Shared header for all tools flyouts. Renders the tool title on the left and optional
+ * source context on the right (expand button, label, badge, timestamp).
*/
export const ToolsFlyoutHeader: FC = memo(
- ({ hit, title, renderCellActions = noopCellActionRenderer, onAlertUpdated = noop }) => {
+ ({ title, onTitleClick, label, iconType, badge, timestamp }) => {
+ const showSourceContext = !!onTitleClick && !!label && !!iconType;
+
return (
= memo(
{title}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {showSourceContext && (
+
+
+
+
+
+
+
+ {badge && {badge}}
+
+
+ {timestamp && {timestamp}}
+
+
+ )}
);
}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.test.tsx
index 3e7688972f7fb..5979a3c9ec49e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.test.tsx
@@ -7,83 +7,23 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
-import type { DataTableRecord } from '@kbn/discover-utils';
import { ToolsFlyoutTitle } from './tools_flyout_title';
import { TOOLS_FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids';
-const mockOpenSystemFlyout = jest.fn();
-
-jest.mock('../../../common/lib/kibana', () => ({
- useKibana: () => ({
- services: {
- overlays: {
- openSystemFlyout: mockOpenSystemFlyout,
- },
- },
- }),
-}));
-
-jest.mock('react-redux', () => ({
- ...jest.requireActual('react-redux'),
- useStore: () => ({}),
-}));
-
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
- useHistory: () => ({ push: jest.fn() }),
-}));
-
-jest.mock('../hooks/use_default_flyout_properties', () => ({
- useDefaultDocumentFlyoutProperties: () => ({ size: 'm' }),
-}));
-
-jest.mock('./flyout_provider', () => ({
- flyoutProviders: jest.fn(({ children }: { children: React.ReactNode }) => children),
-}));
-
-jest.mock('../../document/main', () => ({
- DocumentFlyout: () => ,
-}));
-
-const createMockHit = (flattened: DataTableRecord['flattened']): DataTableRecord =>
- ({
- id: '1',
- raw: {},
- flattened,
- isAnchor: false,
- } as DataTableRecord);
-
-const alertHit = createMockHit({
- 'event.kind': 'signal',
- 'kibana.alert.rule.name': 'Test Rule Name',
-});
-
-const eventHit = createMockHit({
- 'event.kind': 'event',
- 'event.category': 'process',
- 'process.name': 'test-process',
-});
-
-const renderToolsFlyoutTitle = (hit: DataTableRecord) => render();
-
describe('', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should render the alert title', () => {
- const { getByTestId } = renderToolsFlyoutTitle(alertHit);
- expect(getByTestId(TOOLS_FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('Test Rule Name');
- });
-
- it('should render the event title', () => {
- const { getByTestId } = renderToolsFlyoutTitle(eventHit);
- expect(getByTestId(TOOLS_FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('test-process');
+ it('renders the label', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId(TOOLS_FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('my-host');
});
- it('should open the document flyout when clicked', () => {
- const { getByTestId } = renderToolsFlyoutTitle(alertHit);
+ it('calls onTitleClick when clicked', () => {
+ const onTitleClick = jest.fn();
+ const { getByTestId } = render(
+
+ );
fireEvent.click(getByTestId(TOOLS_FLYOUT_HEADER_TITLE_TEST_ID));
- expect(mockOpenSystemFlyout).toHaveBeenCalledTimes(1);
+ expect(onTitleClick).toHaveBeenCalledTimes(1);
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.tsx
index b65dd60691273..578886d51e60a 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/components/tools_flyout_title.tsx
@@ -6,80 +6,36 @@
*/
import type { FC } from 'react';
-import React, { memo, useCallback, useMemo } from 'react';
+import React, { memo } from 'react';
import { EuiButtonEmpty, EuiIcon, useEuiTheme } from '@elastic/eui';
-import type { DataTableRecord } from '@kbn/discover-utils';
-import { getFieldValue } from '@kbn/discover-utils';
-import { EVENT_KIND } from '@kbn/rule-data-utils';
-import { useHistory } from 'react-router-dom';
-import { useStore } from 'react-redux';
-import { EventKind } from '../../document/main/constants/event_kinds';
-import { getDocumentTitle } from '../../document/main/utils/get_header_title';
-import { useKibana } from '../../../common/lib/kibana';
-import type { CellActionRenderer } from './cell_actions';
-import { noopCellActionRenderer } from './cell_actions';
-import { flyoutProviders } from './flyout_provider';
-import { DocumentFlyout } from '../../document/main';
-import { useDefaultDocumentFlyoutProperties } from '../hooks/use_default_flyout_properties';
import { TOOLS_FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids';
-const noop = () => {};
-
export interface ToolsFlyoutTitleProps {
/**
- * The document to display
+ * Callback invoked when the title is clicked.
*/
- hit: DataTableRecord;
+ onTitleClick: () => void;
/**
- * Optional cell action renderer passed to the document flyout.
+ * Text label displayed in the title.
*/
- renderCellActions?: CellActionRenderer;
+ label: string;
/**
- * Optional callback invoked after alert mutations in the document flyout.
+ * EUI icon type rendered next to the label.
*/
- onAlertUpdated?: () => void;
+ iconType: string;
}
/**
- * Clickable title used in tools flyout headers. Renders an expand icon followed by the
- * document type icon and title. Clicking any part of the component opens the document flyout.
+ * Clickable title used in tools flyout headers. Renders an expand icon followed by a
+ * context icon and label. Clicking opens the originating document or entity flyout.
*/
export const ToolsFlyoutTitle: FC = memo(
- ({ hit, renderCellActions = noopCellActionRenderer, onAlertUpdated = noop }) => {
+ ({ onTitleClick, label, iconType }) => {
const { euiTheme } = useEuiTheme();
- const { services } = useKibana();
- const store = useStore();
- const history = useHistory();
- const defaultFlyoutProperties = useDefaultDocumentFlyoutProperties();
-
- const isAlert = useMemo(
- () => (getFieldValue(hit, EVENT_KIND) as string) === EventKind.signal,
- [hit]
- );
- const title = useMemo(() => getDocumentTitle(hit), [hit]);
- const iconType = isAlert ? 'warning' : 'analyzeEvent';
-
- const onShowDocument = useCallback(() => {
- services.overlays?.openSystemFlyout(
- flyoutProviders({
- services,
- store,
- history,
- children: (
-
- ),
- }),
- { ...defaultFlyoutProperties, session: 'inherit' }
- );
- }, [defaultFlyoutProperties, history, hit, onAlertUpdated, renderCellActions, services, store]);
return (
= memo(
aria-hidden={true}
css={{ marginRight: euiTheme.size.xs }}
/>
- {title}
+ {label}
);
}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/hooks/use_document_flyout_title.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/hooks/use_document_flyout_title.test.tsx
new file mode 100644
index 0000000000000..deb8e70ef983c
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/hooks/use_document_flyout_title.test.tsx
@@ -0,0 +1,129 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react';
+import type { DataTableRecord } from '@kbn/discover-utils';
+import { DOC_VIEWER_FLYOUT_HISTORY_KEY } from '@kbn/unified-doc-viewer';
+import { useDocumentFlyoutTitle } from './use_document_flyout_title';
+import { useKibana } from '../../../common/lib/kibana';
+import { useIsInSecurityApp } from '../../../common/hooks/is_in_security_app';
+import { documentFlyoutHistoryKey } from '../constants/flyout_history';
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useStore: () => ({ getState: jest.fn(), dispatch: jest.fn(), subscribe: jest.fn() }),
+}));
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({}),
+}));
+jest.mock('../../../common/lib/kibana');
+jest.mock('../../../common/hooks/is_in_security_app');
+jest.mock('../components/flyout_provider', () => ({
+ flyoutProviders: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+jest.mock('../../document/main', () => ({
+ DocumentFlyout: () => ,
+}));
+jest.mock('../../document/main/components/severity', () => ({
+ DocumentSeverity: () => ,
+}));
+jest.mock('../components/timestamp', () => ({
+ Timestamp: () => ,
+}));
+
+const createHit = (flattened: DataTableRecord['flattened']): DataTableRecord =>
+ ({
+ id: '1',
+ raw: { _id: '1', _index: 'test', _source: {} },
+ flattened,
+ isAnchor: false,
+ } as DataTableRecord);
+
+const alertHit = createHit({
+ 'event.kind': 'signal',
+ 'kibana.alert.rule.name': 'My Rule',
+});
+
+const eventHit = createHit({
+ 'event.kind': 'event',
+ 'event.category': 'host',
+ 'host.name': 'host-1',
+});
+
+describe('useDocumentFlyoutTitle', () => {
+ const mockUseKibana = jest.mocked(useKibana);
+ const mockUseIsInSecurityApp = jest.mocked(useIsInSecurityApp);
+ const openSystemFlyout = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseIsInSecurityApp.mockReturnValue(true);
+ mockUseKibana.mockReturnValue({
+ services: {
+ overlays: { openSystemFlyout },
+ },
+ } as unknown as ReturnType);
+ });
+
+ it('derives label and warning icon for alerts', () => {
+ const { result } = renderHook(() => useDocumentFlyoutTitle({ hit: alertHit }));
+
+ expect(result.current.label).toBe('My Rule');
+ expect(result.current.iconType).toBe('warning');
+ });
+
+ it('derives label and analyzeEvent icon for non-alert events', () => {
+ const { result } = renderHook(() => useDocumentFlyoutTitle({ hit: eventHit }));
+
+ expect(result.current.label).toBe('host-1');
+ expect(result.current.iconType).toBe('analyzeEvent');
+ });
+
+ it('opens the document flyout with documentFlyoutHistoryKey and session inherit when in the Security app', () => {
+ const { result } = renderHook(() => useDocumentFlyoutTitle({ hit: alertHit }));
+
+ act(() => {
+ result.current.onTitleClick();
+ });
+
+ expect(openSystemFlyout).toHaveBeenCalledTimes(1);
+ expect(openSystemFlyout).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ historyKey: documentFlyoutHistoryKey,
+ session: 'inherit',
+ })
+ );
+ });
+
+ it('uses DOC_VIEWER_FLYOUT_HISTORY_KEY when not in the Security app', () => {
+ mockUseIsInSecurityApp.mockReturnValue(false);
+
+ const { result } = renderHook(() => useDocumentFlyoutTitle({ hit: eventHit }));
+
+ act(() => {
+ result.current.onTitleClick();
+ });
+
+ expect(openSystemFlyout).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ historyKey: DOC_VIEWER_FLYOUT_HISTORY_KEY,
+ session: 'inherit',
+ })
+ );
+ });
+
+ it('returns badge and timestamp nodes derived from the hit', () => {
+ const { result } = renderHook(() => useDocumentFlyoutTitle({ hit: alertHit }));
+
+ expect(result.current.badge).toBeTruthy();
+ expect(result.current.timestamp).toBeTruthy();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/hooks/use_document_flyout_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/hooks/use_document_flyout_title.tsx
new file mode 100644
index 0000000000000..bc7cb10991b80
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/hooks/use_document_flyout_title.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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, { useCallback, useMemo } from 'react';
+import type { DataTableRecord } from '@kbn/discover-utils';
+import { getFieldValue } from '@kbn/discover-utils';
+import { EVENT_KIND } from '@kbn/rule-data-utils';
+import { useHistory } from 'react-router-dom';
+import { useStore } from 'react-redux';
+import { noop } from 'lodash/fp';
+import { DOC_VIEWER_FLYOUT_HISTORY_KEY } from '@kbn/unified-doc-viewer';
+import { EventKind } from '../../document/main/constants/event_kinds';
+import { getDocumentTitle } from '../../document/main/utils/get_header_title';
+import { useKibana } from '../../../common/lib/kibana';
+import { useIsInSecurityApp } from '../../../common/hooks/is_in_security_app';
+import type { CellActionRenderer } from '../components/cell_actions';
+import { noopCellActionRenderer } from '../components/cell_actions';
+import { flyoutProviders } from '../components/flyout_provider';
+import { DocumentFlyout } from '../../document/main';
+import { useDefaultDocumentFlyoutProperties } from './use_default_flyout_properties';
+import { documentFlyoutHistoryKey } from '../constants/flyout_history';
+import { DocumentSeverity } from '../../document/main/components/severity';
+import { Timestamp } from '../components/timestamp';
+
+export interface UseDocumentFlyoutTitleOptions {
+ /** The source document to derive display values from. */
+ hit: DataTableRecord;
+ /** Cell action renderer forwarded to the child document flyout. */
+ renderCellActions?: CellActionRenderer;
+ /** Callback invoked after alert mutations in the child document flyout. */
+ onAlertUpdated?: () => void;
+}
+
+export interface DocumentFlyoutTitleResult {
+ /** Document title derived from the hit. */
+ label: string;
+ /** Icon type: `'warning'` for alerts, `'analyzeEvent'` for other documents. */
+ iconType: string;
+ /** Opens the source document as a child flyout. */
+ onTitleClick: () => void;
+ /** Severity badge for the document. */
+ badge: React.ReactNode;
+ /** Formatted timestamp for the document. */
+ timestamp: React.ReactNode;
+}
+
+/**
+ * Derives all `ToolsFlyoutHeader` display values from a source document hit.
+ */
+export const useDocumentFlyoutTitle = ({
+ hit,
+ renderCellActions = noopCellActionRenderer,
+ onAlertUpdated = noop,
+}: UseDocumentFlyoutTitleOptions): DocumentFlyoutTitleResult => {
+ const { services } = useKibana();
+ const store = useStore();
+ const history = useHistory();
+ const defaultFlyoutProperties = useDefaultDocumentFlyoutProperties();
+ const isInSecurityApp = useIsInSecurityApp();
+ const historyKey = isInSecurityApp ? documentFlyoutHistoryKey : DOC_VIEWER_FLYOUT_HISTORY_KEY;
+
+ const isAlert = useMemo(
+ () => (getFieldValue(hit, EVENT_KIND) as string) === EventKind.signal,
+ [hit]
+ );
+
+ const label = useMemo(() => getDocumentTitle(hit), [hit]);
+ const iconType = isAlert ? 'warning' : 'analyzeEvent';
+
+ const onTitleClick = useCallback(() => {
+ services.overlays?.openSystemFlyout(
+ flyoutProviders({
+ services,
+ store,
+ history,
+ children: (
+
+ ),
+ }),
+ { ...defaultFlyoutProperties, historyKey, session: 'inherit' }
+ );
+ }, [
+ defaultFlyoutProperties,
+ history,
+ historyKey,
+ hit,
+ onAlertUpdated,
+ renderCellActions,
+ services,
+ store,
+ ]);
+
+ const badge = ;
+ const timestamp = ;
+
+ return { label, iconType, onTitleClick, badge, timestamp };
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/tools/notes/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/tools/notes/index.tsx
index 292fae7de5eba..3b4be524b93b7 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/tools/notes/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/tools/notes/index.tsx
@@ -11,7 +11,7 @@ import type { DataTableRecord } from '@kbn/discover-utils';
import { EuiFlyoutBody, EuiFlyoutHeader, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useSelector } from 'react-redux';
-import { ToolsFlyoutHeader } from '../../components/tools_flyout_header';
+import { DocumentToolsFlyoutHeader } from '../../components/document_tools_flyout_header';
import { useTimelineConfig } from './hooks/use_timeline_config';
import { useIsInSecurityApp } from '../../../../common/hooks/is_in_security_app';
import type { State } from '../../../../common/store';
@@ -68,7 +68,7 @@ export const NotesDetails = memo(({ hit }: NotesDetailsProps) => {
padding-block: ${euiTheme.size.s} !important;
`}
>
-
+
({
),
}));
+jest.mock('../../entity/host/main', () => ({
+ Host: ({ hostName, hit }: { hostName: string; hit?: { flattened: Record } }) => (
+
+ {hostName}
+
+ ),
+}));
+
jest.mock(
'../../../one_discover/alert_flyout_overview_tab_component/data_view_manager_bootstrap',
() => ({
@@ -35,30 +43,50 @@ jest.mock(
);
describe('buildFlyoutContent', () => {
- it('should return a Network element for a source IP field', () => {
+ it('should return a Network element for a source IP field', async () => {
const result = buildFlyoutContent('source.ip', '10.0.0.1');
expect(result).not.toBeNull();
- const { getByTestId } = render(result!);
- expect(getByTestId('mockNetwork')).toHaveTextContent(`10.0.0.1-${FlowTargetSourceDest.source}`);
+ const { findByTestId } = render(result!);
+ expect(await findByTestId('mockNetwork')).toHaveTextContent(
+ `10.0.0.1-${FlowTargetSourceDest.source}`
+ );
});
- it('should return a Network element for a destination IP field', () => {
+ it('should return a Network element for a destination IP field', async () => {
const result = buildFlyoutContent('destination.ip', '192.168.1.1');
expect(result).not.toBeNull();
- const { getByTestId } = render(result!);
- expect(getByTestId('mockNetwork')).toHaveTextContent(
+ const { findByTestId } = render(result!);
+ expect(await findByTestId('mockNetwork')).toHaveTextContent(
`192.168.1.1-${FlowTargetSourceDest.destination}`
);
});
- it('should return null for a non-IP field', () => {
+ it('should return a Host element for a host.name field', async () => {
const result = buildFlyoutContent('host.name', 'my-host');
- expect(result).toBeNull();
+ expect(result).not.toBeNull();
+
+ const { findByTestId } = render(result!);
+ expect(await findByTestId('mockHost')).toHaveTextContent('my-host');
+ });
+
+ it('should pass hit to Host element when provided', async () => {
+ const mockHit = {
+ id: 'test-doc-id',
+ raw: { _id: 'test-doc-id', _index: 'test-index' },
+ flattened: { 'host.name': 'my-host' },
+ } as unknown as Parameters[2];
+
+ const result = buildFlyoutContent('host.name', 'my-host', mockHit);
+
+ expect(result).not.toBeNull();
+
+ const { findByTestId } = render(result!);
+ expect(await findByTestId('mockHost')).toHaveAttribute('data-has-hit', 'true');
});
it('should return null for an unknown field', () => {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/utils/build_flyout_content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/utils/build_flyout_content.tsx
index 3623dde976fb2..982a65dad97e9 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/utils/build_flyout_content.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout_v2/shared/utils/build_flyout_content.tsx
@@ -5,16 +5,23 @@
* 2.0.
*/
-import React from 'react';
+import React, { lazy, Suspense } from 'react';
+import type { DataTableRecord } from '@kbn/discover-utils';
import { getEcsField } from '../../../flyout/document_details/right/components/table_field_name_cell';
import {
+ HOST_NAME_FIELD_NAME,
IP_FIELD_TYPE,
LEGACY_SIGNAL_RULE_NAME_FIELD_NAME,
SIGNAL_RULE_NAME_FIELD_NAME,
} from '../../../timelines/components/timeline/body/renderers/constants';
import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network';
-import { Network } from '../../network/main';
-import { RuleDetails } from '../../rule/main';
+import { FlyoutLoading } from '../components/flyout_loading';
+
+const Host = lazy(() => import('../../entity/host/main').then((m) => ({ default: m.Host })));
+const Network = lazy(() => import('../../network/main').then((m) => ({ default: m.Network })));
+const RuleDetails = lazy(() => import('../../rule/main').then((m) => ({ default: m.RuleDetails })));
+
+const SuspenseFallback = ;
/**
* Returns the React element to render inside the system flyout for the given field/value,
@@ -23,8 +30,13 @@ import { RuleDetails } from '../../rule/main';
* Currently supports:
* - IP fields → Network details flyout (value = IP address)
* - Rule name field → Rule details flyout (value = rule ID)
+ * - Host name → Host details flyout (pass hit for entity resolution)
*/
-export const buildFlyoutContent = (field: string, value: string): React.ReactElement | null => {
+export const buildFlyoutContent = (
+ field: string,
+ value: string,
+ hit?: DataTableRecord
+): React.ReactElement | null => {
const ecsField = getEcsField(field);
if (ecsField?.type === IP_FIELD_TYPE) {
@@ -32,11 +44,27 @@ export const buildFlyoutContent = (field: string, value: string): React.ReactEle
? FlowTargetSourceDest.destination
: FlowTargetSourceDest.source;
- return ;
+ return (
+
+
+
+ );
}
if (field === SIGNAL_RULE_NAME_FIELD_NAME || field === LEGACY_SIGNAL_RULE_NAME_FIELD_NAME) {
- return ;
+ return (
+
+
+
+ );
+ }
+
+ if (field === HOST_NAME_FIELD_NAME) {
+ return (
+
+
+
+ );
}
return null;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_header_component/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_header_component/index.test.tsx
index fe1fab3084f5a..cff623edd8762 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_header_component/index.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_header_component/index.test.tsx
@@ -59,6 +59,9 @@ jest.mock('../../cases/components/provider/provider', () => ({
jest.mock('../../assistant/provider', () => ({
AssistantProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
}));
+jest.mock('../../common/components/ml/permissions/ml_capabilities_provider', () => ({
+ MlCapabilitiesProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
jest.mock('../../common/hooks/is_in_security_app', () => ({
useIsInSecurityApp: jest.fn(),
}));
@@ -82,6 +85,21 @@ describe('AlertFlyoutHeader', () => {
},
},
upselling: {},
+ data: {
+ query: {
+ timefilter: {
+ timefilter: {
+ getAbsoluteTime: jest.fn().mockReturnValue({
+ from: '2023-01-01T00:00:00.000Z',
+ to: '2023-12-31T23:59:59.999Z',
+ }),
+ },
+ },
+ },
+ },
+ notifications: {
+ toasts: { addError: jest.fn(), addDanger: jest.fn(), addSuccess: jest.fn() },
+ },
} as unknown as StartServices;
it('wraps the header in KibanaContextProvider and ReactQueryClientProvider', async () => {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_overview_tab_component/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_overview_tab_component/index.test.tsx
index a297010557ba3..643c9fdfa5bc8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_overview_tab_component/index.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/one_discover/alert_flyout_overview_tab_component/index.test.tsx
@@ -43,6 +43,9 @@ jest.mock('../../cases/components/provider/provider', () => ({
jest.mock('../../assistant/provider', () => ({
AssistantProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
}));
+jest.mock('../../common/components/ml/permissions/ml_capabilities_provider', () => ({
+ MlCapabilitiesProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
const mockUseInitDataViewManager = jest.fn();
jest.mock('../../data_view_manager/hooks/use_init_data_view_manager', () => ({
@@ -73,6 +76,21 @@ describe('AlertFlyoutOverviewTab', () => {
},
},
upselling: {},
+ data: {
+ query: {
+ timefilter: {
+ timefilter: {
+ getAbsoluteTime: jest.fn().mockReturnValue({
+ from: '2023-01-01T00:00:00.000Z',
+ to: '2023-12-31T23:59:59.999Z',
+ }),
+ },
+ },
+ },
+ },
+ notifications: {
+ toasts: { addError: jest.fn(), addDanger: jest.fn(), addSuccess: jest.fn() },
+ },
} as unknown as StartServices;
beforeEach(() => {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx
index 917a5837263f2..fc5de0b995c55 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx
@@ -23,6 +23,7 @@ import type { StartServices } from '../../types';
import type { SecurityAppStore } from '../../common/store/types';
import { IpCellRenderer } from './ip_cell_renderer';
import { RuleNameCellRenderer } from './rule_name_cell_renderer';
+import { HostCellRenderer, HOST_CELL_RENDERER_FIELDS } from './host_cell_renderer';
import { ONE_DISCOVER_SCOPE_ID } from '../constants';
export type SecuritySolutionRowCellRendererGetter = Awaited<
@@ -35,18 +36,19 @@ export type SecuritySolutionRowCellRendererGetter = Awaited<
* in Discover's contextual View
* Also see: src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/constants.ts
*/
-const ALLOWED_DISCOVER_RENDERED_FIELDS = [
+const ALLOWED_DISCOVER_RENDERED_FIELDS = new Set([
SIGNAL_STATUS_FIELD_NAME,
SIGNAL_RULE_NAME_FIELD_NAME,
LEGACY_SIGNAL_RULE_NAME_FIELD_NAME,
-];
+ ...HOST_CELL_RENDERER_FIELDS,
+]);
export const getCellRendererForGivenRecord = (
services: StartServices,
store: SecurityAppStore
): SecuritySolutionRowCellRendererGetter => {
return (fieldName: string) => {
- if (ALLOWED_DISCOVER_RENDERED_FIELDS.includes(fieldName)) {
+ if (ALLOWED_DISCOVER_RENDERED_FIELDS.has(fieldName)) {
if (
fieldName === SIGNAL_RULE_NAME_FIELD_NAME ||
fieldName === LEGACY_SIGNAL_RULE_NAME_FIELD_NAME
@@ -56,6 +58,12 @@ export const getCellRendererForGivenRecord = (
};
}
+ if (HOST_CELL_RENDERER_FIELDS.has(fieldName)) {
+ return function HostFieldRenderer(props: DataGridCellValueElementProps) {
+ return ;
+ };
+ }
+
return function UnifiedFieldRenderBySecuritySolution(props: DataGridCellValueElementProps) {
// convert discover data format to timeline data format
const data: TimelineNonEcsData[] = useMemo(
diff --git a/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/host_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/host_cell_renderer.tsx
new file mode 100644
index 0000000000000..4023027b6f681
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/host_cell_renderer.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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, { useCallback, useMemo } from 'react';
+import { EuiLink } from '@elastic/eui';
+import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
+import { useHistory } from 'react-router-dom';
+import { DOC_VIEWER_FLYOUT_HISTORY_KEY } from '@kbn/unified-doc-viewer';
+import { getOrEmptyTagFromValue } from '../../common/components/empty_value';
+import { flyoutProviders } from '../../flyout_v2/shared/components/flyout_provider';
+import { useDefaultDocumentFlyoutProperties } from '../../flyout_v2/shared/hooks/use_default_flyout_properties';
+import { documentFlyoutHistoryKey } from '../../flyout_v2/shared/constants/flyout_history';
+import { DataViewManagerBootstrap } from '../alert_flyout_overview_tab_component/data_view_manager_bootstrap';
+import { Host } from '../../flyout_v2/entity/host/main';
+import type { StartServices } from '../../types';
+import type { SecurityAppStore } from '../../common/store/types';
+import { useIsInSecurityApp } from '../../common/hooks/is_in_security_app';
+
+export const HOST_CELL_RENDERER_FIELDS = new Set(['host.name', 'host.hostname']);
+
+export interface HostCellRendererProps extends DataGridCellValueElementProps {
+ services: StartServices;
+ store: SecurityAppStore;
+}
+
+/**
+ * Cell renderer for host-related columns in One Discover.
+ * Renders each value as a clickable link that opens the host details flyout.
+ */
+export const HostCellRenderer = React.memo(
+ ({ services, store, ...props }) => {
+ const history = useHistory();
+ const isInSecurityApp = useIsInSecurityApp();
+ const historyKey = isInSecurityApp ? documentFlyoutHistoryKey : DOC_VIEWER_FLYOUT_HISTORY_KEY;
+ const { overlays } = services;
+ const defaultDocumentFlyoutProperties = useDefaultDocumentFlyoutProperties();
+ const rawValue = props.row.flattened[props.columnId];
+
+ const values: string[] = useMemo(() => {
+ if (Array.isArray(rawValue)) return rawValue.map(String);
+ if (rawValue != null) return [String(rawValue)];
+ return [];
+ }, [rawValue]);
+
+ const handleClick = useCallback(
+ (hostName: string) => {
+ if (!hostName) return;
+
+ overlays.openSystemFlyout(
+ flyoutProviders({
+ services,
+ store,
+ history,
+ children: (
+ <>
+ {!isInSecurityApp && }
+
+ >
+ ),
+ }),
+ {
+ ...defaultDocumentFlyoutProperties,
+ historyKey,
+ session: 'start',
+ }
+ );
+ },
+ [
+ overlays,
+ services,
+ store,
+ history,
+ isInSecurityApp,
+ historyKey,
+ props.row,
+ defaultDocumentFlyoutProperties,
+ ]
+ );
+
+ if (values.length === 0) {
+ return getOrEmptyTagFromValue(null);
+ }
+
+ return (
+ <>
+ {values.map((val, idx) => (
+
+ {idx > 0 && ', '}
+ handleClick(val)} data-test-subj="one-discover-host-link">
+ {val}
+
+
+ ))}
+ >
+ );
+ }
+);
+
+HostCellRenderer.displayName = 'HostCellRenderer';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.test.tsx
index 4abb8e090e02a..106fac51ef241 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.test.tsx
@@ -24,6 +24,10 @@ jest.mock('../../common/lib/kibana', () => ({
}),
}));
+jest.mock('../../common/hooks/is_in_security_app', () => ({
+ useIsInSecurityApp: () => false,
+}));
+
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({ push: jest.fn(), location: { pathname: '/' } }),
diff --git a/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.tsx
index 85ce0047d0a73..8481526a1fd85 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/one_discover/cell_renderers/ip_cell_renderer.tsx
@@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import { EuiLink } from '@elastic/eui';
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import { useHistory } from 'react-router-dom';
+import { useIsInSecurityApp } from '../../common/hooks/is_in_security_app';
import { getOrEmptyTagFromValue } from '../../common/components/empty_value';
import { flyoutProviders } from '../../flyout_v2/shared/components/flyout_provider';
import { useDefaultDocumentFlyoutProperties } from '../../flyout_v2/shared/hooks/use_default_flyout_properties';
@@ -30,6 +31,7 @@ export interface IpCellRendererProps extends DataGridCellValueElementProps {
*/
export const IpCellRenderer = React.memo(({ services, store, ...props }) => {
const history = useHistory();
+ const isInSecurityApp = useIsInSecurityApp();
const defaultDocumentFlyoutProperties = useDefaultDocumentFlyoutProperties();
const { overlays } = services;
const rawValue = props.row.flattened[props.columnId];
@@ -51,7 +53,7 @@ export const IpCellRenderer = React.memo(({ services, store
history,
children: (
<>
-
+ {!isInSecurityApp && }
{flyoutContent}
>
),
@@ -63,7 +65,15 @@ export const IpCellRenderer = React.memo(({ services, store
);
}
},
- [defaultDocumentFlyoutProperties, overlays, services, store, history, props.columnId]
+ [
+ props.columnId,
+ overlays,
+ services,
+ store,
+ history,
+ isInSecurityApp,
+ defaultDocumentFlyoutProperties,
+ ]
);
if (addresses.length === 0) {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx
index b6b8923c77246..c1644d94ea0f7 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx
@@ -20,7 +20,7 @@ import { useKibana } from '../../../common/lib/kibana';
import { NetworkDetailsLink } from '../../../common/components/links';
import { NetworkPanelKey } from '../../../flyout/network_details';
import { FlyoutLink } from '../../../flyout/shared/components/flyout_link';
-import { ChildLink } from '../../../flyout_v2/shared/components/child_link';
+import { OpenFlyoutLink } from '../../../flyout_v2/shared/components/open_flyout_link';
import { Network } from '../../../flyout_v2/network/main';
import { flyoutProviders } from '../../../flyout_v2/shared/components/flyout_provider';
import { useDefaultDocumentFlyoutProperties } from '../../../flyout_v2/shared/hooks/use_default_flyout_properties';
@@ -134,7 +134,12 @@ const AddressLinksItemComponent: React.FC = ({
title={title}
/>
) : newFlyoutSystemEnabled ? (
-
+
) : (
= ({
entityId,
}) => {
const { openFlyout } = useExpandableFlyoutApi();
+ const { services } = useKibana();
+ const { overlays } = services;
+ const store = useStore();
+ const history = useHistory();
+ const newFlyoutSystemEnabled = useIsExperimentalFeatureEnabled('newFlyoutSystemEnabled');
+ const defaultDocumentFlyoutProperties = useDefaultDocumentFlyoutProperties();
const isInSecurityApp = useIsInSecurityApp();
+ const historyKey = isInSecurityApp ? documentFlyoutHistoryKey : DOC_VIEWER_FLYOUT_HISTORY_KEY;
+
const eventContext = useContext(StatefulEventContext);
const hostName = `${value}`;
const isInTimelineContext = hostName && eventContext?.timelineID;
@@ -51,28 +68,55 @@ const HostNameComponent: React.FC = ({
onClick();
}
- /*
- * if and only if renderer is running inside security solution app
- * we check for event and timeline context
- * */
if (!eventContext || !isInTimelineContext || !eventContext.enableHostDetailsFlyout) {
return;
}
- const { timelineID } = eventContext;
- openFlyout({
- right: {
- id: HostPanelKey,
- params: {
- hostName,
- entityId,
- contextID: contextId,
- scopeId: timelineID,
+ if (newFlyoutSystemEnabled) {
+ overlays.openSystemFlyout(
+ flyoutProviders({
+ services,
+ store,
+ history,
+ children: ,
+ }),
+ {
+ ...defaultDocumentFlyoutProperties,
+ historyKey,
+ session: 'start',
+ }
+ );
+ } else {
+ const { timelineID } = eventContext;
+ openFlyout({
+ right: {
+ id: HostPanelKey,
+ params: {
+ hostName,
+ entityId,
+ contextID: contextId,
+ scopeId: timelineID,
+ },
},
- },
- });
+ });
+ }
},
- [onClick, eventContext, isInTimelineContext, hostName, entityId, openFlyout, contextId]
+ [
+ onClick,
+ eventContext,
+ isInTimelineContext,
+ hostName,
+ entityId,
+ openFlyout,
+ contextId,
+ newFlyoutSystemEnabled,
+ overlays,
+ services,
+ store,
+ history,
+ historyKey,
+ defaultDocumentFlyoutProperties,
+ ]
);
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
index 2275a53d67d96..29ad3ac7e8f70 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
@@ -90,6 +90,10 @@ const extractEuiIconText = (str: string) => {
jest.mock('../../../../../../common/lib/kibana');
+jest.mock('../host_name', () => ({
+ HostName: () => null,
+}));
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {