From 86aa348012db43dcc3696c677e8629bc275c679f Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 10 Mar 2022 12:06:34 -0500 Subject: [PATCH 1/6] Cherry-pick security/timelines changes --- .../common/assets/field_maps/ecs_field_map.ts | 15 +++++ .../common/ecs/process/index.ts | 9 +++ .../common/types/timeline/index.ts | 1 + x-pack/plugins/security_solution/kibana.json | 1 + .../common/components/events_viewer/index.tsx | 11 ++-- .../alerts_table/default_config.tsx | 1 + .../components/alerts_table/index.tsx | 2 +- .../navigation/events_query_tab_body.tsx | 2 +- .../components/graph_overlay/index.tsx | 36 ++++++++++- .../components/graph_overlay/translations.ts | 7 +++ .../timeline/body/actions/index.tsx | 35 +++++++++++ .../components/timeline/body/index.tsx | 2 +- .../components/timeline/body/translations.ts | 7 +++ .../timeline/query_tab_content/index.tsx | 2 +- .../timeline/session_tab_content/index.tsx | 48 +++++++++++++++ .../timeline/tabs_content/index.tsx | 61 +++++++++++++------ .../timeline/tabs_content/translations.ts | 7 +++ .../timelines/store/timeline/actions.ts | 10 +++ .../timelines/store/timeline/defaults.ts | 1 + .../timelines/store/timeline/helpers.ts | 40 ++++++++++++ .../public/timelines/store/timeline/model.ts | 4 ++ .../timelines/store/timeline/reducer.ts | 12 ++++ .../plugins/security_solution/public/types.ts | 2 + .../plugins/security_solution/tsconfig.json | 4 +- x-pack/plugins/session_view/public/index.ts | 2 + x-pack/plugins/session_view/public/types.ts | 9 +++ .../timelines/common/ecs/process/index.ts | 9 +++ .../timeline/factory/helpers/constants.ts | 9 +++ 28 files changed, 317 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts index dc81e200032f7..c3e3aa5c604db 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts @@ -2401,6 +2401,21 @@ export const ecsFieldMap = { array: false, required: false, }, + 'process.entry_leader.entity_id': { + type: 'keyword', + array: false, + required: false, + }, + 'process.session_leader.entity_id': { + type: 'keyword', + array: false, + required: false, + }, + 'process.group_leader.entity_id': { + type: 'keyword', + array: false, + required: false, + }, 'process.executable': { type: 'keyword', array: false, diff --git a/x-pack/plugins/security_solution/common/ecs/process/index.ts b/x-pack/plugins/security_solution/common/ecs/process/index.ts index 2a58c6d5b47d0..02122c776e95d 100644 --- a/x-pack/plugins/security_solution/common/ecs/process/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/process/index.ts @@ -11,6 +11,9 @@ export interface ProcessEcs { Ext?: Ext; command_line?: string[]; entity_id?: string[]; + entry_leader?: ProcessSessionData; + session_leader?: ProcessSessionData; + group_leader?: ProcessSessionData; exit_code?: number[]; hash?: ProcessHashData; parent?: ProcessParentData; @@ -25,6 +28,12 @@ export interface ProcessEcs { working_directory?: string[]; } +export interface ProcessSessionData { + entity_id?: string[]; + pid?: string[]; + name?: string[]; +} + export interface ProcessHashData { md5?: string[]; sha1?: string[]; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index ab60d87973983..abe946470467c 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -476,6 +476,7 @@ export enum TimelineTabs { notes = 'notes', pinned = 'pinned', eql = 'eql', + session = 'session', } /** diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 36edfd43d5ea5..8fd6ee8f017ce 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -22,6 +22,7 @@ "licensing", "maps", "ruleRegistry", + "sessionView", "taskManager", "timelines", "triggersActionsUi", diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 0053ed13923d4..946e71eb4856b 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -105,6 +105,7 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, + sessionViewId, showCheckboxes, sort, } = defaultModel, @@ -155,11 +156,11 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; - const graphOverlay = useMemo( - () => - graphEventId != null && graphEventId.length > 0 ? : null, - [graphEventId, id] - ); + const graphOverlay = useMemo(() => { + const shouldShowOverlay = + (graphEventId != null && graphEventId.length > 0) || sessionViewId !== null; + return shouldShowOverlay ? : null; + }, [graphEventId, id, sessionViewId]); const setQuery = useCallback( (inspect, loading, refetch) => { dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index dc8c5bf4de65e..7dc3561628193 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -176,4 +176,5 @@ export const requiredFieldsForActions = [ 'file.hash.sha256', 'host.os.family', 'event.code', + 'process.entry_leader.entity_id', ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index c82c0c11237ee..b4f81e3e5f0e4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -104,7 +104,7 @@ export const AlertsTableComponent: React.FC = ({ const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); - const ACTION_BUTTON_COUNT = 4; + const ACTION_BUTTON_COUNT = 5; const getGlobalQuery = useCallback( (customFilters: Filter[]) => { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 59c3322fb02ed..8285e9cb7ea4e 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -72,7 +72,7 @@ const EventsQueryTabBodyComponent: React.FC = ({ }) => { const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); - const ACTION_BUTTON_COUNT = 4; + const ACTION_BUTTON_COUNT = 5; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 64475147edc9d..af03bc7d0d7d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -28,12 +28,17 @@ import { useGlobalFullScreen, useTimelineFullScreen, } from '../../../common/containers/use_full_screen'; +import { useKibana } from '../../../common/lib/kibana'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; import { isFullScreen } from '../timeline/body/column_headers'; -import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; +import { + updateTimelineGraphEventId, + updateTimelineSessionViewEventId, + updateTimelineSessionViewSessionId, +} from '../../../timelines/store/timeline/actions'; import { inputsActions } from '../../../common/store/actions'; import { Resolver } from '../../../resolver/view'; import { @@ -70,6 +75,12 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` margin: 4px 0 4px 0; `; +const ScrollableFlexItem = styled(EuiFlexItem)` + ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} + overflow: hidden; + width: 100%; +`; + interface OwnProps { timelineId: TimelineId; } @@ -131,6 +142,14 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { const graphEventId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId ); + const { sessionView } = useKibana().services; + const sessionViewId = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId + ); + const sessionViewMain = useMemo(() => { + return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; + }, [sessionView, sessionViewId]); + const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []); @@ -180,6 +199,8 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { } } dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + dispatch(updateTimelineSessionViewEventId({ id: timelineId, eventId: null })); + dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]); useEffect(() => { @@ -219,7 +240,18 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { [defaultDataView.patternList, isInTimeline, timelinePatterns] ); - if (fullScreen && !isInTimeline) { + if (!isInTimeline && sessionViewId !== null) { + return ( + + + + {i18n.CLOSE_SESSION} + + + {sessionViewMain} + + ); + } else if (fullScreen && !isInTimeline) { return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts index 53e40c79f74ac..b2343b667dcda 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts @@ -13,3 +13,10 @@ export const CLOSE_ANALYZER = i18n.translate( defaultMessage: 'Close analyzer', } ); + +export const CLOSE_SESSION = i18n.translate( + 'xpack.securitySolution.timeline.graphOverlay.closeSessionButton', + { + defaultMessage: 'Close Session', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 8be6200d1e84a..c783107cee362 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -24,6 +24,8 @@ import { useShallowEqualSelector } from '../../../../../common/hooks/use_selecto import { setActiveTabTimeline, updateTimelineGraphEventId, + updateTimelineSessionViewSessionId, + updateTimelineSessionViewEventId, } from '../../../../store/timeline/actions'; import { useGlobalFullScreen, @@ -128,6 +130,24 @@ const ActionsComponent: React.FC = ({ } }, [dispatch, ecsData._id, timelineId, setGlobalFullScreen, setTimelineFullScreen]); + const entryLeader = useMemo(() => { + const { process } = ecsData; + const entryLeaderIds = process?.entry_leader?.entity_id; + if (entryLeaderIds !== undefined) { + return entryLeaderIds[0]; + } else { + return null; + } + }, [ecsData]); + + const openSessionView = useCallback(() => { + if (entryLeader !== null) { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session })); + dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: entryLeader })); + dispatch(updateTimelineSessionViewEventId({ id: timelineId, eventId: entryLeader })); + } + }, [dispatch, timelineId, entryLeader]); + return ( {showCheckboxes && !tGridEnabled && ( @@ -220,6 +240,21 @@ const ActionsComponent: React.FC = ({ ) : null} + {entryLeader !== null ? ( +
+ + + + + +
+ ) : null}
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index c41544c1c4b4c..26fabddc329d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -108,7 +108,7 @@ export const BodyComponent = React.memo( const { queryFields, selectAll } = useDeepEqualSelector((state) => getManageTimeline(state, id) ); - const ACTION_BUTTON_COUNT = 5; + const ACTION_BUTTON_COUNT = 6; const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts index d104dc3a85f72..40649b7d2313a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts @@ -28,6 +28,13 @@ export const COPY_TO_CLIPBOARD = i18n.translate( } ); +export const OPEN_SESSION_VIEW = i18n.translate( + 'xpack.securitySolution.timeline.body.openSessionViewLabel', + { + defaultMessage: 'Open Session View', + } +); + export const INVESTIGATE = i18n.translate( 'xpack.securitySolution.timeline.body.actions.investigateLabel', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index d23d09280aaa9..1a6fcbf7c25ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -202,7 +202,7 @@ export const QueryTabContentComponent: React.FC = ({ } = useSourcererDataView(SourcererScopeName.timeline); const { uiSettings } = useKibana().services; - const ACTION_BUTTON_COUNT = 5; + const ACTION_BUTTON_COUNT = 6; const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); const { filterManager: activeFilterManager } = useDeepEqualSelector((state) => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx new file mode 100644 index 0000000000000..700462f00aa79 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx @@ -0,0 +1,48 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { timelineSelectors } from '../../../store/timeline'; +import { useKibana } from '../../../../common/lib/kibana'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; + +const FullWidthFlexGroup = styled(EuiFlexGroup)` + margin: 0; + width: 100%; + overflow: hidden; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} + overflow: hidden; +`; + +interface Props { + timelineId: TimelineId; +} + +const SessionTabContent: React.FC = ({ timelineId }) => { + const { sessionView } = useKibana().services; + + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + + const sessionViewId = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId + ); + const sessionViewMain = useMemo(() => { + return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; + }, [sessionView, sessionViewId]); + + return {sessionViewMain}; +}; + +// eslint-disable-next-line import/no-default-export +export default SessionTabContent; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index e38e380292260..8477e9ed136dd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -51,6 +51,7 @@ const EqlTabContent = lazy(() => import('../eql_tab_content')); const GraphTabContent = lazy(() => import('../graph_tab_content')); const NotesTabContent = lazy(() => import('../notes_tab_content')); const PinnedTabContent = lazy(() => import('../pinned_tab_content')); +const SessionTabContent = lazy(() => import('../session_tab_content')); interface BasicTimelineTab { renderCellValue: (props: CellValueElementProps) => React.ReactNode; @@ -106,6 +107,13 @@ const NotesTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => )); NotesTab.displayName = 'NotesTab'; +const SessionTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( + }> + + +)); +SessionTab.displayName = 'SessionTab'; + const PinnedTab: React.FC<{ renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; @@ -132,6 +140,8 @@ const ActiveTimelineTab = memo( return ; case TimelineTabs.notes: return ; + case TimelineTabs.session: + return ; default: return null; } @@ -140,7 +150,8 @@ const ActiveTimelineTab = memo( ); const isGraphOrNotesTabs = useMemo( - () => [TimelineTabs.graph, TimelineTabs.notes].includes(activeTimelineTab), + () => + [TimelineTabs.graph, TimelineTabs.notes, TimelineTabs.session].includes(activeTimelineTab), [activeTimelineTab] ); @@ -262,33 +273,36 @@ const TabsContentComponent: React.FC = ({ [appNotes, allTimelineNoteIds, timelineDescription] ); + const setActiveTab = useCallback( + (tab: TimelineTabs) => { + dispatch(timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: tab })); + }, + [dispatch, timelineId] + ); + const setQueryAsActiveTab = useCallback(() => { - dispatch( - timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query }) - ); - }, [dispatch, timelineId]); + setActiveTab(TimelineTabs.query); + }, [setActiveTab]); const setEqlAsActiveTab = useCallback(() => { - dispatch(timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.eql })); - }, [dispatch, timelineId]); + setActiveTab(TimelineTabs.eql); + }, [setActiveTab]); const setGraphAsActiveTab = useCallback(() => { - dispatch( - timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }) - ); - }, [dispatch, timelineId]); + setActiveTab(TimelineTabs.graph); + }, [setActiveTab]); const setNotesAsActiveTab = useCallback(() => { - dispatch( - timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.notes }) - ); - }, [dispatch, timelineId]); + setActiveTab(TimelineTabs.notes); + }, [setActiveTab]); const setPinnedAsActiveTab = useCallback(() => { - dispatch( - timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.pinned }) - ); - }, [dispatch, timelineId]); + setActiveTab(TimelineTabs.pinned); + }, [setActiveTab]); + + const setSessionAsActiveTab = useCallback(() => { + setActiveTab(TimelineTabs.session); + }, [setActiveTab]); useEffect(() => { if (!graphEventId && activeTab === TimelineTabs.graph) { @@ -358,6 +372,15 @@ const TabsContentComponent: React.FC = ({ )} + + {i18n.SESSION_TAB} + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/translations.ts index 6e58beaca8209..b116d2f551045 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/translations.ts @@ -38,3 +38,10 @@ export const PINNED_TAB = i18n.translate( defaultMessage: 'Pinned', } ); + +export const SESSION_TAB = i18n.translate( + 'pack.securitySolution.timeline.tabs.sessionTabTimelineTitle', + { + defaultMessage: 'Session View', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index fc13d163c1883..3f1159c3db1e7 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -81,6 +81,16 @@ export const updateTimelineGraphEventId = actionCreator<{ id: string; graphEvent 'UPDATE_TIMELINE_GRAPH_EVENT_ID' ); +export const updateTimelineSessionViewEventId = actionCreator<{ + id: string; + eventId: string | null; +}>('UPDATE_TIMELINE_SESSION_VIEW_EVENT_ID'); + +export const updateTimelineSessionViewSessionId = actionCreator<{ + id: string; + eventId: string | null; +}>('UPDATE_TIMELINE_SESSION_VIEW_SESSION_ID'); + export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT'); export const updateTimeline = actionCreator<{ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 1ebb815480c82..7362ee9e76759 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -65,6 +65,7 @@ export const timelineDefaults: SubsetTimelineModel & savedObjectId: null, selectAll: false, selectedEventIds: {}, + sessionViewId: null, show: false, showCheckboxes: false, sort: [ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index a123cdeb8f928..1a2c11925bfa3 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -287,6 +287,46 @@ export const updateGraphEventId = ({ }; }; +export const updateSessionViewEventId = ({ + id, + eventId, + timelineById, +}: { + id: string; + eventId: string; + timelineById: TimelineById; +}): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + sessionViewId: eventId, + }, + }; +}; + +export const updateSessionViewSessionId = ({ + id, + eventId, + timelineById, +}: { + id: string; + eventId: string; + timelineById: TimelineById; +}): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + sessionViewSessionId: eventId, + }, + }; +}; + const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { return true; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 18b6566b13b6c..735ef7803d07c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -63,6 +63,8 @@ export type TimelineModel = TGridModelForTimeline & { resolveTimelineConfig?: ResolveTimelineConfig; showSaveModal?: boolean; savedQueryId?: string | null; + sessionViewId: string | null; + sessionViewSessionId: string | null; /** When true, show the timeline flyover */ show: boolean; /** status: active | draft */ @@ -118,6 +120,8 @@ export type SubsetTimelineModel = Readonly< | 'dateRange' | 'selectAll' | 'selectedEventIds' + | 'sessionViewId' + | 'sessionViewSessionId' | 'show' | 'showCheckboxes' | 'sort' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 6a3aa68908bb5..d2110d1ff075a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -44,6 +44,8 @@ import { updateTimeline, updateTimelineGraphEventId, updateTitleAndDescription, + updateTimelineSessionViewEventId, + updateTimelineSessionViewSessionId, toggleModalSaveTimeline, updateEqlOptions, setTimelineUpdatedAt, @@ -77,6 +79,8 @@ import { updateGraphEventId, updateFilters, updateTimelineEventType, + updateSessionViewEventId, + updateSessionViewSessionId, } from './helpers'; import { TimelineState, EMPTY_TIMELINE_BY_ID } from './types'; @@ -146,6 +150,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: updateGraphEventId({ id, graphEventId, timelineById: state.timelineById }), })) + .case(updateTimelineSessionViewEventId, (state, { id, eventId }) => ({ + ...state, + timelineById: updateSessionViewEventId({ id, eventId, timelineById: state.timelineById }), + })) + .case(updateTimelineSessionViewSessionId, (state, { id, eventId }) => ({ + ...state, + timelineById: updateSessionViewSessionId({ id, eventId, timelineById: state.timelineById }), + })) .case(pinEvent, (state, { id, eventId }) => ({ ...state, timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }), diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 0916bc73f4198..24ef98f71856c 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -25,6 +25,7 @@ import type { import type { CasesUiStart } from '../../cases/public'; import type { SecurityPluginSetup } from '../../security/public'; import type { TimelinesUIStart } from '../../timelines/public'; +import type { SessionViewStart } from '../../session_view/public'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; @@ -66,6 +67,7 @@ export interface StartPlugins { newsfeed?: NewsfeedPublicPluginStart; triggersActionsUi: TriggersActionsStart; timelines: TimelinesUIStart; + sessionView: SessionViewStart; uiActions: UiActionsStart; ml?: MlPluginStart; spaces?: SpacesPluginStart; diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index b1cb49b737952..ff5316e7f9c93 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -42,6 +42,6 @@ { "path": "../osquery/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../security/tsconfig.json" }, - { "path": "../timelines/tsconfig.json" } - ] + { "path": "../timelines/tsconfig.json" }, + { "path": "../session_view/tsconfig.json"} ] } diff --git a/x-pack/plugins/session_view/public/index.ts b/x-pack/plugins/session_view/public/index.ts index 90043e9a691dc..a9f5fcaa858ac 100644 --- a/x-pack/plugins/session_view/public/index.ts +++ b/x-pack/plugins/session_view/public/index.ts @@ -7,6 +7,8 @@ import { SessionViewPlugin } from './plugin'; +export type { SessionViewStart } from './types'; + export function plugin() { return new SessionViewPlugin(); } diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index 3a7ef376bd426..6cc051bee0795 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -63,3 +63,12 @@ export interface DetailPanelProcessLeader { entryMetaSourceIp: string; executable: string; } + +export interface SessionViewStart { + getSessionViewTableProcessTree: ({ + onOpenSessionView, + }: { + onOpenSessionView: (eventId: string) => void; + }) => JSX.Element; + getSessionView: (sessionEntityId: string) => JSX.Element; +} diff --git a/x-pack/plugins/timelines/common/ecs/process/index.ts b/x-pack/plugins/timelines/common/ecs/process/index.ts index 820ecc5560e6c..b3abf363e2876 100644 --- a/x-pack/plugins/timelines/common/ecs/process/index.ts +++ b/x-pack/plugins/timelines/common/ecs/process/index.ts @@ -10,6 +10,9 @@ import { Ext } from '../file'; export interface ProcessEcs { Ext?: Ext; entity_id?: string[]; + entry_leader?: ProcessSessionData; + session_leader?: ProcessSessionData; + group_leader?: ProcessSessionData; exit_code?: number[]; hash?: ProcessHashData; parent?: ProcessParentData; @@ -23,6 +26,12 @@ export interface ProcessEcs { working_directory?: string[]; } +export interface ProcessSessionData { + entity_id?: string[]; + pid?: string[]; + name?: string[]; +} + export interface ProcessHashData { md5?: string[]; sha1?: string[]; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts index e764e32243c18..613d35617394e 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts @@ -213,6 +213,15 @@ export const TIMELINE_EVENTS_FIELDS = [ 'process.executable', 'process.title', 'process.working_directory', + 'process.entry_leader.entity_id', + 'process.entry_leader.name', + 'process.entry_leader.pid', + 'process.session_leader.entity_id', + 'process.session_leader.name', + 'process.session_leader.pid', + 'process.group_leader.entity_id', + 'process.group_leader.name', + 'process.group_leader.pid', 'zeek.session_id', 'zeek.connection.local_resp', 'zeek.connection.local_orig', From c3e77086c551959a6f46b9945b7c402076b9788f Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 10 Mar 2022 17:06:04 -0500 Subject: [PATCH 2/6] Changes to make session_view work with generated data --- .../public/components/process_tree/helpers.ts | 11 ++++++----- .../public/components/process_tree/hooks.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts index df4a6cf70abec..4493db36fd95e 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts @@ -77,7 +77,6 @@ export const buildProcessTree = ( events.forEach((event) => { const process = processMap[event.process.entity_id]; const parentProcess = processMap[event.process.parent?.entity_id]; - // if session leader, or process already has a parent, return if (process.id === sessionEntityId || process.parent) { return; @@ -105,12 +104,14 @@ export const buildProcessTree = ( // with this new page of events processed, lets try re-parent any orphans orphans?.forEach((process) => { - const parentProcess = processMap[process.getDetails().process.parent.entity_id]; + const parentProcessId = process.getDetails().process.parent?.entity_id; - if (parentProcess) { + if (parentProcessId) { + const parentProcess = processMap[parentProcessId]; process.parent = parentProcess; // handy for recursive operations (like auto expand) - - parentProcess.children.push(process); + if (parentProcess !== undefined) { + parentProcess.children.push(process); + } } else { newOrphans.push(process); } diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index fb00344d5e280..73ac5ee682746 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -149,11 +149,11 @@ export class ProcessImpl implements Process { group_leader: groupLeader, } = event.process; - const parentIsASessionLeader = parent.pid === sessionLeader.pid; // possibly bash, zsh or some other shell - const processIsAGroupLeader = pid === groupLeader.pid; + const parentIsASessionLeader = parent && sessionLeader && parent.pid === sessionLeader.pid; + const processIsAGroupLeader = groupLeader && pid === groupLeader.pid; const sessionIsInteractive = !!tty; - return sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader; + return !!(sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader); } getMaxAlertLevel() { @@ -181,15 +181,16 @@ export class ProcessImpl implements Process { // to be used as a source for the most up to date details // on the processes lifecycle. getDetailsMemo = memoizeOne((events: ProcessEvent[]) => { + // TODO: add these to generator const actionsToFind = [EventAction.fork, EventAction.exec, EventAction.end]; const filtered = events.filter((processEvent) => { - return actionsToFind.includes(processEvent.event.action); + return true; }); // because events is already ordered by @timestamp we take the last event // which could be a fork (w no exec or exit), most recent exec event (there can be multiple), or end event. // If a process has an 'end' event will always be returned (since it is last and includes details like exit_code and end time) - return filtered[filtered.length - 1] || ({} as ProcessEvent); + return filtered[filtered.length - 1]; }); } From d7cbe6aa33b1cf9c1a7902180d9722bba617edc6 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Wed, 23 Mar 2022 08:43:18 -0400 Subject: [PATCH 3/6] create detail panel hook for session view --- .../common/components/events_viewer/index.tsx | 32 +++-- .../components/graph_overlay/index.tsx | 14 +- .../hooks/use_load_detail_panel.tsx | 136 ++++++++++++++++++ .../timeline/session_tab_content/index.tsx | 39 ++++- .../components/process_tree_node/index.tsx | 2 +- x-pack/plugins/session_view/public/types.ts | 2 +- .../timelines/common/types/timeline/index.ts | 1 + 7 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_load_detail_panel.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 946e71eb4856b..322203f0caf28 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -11,7 +11,12 @@ import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; import { inputsModel, State } from '../../store'; import { inputsActions } from '../../store/actions'; -import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; +import { + ControlColumnProps, + RowRenderer, + TimelineId, + TimelineTabs, +} from '../../../../common/types/timeline'; import { APP_ID, APP_UI_ID } from '../../../../common/constants'; import { timelineActions } from '../../../timelines/store/timeline'; import type { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; @@ -33,6 +38,7 @@ import { useFieldBrowserOptions, FieldEditorActions, } from '../../../timelines/components/fields_browser'; +import { useLoadDetailPanel } from '../../../timelines/components/side_panel/hooks/use_load_detail_panel'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -156,11 +162,22 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; + + const { openDetailsPanel, FlyoutDetailsPanel } = useLoadDetailPanel({ + isFlyoutView: true, + entityType, + sourcerScope: SourcererScopeName.timeline, + timelineId: id, + tabType: TimelineTabs.query, + }); + const graphOverlay = useMemo(() => { const shouldShowOverlay = (graphEventId != null && graphEventId.length > 0) || sessionViewId !== null; - return shouldShowOverlay ? : null; - }, [graphEventId, id, sessionViewId]); + return shouldShowOverlay ? ( + + ) : null; + }, [graphEventId, id, sessionViewId, openDetailsPanel]); const setQuery = useCallback( (inspect, loading, refetch) => { dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); @@ -240,14 +257,7 @@ const StatefulEventsViewerComponent: React.FC = ({ })} - + {FlyoutDetailsPanel} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index af03bc7d0d7d2..245e86a96b41b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -81,7 +81,8 @@ const ScrollableFlexItem = styled(EuiFlexItem)` width: 100%; `; -interface OwnProps { +interface GraphOverlayProps { + openDetailsPanel: (eventId?: string, onClose?: () => void) => void; timelineId: TimelineId; } @@ -133,7 +134,7 @@ NavigationComponent.displayName = 'NavigationComponent'; const Navigation = React.memo(NavigationComponent); -const GraphOverlayComponent: React.FC = ({ timelineId }) => { +const GraphOverlayComponent: React.FC = ({ timelineId, openDetailsPanel }) => { const dispatch = useDispatch(); const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); @@ -147,8 +148,13 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); const sessionViewMain = useMemo(() => { - return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; - }, [sessionView, sessionViewId]); + return sessionViewId !== null + ? sessionView.getSessionView({ + sessionEntityId: sessionViewId, + loadAlertDetails: openDetailsPanel, + }) + : null; + }, [sessionView, sessionViewId, openDetailsPanel]); const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_load_detail_panel.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_load_detail_panel.tsx new file mode 100644 index 0000000000000..b8b8d2ccbfc67 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_load_detail_panel.tsx @@ -0,0 +1,136 @@ +/* + * 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, { useMemo, useCallback, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import type { EntityType } from '../../../../../../timelines/common'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { activeTimeline } from '../../../containers/active_timeline_context'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { DetailsPanel } from '..'; + +export interface UseLoadDetailPanelConfig { + entityType?: EntityType; + isFlyoutView?: boolean; + sourcerScope: SourcererScopeName; + timelineId: TimelineId; + tabType?: TimelineTabs; +} + +export interface UseLoadDetailPanelReturn { + openDetailsPanel: (eventId?: string, onClose?: () => void) => void; + handleOnDetailsPanelClosed: () => void; + FlyoutDetailsPanel: JSX.Element; + shouldShowFlyoutDetailsPanel: boolean; +} + +export const useLoadDetailPanel = ({ + entityType, + isFlyoutView, + sourcerScope, + timelineId, + tabType, +}: UseLoadDetailPanelConfig): UseLoadDetailPanelReturn => { + const { browserFields, docValueFields, selectedPatterns, runtimeMappings } = + useSourcererDataView(sourcerScope); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const dispatch = useDispatch(); + + const expandedDetail = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedDetail + ); + const onFlyoutClose = useRef(() => {}); + + const shouldShowFlyoutDetailsPanel = useMemo(() => { + if ( + tabType && + expandedDetail && + expandedDetail[tabType] && + !!expandedDetail[tabType]?.panelView + ) { + return true; + } + return false; + }, [expandedDetail, tabType]); + + const loadDetailsPanel = useCallback( + (eventId?: string) => { + if (eventId) { + dispatch( + timelineActions.toggleDetailPanel({ + panelView: 'eventDetail', + tabType, + timelineId, + params: { + eventId, + indexName: selectedPatterns.join(','), + }, + }) + ); + } + }, + [dispatch, selectedPatterns, tabType, timelineId] + ); + + const openDetailsPanel = useCallback( + (eventId?: string, onClose?: () => void) => { + loadDetailsPanel(eventId); + onFlyoutClose.current = onClose ?? (() => {}); + }, + [loadDetailsPanel] + ); + + const handleOnDetailsPanelClosed = useCallback(() => { + if (onFlyoutClose.current) onFlyoutClose.current(); + dispatch(timelineActions.toggleDetailPanel({ tabType, timelineId })); + + if ( + tabType && + expandedDetail[tabType]?.panelView && + timelineId === TimelineId.active && + shouldShowFlyoutDetailsPanel + ) { + activeTimeline.toggleExpandedDetail({}); + } + }, [dispatch, timelineId, expandedDetail, tabType, shouldShowFlyoutDetailsPanel]); + + const FlyoutDetailsPanel = useMemo( + () => ( + + ), + [ + browserFields, + docValueFields, + entityType, + handleOnDetailsPanelClosed, + isFlyoutView, + runtimeMappings, + tabType, + timelineId, + ] + ); + + return { + openDetailsPanel, + handleOnDetailsPanelClosed, + shouldShowFlyoutDetailsPanel, + FlyoutDetailsPanel, + }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx index 700462f00aa79..299018d96154a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx @@ -10,9 +10,11 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import { timelineSelectors } from '../../../store/timeline'; import { useKibana } from '../../../../common/lib/kibana'; -import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useLoadDetailPanel } from '../../side_panel/hooks/use_load_detail_panel'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; const FullWidthFlexGroup = styled(EuiFlexGroup)` margin: 0; @@ -25,23 +27,50 @@ const ScrollableFlexItem = styled(EuiFlexItem)` overflow: hidden; `; +const VerticalRule = styled.div` + width: 2px; + height: 100%; + background: ${({ theme }) => theme.eui.euiColorLightShade}; +`; + interface Props { timelineId: TimelineId; } const SessionTabContent: React.FC = ({ timelineId }) => { const { sessionView } = useKibana().services; - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const sessionViewId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); + const { openDetailsPanel, shouldShowFlyoutDetailsPanel, FlyoutDetailsPanel } = useLoadDetailPanel( + { + sourcerScope: SourcererScopeName.timeline, + timelineId, + tabType: TimelineTabs.session, + } + ); const sessionViewMain = useMemo(() => { - return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; - }, [sessionView, sessionViewId]); + return sessionViewId !== null + ? sessionView.getSessionView({ + sessionEntityId: sessionViewId, + loadAlertDetails: openDetailsPanel, + }) + : null; + }, [openDetailsPanel, sessionView, sessionViewId]); - return {sessionViewMain}; + return ( + + {sessionViewMain} + {shouldShowFlyoutDetailsPanel && ( + <> + + {FlyoutDetailsPanel} + + )} + + ); }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index b1c42dd95efb9..e93a92d4eac6f 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -166,7 +166,7 @@ export function ProcessTreeNode({ const shouldRenderChildren = childrenExpanded && children && children.length > 0; const childrenTreeDepth = depth + 1; - const showUserEscalation = user.id !== parent.user.id; + const showUserEscalation = user?.id !== parent?.user?.id; const interactiveSession = !!tty; const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; const hasExec = process.hasExec(); diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index 6cc051bee0795..a9778d454a226 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -70,5 +70,5 @@ export interface SessionViewStart { }: { onOpenSessionView: (eventId: string) => void; }) => JSX.Element; - getSessionView: (sessionEntityId: string) => JSX.Element; + getSessionView: (sessionDeps: SessionViewDeps) => JSX.Element; } diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index a6c8ed1b74bff..6572d6cb695fa 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -462,6 +462,7 @@ export enum TimelineTabs { graph = 'graph', notes = 'notes', pinned = 'pinned', + session = 'session', eql = 'eql', } From dd9452cdc21813947c980640cf0d93cff22aa2f2 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Wed, 23 Mar 2022 16:08:08 -0400 Subject: [PATCH 4/6] add some tests --- .../common/components/events_viewer/index.tsx | 6 +- .../hooks/use_detail_panel.test.tsx | 64 +++++++++++++++++++ ..._detail_panel.tsx => use_detail_panel.tsx} | 16 ++--- .../timeline/session_tab_content/index.tsx | 14 ++-- 4 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx rename x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/{use_load_detail_panel.tsx => use_detail_panel.tsx} (92%) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 322203f0caf28..40616a1c02714 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -38,7 +38,7 @@ import { useFieldBrowserOptions, FieldEditorActions, } from '../../../timelines/components/fields_browser'; -import { useLoadDetailPanel } from '../../../timelines/components/side_panel/hooks/use_load_detail_panel'; +import { useDetailPanel } from '../../../timelines/components/side_panel/hooks/use_detail_panel'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -163,10 +163,10 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; - const { openDetailsPanel, FlyoutDetailsPanel } = useLoadDetailPanel({ + const { openDetailsPanel, FlyoutDetailsPanel } = useDetailPanel({ isFlyoutView: true, entityType, - sourcerScope: SourcererScopeName.timeline, + sourcererScope: SourcererScopeName.timeline, timelineId: id, tabType: TimelineTabs.query, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx new file mode 100644 index 0000000000000..aea7dd9f108d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useDetailPanel, UseDetailPanelConfig } from './use_detail_panel'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { TimelineId } from '../../../../../common/types'; + +const mockDispatch = jest.fn(); +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/hooks/use_selector'); +jest.mock('../../../store/timeline'); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +jest.mock('../../../../common/containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + browserFields: {}, + docValueFields: [], + loading: true, + indexPattern: {}, + selectedPatterns: [], + missingPatterns: [], + }), +})); + +describe('useHoverActionItems', () => { + const defaultProps: UseDetailPanelConfig = { + sourcererScope: SourcererScopeName.detections, + timelineId: TimelineId.test, + }; + + beforeEach(() => { + (useDeepEqualSelector as jest.Mock).mockImplementation((cb) => { + return {}; + }); + }); + afterEach(() => { + (useDeepEqualSelector as jest.Mock).mockClear(); + }); + + test('should return expected options when given required props', async () => { + const testEventId = '123'; + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + return useDetailPanel(defaultProps); + }); + await waitForNextUpdate(); + result.current?.openDetailsPanel(testEventId); + + expect(mockDispatch).toHaveBeenCalled(); + expect(result.current.shouldShowFlyoutDetailsPanel).toBe(false); + expect(result.current.FlyoutDetailsPanel).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_load_detail_panel.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_load_detail_panel.tsx rename to x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx index b8b8d2ccbfc67..dbe1fcdd371ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_load_detail_panel.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx @@ -17,35 +17,35 @@ import { timelineDefaults } from '../../../store/timeline/defaults'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { DetailsPanel } from '..'; -export interface UseLoadDetailPanelConfig { +export interface UseDetailPanelConfig { entityType?: EntityType; isFlyoutView?: boolean; - sourcerScope: SourcererScopeName; + sourcererScope: SourcererScopeName; timelineId: TimelineId; tabType?: TimelineTabs; } -export interface UseLoadDetailPanelReturn { +export interface UseDetailPanelReturn { openDetailsPanel: (eventId?: string, onClose?: () => void) => void; handleOnDetailsPanelClosed: () => void; FlyoutDetailsPanel: JSX.Element; shouldShowFlyoutDetailsPanel: boolean; } -export const useLoadDetailPanel = ({ +export const useDetailPanel = ({ entityType, isFlyoutView, - sourcerScope, + sourcererScope, timelineId, tabType, -}: UseLoadDetailPanelConfig): UseLoadDetailPanelReturn => { +}: UseDetailPanelConfig): UseDetailPanelReturn => { const { browserFields, docValueFields, selectedPatterns, runtimeMappings } = - useSourcererDataView(sourcerScope); + useSourcererDataView(sourcererScope); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const dispatch = useDispatch(); const expandedDetail = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedDetail + (state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedDetail ); const onFlyoutClose = useRef(() => {}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx index 299018d96154a..349253bee4e6c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx @@ -13,7 +13,7 @@ import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useLoadDetailPanel } from '../../side_panel/hooks/use_load_detail_panel'; +import { useDetailPanel } from '../../side_panel/hooks/use_detail_panel'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; const FullWidthFlexGroup = styled(EuiFlexGroup)` @@ -44,13 +44,11 @@ const SessionTabContent: React.FC = ({ timelineId }) => { const sessionViewId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); - const { openDetailsPanel, shouldShowFlyoutDetailsPanel, FlyoutDetailsPanel } = useLoadDetailPanel( - { - sourcerScope: SourcererScopeName.timeline, - timelineId, - tabType: TimelineTabs.session, - } - ); + const { openDetailsPanel, shouldShowFlyoutDetailsPanel, FlyoutDetailsPanel } = useDetailPanel({ + sourcererScope: SourcererScopeName.timeline, + timelineId, + tabType: TimelineTabs.session, + }); const sessionViewMain = useMemo(() => { return sessionViewId !== null ? sessionView.getSessionView({ From fa00f8b66146b6ba2147494f724554d4b2a89a8e Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 10 Mar 2022 17:06:04 -0500 Subject: [PATCH 5/6] Changes to make session_view work with generated data --- .../common/components/events_viewer/index.tsx | 33 ++-- .../components/graph_overlay/index.tsx | 14 +- .../hooks/use_detail_panel.test.tsx | 150 ++++++++++++++++++ .../side_panel/hooks/use_detail_panel.tsx | 139 ++++++++++++++++ .../timeline/session_tab_content/index.tsx | 37 ++++- .../public/components/process_tree/helpers.ts | 11 +- .../public/components/process_tree/hooks.ts | 11 +- .../components/process_tree_node/index.tsx | 2 +- x-pack/plugins/session_view/public/types.ts | 2 +- .../timelines/common/types/timeline/index.ts | 1 + 10 files changed, 367 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 946e71eb4856b..3c25b5abfc3ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -11,7 +11,12 @@ import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; import { inputsModel, State } from '../../store'; import { inputsActions } from '../../store/actions'; -import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; +import { + ControlColumnProps, + RowRenderer, + TimelineId, + TimelineTabs, +} from '../../../../common/types/timeline'; import { APP_ID, APP_UI_ID } from '../../../../common/constants'; import { timelineActions } from '../../../timelines/store/timeline'; import type { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; @@ -24,7 +29,6 @@ import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererDataView } from '../../containers/sourcerer'; import type { EntityType } from '../../../../../timelines/common'; import { TGridCellAction } from '../../../../../timelines/common/types'; -import { DetailsPanel } from '../../../timelines/components/side_panel'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; @@ -33,6 +37,7 @@ import { useFieldBrowserOptions, FieldEditorActions, } from '../../../timelines/components/fields_browser'; +import { useDetailPanel } from '../../../timelines/components/side_panel/hooks/use_detail_panel'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -156,11 +161,22 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; + + const { openDetailsPanel, DetailsPanel } = useDetailPanel({ + isFlyoutView: true, + entityType, + sourcererScope: SourcererScopeName.timeline, + timelineId: id, + tabType: TimelineTabs.query, + }); + const graphOverlay = useMemo(() => { const shouldShowOverlay = (graphEventId != null && graphEventId.length > 0) || sessionViewId !== null; - return shouldShowOverlay ? : null; - }, [graphEventId, id, sessionViewId]); + return shouldShowOverlay ? ( + + ) : null; + }, [graphEventId, id, sessionViewId, openDetailsPanel]); const setQuery = useCallback( (inspect, loading, refetch) => { dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); @@ -240,14 +256,7 @@ const StatefulEventsViewerComponent: React.FC = ({ })} - + {DetailsPanel} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index af03bc7d0d7d2..245e86a96b41b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -81,7 +81,8 @@ const ScrollableFlexItem = styled(EuiFlexItem)` width: 100%; `; -interface OwnProps { +interface GraphOverlayProps { + openDetailsPanel: (eventId?: string, onClose?: () => void) => void; timelineId: TimelineId; } @@ -133,7 +134,7 @@ NavigationComponent.displayName = 'NavigationComponent'; const Navigation = React.memo(NavigationComponent); -const GraphOverlayComponent: React.FC = ({ timelineId }) => { +const GraphOverlayComponent: React.FC = ({ timelineId, openDetailsPanel }) => { const dispatch = useDispatch(); const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); @@ -147,8 +148,13 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); const sessionViewMain = useMemo(() => { - return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; - }, [sessionView, sessionViewId]); + return sessionViewId !== null + ? sessionView.getSessionView({ + sessionEntityId: sessionViewId, + loadAlertDetails: openDetailsPanel, + }) + : null; + }, [sessionView, sessionViewId, openDetailsPanel]); const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx new file mode 100644 index 0000000000000..4bc5ba44c1695 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx @@ -0,0 +1,150 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useDetailPanel, UseDetailPanelConfig } from './use_detail_panel'; +import { timelineActions } from '../../../store/timeline'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { TimelineId, TimelineTabs } from '../../../../../common/types'; + +const mockDispatch = jest.fn(); +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/hooks/use_selector'); +jest.mock('../../../store/timeline'); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +jest.mock('../../../../common/containers/sourcerer', () => { + const mockSourcererReturn = { + browserFields: {}, + docValueFields: [], + loading: true, + indexPattern: {}, + selectedPatterns: [], + missingPatterns: [], + }; + return { + useSourcererDataView: jest.fn().mockReturnValue(mockSourcererReturn), + }; +}); + +describe('useDetailPanel', () => { + const defaultProps: UseDetailPanelConfig = { + sourcererScope: SourcererScopeName.detections, + timelineId: TimelineId.test, + }; + const mockGetExpandedDetail = jest.fn().mockImplementation(() => ({})); + beforeEach(() => { + (useDeepEqualSelector as jest.Mock).mockImplementation((cb) => { + return mockGetExpandedDetail(); + }); + }); + afterEach(() => { + (useDeepEqualSelector as jest.Mock).mockClear(); + }); + + test('should return openDetailsPanel fn, handleOnDetailsPanelClosed fn, shouldShowDetailsPanel, and the DetailsPanel component', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + return useDetailPanel(defaultProps); + }); + await waitForNextUpdate(); + + expect(result.current.openDetailsPanel).toBeDefined(); + expect(result.current.handleOnDetailsPanelClosed).toBeDefined(); + expect(result.current.shouldShowDetailsPanel).toBe(false); + expect(result.current.DetailsPanel).toBeNull(); + }); + }); + + test('should fire redux action to open details panel', async () => { + const testEventId = '123'; + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + return useDetailPanel(defaultProps); + }); + await waitForNextUpdate(); + + result.current?.openDetailsPanel(testEventId); + + expect(mockDispatch).toHaveBeenCalled(); + expect(timelineActions.toggleDetailPanel).toHaveBeenCalled(); + }); + }); + + test('should call provided onClose callback provided to openDetailsPanel fn', async () => { + const testEventId = '123'; + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + return useDetailPanel(defaultProps); + }); + await waitForNextUpdate(); + + const mockOnClose = jest.fn(); + result.current?.openDetailsPanel(testEventId, mockOnClose); + result.current?.handleOnDetailsPanelClosed(); + + expect(mockOnClose).toHaveBeenCalled(); + }); + }); + + test('should call the last onClose callback provided to openDetailsPanel fn', async () => { + // Test that the onClose ref is properly updated + const testEventId = '123'; + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + return useDetailPanel(defaultProps); + }); + await waitForNextUpdate(); + + const mockOnClose = jest.fn(); + const secondMockOnClose = jest.fn(); + result.current?.openDetailsPanel(testEventId, mockOnClose); + result.current?.handleOnDetailsPanelClosed(); + + expect(mockOnClose).toHaveBeenCalled(); + + result.current?.openDetailsPanel(testEventId, secondMockOnClose); + result.current?.handleOnDetailsPanelClosed(); + + expect(secondMockOnClose).toHaveBeenCalled(); + }); + }); + + test('should show the details panel', async () => { + mockGetExpandedDetail.mockImplementation(() => ({ + [TimelineTabs.session]: { + panelView: 'somePanel', + }, + })); + const updatedProps = { + ...defaultProps, + tabType: TimelineTabs.session, + }; + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + return useDetailPanel(updatedProps); + }); + await waitForNextUpdate(); + + expect(result.current.DetailsPanel).toMatchInlineSnapshot(` + + `); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx new file mode 100644 index 0000000000000..3b50f17e16c04 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx @@ -0,0 +1,139 @@ +/* + * 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, { useMemo, useCallback, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import type { EntityType } from '../../../../../../timelines/common'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { activeTimeline } from '../../../containers/active_timeline_context'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { DetailsPanel as DetailsPanelComponent } from '..'; + +export interface UseDetailPanelConfig { + entityType?: EntityType; + isFlyoutView?: boolean; + sourcererScope: SourcererScopeName; + timelineId: TimelineId; + tabType?: TimelineTabs; +} + +export interface UseDetailPanelReturn { + openDetailsPanel: (eventId?: string, onClose?: () => void) => void; + handleOnDetailsPanelClosed: () => void; + DetailsPanel: JSX.Element | null; + shouldShowDetailsPanel: boolean; +} + +export const useDetailPanel = ({ + entityType, + isFlyoutView, + sourcererScope, + timelineId, + tabType, +}: UseDetailPanelConfig): UseDetailPanelReturn => { + const { browserFields, docValueFields, selectedPatterns, runtimeMappings } = + useSourcererDataView(sourcererScope); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const dispatch = useDispatch(); + + const expandedDetail = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedDetail + ); + const onPanelClose = useRef(() => {}); + + const shouldShowDetailsPanel = useMemo(() => { + if ( + tabType && + expandedDetail && + expandedDetail[tabType] && + !!expandedDetail[tabType]?.panelView + ) { + return true; + } + return false; + }, [expandedDetail, tabType]); + + const loadDetailsPanel = useCallback( + (eventId?: string) => { + if (eventId) { + dispatch( + timelineActions.toggleDetailPanel({ + panelView: 'eventDetail', + tabType, + timelineId, + params: { + eventId, + indexName: selectedPatterns.join(','), + }, + }) + ); + } + }, + [dispatch, selectedPatterns, tabType, timelineId] + ); + + const openDetailsPanel = useCallback( + (eventId?: string, onClose?: () => void) => { + loadDetailsPanel(eventId); + onPanelClose.current = onClose ?? (() => {}); + }, + [loadDetailsPanel] + ); + + const handleOnDetailsPanelClosed = useCallback(() => { + console.log('ON PANEL CLOSE: ', onPanelClose.current); // eslint-disable-line + if (onPanelClose.current) onPanelClose.current(); + dispatch(timelineActions.toggleDetailPanel({ tabType, timelineId })); + + if ( + tabType && + expandedDetail[tabType]?.panelView && + timelineId === TimelineId.active && + shouldShowDetailsPanel + ) { + activeTimeline.toggleExpandedDetail({}); + } + }, [dispatch, timelineId, expandedDetail, tabType, shouldShowDetailsPanel]); + + const DetailsPanel = useMemo( + () => + shouldShowDetailsPanel ? ( + + ) : null, + [ + browserFields, + docValueFields, + entityType, + handleOnDetailsPanelClosed, + isFlyoutView, + runtimeMappings, + shouldShowDetailsPanel, + tabType, + timelineId, + ] + ); + + return { + openDetailsPanel, + handleOnDetailsPanelClosed, + shouldShowDetailsPanel, + DetailsPanel, + }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx index 700462f00aa79..dbf7731160115 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx @@ -10,9 +10,11 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import { timelineSelectors } from '../../../store/timeline'; import { useKibana } from '../../../../common/lib/kibana'; -import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useDetailPanel } from '../../side_panel/hooks/use_detail_panel'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; const FullWidthFlexGroup = styled(EuiFlexGroup)` margin: 0; @@ -25,23 +27,48 @@ const ScrollableFlexItem = styled(EuiFlexItem)` overflow: hidden; `; +const VerticalRule = styled.div` + width: 2px; + height: 100%; + background: ${({ theme }) => theme.eui.euiColorLightShade}; +`; + interface Props { timelineId: TimelineId; } const SessionTabContent: React.FC = ({ timelineId }) => { const { sessionView } = useKibana().services; - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const sessionViewId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); + const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ + sourcererScope: SourcererScopeName.timeline, + timelineId, + tabType: TimelineTabs.session, + }); const sessionViewMain = useMemo(() => { - return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; - }, [sessionView, sessionViewId]); + return sessionViewId !== null + ? sessionView.getSessionView({ + sessionEntityId: sessionViewId, + loadAlertDetails: openDetailsPanel, + }) + : null; + }, [openDetailsPanel, sessionView, sessionViewId]); - return {sessionViewMain}; + return ( + + {sessionViewMain} + {shouldShowDetailsPanel && ( + <> + + {DetailsPanel} + + )} + + ); }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts index df4a6cf70abec..4493db36fd95e 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts @@ -77,7 +77,6 @@ export const buildProcessTree = ( events.forEach((event) => { const process = processMap[event.process.entity_id]; const parentProcess = processMap[event.process.parent?.entity_id]; - // if session leader, or process already has a parent, return if (process.id === sessionEntityId || process.parent) { return; @@ -105,12 +104,14 @@ export const buildProcessTree = ( // with this new page of events processed, lets try re-parent any orphans orphans?.forEach((process) => { - const parentProcess = processMap[process.getDetails().process.parent.entity_id]; + const parentProcessId = process.getDetails().process.parent?.entity_id; - if (parentProcess) { + if (parentProcessId) { + const parentProcess = processMap[parentProcessId]; process.parent = parentProcess; // handy for recursive operations (like auto expand) - - parentProcess.children.push(process); + if (parentProcess !== undefined) { + parentProcess.children.push(process); + } } else { newOrphans.push(process); } diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index fb00344d5e280..73ac5ee682746 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -149,11 +149,11 @@ export class ProcessImpl implements Process { group_leader: groupLeader, } = event.process; - const parentIsASessionLeader = parent.pid === sessionLeader.pid; // possibly bash, zsh or some other shell - const processIsAGroupLeader = pid === groupLeader.pid; + const parentIsASessionLeader = parent && sessionLeader && parent.pid === sessionLeader.pid; + const processIsAGroupLeader = groupLeader && pid === groupLeader.pid; const sessionIsInteractive = !!tty; - return sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader; + return !!(sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader); } getMaxAlertLevel() { @@ -181,15 +181,16 @@ export class ProcessImpl implements Process { // to be used as a source for the most up to date details // on the processes lifecycle. getDetailsMemo = memoizeOne((events: ProcessEvent[]) => { + // TODO: add these to generator const actionsToFind = [EventAction.fork, EventAction.exec, EventAction.end]; const filtered = events.filter((processEvent) => { - return actionsToFind.includes(processEvent.event.action); + return true; }); // because events is already ordered by @timestamp we take the last event // which could be a fork (w no exec or exit), most recent exec event (there can be multiple), or end event. // If a process has an 'end' event will always be returned (since it is last and includes details like exit_code and end time) - return filtered[filtered.length - 1] || ({} as ProcessEvent); + return filtered[filtered.length - 1]; }); } diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index b1c42dd95efb9..e93a92d4eac6f 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -166,7 +166,7 @@ export function ProcessTreeNode({ const shouldRenderChildren = childrenExpanded && children && children.length > 0; const childrenTreeDepth = depth + 1; - const showUserEscalation = user.id !== parent.user.id; + const showUserEscalation = user?.id !== parent?.user?.id; const interactiveSession = !!tty; const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; const hasExec = process.hasExec(); diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index 6cc051bee0795..a9778d454a226 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -70,5 +70,5 @@ export interface SessionViewStart { }: { onOpenSessionView: (eventId: string) => void; }) => JSX.Element; - getSessionView: (sessionEntityId: string) => JSX.Element; + getSessionView: (sessionDeps: SessionViewDeps) => JSX.Element; } diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index a6c8ed1b74bff..6572d6cb695fa 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -462,6 +462,7 @@ export enum TimelineTabs { graph = 'graph', notes = 'notes', pinned = 'pinned', + session = 'session', eql = 'eql', } From 8e9c818e1376a5db9b17011ed36327710594c2dc Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Fri, 25 Mar 2022 11:59:49 -0400 Subject: [PATCH 6/6] remove duplicate sessionViewId prop --- .../public/common/components/events_viewer/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 54c0f3da60c69..3c25b5abfc3ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -112,7 +112,6 @@ const StatefulEventsViewerComponent: React.FC = ({ kqlMode, sessionViewId, showCheckboxes, - sessionViewId, sort, } = defaultModel, } = useSelector((state: State) => eventsViewerSelector(state, id));