From 5107c10b4c97b37cee4fbbe30c954ca285a200f7 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Mon, 16 Dec 2024 17:28:22 -0600 Subject: [PATCH] [Security Solution][Alert Details] - SessionView changes to render the detailed panel as a flyout preview panel --- .../left/components/session_view.tsx | 116 ++++++-- .../components/session_preview_container.tsx | 4 +- .../document_details/session_view/content.tsx | 36 +++ .../document_details/session_view/context.tsx | 147 ++++++++++ .../document_details/session_view/header.tsx | 59 ++++ .../document_details/session_view/index.tsx | 112 ++++++++ .../document_details/session_view/tabs.tsx | 58 ++++ .../session_view/tabs/alerts_tab.tsx | 124 ++++++++ .../session_view/tabs/metadata_tab.tsx | 40 +++ .../session_view/tabs/process_tab.tsx | 50 ++++ .../document_details/session_view/test_ids.ts | 12 + .../shared/constants/panel_keys.ts | 1 + .../document_details/shared/context.tsx | 26 +- .../hooks/use_session_view_config.test.tsx} | 20 +- .../hooks/use_session_view_config.ts} | 12 +- .../flyout/document_details/shared/types.tsx | 2 + .../security_solution/public/flyout/index.tsx | 12 + .../resolver/view/controls/show_panel.tsx | 2 +- .../components/process_tree_node/index.tsx | 7 +- .../public/components/session_view/index.tsx | 264 +++++++++++------- .../plugins/session_view/public/index.ts | 5 + .../plugins/session_view/public/types.ts | 14 + 22 files changed, 975 insertions(+), 148 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/content.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/context.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/header.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/index.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/alerts_tab.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/metadata_tab.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/process_tab.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/session_view/test_ids.ts rename x-pack/plugins/security_solution/public/flyout/document_details/{right/hooks/use_session_preview.test.tsx => shared/hooks/use_session_view_config.test.tsx} (79%) rename x-pack/plugins/security_solution/public/flyout/document_details/{right/hooks/use_session_preview.ts => shared/hooks/use_session_view_config.ts} (82%) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx index 3b45cd71b0a6f..dfb2c9821fc95 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx @@ -10,23 +10,23 @@ import React, { useCallback, useMemo } from 'react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { TableId } from '@kbn/securitysolution-data-table'; import { EuiPanel } from '@elastic/eui'; -import { - ANCESTOR_INDEX, - ENTRY_LEADER_ENTITY_ID, - ENTRY_LEADER_START, -} from '../../shared/constants/field_names'; -import { getField } from '../../shared/utils'; +import type { Process } from '@kbn/session-view-plugin/common'; +import type { CustomProcess } from '../../session_view/context'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { SESSION_VIEW_TEST_ID } from './test_ids'; import { isActiveTimeline } from '../../../../helpers'; import { useSourcererDataView } from '../../../../sourcerer/containers'; -import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys'; +import { + DocumentDetailsPreviewPanelKey, + DocumentDetailsSessionViewPanelKey, +} from '../../shared/constants/panel_keys'; import { useKibana } from '../../../../common/lib/kibana'; import { useDocumentDetailsContext } from '../../shared/context'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { detectionsTimelineIds } from '../../../../timelines/containers/helpers'; import { ALERT_PREVIEW_BANNER } from '../../preview/constants'; import { useLicense } from '../../../../common/hooks/use_license'; -import { useSessionPreview } from '../../right/hooks/use_session_preview'; +import { useSessionViewConfig } from '../../shared/hooks/use_session_view_config'; import { SessionViewNoDataMessage } from '../../shared/components/session_view_no_data_message'; import { DocumentEventTypes } from '../../../../common/lib/telemetry'; @@ -37,18 +37,22 @@ export const SESSION_VIEW_ID = 'session-view'; */ export const SessionView: FC = () => { const { sessionView, telemetry } = useKibana().services; - const { getFieldsData, indexName, scopeId, dataFormattedForFieldBrowser } = - useDocumentDetailsContext(); + const { + eventId, + indexName, + getFieldsData, + scopeId, + dataFormattedForFieldBrowser, + jumpToEntityId, + jumpToCursor, + } = useDocumentDetailsContext(); + + const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges; - const sessionViewConfig = useSessionPreview({ getFieldsData, dataFormattedForFieldBrowser }); + const sessionViewConfig = useSessionViewConfig({ getFieldsData, dataFormattedForFieldBrowser }); const isEnterprisePlus = useLicense().isEnterprise(); const isEnabled = sessionViewConfig && isEnterprisePlus; - const ancestorIndex = getField(getFieldsData(ANCESTOR_INDEX)); // e.g in case of alert, we want to grab it's origin index - const sessionEntityId = getField(getFieldsData(ENTRY_LEADER_ENTITY_ID)) || ''; - const sessionStartTime = getField(getFieldsData(ENTRY_LEADER_START)) || ''; - const index = ancestorIndex || indexName; - const sourcererScope = useMemo(() => { if (isActiveTimeline(scopeId)) { return SourcererScopeName.timeline; @@ -64,17 +68,24 @@ export const SessionView: FC = () => { const { openPreviewPanel } = useExpandableFlyoutApi(); const openAlertDetailsPreview = useCallback( - (eventId?: string, onClose?: () => void) => { - openPreviewPanel({ - id: DocumentDetailsPreviewPanelKey, - params: { - id: eventId, - indexName: eventDetailsIndex, - scopeId, - banner: ALERT_PREVIEW_BANNER, - isPreviewMode: true, - }, - }); + (evtId?: string, onClose?: () => void) => { + // In the SessionView component, when the user clicks on the + // expand button to open a alert in the preview panel, this actually also selects the row and opens + // the detailed panel in preview. + // In order to NOT modify the SessionView code, the setTimeout here guarantees that the alert details preview + // will be opened in second, so that we have a correct order in the opened preview panels + setTimeout(() => { + openPreviewPanel({ + id: DocumentDetailsPreviewPanelKey, + params: { + id: evtId, + indexName: eventDetailsIndex, + scopeId, + banner: ALERT_PREVIEW_BANNER, + isPreviewMode: true, + }, + }); + }, 100); telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'preview', @@ -83,14 +94,59 @@ export const SessionView: FC = () => { [openPreviewPanel, eventDetailsIndex, scopeId, telemetry] ); + const openDetails = useCallback( + (selectedProcess: Process | null) => { + // We cannot pass the original Process object sent from the SessionView component + // as it contains functions (that should not put into Redux) + // and also some recursive properties (that will break rison.encode when updating the URL) + const simplifiedSelectedProcess: CustomProcess | null = selectedProcess + ? { + id: selectedProcess.id, + details: selectedProcess.getDetails(), + endTime: selectedProcess.getEndTime(), + } + : null; + + openPreviewPanel({ + id: DocumentDetailsSessionViewPanelKey, + params: { + eventId, + indexName, + selectedProcess: simplifiedSelectedProcess, + index: sessionViewConfig?.index, + sessionEntityId: sessionViewConfig?.sessionEntityId, + sessionStartTime: sessionViewConfig?.sessionStartTime, + investigatedAlertId: sessionViewConfig?.investigatedAlertId, + scopeId, + jumpToEntityId, + jumpToCursor, + }, + }); + }, + [ + openPreviewPanel, + eventId, + indexName, + sessionViewConfig?.index, + sessionViewConfig?.sessionEntityId, + sessionViewConfig?.sessionStartTime, + sessionViewConfig?.investigatedAlertId, + scopeId, + jumpToEntityId, + jumpToCursor, + ] + ); + return isEnabled ? (
{sessionView.getSessionView({ - index, - sessionEntityId, - sessionStartTime, + ...sessionViewConfig, isFullScreen: true, loadAlertDetails: openAlertDetailsPreview, + openDetails: (selectedProcess: Process | null) => openDetails(selectedProcess), + canReadPolicyManagement, + resetJumpToEntityId: jumpToEntityId, + resetJumpToCursor: jumpToCursor, })}
) : ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx index 4098b35a0abfc..4072dfbf250f8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx @@ -13,7 +13,7 @@ import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants'; import { useLicense } from '../../../../common/hooks/use_license'; import { SessionPreview } from './session_preview'; -import { useSessionPreview } from '../hooks/use_session_preview'; +import { useSessionViewConfig } from '../../shared/hooks/use_session_view_config'; import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; import { useDocumentDetailsContext } from '../../shared/context'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; @@ -47,7 +47,7 @@ export const SessionPreviewContainer: FC = () => { ); // decide whether to show the session view or not - const sessionViewConfig = useSessionPreview({ getFieldsData, dataFormattedForFieldBrowser }); + const sessionViewConfig = useSessionViewConfig({ getFieldsData, dataFormattedForFieldBrowser }); const isEnterprisePlus = useLicense().isEnterprise(); const isEnabled = sessionViewConfig && isEnterprisePlus; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/content.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/content.tsx new file mode 100644 index 0000000000000..ec6ec077f285d --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/content.tsx @@ -0,0 +1,36 @@ +/* + * 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 type { SessionViewPanelPaths } from '.'; +import type { SessionViewPanelTabType } from './tabs'; +import { FlyoutBody } from '../../shared/components/flyout_body'; + +export interface PanelContentProps { + /** + * Id of the tab selected in the parent component to display its content + */ + selectedTabId: SessionViewPanelPaths; + /** + * Tabs display right below the flyout's header + */ + tabs: SessionViewPanelTabType[]; +} + +/** + * SessionView preview panel content, that renders the process, metadata and alerts tab contents. + */ +export const PanelContent: FC = ({ selectedTabId, tabs }) => { + const selectedTabContent = useMemo(() => { + return tabs.find((tab) => tab.id === selectedTabId)?.content; + }, [selectedTabId, tabs]); + + return {selectedTabContent}; +}; + +PanelContent.displayName = 'PanelContent'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/context.tsx new file mode 100644 index 0000000000000..6dd2e878957ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/context.tsx @@ -0,0 +1,147 @@ +/* + * 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, { createContext, memo, useContext, useMemo } from 'react'; +import type { ProcessEvent } from '@kbn/session-view-plugin/common'; +import { FlyoutError } from '../../shared/components/flyout_error'; +import type { SessionViewPanelProps } from '.'; + +export interface CustomProcess { + /** + * Id of the process + */ + id: string; + /** + * Details of the process (see implementation under getDetailsMemo here: x-pack/plugins/session_view/public/components/process_tree/hooks.ts) + */ + details: ProcessEvent; + /** + * Timestamp of the 'end' event (see implementation under getEndTime here x-pack/plugins/session_view/public/components/process_tree/hooks.ts) + */ + endTime: string; +} + +export interface SessionViewPanelContext { + /** + * Id of the document that was initially being investigated in the expandable flyout. + * This context needs to store it as it is used within the SessionView preview panel to be able to reopen the left panel with the same document. + */ + eventId: string; + /** + * Index used when investigating the initial document in the expandable flyout. + * This context needs to store it as it is used within the SessionView preview panel to be able to reopen the left panel with the same document. + */ + indexName: string; + /** + * ScopeId used when investigating the initial document in the expandable flyout. + * This context needs to store it as it is used within the SessionView preview panel to be able to reopen the left panel with the same document. + */ + scopeId: string; + /** + * Store a subset of properties from the SessionView component. + * The original object had functions as well as recursive properties, which we should not store in the context. + */ + selectedProcess: CustomProcess | null; + /** + * index used within the SessionView component + */ + index: string; + /** + * sessionEntityId value used to correctly render the SessionView component + */ + sessionEntityId: string; + /** + * sessionStartTime value used to correctly render the SessionView component + */ + sessionStartTime: string; + /** + * investigatedAlertId value used to correctly render the SessionView component + */ + investigatedAlertId: string; +} + +export const SessionViewPanelContext = createContext( + undefined +); + +export type SessionViewPanelProviderProps = { + /** + * React components to render + */ + children: React.ReactNode; +} & Partial; + +export const SessionViewPanelProvider = memo( + ({ + eventId, + indexName, + selectedProcess, + index, + sessionEntityId, + sessionStartTime, + scopeId, + investigatedAlertId, + children, + }: SessionViewPanelProviderProps) => { + const contextValue = useMemo( + () => + eventId && + indexName && + selectedProcess && + index && + sessionEntityId && + sessionStartTime && + scopeId && + investigatedAlertId + ? { + eventId, + indexName, + selectedProcess, + index, + sessionEntityId, + sessionStartTime, + scopeId, + investigatedAlertId, + } + : undefined, + [ + eventId, + indexName, + selectedProcess, + index, + sessionEntityId, + sessionStartTime, + scopeId, + investigatedAlertId, + ] + ); + + if (!contextValue) { + return ; + } + + return ( + + {children} + + ); + } +); + +SessionViewPanelProvider.displayName = 'SessionViewPanelProvider'; + +export const useSessionViewPanelContext = (): SessionViewPanelContext => { + const contextValue = useContext(SessionViewPanelContext); + + if (!contextValue) { + throw new Error( + 'SessionViewPanelContext can only be used within SessionViewPanelContext provider' + ); + } + + return contextValue; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/header.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/header.tsx new file mode 100644 index 0000000000000..e1fa7aae991c6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/header.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiFlyoutHeader } from '@elastic/eui'; +import { EuiTab } from '@elastic/eui'; +import type { FC } from 'react'; +import React, { memo } from 'react'; +import type { SessionViewPanelTabType } from './tabs'; +import type { SessionViewPanelPaths } from '.'; +import { FlyoutHeader } from '../../shared/components/flyout_header'; +import { FlyoutHeaderTabs } from '../../shared/components/flyout_header_tabs'; + +export interface PanelHeaderProps extends React.ComponentProps { + /** + * Id of the tab selected in the parent component to display its content + */ + selectedTabId: SessionViewPanelPaths; + /** + * Callback to set the selected tab id in the parent component + * @param selected + */ + setSelectedTabId: (selected: SessionViewPanelPaths) => void; + /** + * Tabs to display in the header + */ + tabs: SessionViewPanelTabType[]; +} + +/** + * Renders the process, metadata and alerts tabs in the SessionView preview panel header. + */ +export const PanelHeader: FC = memo( + ({ selectedTabId, setSelectedTabId, tabs, ...flyoutHeaderProps }) => { + const onSelectedTabChanged = (id: SessionViewPanelPaths) => setSelectedTabId(id); + + const renderTabs = tabs.map((tab, index) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + key={index} + data-test-subj={tab['data-test-subj']} + > + {tab.name} + + )); + + return ( + + {renderTabs} + + ); + } +); + +PanelHeader.displayName = 'PanelHeader'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/index.tsx new file mode 100644 index 0000000000000..ee80994f54563 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { type PanelPath, useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { PanelContent } from './content'; +import { PanelHeader } from './header'; +import type { CustomProcess } from './context'; +import { useSessionViewPanelContext } from './context'; +import * as tabs from './tabs'; +import { DocumentDetailsSessionViewPanelKey } from '../shared/constants/panel_keys'; +import type { SessionViewPanelTabType } from './tabs'; + +export const allTabs = [tabs.processTab, tabs.metadataTab, tabs.alertsTab]; +export type SessionViewPanelPaths = 'process' | 'metadata' | 'alerts'; + +export interface SessionViewPanelProps extends FlyoutPanelProps { + key: typeof DocumentDetailsSessionViewPanelKey; + path?: PanelPath; + params: { + eventId: string; + indexName: string; + selectedProcess: CustomProcess | null; + index: string; + sessionEntityId: string; + sessionStartTime: string; + scopeId: string; + investigatedAlertId: string; + }; +} + +/** + * Displays node details panel for session view + */ +export const SessionViewPanel: FC> = memo(({ path }) => { + const { openPreviewPanel } = useExpandableFlyoutApi(); + const { + eventId, + indexName, + selectedProcess, + index, + sessionEntityId, + sessionStartTime, + scopeId, + investigatedAlertId, + } = useSessionViewPanelContext(); + + const selectedTabId = useMemo(() => { + // we use the value passed from the url and use it if it exists in the list of tabs to display + if (path) { + const selectedTab = allTabs.map((tab) => tab.id).find((tabId) => tabId === path.tab); + if (selectedTab) { + return selectedTab; + } + } + + // we default back to the first tab of the list of tabs to display in case everything else has failed + const defaultTab = allTabs[0].id; + return defaultTab; + }, [path]); + + const setSelectedTabId = useCallback( + (tabId: SessionViewPanelTabType['id']) => { + openPreviewPanel({ + id: DocumentDetailsSessionViewPanelKey, + path: { + tab: tabId, + }, + params: { + eventId, + indexName, + selectedProcess, + index, + sessionEntityId, + sessionStartTime, + scopeId, + investigatedAlertId, + }, + }); + }, + [ + eventId, + index, + indexName, + investigatedAlertId, + openPreviewPanel, + scopeId, + selectedProcess, + sessionEntityId, + sessionStartTime, + ] + ); + + return ( + <> + + + + ); +}); + +SessionViewPanel.displayName = 'SessionViewPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs.tsx new file mode 100644 index 0000000000000..e536448840837 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs.tsx @@ -0,0 +1,58 @@ +/* + * 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 { ReactElement } from 'react'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { ProcessTab } from './tabs/process_tab'; +import { MetadataTab } from './tabs/metadata_tab'; +import { AlertsTab } from './tabs/alerts_tab'; +import { ALERTS_TAB_TEST_ID, METADATA_TAB_TEST_ID, PROCESS_TAB_TEST_ID } from './test_ids'; +import type { SessionViewPanelPaths } from '.'; + +export interface SessionViewPanelTabType { + id: SessionViewPanelPaths; + name: ReactElement; + content: React.ReactElement; + 'data-test-subj': string; +} + +export const processTab: SessionViewPanelTabType = { + id: 'process', + 'data-test-subj': PROCESS_TAB_TEST_ID, + name: ( + + ), + content: , +}; + +export const metadataTab: SessionViewPanelTabType = { + id: 'metadata', + 'data-test-subj': METADATA_TAB_TEST_ID, + name: ( + + ), + content: , +}; + +export const alertsTab: SessionViewPanelTabType = { + id: 'alerts', + 'data-test-subj': ALERTS_TAB_TEST_ID, + name: ( + + ), + content: , +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/alerts_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/alerts_tab.tsx new file mode 100644 index 0000000000000..7bb55d43c5a0b --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/alerts_tab.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DetailPanelAlertTab, useFetchSessionViewAlerts } from '@kbn/session-view-plugin/public'; +import type { ProcessEvent } from '@kbn/session-view-plugin/common'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { SESSION_VIEW_ID } from '../../left/components/session_view'; +import { + DocumentDetailsLeftPanelKey, + DocumentDetailsPreviewPanelKey, +} from '../../shared/constants/panel_keys'; +import { ALERT_PREVIEW_BANNER } from '../../preview/constants'; +import { useSessionViewPanelContext } from '../context'; + +/** + * Tab displayed in the SessionView preview panel, shows alerts related to the session. + */ +export const AlertsTab = memo(() => { + const { eventId, indexName, investigatedAlertId, sessionEntityId, sessionStartTime, scopeId } = + useSessionViewPanelContext(); + const { + data: alertsData, + fetchNextPage: fetchNextPageAlerts, + isFetching: isFetchingAlerts, + hasNextPage: hasNextPageAlerts, + } = useFetchSessionViewAlerts(sessionEntityId, sessionStartTime, undefined); + + // this code mimics what is being done in the x-pack/plugins/session_view/public/components/session_view/index.tsx file + const alerts = useMemo(() => { + let events: ProcessEvent[] = []; + + if (alertsData) { + alertsData.pages.forEach((page) => { + events = events.concat(page.events); + }); + } + + return events; + }, [alertsData]); + + const { openPreviewPanel, openLeftPanel } = useExpandableFlyoutApi(); + const openAlertDetailsPreview = useCallback( + (evtId?: string, onClose?: () => void) => { + openPreviewPanel({ + id: DocumentDetailsPreviewPanelKey, + params: { + id: evtId, + indexName, + scopeId, + banner: ALERT_PREVIEW_BANNER, + isPreviewMode: true, + }, + }); + }, + [openPreviewPanel, indexName, scopeId] + ); + + // this code mimics what is being done in the x-pack/plugins/session_view/public/components/session_view/index.tsx file + const jumpToEvent = useCallback( + (event: ProcessEvent) => { + let jumpToEntityId = null; + let jumpToCursor = null; + if (event.process) { + const { entity_id: entityId } = event.process; + if (entityId !== sessionEntityId) { + const alert = event.kibana?.alert; + const cursor = alert ? alert?.original_time : event['@timestamp']; + + if (cursor) { + jumpToEntityId = entityId; + jumpToCursor = cursor; + } + } + } + + openLeftPanel({ + id: DocumentDetailsLeftPanelKey, + params: { + id: eventId, + indexName, + scopeId, + jumpToEntityId, + jumpToCursor, + }, + path: { + tab: 'visualize', + subTab: SESSION_VIEW_ID, + }, + }); + }, + [eventId, indexName, openLeftPanel, scopeId, sessionEntityId] + ); + + return ( + + + + ); +}); + +AlertsTab.displayName = 'AlertsTab'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/metadata_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/metadata_tab.tsx new file mode 100644 index 0000000000000..2d4486bf7fa68 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/metadata_tab.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DetailPanelMetadataTab } from '@kbn/session-view-plugin/public'; +import { useSessionViewPanelContext } from '../context'; + +/** + * Tab displayed in the SessionView preview panel, shows metadata related process selected in the SessionView tree. + */ +export const MetadataTab = memo(() => { + const { selectedProcess } = useSessionViewPanelContext(); + + return ( + + + + ); +}); + +MetadataTab.displayName = 'MetadataTab'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/process_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/process_tab.tsx new file mode 100644 index 0000000000000..b5117a2be0f6f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/tabs/process_tab.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DetailPanelProcessTab } from '@kbn/session-view-plugin/public'; +import type { Process } from '@kbn/session-view-plugin/common'; +import { useSessionViewPanelContext } from '../context'; + +/** + * Tab displayed in the SessionView preview panel, shows the details related to the process selected in the SessionView tree. + */ +export const ProcessTab = memo(() => { + const { selectedProcess, index } = useSessionViewPanelContext(); + + // We need to partially recreate the Process object here, as the SessionView code + // is expecting a Process object with at least the following properties + const process: Process | null = useMemo( + () => + selectedProcess + ? ({ + getDetails: () => selectedProcess.details, + id: selectedProcess.id, + getEndTime: () => selectedProcess.endTime, + } as Process) + : null, + [selectedProcess] + ); + + return ( + + + + ); +}); + +ProcessTab.displayName = 'ProcessTab'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/session_view/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/test_ids.ts new file mode 100644 index 0000000000000..c9f19314ef450 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/session_view/test_ids.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PREFIX } from '../../shared/test_ids'; + +export const PROCESS_TAB_TEST_ID = `${PREFIX}ProcessTab` as const; +export const METADATA_TAB_TEST_ID = `${PREFIX}MetadataTab` as const; +export const ALERTS_TAB_TEST_ID = `${PREFIX}AlertsTab` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts index fa40f1e0e6674..e68313ed6707e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts @@ -12,3 +12,4 @@ export const DocumentDetailsPreviewPanelKey = 'document-details-preview' as cons export const DocumentDetailsIsolateHostPanelKey = 'document-details-isolate-host' as const; export const DocumentDetailsAlertReasonPanelKey = 'document-details-alert-reason' as const; export const DocumentDetailsAnalyzerPanelKey = 'document-details-analyzer-details' as const; +export const DocumentDetailsSessionViewPanelKey = 'document-details-sessions-view-details' as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx index 12e2ad4f2a0b6..72da35f9286b2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx @@ -67,6 +67,14 @@ export interface DocumentDetailsContext { * Boolean to indicate whether it is a preview panel */ isPreviewMode: boolean; + /** + * To allow communication between the SessionView in the left panel and its preview panels + */ + jumpToEntityId?: string; + /** + * To allow communication between the SessionView in the left panel and its preview panels + */ + jumpToCursor?: string; } /** @@ -82,7 +90,15 @@ export type DocumentDetailsProviderProps = { } & Partial; export const DocumentDetailsProvider = memo( - ({ id, indexName, scopeId, isPreviewMode, children }: DocumentDetailsProviderProps) => { + ({ + id, + indexName, + scopeId, + jumpToEntityId, + jumpToCursor, + isPreviewMode, + children, + }: DocumentDetailsProviderProps) => { const { browserFields, dataAsNestedObject, @@ -117,20 +133,24 @@ export const DocumentDetailsProvider = memo( getFieldsData, isPreview: scopeId === TableId.rulePreview, isPreviewMode: Boolean(isPreviewMode), + jumpToEntityId, + jumpToCursor, } : undefined, [ id, - maybeRule, indexName, scopeId, - browserFields, dataAsNestedObject, dataFormattedForFieldBrowser, searchHit, + browserFields, + maybeRule?.investigation_fields?.field_names, refetchFlyoutData, getFieldsData, isPreviewMode, + jumpToEntityId, + jumpToCursor, ] ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.test.tsx similarity index 79% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.test.tsx index 4e2e19c6b54fa..11cb97548e94c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.test.tsx @@ -7,15 +7,15 @@ import type { RenderHookResult } from '@testing-library/react'; import { renderHook } from '@testing-library/react'; -import type { UseSessionPreviewParams } from './use_session_preview'; -import { useSessionPreview } from './use_session_preview'; +import type { UseSessionViewConfigParams } from './use_session_view_config'; +import { useSessionViewConfig } from './use_session_view_config'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; -import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; -import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; -import { mockFieldData, mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; +import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; +import { mockFieldData, mockGetFieldsData } from '../mocks/mock_get_fields_data'; -describe('useSessionPreview', () => { - let hookResult: RenderHookResult; +describe('useSessionViewConfig', () => { + let hookResult: RenderHookResult; it(`should return a session view config object if alert ancestor index is available`, () => { const getFieldsData: GetFieldsData = (field: string) => { @@ -36,7 +36,7 @@ describe('useSessionPreview', () => { }, ]; - hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), { + hookResult = renderHook((props: UseSessionViewConfigParams) => useSessionViewConfig(props), { initialProps: { getFieldsData, dataFormattedForFieldBrowser, @@ -71,7 +71,7 @@ describe('useSessionPreview', () => { isObjectArray: false, }, ]; - hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), { + hookResult = renderHook((props: UseSessionViewConfigParams) => useSessionViewConfig(props), { initialProps: { getFieldsData: mockGetFieldsData, dataFormattedForFieldBrowser, @@ -91,7 +91,7 @@ describe('useSessionPreview', () => { it(`should return null if data isn't ready for session view`, () => { const getFieldsData: GetFieldsData = (field: string) => ''; - hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), { + hookResult = renderHook((props: UseSessionViewConfigParams) => useSessionViewConfig(props), { initialProps: { getFieldsData, dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.ts similarity index 82% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.ts index 4b2132d265871..891b550c1aee8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.ts @@ -7,11 +7,11 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; -import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; -import { getField } from '../../shared/utils'; -import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; +import type { GetFieldsData } from './use_get_fields_data'; +import { getField } from '../utils'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; -export interface UseSessionPreviewParams { +export interface UseSessionViewConfigParams { /** * Retrieves searchHit values for the provided field */ @@ -25,10 +25,10 @@ export interface UseSessionPreviewParams { /** * Hook that returns the session view configuration if the session view is available for the alert */ -export const useSessionPreview = ({ +export const useSessionViewConfig = ({ getFieldsData, dataFormattedForFieldBrowser, -}: UseSessionPreviewParams): SessionViewConfig | null => { +}: UseSessionViewConfigParams): SessionViewConfig | null => { const { indexName: _index, alertId: _id } = useBasicDataFromDetailsData( dataFormattedForFieldBrowser ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/types.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/types.tsx index 00fb1da32449c..bc32e3e5b33bb 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/types.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/types.tsx @@ -19,5 +19,7 @@ export interface DocumentDetailsProps extends FlyoutPanelProps { indexName: string; scopeId: string; isPreviewMode?: boolean; + jumpToEntityId?: string; + jumpToCursor?: string; }; } diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx index 64fac23dfa98e..fd83f121265a4 100644 --- a/x-pack/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/index.tsx @@ -8,6 +8,9 @@ import React, { memo, useCallback } from 'react'; import { ExpandableFlyout, type ExpandableFlyoutProps } from '@kbn/expandable-flyout'; import { useEuiTheme } from '@elastic/eui'; +import { SessionViewPanelProvider } from './document_details/session_view/context'; +import type { SessionViewPanelProps } from './document_details/session_view'; +import { SessionViewPanel } from './document_details/session_view'; import type { NetworkExpandableFlyoutProps } from './network_details'; import { Flyouts } from './document_details/shared/constants/flyouts'; import { @@ -17,6 +20,7 @@ import { DocumentDetailsPreviewPanelKey, DocumentDetailsAlertReasonPanelKey, DocumentDetailsAnalyzerPanelKey, + DocumentDetailsSessionViewPanelKey, } from './document_details/shared/constants/panel_keys'; import type { IsolateHostPanelProps } from './document_details/isolate_host'; import { IsolateHostPanel } from './document_details/isolate_host'; @@ -104,6 +108,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] ), }, + { + key: DocumentDetailsSessionViewPanelKey, + component: (props) => ( + + + + ), + }, { key: UserPanelKey, component: (props) => , diff --git a/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx index 72fa6c925f680..9a63ea3c233a1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx @@ -27,7 +27,7 @@ export const ShowPanelButton = memo(({ showPanelOnClick }: { showPanelOnClick: ( title={showPanelButtonTitle} aria-label={showPanelButtonTitle} onClick={showPanelOnClick} - iconType={'eye'} + iconType={'expand'} $backgroundColor={colorMap.graphControlsBackground} $iconColor={colorMap.graphControls} $borderColor={colorMap.graphControlsBorderColor} diff --git a/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx index a6201ea2ed504..b493591b94370 100644 --- a/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx @@ -316,7 +316,12 @@ export function ProcessTreeNode({ - + diff --git a/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx b/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx index bdb7e3ddc8c2b..bf6cf9e7c67f3 100644 --- a/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx @@ -62,6 +62,9 @@ export const SessionView = ({ loadAlertDetails, canReadPolicyManagement, trackEvent, + openDetails, + resetJumpToEntityId, + resetJumpToCursor, }: SessionViewDeps & { trackEvent: (name: SessionViewTelemetryKey) => void }) => { // don't engage jumpTo if jumping to session leader. if (jumpToEntityId === sessionEntityId) { @@ -114,9 +117,18 @@ export const SessionView = ({ return !!(!displayOptions?.verboseMode && searchQuery && searchResults?.length === 0); }, [displayOptions?.verboseMode, searchResults, searchQuery]); - const onProcessSelected = useCallback((process: Process | null) => { - setSelectedProcess(process); - }, []); + const onProcessSelected = useCallback( + (process: Process | null) => { + setSelectedProcess(process); + + // used when SessionView is displayed in the expandable flyout + // This refreshes the detailed panel rendered in the flyout preview panel + if (openDetails) { + openDetails(process); + } + }, + [openDetails] + ); const onJumpToEvent = useCallback( (event: ProcessEvent) => { @@ -220,6 +232,19 @@ export const SessionView = ({ fetchAlertStatus[0] ?? '' ); + /** + * This useEffect should only impact the SessionView component when displayed in the expandable flyout. + * The SessionView tree and its detailed panel are separated and this allows the detailed panel to reset the + * view of the tree from the preview panel. + */ + useEffect(() => { + if (resetJumpToEntityId && resetJumpToCursor) { + setSelectedProcess(null); + setCurrentJumpToEntityId(resetJumpToEntityId); + setCurrentJumpToCursor(resetJumpToCursor); + } + }, [resetJumpToCursor, resetJumpToEntityId]); + useEffect(() => { if (newUpdatedAlertsStatus) { setUpdatedAlertsStatus({ ...newUpdatedAlertsStatus }); @@ -261,6 +286,12 @@ export const SessionView = ({ } }, [isDetailOpen, trackEvent]); + const toggleDetailPanelInFlyout = useCallback(() => { + if (openDetails) { + openDetails(selectedProcess); + } + }, [openDetails, selectedProcess]); + const onShowAlertDetails = useCallback( (alertUuid: string) => { if (loadAlertDetails) { @@ -294,6 +325,86 @@ export const SessionView = ({ [displayOptions?.timestamp, displayOptions?.verboseMode, setDisplayOptions, trackEvent] ); + const errorEmptyPrompt = useMemo( + () => + hasError ? ( + + + + } + body={ +

+ +

+ } + /> + ) : null, + [hasError] + ); + + const processTree = useMemo( + () => + hasData ? ( +
+ +
+ ) : null, + [ + currentJumpToCursor, + currentJumpToEntityId, + data?.pages, + displayOptions?.timestamp, + displayOptions?.verboseMode, + fetchNextPage, + fetchPreviousPage, + hasData, + hasNextPage, + hasPreviousPage, + investigatedAlertId, + isFetching, + onJumpToOutput, + onProcessSelected, + onShowAlertDetails, + searchQuery, + selectedProcess, + sessionEntityId, + styles.processTree, + trackEvent, + updatedAlertsStatus, + ] + ); + if (renderIsLoading) { return ( @@ -390,103 +501,66 @@ export const SessionView = ({ - - {DETAIL_PANEL} - + {openDetails ? ( + + ) : ( + + {DETAIL_PANEL} + + )} - - {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { - detailPanelCollapseFn.current = () => { - togglePanel?.(sessionViewId, { direction: 'left' }); - }; - - return ( - <> - - {hasError ? ( - - - - } - body={ -

- -

- } + {openDetails ? ( + <> + {errorEmptyPrompt} + {processTree} + + ) : ( + + {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { + detailPanelCollapseFn.current = () => { + togglePanel?.(sessionViewId, { direction: 'left' }); + }; + + return ( + <> + + {errorEmptyPrompt} + {processTree} + + + + - ) : null} - - {hasData && ( -
- -
- )} -
- - - - - - - ); - }} -
+
+ + ); + }} +
+ )} void ) => void; canReadPolicyManagement?: boolean; + /** + * Function used when SessionView is used within the expandable flyout, where the tree and the detailed panel are + * separated and need to communicate with each other. + */ + openDetails?: (selectedProcess: Process | null) => void; + /** + * + */ + resetJumpToEntityId?: string; + /** + * + */ + resetJumpToCursor?: string; } export interface EuiTabProps {