diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx index 348fd86615c4f2..a72e07e8526814 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx @@ -59,7 +59,7 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import { ENTITY_RISK_LEVEL } from '../../../../entity_analytics/components/risk_score/translations'; import { useHasSecurityCapability } from '../../../../helper_hooks'; -import { PreviewLink } from '../../shared/components/preview_link'; +import { PreviewLink } from '../../../shared/components/preview_link'; import { HostPreviewPanelKey } from '../../../entity_details/host_right'; import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; @@ -170,6 +170,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s ) : ( @@ -201,6 +202,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s ) : ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx index 4146e504516bbf..e8b1b71e9cd0fd 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx @@ -31,9 +31,21 @@ import { HostPreviewPanelKey } from '../../../entity_details/host_right'; import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; +import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; jest.mock('@kbn/expandable-flyout'); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); + jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx index 5b52e515272652..6491bc9ad27475 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx @@ -48,7 +48,7 @@ import { } from '../../../../common/components/event_details/use_action_cell_data_provider'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { IS_OPERATOR } from '../../../../../common/types'; -import { hasPreview, PreviewLink } from '../../shared/components/preview_link'; +import { hasPreview, PreviewLink } from '../../../shared/components/preview_link'; import { CellActions } from '../../shared/components/cell_actions'; export const PREVALENCE_TAB_ID = 'prevalence'; @@ -85,6 +85,10 @@ interface PrevalenceDetailsRow extends PrevalenceData { * If enabled, clicking host or user should open an entity preview */ isPreviewEnabled: boolean; + /** + * Scope id to pass to the preview link + */ + scopeId: string; } const columns: Array> = [ @@ -118,6 +122,7 @@ const columns: Array> = [ {value} @@ -322,7 +327,8 @@ const columns: Array> = [ * Prevalence table displayed in the document details expandable flyout left section under the Insights tab */ export const PrevalenceDetails: React.FC = () => { - const { dataFormattedForFieldBrowser, investigationFields } = useDocumentDetailsContext(); + const { dataFormattedForFieldBrowser, investigationFields, scopeId } = + useDocumentDetailsContext(); const isPlatinumPlus = useLicense().isPlatinumPlus(); const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled'); @@ -377,8 +383,9 @@ export const PrevalenceDetails: React.FC = () => { to: absoluteEnd, isPlatinumPlus, isPreviewEnabled, + scopeId, })), - [data, absoluteStart, absoluteEnd, isPlatinumPlus, isPreviewEnabled] + [data, absoluteStart, absoluteEnd, isPlatinumPlus, isPreviewEnabled, scopeId] ); const upsell = ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx index 5e3228e88ea800..9e3b2dd77397c1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx @@ -61,7 +61,7 @@ import { ENTITY_RISK_LEVEL } from '../../../../entity_analytics/components/risk_ import { useHasSecurityCapability } from '../../../../helper_hooks'; import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; -import { PreviewLink } from '../../shared/components/preview_link'; +import { PreviewLink } from '../../../shared/components/preview_link'; const USER_DETAILS_ID = 'entities-users-details'; const RELATED_HOSTS_ID = 'entities-users-related-hosts'; @@ -171,6 +171,7 @@ export const UserDetails: React.FC = ({ userName, timestamp, s ) : ( @@ -202,6 +203,7 @@ export const UserDetails: React.FC = ({ userName, timestamp, s ) : ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx index 2bf185a44942d8..4abd9ab7763aca 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.test.tsx @@ -14,9 +14,21 @@ import { mockContextValue } from '../shared/mocks/mock_context'; import { DocumentDetailsContext } from '../shared/context'; import { PreviewPanelFooter } from './footer'; import { PREVIEW_FOOTER_TEST_ID, PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids'; +import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; jest.mock('@kbn/expandable-flyout'); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); + describe('', () => { beforeAll(() => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx index e3d2513ae462b1..d819365da00b7a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx @@ -27,12 +27,24 @@ import { HOST_PREVIEW_BANNER } from './host_entity_overview'; import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from './user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details'; +import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; jest.mock('../../../../management/hooks'); jest.mock('../../../../management/hooks/agents/use_get_agent_status'); jest.mock('@kbn/expandable-flyout'); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); + const useGetAgentStatusMock = useGetAgentStatus as jest.Mock; jest.mock('../../../../common/hooks/use_experimental_features'); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx index aa1794b4e84906..e11dee4a74adfb 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx @@ -24,7 +24,7 @@ import { HIGHLIGHTED_FIELDS_CELL_TEST_ID, HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID, } from './test_ids'; -import { hasPreview, PreviewLink } from '../../shared/components/preview_link'; +import { hasPreview, PreviewLink } from '../../../shared/components/preview_link'; export interface HighlightedFieldsCellProps { /** @@ -83,6 +83,7 @@ export const HighlightedFieldsCell: VFC = ({ ) : hasPreview(field) ? ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx index dcaf51761239d8..b710df84e1a130 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx @@ -28,6 +28,7 @@ import { LeftPanelInsightsTab } from '../../left'; import { ENTITIES_TAB_ID } from '../../left/components/entities_details'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; +import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; const hostName = 'host'; const osFamily = 'Windows'; @@ -46,6 +47,19 @@ const panelContextValue = { jest.mock('@kbn/expandable-flyout'); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); + jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx index c8f8f3de51ef32..ca6a68eb23be8e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx @@ -56,7 +56,7 @@ import { import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '../../left'; import { RiskScoreDocTooltip } from '../../../../overview/components/common'; -import { PreviewLink } from '../../shared/components/preview_link'; +import { PreviewLink } from '../../../shared/components/preview_link'; const HOST_ICON = 'storage'; @@ -210,6 +210,7 @@ export const HostEntityOverview: React.FC = ({ hostName ({ useExpandableFlyoutApi: jest.fn(), ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}, + withExpandableFlyoutProvider: (Component: React.ComponentType) => { + return (props: T) => { + return ; + }; + }, })); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); + jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; @@ -32,7 +49,7 @@ const panelContextValue = { scopeId: 'scopeId', } as unknown as DocumentDetailsContext; -const contextId = 'test'; +const scopeId = 'scopeId'; const eventId = 'TUWyf3wBFCFU0qRJTauW'; @@ -63,7 +80,7 @@ describe('TableFieldValueCell', () => { { { { ) : ( = ({ userName { + return { + useKibana: () => ({ + services: { + telemetry: mockedTelemetry, + }, + }), + }; +}); jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn(), ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}, + withExpandableFlyoutProvider: (Component: React.ComponentType) => { + return (props: T) => { + return ; + }; + }, })); -const panelContextValue = { - eventId: 'event id', - indexName: 'indexName', - scopeId: 'scopeId', -} as unknown as DocumentDetailsContext; - const renderPreviewLink = (field: string, value: string, dataTestSuj?: string) => render( - - - + ); @@ -52,11 +60,9 @@ describe('', () => { it('should render children without link if field does not have preview', () => { const { queryByTestId, getByTestId } = render( - - -
{'children'}
-
-
+ +
{'children'}
+
); @@ -72,7 +78,7 @@ describe('', () => { id: HostPreviewPanelKey, params: { hostName: 'host', - scopeId: panelContextValue.scopeId, + scopeId: 'scopeId', banner: HOST_PREVIEW_BANNER, }, }); @@ -86,7 +92,7 @@ describe('', () => { id: UserPreviewPanelKey, params: { userName: 'user', - scopeId: panelContextValue.scopeId, + scopeId: 'scopeId', banner: USER_PREVIEW_BANNER, }, }); @@ -106,3 +112,24 @@ describe('', () => { }); }); }); + +describe('hasPreview', () => { + it('should return true if field is host.name', () => { + expect(hasPreview('host.name')).toBe(true); + }); + + it('should return true if field is user.name', () => { + expect(hasPreview('user.name')).toBe(true); + }); + + it('should return true if field type is source.ip', () => { + expect(hasPreview('source.ip')).toBe(true); + expect(hasPreview('destination.ip')).toBe(true); + expect(hasPreview('host.ip')).toBe(true); + }); + + it('should return false if field is not host.name, user.name, or ip type', () => { + expect(hasPreview('field')).toBe(false); // non-ecs field + expect(hasPreview('event.category')).toBe(false); // ecs field but not ip type + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/preview_link.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx similarity index 50% rename from x-pack/plugins/security_solution/public/flyout/document_details/shared/components/preview_link.tsx rename to x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx index dda6ee380dc313..ceb1f216703c7b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/preview_link.tsx +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx @@ -8,21 +8,61 @@ import type { FC } from 'react'; import React, { useCallback } from 'react'; import { EuiLink } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { FlowTargetSourceDest } from '../../../../../common/search_strategy/security_solution/network'; -import { useDocumentDetailsContext } from '../context'; -import { getEcsField } from '../../right/components/table_field_name_cell'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; +import { getEcsField } from '../../document_details/right/components/table_field_name_cell'; import { HOST_NAME_FIELD_NAME, USER_NAME_FIELD_NAME, IP_FIELD_TYPE, -} from '../../../../timelines/components/timeline/body/renderers/constants'; -import { useKibana } from '../../../../common/lib/kibana'; -import { FLYOUT_PREVIEW_LINK_TEST_ID } from './test_ids'; -import { HostPreviewPanelKey } from '../../../entity_details/host_right'; -import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; -import { UserPreviewPanelKey } from '../../../entity_details/user_right'; -import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; -import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details'; +} from '../../../timelines/components/timeline/body/renderers/constants'; +import { useKibana } from '../../../common/lib/kibana'; +import { FLYOUT_PREVIEW_LINK_TEST_ID } from '../../document_details/shared/components/test_ids'; +import { HostPreviewPanelKey } from '../../entity_details/host_right'; +import { HOST_PREVIEW_BANNER } from '../../document_details/right/components/host_entity_overview'; +import { UserPreviewPanelKey } from '../../entity_details/user_right'; +import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview'; +import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details'; + +// Helper function to check if the field has a preview link +export const hasPreview = (field: string) => + field === HOST_NAME_FIELD_NAME || + field === USER_NAME_FIELD_NAME || + getEcsField(field)?.type === IP_FIELD_TYPE; + +interface PreviewParams { + id: string; + params: Record; +} + +// Helper get function to get the preview parameters +const getPreviewParams = (value: string, field: string, scopeId: string): PreviewParams | null => { + if (getEcsField(field)?.type === IP_FIELD_TYPE) { + return { + id: NetworkPanelKey, + params: { + ip: value, + flowTarget: field.includes(FlowTargetSourceDest.destination) + ? FlowTargetSourceDest.destination + : FlowTargetSourceDest.source, + banner: NETWORK_PREVIEW_BANNER, + }, + }; + } + switch (field) { + case HOST_NAME_FIELD_NAME: + return { + id: HostPreviewPanelKey, + params: { hostName: value, scopeId, banner: HOST_PREVIEW_BANNER }, + }; + case USER_NAME_FIELD_NAME: + return { + id: UserPreviewPanelKey, + params: { userName: value, scopeId, banner: USER_PREVIEW_BANNER }, + }; + default: + return null; + } +}; interface PreviewLinkProps { /** @@ -34,6 +74,10 @@ interface PreviewLinkProps { * (used for host name and username fields) */ value: string; + /** + * Scope id to use for the preview panel + */ + scopeId: string; /** * Optional data-test-subj value */ @@ -44,62 +88,25 @@ interface PreviewLinkProps { children?: React.ReactNode; } -// Helper function to check if the field has a preview link -export const hasPreview = (field: string) => - field === HOST_NAME_FIELD_NAME || - field === USER_NAME_FIELD_NAME || - getEcsField(field)?.type === IP_FIELD_TYPE; - /** * Renders a preview link for entities and ip addresses */ export const PreviewLink: FC = ({ field, value, + scopeId, children, 'data-test-subj': dataTestSubj = FLYOUT_PREVIEW_LINK_TEST_ID, }) => { - const { scopeId } = useDocumentDetailsContext(); const { openPreviewPanel } = useExpandableFlyoutApi(); const { telemetry } = useKibana().services; const onClick = useCallback(() => { - if (getEcsField(field)?.type === IP_FIELD_TYPE) { - openPreviewPanel({ - id: NetworkPanelKey, - params: { - ip: value, - flowTarget: field.includes(FlowTargetSourceDest.destination) - ? FlowTargetSourceDest.destination - : FlowTargetSourceDest.source, - banner: NETWORK_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - } else if (field === HOST_NAME_FIELD_NAME) { + const previewParams = getPreviewParams(value, field, scopeId); + if (previewParams) { openPreviewPanel({ - id: HostPreviewPanelKey, - params: { - hostName: value, - scopeId, - banner: HOST_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - } else if (field === USER_NAME_FIELD_NAME) { - openPreviewPanel({ - id: UserPreviewPanelKey, - params: { - userName: value, - scopeId, - banner: USER_PREVIEW_BANNER, - }, + id: previewParams.id, + params: previewParams.params, }); telemetry.reportDetailsFlyoutOpened({ location: scopeId,