diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx index 7ba3d7823f24d4..46288434f48bb2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx @@ -23,6 +23,7 @@ import { HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID, HOST_DETAILS_LINK_TEST_ID, HOST_DETAILS_RELATED_USERS_LINK_TEST_ID, + HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID, } from './test_ids'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; @@ -33,6 +34,7 @@ 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'; jest.mock('@kbn/expandable-flyout'); @@ -164,7 +166,7 @@ describe('', () => { expect(queryByTestId(HOST_DETAILS_LINK_TEST_ID)).not.toBeInTheDocument(); }); - it('should render host name as clicable link when feature flag is true', () => { + it('should render host name as clicable link when preview is not disabled', () => { mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { getByTestId } = renderHostDetails(mockContextValue); expect(getByTestId(HOST_DETAILS_LINK_TEST_ID)).toBeInTheDocument(); @@ -268,7 +270,7 @@ describe('', () => { ); }); - it('should render user name as clicable link when feature flag is true', () => { + it('should render user name as clicable link when preview is not disabled', () => { mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { getAllByTestId } = renderHostDetails(mockContextValue); expect(getAllByTestId(HOST_DETAILS_RELATED_USERS_LINK_TEST_ID).length).toBe(1); @@ -282,6 +284,16 @@ describe('', () => { banner: USER_PREVIEW_BANNER, }, }); + + getAllByTestId(HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID)[0].click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: NetworkPanelKey, + params: { + ip: '100.XXX.XXX', + flowTarget: 'source', + banner: NETWORK_PREVIEW_BANNER, + }, + }); }); }); }); 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 49813f43d3de1c..348fd86615c4f2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx @@ -18,7 +18,6 @@ import { EuiToolTip, EuiIcon, EuiPanel, - EuiLink, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -36,7 +35,7 @@ import { RiskScoreEntity } from '../../../../../common/search_strategy'; import { RiskScoreLevel } from '../../../../entity_analytics/components/severity/common'; import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/default_renderer'; import { InputsModelId } from '../../../../common/store/inputs/constants'; -import { CellActions } from './cell_actions'; +import { CellActions } from '../../shared/components/cell_actions'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { manageQuery } from '../../../../common/components/page/manage_query'; @@ -51,14 +50,18 @@ import { HOST_DETAILS_TEST_ID, HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID, HOST_DETAILS_RELATED_USERS_LINK_TEST_ID, + HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID, } from './test_ids'; +import { + USER_NAME_FIELD_NAME, + HOST_IP_FIELD_NAME, +} from '../../../../timelines/components/timeline/body/renderers/constants'; 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 { 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'; const HOST_DETAILS_ID = 'entities-hosts-details'; const RELATED_USERS_ID = 'entities-hosts-related-users'; @@ -128,24 +131,6 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s }); }, [openPreviewPanel, hostName, scopeId, telemetry]); - const openUserPreview = useCallback( - (userName: string) => { - openPreviewPanel({ - id: UserPreviewPanelKey, - params: { - userName, - scopeId, - banner: USER_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, - [openPreviewPanel, scopeId, telemetry] - ); - const [isHostLoading, { inspect, hostDetails, refetch }] = useHostDetails({ id: hostDetailsQueryId, startDate: from, @@ -180,14 +165,13 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s ), render: (user: string) => ( - + {isPreviewEnabled ? ( - openUserPreview(user)} - > - {user} - + /> ) : ( <>{user} )} @@ -207,10 +191,22 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s return ( (ip != null ? : getEmptyTagValue())} + render={(ip) => + ip == null ? ( + getEmptyTagValue() + ) : isPreviewEnabled ? ( + + ) : ( + + ) + } scopeId={scopeId} /> ); @@ -234,7 +230,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s ] : []), ], - [isEntityAnalyticsAuthorized, scopeId, isPreviewEnabled, openUserPreview] + [isEntityAnalyticsAuthorized, scopeId, isPreviewEnabled] ); const relatedUsersCount = useMemo( 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 f1abc48f0e87a3..4146e504516bbf 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 @@ -18,8 +18,7 @@ import { PREVALENCE_DETAILS_UPSELL_TEST_ID, PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, - PREVALENCE_DETAILS_TABLE_HOST_LINK_CELL_TEST_ID, - PREVALENCE_DETAILS_TABLE_USER_LINK_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_UPSELL_CELL_TEST_ID, } from './test_ids'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; @@ -151,19 +150,19 @@ describe('PrevalenceDetails', () => { ).toBeGreaterThan(1); expect(queryByTestId(PREVALENCE_DETAILS_UPSELL_TEST_ID)).not.toBeInTheDocument(); expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument(); - expect(queryByTestId(PREVALENCE_DETAILS_TABLE_HOST_LINK_CELL_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(PREVALENCE_DETAILS_TABLE_USER_LINK_CELL_TEST_ID)).not.toBeInTheDocument(); + expect( + queryByTestId(PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID) + ).not.toBeInTheDocument(); }); - it('should render host and user name as clickable link if feature flag is true', () => { + it('should render host and user name as clickable link if preview is enabled', () => { mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); (usePrevalence as jest.Mock).mockReturnValue(mockPrevelanceReturnValue); - const { getByTestId } = renderPrevalenceDetails(); - expect(getByTestId(PREVALENCE_DETAILS_TABLE_HOST_LINK_CELL_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(PREVALENCE_DETAILS_TABLE_USER_LINK_CELL_TEST_ID)).toBeInTheDocument(); + const { getAllByTestId } = renderPrevalenceDetails(); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID)).toHaveLength(2); - getByTestId(PREVALENCE_DETAILS_TABLE_HOST_LINK_CELL_TEST_ID).click(); + getAllByTestId(PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID)[0].click(); expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ id: HostPreviewPanelKey, params: { @@ -173,7 +172,7 @@ describe('PrevalenceDetails', () => { }, }); - getByTestId(PREVALENCE_DETAILS_TABLE_USER_LINK_CELL_TEST_ID).click(); + getAllByTestId(PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID)[1].click(); expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ id: UserPreviewPanelKey, params: { 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 5aae6c4b392696..5b52e515272652 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 @@ -6,7 +6,7 @@ */ import dateMath from '@elastic/datemath'; -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useState } from 'react'; import type { EuiBasicTableColumn, OnTimeChangeProps } from '@elastic/eui'; import { EuiCallOut, @@ -22,7 +22,6 @@ import { useEuiTheme, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { FormattedCount } from '../../../../common/components/formatted_number'; import { useLicense } from '../../../../common/hooks/use_license'; @@ -34,9 +33,8 @@ import { PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, - PREVALENCE_DETAILS_TABLE_HOST_LINK_CELL_TEST_ID, - PREVALENCE_DETAILS_TABLE_USER_LINK_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, PREVALENCE_DETAILS_DATE_PICKER_TEST_ID, PREVALENCE_DETAILS_TABLE_TEST_ID, @@ -50,15 +48,8 @@ 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 { useKibana } from '../../../../common/lib/kibana'; -import { - HOST_NAME_FIELD_NAME, - USER_NAME_FIELD_NAME, -} from '../../../../timelines/components/timeline/body/renderers/constants'; -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 { hasPreview, PreviewLink } from '../../shared/components/preview_link'; +import { CellActions } from '../../shared/components/cell_actions'; export const PREVALENCE_TAB_ID = 'prevalence'; const DEFAULT_FROM = 'now-30d'; @@ -94,14 +85,6 @@ interface PrevalenceDetailsRow extends PrevalenceData { * If enabled, clicking host or user should open an entity preview */ isPreviewEnabled: boolean; - /** - * Callback to open host preview - */ - openHostPreview: (hostName: string) => void; - /** - * Callback to open user preview - */ - openUserPreview: (userName: string) => void; } const columns: Array> = [ @@ -128,33 +111,26 @@ const columns: Array> = [ render: (data: PrevalenceDetailsRow) => ( {data.values.map((value) => { - if (data.isPreviewEnabled && data.field === HOST_NAME_FIELD_NAME) { - return ( - - data.openHostPreview(value)} - > - {value} - - - ); - } - if (data.isPreviewEnabled && data.field === USER_NAME_FIELD_NAME) { + if (data.isPreviewEnabled && hasPreview(data.field)) { return ( - data.openUserPreview(value)} - > - {value} - + + + {value} + + ); } return ( - {value} + + {value} + ); })} @@ -346,10 +322,7 @@ 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, scopeId } = - useDocumentDetailsContext(); - const { openPreviewPanel } = useExpandableFlyoutApi(); - const { telemetry } = useKibana().services; + const { dataFormattedForFieldBrowser, investigationFields } = useDocumentDetailsContext(); const isPlatinumPlus = useLicense().isPlatinumPlus(); const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled'); @@ -395,42 +368,6 @@ export const PrevalenceDetails: React.FC = () => { }, }); - const openHostPreview = useCallback( - (hostName: string) => { - openPreviewPanel({ - id: HostPreviewPanelKey, - params: { - hostName, - scopeId, - banner: HOST_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, - [openPreviewPanel, scopeId, telemetry] - ); - - const openUserPreview = useCallback( - (userName: string) => { - openPreviewPanel({ - id: UserPreviewPanelKey, - params: { - userName, - scopeId, - banner: USER_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, - [openPreviewPanel, scopeId, telemetry] - ); - // add timeRange to pass it down to timeline and license to drive the rendering of the last 2 prevalence columns const items = useMemo( () => @@ -440,18 +377,8 @@ export const PrevalenceDetails: React.FC = () => { to: absoluteEnd, isPlatinumPlus, isPreviewEnabled, - openHostPreview, - openUserPreview, })), - [ - data, - absoluteStart, - absoluteEnd, - isPlatinumPlus, - isPreviewEnabled, - openHostPreview, - openUserPreview, - ] + [data, absoluteStart, absoluteEnd, isPlatinumPlus, isPreviewEnabled] ); const upsell = ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts index fc940979ae05f5..23be7cc9b801fe 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts @@ -25,10 +25,8 @@ export const PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID = `${PREVALENCE_DETAILS_TABLE_TEST_ID}FieldCell` as const; export const PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID = `${PREVALENCE_DETAILS_TABLE_TEST_ID}ValueCell` as const; -export const PREVALENCE_DETAILS_TABLE_HOST_LINK_CELL_TEST_ID = - `${PREVALENCE_DETAILS_TABLE_TEST_ID}HostCell` as const; -export const PREVALENCE_DETAILS_TABLE_USER_LINK_CELL_TEST_ID = - `${PREVALENCE_DETAILS_TABLE_TEST_ID}UserCell` as const; +export const PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID = + `${PREVALENCE_DETAILS_TABLE_TEST_ID}PreviewLinkCell` as const; export const PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID = `${PREVALENCE_DETAILS_TABLE_TEST_ID}AlertCountCell` as const; export const PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID = @@ -49,6 +47,8 @@ export const USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID = `${USER_DETAILS_TEST_ID}RelatedHostsTable` as const; export const USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID = `${USER_DETAILS_TEST_ID}RelatedHostsLink` as const; +export const USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID = + `${USER_DETAILS_TEST_ID}RelatedHostsIPLink` as const; export const USER_DETAILS_INFO_TEST_ID = 'user-overview' as const; export const HOST_DETAILS_TEST_ID = `${PREFIX}HostsDetails` as const; @@ -57,6 +57,8 @@ export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID = `${HOST_DETAILS_TEST_ID}RelatedUsersTable` as const; export const HOST_DETAILS_RELATED_USERS_LINK_TEST_ID = `${HOST_DETAILS_TEST_ID}RelatedUsersLink` as const; +export const HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID = + `${HOST_DETAILS_TEST_ID}RelatedUsersIPLink` as const; export const HOST_DETAILS_INFO_TEST_ID = 'host-overview' as const; /* Threat Intelligence */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx index ddf0b51f929b8b..c1ed881e80a952 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx @@ -23,6 +23,7 @@ import { USER_DETAILS_INFO_TEST_ID, USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID, USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID, + USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID, } from './test_ids'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; @@ -33,6 +34,7 @@ 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'; jest.mock('@kbn/expandable-flyout'); @@ -250,7 +252,7 @@ describe('', () => { ); }); - it('should render host name as clicable link when feature flag is true', () => { + it('should render host name and ip as clicable link when preview is enabled', () => { mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { getAllByTestId } = renderUserDetails(mockContextValue); expect(getAllByTestId(USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID).length).toBe(1); @@ -264,6 +266,16 @@ describe('', () => { banner: HOST_PREVIEW_BANNER, }, }); + + getAllByTestId(USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID)[0].click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: NetworkPanelKey, + params: { + ip: '100.XXX.XXX', + flowTarget: 'source', + banner: NETWORK_PREVIEW_BANNER, + }, + }); }); }); }); 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 1a93eab1c46e4b..5e3228e88ea800 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx @@ -18,7 +18,6 @@ import { EuiFlexItem, EuiToolTip, EuiPanel, - EuiLink, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -35,7 +34,7 @@ import { NetworkDetailsLink } from '../../../../common/components/links'; import { RiskScoreEntity } from '../../../../../common/search_strategy'; import { RiskScoreLevel } from '../../../../entity_analytics/components/severity/common'; import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/default_renderer'; -import { CellActions } from './cell_actions'; +import { CellActions } from '../../shared/components/cell_actions'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useSourcererDataView } from '../../../../sourcerer/containers'; @@ -51,14 +50,18 @@ import { USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID, USER_DETAILS_TEST_ID, USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID, + USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID, } from './test_ids'; +import { + HOST_NAME_FIELD_NAME, + HOST_IP_FIELD_NAME, +} from '../../../../timelines/components/timeline/body/renderers/constants'; import { useKibana } from '../../../../common/lib/kibana'; import { ENTITY_RISK_LEVEL } from '../../../../entity_analytics/components/risk_score/translations'; import { useHasSecurityCapability } from '../../../../helper_hooks'; -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 { PreviewLink } from '../../shared/components/preview_link'; const USER_DETAILS_ID = 'entities-users-details'; const RELATED_HOSTS_ID = 'entities-users-related-hosts'; @@ -129,24 +132,6 @@ export const UserDetails: React.FC = ({ userName, timestamp, s }); }, [openPreviewPanel, userName, scopeId, telemetry]); - const openHostPreview = useCallback( - (hostName: string) => { - openPreviewPanel({ - id: HostPreviewPanelKey, - params: { - hostName, - scopeId, - banner: HOST_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, - [openPreviewPanel, scopeId, telemetry] - ); - const [isUserLoading, { inspect, userDetails, refetch }] = useObservedUserDetails({ id: userDetailsQueryId, startDate: from, @@ -181,14 +166,13 @@ export const UserDetails: React.FC = ({ userName, timestamp, s ), render: (host: string) => ( - + {isPreviewEnabled ? ( - openHostPreview(host)} - > - {host} - + /> ) : ( <>{host} )} @@ -208,10 +192,22 @@ export const UserDetails: React.FC = ({ userName, timestamp, s return ( (ip != null ? : getEmptyTagValue())} + render={(ip) => + ip == null ? ( + getEmptyTagValue() + ) : isPreviewEnabled ? ( + + ) : ( + + ) + } scopeId={scopeId} /> ); @@ -235,7 +231,7 @@ export const UserDetails: React.FC = ({ userName, timestamp, s ] : []), ], - [isEntityAnalyticsAuthorized, scopeId, openHostPreview, isPreviewEnabled] + [isEntityAnalyticsAuthorized, scopeId, isPreviewEnabled] ); const relatedHostsCount = useMemo( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx index 99072977ac9827..e37a6daf19a8fe 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx @@ -24,7 +24,7 @@ import { import { useDocumentDetailsContext } from '../../shared/context'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '..'; -import { ENTITIES_TAB_ID, EntitiesDetails } from '../components/entities_details'; +import { EntitiesDetails } from '../components/entities_details'; import { THREAT_INTELLIGENCE_TAB_ID, ThreatIntelligenceDetails, @@ -34,6 +34,8 @@ import { CORRELATIONS_TAB_ID, CorrelationsDetails } from '../components/correlat import { getField } from '../../shared/utils'; import { EventKind } from '../../shared/constants/event_kinds'; +const ENTITIES_TAB_ID = 'entity'; + const insightsButtons: EuiButtonGroupOptionProps[] = [ { id: ENTITIES_TAB_ID, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/cell_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/cell_actions.tsx deleted file mode 100644 index 96a0f2b100291a..00000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/cell_actions.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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, { useMemo } from 'react'; -import { useDocumentDetailsContext } from '../../shared/context'; -import { getSourcererScopeId } from '../../../../helpers'; -import { SecurityCellActionType } from '../../../../app/actions/constants'; -import { - CellActionsMode, - SecurityCellActions, - SecurityCellActionsTrigger, -} from '../../../../common/components/cell_actions'; - -interface CellActionsProps { - /** - * Field name - */ - field: string; - /** - * Field value - */ - value: string[] | string | null | undefined; - /** - * Boolean to indicate if value is an object array - */ - isObjectArray?: boolean; - /** - * React components to render - */ - children: React.ReactNode | string; -} - -/** - * Security cell action wrapper for document details flyout - */ -export const CellActions: FC = ({ field, value, isObjectArray, children }) => { - const { scopeId, isPreview } = useDocumentDetailsContext(); - - const data = useMemo(() => ({ field, value }), [field, value]); - const metadata = useMemo(() => ({ scopeId, isObjectArray }), [scopeId, isObjectArray]); - const disabledActionTypes = useMemo( - () => (isPreview ? [SecurityCellActionType.FILTER, SecurityCellActionType.TOGGLE_COLUMN] : []), - [isPreview] - ); - - return ( - - {children} - - ); -}; - -CellActions.displayName = 'CellActions'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx index 32e170bf757d4b..3824860bf56774 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx @@ -14,7 +14,7 @@ import { convertHighlightedFieldsToTableRow } from '../../shared/utils/highlight import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { HighlightedFieldsCell } from './highlighted_fields_cell'; -import { CellActions } from './cell_actions'; +import { CellActions } from '../../shared/components/cell_actions'; import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids'; import { useDocumentDetailsContext } from '../../shared/context'; import { useHighlightedFields } from '../../shared/hooks/use_highlighted_fields'; 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 0426d9861b93cc..e3d2513ae462b1 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 @@ -26,6 +26,7 @@ import { HostPreviewPanelKey } from '../../../entity_details/host_right'; 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'; jest.mock('../../../../management/hooks'); jest.mock('../../../../management/hooks/agents/use_get_agent_status'); @@ -61,26 +62,16 @@ describe('', () => { it('should render a basic cell', () => { const { getByTestId } = render( - + + + ); expect(getByTestId(HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID)).toBeInTheDocument(); }); - it('should render a link cell if field is `host.name`', () => { - const { getByTestId } = renderHighlightedFieldsCell(['value'], 'host.name'); - - expect(getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID)).toBeInTheDocument(); - }); - - it('should render a link cell if field is `user.name`', () => { - const { getByTestId } = renderHighlightedFieldsCell(['value'], 'user.name'); - - expect(getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID)).toBeInTheDocument(); - }); - - it('should open left panel when clicking on the link within a a link cell when feature flag is off', () => { + it('should open left panel when clicking on the link within a a link cell when preview is disabled', () => { const { getByTestId } = renderHighlightedFieldsCell(['value'], 'user.name'); getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID).click(); @@ -95,9 +86,10 @@ describe('', () => { }); }); - it('should open host preview when click on host when feature flag is on', () => { + it('should open host preview when click on host when preview is not disabled', () => { mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { getByTestId } = renderHighlightedFieldsCell(['test host'], 'host.name'); + expect(getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID)).toBeInTheDocument(); getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID).click(); expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ @@ -110,9 +102,10 @@ describe('', () => { }); }); - it('should open user preview when click on user when feature flag is on', () => { + it('should open user preview when click on user when preview is not disabled', () => { mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { getByTestId } = renderHighlightedFieldsCell(['test user'], 'user.name'); + expect(getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID)).toBeInTheDocument(); getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID).click(); expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ @@ -125,6 +118,22 @@ describe('', () => { }); }); + it('should open ip preview when click on ip when preview is not disabled', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + const { getByTestId } = renderHighlightedFieldsCell(['100:XXX:XXX'], 'source.ip'); + expect(getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID)).toBeInTheDocument(); + + getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: NetworkPanelKey, + params: { + ip: '100:XXX:XXX', + flowTarget: 'source', + banner: NETWORK_PREVIEW_BANNER, + }, + }); + }); + it('should render agent status cell if field is `agent.status`', () => { useGetAgentStatusMock.mockReturnValue({ isFetched: true, @@ -132,7 +141,9 @@ describe('', () => { }); const { getByTestId } = render( - + + + ); @@ -147,11 +158,13 @@ describe('', () => { const { getByTestId } = render( - + + + ); @@ -166,11 +179,13 @@ describe('', () => { const { getByTestId } = render( - + + + ); @@ -179,7 +194,9 @@ describe('', () => { it('should not render if values is null', () => { const { container } = render( - + + + ); 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 c23ebe3b89c3db..aa1794b4e84906 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 @@ -13,15 +13,10 @@ import { getAgentTypeForAgentIdField } from '../../../../common/lib/endpoint/uti import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants'; import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status'; import { useDocumentDetailsContext } from '../../shared/context'; +import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { - AGENT_STATUS_FIELD_NAME, - HOST_NAME_FIELD_NAME, - USER_NAME_FIELD_NAME, -} from '../../../../timelines/components/timeline/body/renderers/constants'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '../../left'; -import { useKibana } from '../../../../common/lib/kibana'; import { ENTITIES_TAB_ID } from '../../left/components/entities_details'; import { HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID, @@ -29,31 +24,34 @@ import { HIGHLIGHTED_FIELDS_CELL_TEST_ID, HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID, } from './test_ids'; -import { HostPreviewPanelKey } from '../../../entity_details/host_right'; -import { HOST_PREVIEW_BANNER } from './host_entity_overview'; -import { UserPreviewPanelKey } from '../../../entity_details/user_right'; -import { USER_PREVIEW_BANNER } from './user_entity_overview'; +import { hasPreview, PreviewLink } from '../../shared/components/preview_link'; -interface LinkFieldCellProps { +export interface HighlightedFieldsCellProps { /** - * Highlighted field's field name + * Highlighted field's name used to know what component to display */ field: string; /** - * Highlighted field's value to display as a EuiLink to open the expandable left panel - * (used for host name and username fields) + * Highlighted field's original name, when the field is overridden + */ + originalField?: string; + /** + * Highlighted field's value to display */ - value: string; + values: string[] | null | undefined; } /** - * // Currently we can use the same component for both host name and username + * Renders a component in the highlighted fields table cell based on the field name */ -const LinkFieldCell: VFC = ({ field, value }) => { +export const HighlightedFieldsCell: VFC = ({ + values, + field, + originalField = '', +}) => { const { scopeId, eventId, indexName } = useDocumentDetailsContext(); - const { openLeftPanel, openPreviewPanel } = useExpandableFlyoutApi(); + const { openLeftPanel } = useExpandableFlyoutApi(); const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled'); - const { telemetry } = useKibana().services; const goToInsightsEntities = useCallback(() => { openLeftPanel({ @@ -67,76 +65,6 @@ const LinkFieldCell: VFC = ({ field, value }) => { }); }, [eventId, indexName, openLeftPanel, scopeId]); - const openHostPreview = useCallback(() => { - openPreviewPanel({ - id: HostPreviewPanelKey, - params: { - hostName: value, - scopeId, - banner: HOST_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, [openPreviewPanel, value, scopeId, telemetry]); - - const openUserPreview = useCallback(() => { - openPreviewPanel({ - id: UserPreviewPanelKey, - params: { - userName: value, - scopeId, - banner: USER_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, [openPreviewPanel, value, scopeId, telemetry]); - - const onClick = useMemo(() => { - if (isPreviewEnabled && field === HOST_NAME_FIELD_NAME) { - return openHostPreview; - } - if (isPreviewEnabled && field === USER_NAME_FIELD_NAME) { - return openUserPreview; - } - return goToInsightsEntities; - }, [isPreviewEnabled, field, openHostPreview, openUserPreview, goToInsightsEntities]); - - return ( - - {value} - - ); -}; - -export interface HighlightedFieldsCellProps { - /** - * Highlighted field's name used to know what component to display - */ - field: string; - /** - * Highlighted field's original name, when the field is overridden - */ - originalField?: string; - /** - * Highlighted field's value to display - */ - values: string[] | null | undefined; -} - -/** - * Renders a component in the highlighted fields table cell based on the field name - */ -export const HighlightedFieldsCell: VFC = ({ - values, - field, - originalField = '', -}) => { const agentType: ResponseActionAgentType = useMemo(() => { return getAgentTypeForAgentIdField(originalField); }, [originalField]); @@ -151,8 +79,19 @@ export const HighlightedFieldsCell: VFC = ({ key={`${i}-${value}`} data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`} > - {field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? ( - + {isPreviewEnabled && hasPreview(field) ? ( + + ) : hasPreview(field) ? ( + + {value} + ) : field === AGENT_STATUS_FIELD_NAME ? ( = ({ hostName }) => { const { eventId, indexName, scopeId } = useDocumentDetailsContext(); - const { openLeftPanel, openPreviewPanel } = useExpandableFlyoutApi(); - const { telemetry } = useKibana().services; - + const { openLeftPanel } = useExpandableFlyoutApi(); const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled'); const goToEntitiesTab = useCallback(() => { @@ -95,22 +94,6 @@ export const HostEntityOverview: React.FC = ({ hostName }, }); }, [eventId, openLeftPanel, indexName, scopeId]); - - const openHostPreview = useCallback(() => { - openPreviewPanel({ - id: HostPreviewPanelKey, - params: { - hostName, - scopeId, - banner: HOST_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, [openPreviewPanel, hostName, scopeId, telemetry]); - const { from, to } = useGlobalTime(); const { selectedPatterns } = useSourcererDataView(); @@ -172,7 +155,7 @@ export const HostEntityOverview: React.FC = ({ hostName description: ( @@ -223,16 +206,33 @@ export const HostEntityOverview: React.FC = ({ hostName - - {hostName} - + {isPreviewEnabled ? ( + + + {hostName} + + + ) : ( + + {hostName} + + )} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx index 0ecd0928697c44..7ae0d243d236f3 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { ALERT_SEVERITY } from '@kbn/rule-data-utils'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; -import { CellActions } from './cell_actions'; +import { CellActions } from '../../shared/components/cell_actions'; import { useDocumentDetailsContext } from '../../shared/context'; import { SeverityBadge } from '../../../../common/components/severity_badge'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.tsx index c75e1426da10bb..420eb7feafa4a3 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.tsx @@ -16,7 +16,7 @@ import { StatusPopoverButton } from './status_popover_button'; import { useDocumentDetailsContext } from '../../shared/context'; import type { EnrichedFieldInfo, EnrichedFieldInfoWithValues } from '../utils/enriched_field_info'; import { getEnrichedFieldInfo } from '../utils/enriched_field_info'; -import { CellActions } from './cell_actions'; +import { CellActions } from '../../shared/components/cell_actions'; import { STATUS_TITLE_TEST_ID } from './test_ids'; /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_name_cell.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_name_cell.tsx index 81657e3280ec82..19600b7aaf4b52 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_name_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_name_cell.tsx @@ -17,11 +17,14 @@ import { } from './test_ids'; import { getExampleText } from '../../../../common/components/event_details/helpers'; -const getEcsField = (field: string): { example?: string; description?: string } | undefined => { +export const getEcsField = ( + field: string +): { example?: string; description?: string; type?: string } | undefined => { return EcsFlat[field as keyof typeof EcsFlat] as | { example?: string; description?: string; + type?: string; } | undefined; }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx index f4c0f54a11f02b..91682cc17c1b02 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx @@ -8,9 +8,29 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import type { FieldSpec } from '@kbn/data-plugin/common'; +import { DocumentDetailsContext } from '../../shared/context'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { EventFieldsData } from '../../../../common/components/event_details/types'; import { TableFieldValueCell } from './table_field_value_cell'; import { TestProviders } from '../../../../common/mock'; +import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details'; +import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; +import { FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID } from './test_ids'; + +jest.mock('@kbn/expandable-flyout', () => ({ + useExpandableFlyoutApi: jest.fn(), + ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}, +})); + +jest.mock('../../../../common/hooks/use_experimental_features'); +const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; + +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + scopeId: 'scopeId', +} as unknown as DocumentDetailsContext; const contextId = 'test'; @@ -31,16 +51,24 @@ const hostIpData: EventFieldsData = { const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', 'fe80::4001:aff:fec8:32']; describe('TableFieldValueCell', () => { + beforeAll(() => { + jest.clearAllMocks(); + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + }); + describe('common behavior', () => { beforeEach(() => { render( - + + + ); }); @@ -54,13 +82,15 @@ describe('TableFieldValueCell', () => { beforeEach(() => { render( - + + + ); }); @@ -98,13 +128,15 @@ describe('TableFieldValueCell', () => { beforeEach(() => { render( - + + + ); }); @@ -132,13 +164,15 @@ describe('TableFieldValueCell', () => { beforeEach(() => { render( - + + + ); }); @@ -158,5 +192,18 @@ describe('TableFieldValueCell', () => { expect(screen.getByText(value)).toBeInTheDocument(); }); }); + + it('should open preview when preview is not disabled', () => { + screen.getByTestId(`${FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID}-0`).click(); + + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: NetworkPanelKey, + params: { + ip: '127.0.0.1', + flowTarget: 'source', + banner: NETWORK_PREVIEW_BANNER, + }, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx index 86950fa31837f6..867c64e8fb88eb 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx @@ -8,11 +8,14 @@ import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import type { FieldSpec } from '@kbn/data-plugin/common'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { getFieldFormat } from '../utils/get_field_format'; import type { EventFieldsData } from '../../../../common/components/event_details/types'; import { OverflowField } from '../../../../common/components/tables/helpers'; import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; import { MESSAGE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants'; +import { FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID } from './test_ids'; +import { hasPreview, PreviewLink } from '../../shared/components/preview_link'; export interface FieldValueCellProps { /** @@ -53,6 +56,7 @@ export const TableFieldValueCell = memo( getLinkValue, values, }: FieldValueCellProps) => { + const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled'); if (values == null) { return null; } @@ -78,6 +82,12 @@ export const TableFieldValueCell = memo( {data.field === MESSAGE_FIELD_NAME ? ( + ) : isPreviewEnabled && hasPreview(data.field) ? ( + ) : ( = ({ userName }) => { const { eventId, indexName, scopeId } = useDocumentDetailsContext(); - const { openLeftPanel, openPreviewPanel } = useExpandableFlyoutApi(); - const { telemetry } = useKibana().services; + const { openLeftPanel } = useExpandableFlyoutApi(); const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled'); @@ -95,22 +95,6 @@ export const UserEntityOverview: React.FC = ({ userName }, }); }, [eventId, openLeftPanel, indexName, scopeId]); - - const openUserPreview = useCallback(() => { - openPreviewPanel({ - id: UserPreviewPanelKey, - params: { - userName, - scopeId, - banner: USER_PREVIEW_BANNER, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'preview', - }); - }, [openPreviewPanel, userName, scopeId, telemetry]); - const { from, to } = useGlobalTime(); const { selectedPatterns } = useSourcererDataView(); @@ -170,7 +154,7 @@ export const UserEntityOverview: React.FC = ({ userName description: ( @@ -222,16 +206,33 @@ export const UserEntityOverview: React.FC = ({ userName - - {userName} - + {isPreviewEnabled ? ( + + + {userName} + + + ) : ( + + {userName} + + )} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx index 181ba46a67e2a2..3eb4672b37d835 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx @@ -23,7 +23,7 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { timelineDefaults } from '../../../../timelines/store/defaults'; import { timelineSelectors } from '../../../../timelines/store'; import type { EventFieldsData } from '../../../../common/components/event_details/types'; -import { CellActions } from '../components/cell_actions'; +import { CellActions } from '../../shared/components/cell_actions'; import { useDocumentDetailsContext } from '../../shared/context'; import { isInTableScope, isTimelineScope } from '../../../../helpers'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/cell_actions.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/components/cell_actions.tsx index 173520ce2d55ee..e6973d2e83c699 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/cell_actions.tsx @@ -7,7 +7,7 @@ import type { FC } from 'react'; import React, { useMemo } from 'react'; -import { useDocumentDetailsContext } from '../../shared/context'; +import { useDocumentDetailsContext } from '../context'; import { getSourcererScopeId } from '../../../../helpers'; import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { SecurityCellActionType } from '../../../../app/actions/constants'; @@ -57,7 +57,7 @@ export const CellActions: FC = ({ field, value, isObjectArray, return ( ({ + useExpandableFlyoutApi: jest.fn(), + ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}, +})); + +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + scopeId: 'scopeId', +} as unknown as DocumentDetailsContext; + +const renderPreviewLink = (field: string, value: string, dataTestSuj?: string) => + render( + + + + + + ); + +describe('', () => { + beforeAll(() => { + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + + it('should not render a link if field does not have preview', () => { + const { queryByTestId } = renderPreviewLink('field', 'value'); + expect(queryByTestId(FLYOUT_PREVIEW_LINK_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render children without link if field does not have preview', () => { + const { queryByTestId, getByTestId } = render( + + + +
{'children'}
+
+
+
+ ); + + expect(queryByTestId(FLYOUT_PREVIEW_LINK_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId('children')).toBeInTheDocument(); + }); + + it('should render a link to open host preview', () => { + const { getByTestId } = renderPreviewLink('host.name', 'host', 'host-link'); + getByTestId('host-link').click(); + + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: HostPreviewPanelKey, + params: { + hostName: 'host', + scopeId: panelContextValue.scopeId, + banner: HOST_PREVIEW_BANNER, + }, + }); + }); + + it('should render a link to open user preview', () => { + const { getByTestId } = renderPreviewLink('user.name', 'user', 'user-link'); + getByTestId('user-link').click(); + + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: UserPreviewPanelKey, + params: { + userName: 'user', + scopeId: panelContextValue.scopeId, + banner: USER_PREVIEW_BANNER, + }, + }); + }); + + it('should render a link to open network preview', () => { + const { getByTestId } = renderPreviewLink('source.ip', '100:XXX:XXX', 'ip-link'); + getByTestId('ip-link').click(); + + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: NetworkPanelKey, + params: { + ip: '100:XXX:XXX', + flowTarget: 'source', + banner: NETWORK_PREVIEW_BANNER, + }, + }); + }); +}); 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/document_details/shared/components/preview_link.tsx new file mode 100644 index 00000000000000..dda6ee380dc313 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/preview_link.tsx @@ -0,0 +1,120 @@ +/* + * 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, { 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 { + 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'; + +interface PreviewLinkProps { + /** + * Highlighted field's field name + */ + field: string; + /** + * Highlighted field's value to display as a EuiLink to open the expandable left panel + * (used for host name and username fields) + */ + value: string; + /** + * Optional data-test-subj value + */ + ['data-test-subj']?: string; + /** + * React components to render, if none provided, the value will be rendered + */ + 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, + 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) { + 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, + }, + }); + telemetry.reportDetailsFlyoutOpened({ + location: scopeId, + panel: 'preview', + }); + } + }, [field, scopeId, value, telemetry, openPreviewPanel]); + + if (!hasPreview(field)) { + return <>{children ?? value}; + } + + return ( + + {children ?? value} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts index eb462cf0987177..9939ca27886b7d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts @@ -8,3 +8,4 @@ import { PREFIX } from '../../../shared/test_ids'; export const FLYOUT_TOUR_TEST_ID = `${PREFIX}Tour` as const; +export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx index e3f2bb8c82d8c0..8b0cc7f7992d02 100644 --- a/x-pack/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/index.tsx @@ -9,7 +9,6 @@ import React, { memo, useCallback } from 'react'; import { ExpandableFlyout, type ExpandableFlyoutProps } from '@kbn/expandable-flyout'; import { useEuiTheme } from '@elastic/eui'; import type { NetworkExpandableFlyoutProps } from './network_details'; -import { NetworkPanel, NetworkPanelKey } from './network_details'; import { Flyouts } from './document_details/shared/constants/flyouts'; import { DocumentDetailsIsolateHostPanelKey, @@ -41,6 +40,7 @@ import type { HostPanelExpandableFlyoutProps } from './entity_details/host_right import { HostPanel, HostPanelKey, HostPreviewPanelKey } from './entity_details/host_right'; import type { HostDetailsExpandableFlyoutProps } from './entity_details/host_details_left'; import { HostDetailsPanel, HostDetailsPanelKey } from './entity_details/host_details_left'; +import { NetworkPanel, NetworkPanelKey } from './network_details'; /** * List of all panels that will be used within the document details expandable flyout. diff --git a/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx b/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx index 59985e98d561f3..d7e9d3519e4b28 100644 --- a/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx @@ -7,6 +7,7 @@ import React, { memo } from 'react'; import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { i18n } from '@kbn/i18n'; import type { FlowTargetSourceDest } from '../../../common/search_strategy'; import { PanelHeader } from './header'; import { PanelContent } from './content'; @@ -18,6 +19,14 @@ export interface NetworkExpandableFlyoutProps extends FlyoutPanelProps { export const NetworkPanelKey: NetworkExpandableFlyoutProps['key'] = 'network-details'; +export const NETWORK_PREVIEW_BANNER = { + title: i18n.translate('xpack.securitySolution.flyout.right.network.networkPreviewTitle', { + defaultMessage: 'Preview network details', + }), + backgroundColor: 'warning', + textColor: 'warning', +}; + export interface NetworkPanelProps extends Record { /** * IP value diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap index 2f8eb7d1fbb57c..ca2c9900772be0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap @@ -119,13 +119,12 @@ tr:hover .c2:focus::before { margin-right: 5px; } -.c14 { - position: relative; - top: 1px; +.c6 { + margin-right: 3px; } -.c13 { - margin-right: 5px; +.c7 { + margin: 0 5px; } .c16 { @@ -156,12 +155,13 @@ tr:hover .c2:focus::before { margin: 0 5px; } -.c6 { - margin-right: 3px; +.c14 { + position: relative; + top: 1px; } -.c7 { - margin: 0 5px; +.c13 { + margin-right: 5px; } .c11 { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index 9308204e693181..030005b45ac62c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -8,6 +8,7 @@ export const DATE_FIELD_TYPE = 'date'; export const HOST_NAME_FIELD_NAME = 'host.name'; export const USER_NAME_FIELD_NAME = 'user.name'; +export const HOST_IP_FIELD_NAME = 'host.ip'; export const IP_FIELD_TYPE = 'ip'; export const GEO_FIELD_TYPE = 'geo_point'; export const MESSAGE_FIELD_NAME = 'message'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts index 354f0e01647718..c05f423ac45812 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts @@ -19,8 +19,7 @@ import { DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL, DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL, DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_DATE_PICKER, - DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_CELL, - DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_CELL, + DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_LINK_CELL, } from '../../../../screens/expandable_flyout/alert_details_left_panel_prevalence_tab'; import { HOST_PANEL_HEADER, @@ -100,8 +99,8 @@ describe( ); }); - it('should open host preview when click on host details title', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_CELL).click(); + it('should open host preview when click on host name', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_LINK_CELL).eq(0).click(); cy.get(PREVIEW_SECTION).should('exist'); cy.get(PREVIEW_BANNER).should('have.text', 'Preview host details'); @@ -116,8 +115,8 @@ describe( cy.get(HOST_PREVIEW_PANEL_FOOTER).should('not.exist'); }); - it('should open user preview when click on user details title', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_CELL).click(); + it('should open user preview when click on user name', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_LINK_CELL).eq(1).click(); cy.get(PREVIEW_SECTION).should('exist'); cy.get(PREVIEW_BANNER).should('have.text', 'Preview user details'); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_left_panel_prevalence_tab.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_left_panel_prevalence_tab.ts index 5fe3d1ef613386..81a5d341e03f32 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_left_panel_prevalence_tab.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_left_panel_prevalence_tab.ts @@ -25,7 +25,5 @@ export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALEN export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL = getDataTestSubjectSelector('securitySolutionFlyoutPrevalenceDetailsTableUserPrevalenceCell'); -export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_CELL = - getDataTestSubjectSelector('securitySolutionFlyoutPrevalenceDetailsTableHostCell'); -export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_CELL = - getDataTestSubjectSelector('securitySolutionFlyoutPrevalenceDetailsTableUserCell'); +export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_LINK_CELL = + getDataTestSubjectSelector('securitySolutionFlyoutPrevalenceDetailsTablePreviewLinkCell');