From 57ae12ccae8678a769dea690593be8ef07c417cf Mon Sep 17 00:00:00 2001
From: christineweng <18648970+christineweng@users.noreply.github.com>
Date: Thu, 5 Sep 2024 15:04:17 -0500
Subject: [PATCH] [Security Solution][Alert Flyout] Enable network preview and
previews in table tab (#190560)
## Summary
This PR is a refactor of preview links in document details flyout:
1) Replace all the `EuiLink` component with a shared `PreviewLink` that
renders a link based on field
2) Added IP (network) flyout previews through out document details
flyout
3) Added preview capabilities for fields in table tab
**Network previews**
**After**
https://github.com/user-attachments/assets/451292f6-3056-4362-87e4-e8170f0bda3f
**Exceptions**
IP addresses in entity details section are not yet worked on, as it is
owned by the Explore team and therefore has impacts outside of alerts
flyout.
**How to test**
- Using the normal resolver script should generate host, user and ip
data for testing.
- To test ip's in highlighted fields, user should create a custom rule
and add some ip fields to highlighted fields
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../left/components/host_details.test.tsx | 16 ++-
.../left/components/host_details.tsx | 60 ++++----
.../components/prevalence_details.test.tsx | 31 ++--
.../left/components/prevalence_details.tsx | 110 +++-----------
.../left/components/test_ids.ts | 10 +-
.../left/components/user_details.test.tsx | 14 +-
.../left/components/user_details.tsx | 60 ++++----
.../left/tabs/insights_tab.tsx | 4 +-
.../document_details/preview/footer.test.tsx | 12 ++
.../right/components/cell_actions.tsx | 66 ---------
.../right/components/highlighted_fields.tsx | 2 +-
.../highlighted_fields_cell.test.tsx | 85 +++++++----
.../components/highlighted_fields_cell.tsx | 122 ++++------------
.../components/host_entity_overview.test.tsx | 14 ++
.../right/components/host_entity_overview.tsx | 67 ++++-----
.../right/components/severity.tsx | 2 +-
.../right/components/status.tsx | 2 +-
.../components/table_field_name_cell.tsx | 5 +-
.../table_field_value_cell.test.tsx | 120 ++++++++++++----
.../components/table_field_value_cell.tsx | 17 ++-
.../right/components/test_ids.ts | 2 +
.../right/components/user_entity_overview.tsx | 66 ++++-----
.../document_details/right/tabs/table_tab.tsx | 4 +-
.../components/cell_actions.tsx | 6 +-
.../shared/components/test_ids.ts | 1 +
.../security_solution/public/flyout/index.tsx | 2 +-
.../public/flyout/network_details/index.tsx | 9 ++
.../shared/components/preview_link.test.tsx | 135 ++++++++++++++++++
.../flyout/shared/components/preview_link.tsx | 127 ++++++++++++++++
.../netflow/__snapshots__/index.test.tsx.snap | 18 +--
.../timeline/body/renderers/constants.tsx | 1 +
...rt_details_left_panel_prevalence_tab.cy.ts | 11 +-
...alert_details_left_panel_prevalence_tab.ts | 6 +-
33 files changed, 729 insertions(+), 478 deletions(-)
delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/right/components/cell_actions.tsx
rename x-pack/plugins/security_solution/public/flyout/document_details/{left => shared}/components/cell_actions.tsx (90%)
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx
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 20e2979647e408..33b8bb22fce53f 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';
import type { NarrowDateRange } from '../../../../common/components/ml/types';
const HOST_DETAILS_ID = 'entities-hosts-details';
@@ -129,24 +132,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,
@@ -181,14 +166,14 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s
),
render: (user: string) => (
-
+
{isPreviewEnabled ? (
- openUserPreview(user)}
- >
- {user}
-
+ />
) : (
<>{user}>
)}
@@ -208,10 +193,23 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s
return (
(ip != null ? : getEmptyTagValue())}
+ render={(ip) =>
+ ip == null ? (
+ getEmptyTagValue()
+ ) : isPreviewEnabled ? (
+
+ ) : (
+
+ )
+ }
scopeId={scopeId}
/>
);
@@ -235,7 +233,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..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
@@ -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';
@@ -32,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;
@@ -151,19 +162,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 +184,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..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
@@ -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';
@@ -95,13 +86,9 @@ interface PrevalenceDetailsRow extends PrevalenceData {
*/
isPreviewEnabled: boolean;
/**
- * Callback to open host preview
- */
- openHostPreview: (hostName: string) => void;
- /**
- * Callback to open user preview
+ * Scope id to pass to the preview link
*/
- openUserPreview: (userName: string) => void;
+ scopeId: string;
}
const columns: Array> = [
@@ -128,33 +115,27 @@ 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}
+
);
})}
@@ -348,8 +329,6 @@ const columns: Array> = [
export const PrevalenceDetails: React.FC = () => {
const { dataFormattedForFieldBrowser, investigationFields, scopeId } =
useDocumentDetailsContext();
- const { openPreviewPanel } = useExpandableFlyoutApi();
- const { telemetry } = useKibana().services;
const isPlatinumPlus = useLicense().isPlatinumPlus();
const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled');
@@ -395,42 +374,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 +383,9 @@ export const PrevalenceDetails: React.FC = () => {
to: absoluteEnd,
isPlatinumPlus,
isPreviewEnabled,
- openHostPreview,
- openUserPreview,
+ scopeId,
})),
- [
- data,
- absoluteStart,
- absoluteEnd,
- isPlatinumPlus,
- isPreviewEnabled,
- openHostPreview,
- openUserPreview,
- ]
+ [data, absoluteStart, absoluteEnd, isPlatinumPlus, isPreviewEnabled, scopeId]
);
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 1e377d14dd7c38..13d3e825053ba5 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';
import type { NarrowDateRange } from '../../../../common/components/ml/types';
const USER_DETAILS_ID = 'entities-users-details';
@@ -130,24 +133,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,
@@ -182,14 +167,14 @@ export const UserDetails: React.FC = ({ userName, timestamp, s
),
render: (host: string) => (
-
+
{isPreviewEnabled ? (
- openHostPreview(host)}
- >
- {host}
-
+ />
) : (
<>{host}>
)}
@@ -209,10 +194,23 @@ export const UserDetails: React.FC = ({ userName, timestamp, s
return (
(ip != null ? : getEmptyTagValue())}
+ render={(ip) =>
+ ip == null ? (
+ getEmptyTagValue()
+ ) : isPreviewEnabled ? (
+
+ ) : (
+
+ )
+ }
scopeId={scopeId}
/>
);
@@ -236,7 +234,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/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/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..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
@@ -26,12 +26,25 @@ 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';
+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');
@@ -61,26 +74,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 +98,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 +114,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 +130,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 +153,9 @@ describe('', () => {
});
const { getByTestId } = render(
-
+
+
+
);
@@ -147,11 +170,13 @@ describe('', () => {
const { getByTestId } = render(
-
+
+
+
);
@@ -166,11 +191,13 @@ describe('', () => {
const { getByTestId } = render(
-
+
+
+
);
@@ -179,7 +206,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..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
@@ -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,20 @@ 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 ? (
{
+ 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 815bbcf93e02aa..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
@@ -10,6 +10,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
+ EuiText,
EuiIcon,
useEuiTheme,
useEuiFontSize,
@@ -20,6 +21,7 @@ import { getOr } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
+import { HOST_NAME_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
import { useDocumentDetailsContext } from '../../shared/context';
import type { DescriptionList } from '../../../../../common/utility_types';
@@ -36,7 +38,7 @@ import { useSourcererDataView } from '../../../../sourcerer/containers';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details';
import { getField } from '../../shared/utils';
-import { CellActions } from './cell_actions';
+import { CellActions } from '../../shared/components/cell_actions';
import {
FAMILY,
LAST_SEEN,
@@ -54,8 +56,7 @@ import {
import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys';
import { LeftPanelInsightsTab } from '../../left';
import { RiskScoreDocTooltip } from '../../../../overview/components/common';
-import { HostPreviewPanelKey } from '../../../entity_details/host_right';
-import { useKibana } from '../../../../common/lib/kibana';
+import { PreviewLink } from '../../../shared/components/preview_link';
const HOST_ICON = 'storage';
@@ -79,9 +80,7 @@ export const HOST_PREVIEW_BANNER = {
*/
export const HostEntityOverview: React.FC = ({ 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,34 @@ 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..16f72108913eb6 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,11 +8,48 @@
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';
+import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
+
+jest.mock('@kbn/expandable-flyout', () => ({
+ 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;
-const contextId = 'test';
+const panelContextValue = {
+ eventId: 'event id',
+ indexName: 'indexName',
+ scopeId: 'scopeId',
+} as unknown as DocumentDetailsContext;
+
+const scopeId = 'scopeId';
const eventId = 'TUWyf3wBFCFU0qRJTauW';
@@ -31,16 +68,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 +99,15 @@ describe('TableFieldValueCell', () => {
beforeEach(() => {
render(
-
+
+
+
);
});
@@ -98,13 +145,15 @@ describe('TableFieldValueCell', () => {
beforeEach(() => {
render(
-
+
+
+
);
});
@@ -132,13 +181,15 @@ describe('TableFieldValueCell', () => {
beforeEach(() => {
render(
-
+
+
+
);
});
@@ -158,5 +209,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..e72b9755f3ca95 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,17 +8,20 @@
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 {
/**
* Value used to create a unique identifier in children components
*/
- contextId: string;
+ scopeId: string;
/**
* Datq retrieved from the row
*/
@@ -46,13 +49,14 @@ export interface FieldValueCellProps {
*/
export const TableFieldValueCell = memo(
({
- contextId,
+ scopeId,
data,
eventId,
fieldFromBrowserField,
getLinkValue,
values,
}: FieldValueCellProps) => {
+ const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled');
if (values == null) {
return null;
}
@@ -78,9 +82,16 @@ 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,34 @@ 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..9bd1cf26bc83d4 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';
@@ -108,7 +108,7 @@ export const getColumns: ColumnsProvider = ({ browserFields, eventId, scopeId, g
return (
= ({ field, value, isObjectArray,
return (
{
/**
* IP value
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx
new file mode 100644
index 00000000000000..6ffc418c7e54c4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx
@@ -0,0 +1,135 @@
+/*
+ * 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 { FLYOUT_PREVIEW_LINK_TEST_ID } from '../../document_details/shared/components/test_ids';
+import { PreviewLink, hasPreview } from './preview_link';
+import { TestProviders } from '../../../common/mock';
+import { mockFlyoutApi } from '../../document_details/shared/mocks/mock_flyout_context';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+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';
+import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
+
+const mockedTelemetry = createTelemetryServiceMock();
+jest.mock('../../../common/lib/kibana', () => {
+ 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 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: '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: '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,
+ },
+ });
+ });
+});
+
+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/shared/components/preview_link.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx
new file mode 100644
index 00000000000000..ceb1f216703c7b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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 { 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 '../../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 {
+ /**
+ * 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;
+ /**
+ * Scope id to use for the preview panel
+ */
+ scopeId: 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;
+}
+
+/**
+ * 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 { openPreviewPanel } = useExpandableFlyoutApi();
+ const { telemetry } = useKibana().services;
+
+ const onClick = useCallback(() => {
+ const previewParams = getPreviewParams(value, field, scopeId);
+ if (previewParams) {
+ openPreviewPanel({
+ id: previewParams.id,
+ params: previewParams.params,
+ });
+ 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/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');