Skip to content

Commit

Permalink
[Security Solution][Alert Details] - SessionView changes to render th…
Browse files Browse the repository at this point in the history
…e detailed panel as a flyout preview panel
  • Loading branch information
PhilippeOberti committed Dec 16, 2024
1 parent e3877e0 commit 5107c10
Show file tree
Hide file tree
Showing 22 changed files with 975 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand All @@ -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',
Expand All @@ -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 ? (
<div data-test-subj={SESSION_VIEW_TEST_ID}>
{sessionView.getSessionView({
index,
sessionEntityId,
sessionStartTime,
...sessionViewConfig,
isFullScreen: true,
loadAlertDetails: openAlertDetailsPreview,
openDetails: (selectedProcess: Process | null) => openDetails(selectedProcess),
canReadPolicyManagement,
resetJumpToEntityId: jumpToEntityId,
resetJumpToCursor: jumpToCursor,
})}
</div>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<PanelContentProps> = ({ selectedTabId, tabs }) => {
const selectedTabContent = useMemo(() => {
return tabs.find((tab) => tab.id === selectedTabId)?.content;
}, [selectedTabId, tabs]);

return <FlyoutBody>{selectedTabContent}</FlyoutBody>;
};

PanelContent.displayName = 'PanelContent';
Original file line number Diff line number Diff line change
@@ -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<SessionViewPanelContext | undefined>(
undefined
);

export type SessionViewPanelProviderProps = {
/**
* React components to render
*/
children: React.ReactNode;
} & Partial<SessionViewPanelProps['params']>;

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 <FlyoutError />;
}

return (
<SessionViewPanelContext.Provider value={contextValue}>
{children}
</SessionViewPanelContext.Provider>
);
}
);

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;
};
Loading

0 comments on commit 5107c10

Please sign in to comment.