From af077cde73b61a9564e3d79d88634ec9ff20ea30 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 10 Mar 2022 12:06:34 -0500 Subject: [PATCH 01/19] 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 | 14 +++-- .../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, 320 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 a8a47d0ca9719..a9f0f8bc7c1f9 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -477,6 +477,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 9f22f229b33c1..bbb322eeea0c1 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -21,6 +21,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 5f88bb3f9aabd..ff6813b2eff2c 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 @@ -98,6 +98,7 @@ const StatefulEventsViewerComponent: React.FC = ({ rowRenderers, start, scopeId, + sessionViewId, showCheckboxes, sort, timelineQuery, @@ -153,11 +154,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 })); @@ -271,6 +272,7 @@ const makeMapStateToProps = () => { kqlMode, sort, showCheckboxes, + sessionViewId, } = timeline; return { @@ -288,6 +290,7 @@ const makeMapStateToProps = () => { query: getGlobalQuerySelector(state), sort, showCheckboxes, + sessionViewId, // Used to determine whether the footer should show (since it is hidden if the graph is showing.) // `getTimeline` actually returns `TimelineModel | undefined` graphEventId, @@ -333,6 +336,7 @@ export const StatefulEventsViewer = connector( prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.sessionViewId === nextProps.sessionViewId && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar && prevProps.additionalFilters === nextProps.additionalFilters && 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 c52d7a55d8449..0a8d42f6a8bab 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 a4889183cdf3d..d162a145e99fb 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 @@ -70,7 +70,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 d43f8752c9122..0b93fce0992db 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'; @@ -65,6 +66,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 d518eaf7f8243..2492508a53905 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -41,6 +41,6 @@ { "path": "../ml/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 2349b8423eb36..b40bfa01b651a 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -47,3 +47,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 0abc22b1e2cc3313fac139c7cf90e09189fae4ac Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 10 Mar 2022 17:06:04 -0500 Subject: [PATCH 02/19] 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 d3d7af1c62eda..5bf88bfcf78fb 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 @@ -46,7 +46,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; @@ -74,12 +73,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 a8c6ffe8e75d3..2706ac30de8a2 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 @@ -138,11 +138,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() { @@ -170,15 +170,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 821c0c245dc3cff89b52f1228ee22a59b14a706c Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 17 Mar 2022 09:59:08 -0400 Subject: [PATCH 03/19] Demo tweaks --- .../components/events_viewer/index.test.tsx | 28 +++++++++++++++++++ .../public/hosts/pages/hosts.tsx | 2 +- .../components/process_tree_alerts/styles.ts | 2 +- .../public/components/session_view/index.tsx | 6 ++-- .../public/components/session_view/styles.ts | 7 +++++ 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index cdc9cc9b6f32d..6a43b1c1d5c12 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -110,4 +110,32 @@ describe('StatefulEventsViewer', () => { wrapper.unmount(); expect(mockCloseEditor).toHaveBeenCalled(); }); + + test('renders the graph overlay when a graph event id is set', async () => { + const propsWithGraphId = { + ...testProps, + graphEventId: 'test', + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('overlayContainer').exists()).toBe(true); + }); + + test('renders the graph overlay when a session event id is set', async () => { + const propsWithGraphId = { + ...testProps, + sessionViewId: 'test', + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('overlayContainer').exists()).toBe(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index f850ff4c63026..5cd94cd0d01a6 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -178,7 +178,7 @@ const HostsComponent = () => { return ( <> - {indicesExist ? ( + {true ? ( diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts index d601891591305..79b74b4cb7f40 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts @@ -25,7 +25,7 @@ export const useStyles = () => { borderWidth: border.width.thin, borderRadius: border.radius.medium, maxWidth: 800, - backgroundColor: 'white', + backgroundColor: colors.mediumShade, '&>div': { borderTop: border.thin, marginTop: size.m, diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 7a82edc94ff1b..4a2f89a79db97 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -87,10 +87,10 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie return ( <> - +
- +
{(EuiResizablePanel, EuiResizableButton) => ( <> diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts index d7159ec5b1b39..4d46d245c3f9c 100644 --- a/x-pack/plugins/session_view/public/components/session_view/styles.ts +++ b/x-pack/plugins/session_view/public/components/session_view/styles.ts @@ -26,9 +26,16 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { height: `${height}px`, }; + const nonGrowGroup: CSSObject = { + display: 'flex', + flexGrow: 0, + alignItems: 'stretch', + }; + return { processTree, detailPanel, + nonGrowGroup, }; }, [height, euiTheme]); From 86aa348012db43dcc3696c677e8629bc275c679f Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 10 Mar 2022 12:06:34 -0500 Subject: [PATCH 04/19] 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 05/19] 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 06/19] 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 07/19] 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 08/19] 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 09/19] 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)); From a8d9d13369349e64142fc0cad45f261ceca7c771 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Fri, 25 Mar 2022 12:22:53 -0400 Subject: [PATCH 10/19] Update with main --- .../security_solution/common/constants.ts | 3 +++ .../components/alerts_viewer/alerts_table.tsx | 18 +++++++++++--- .../events_tab/events_query_tab_body.tsx | 13 ++++++++-- .../components/alerts_table/index.tsx | 19 +++++++++++---- .../components/graph_overlay/index.tsx | 6 ++--- .../timeline/body/actions/index.tsx | 2 -- .../timeline/eql_tab_content/index.tsx | 10 ++++++-- .../timeline/pinned_tab_content/index.tsx | 10 ++++++-- .../timeline/query_tab_content/index.tsx | 7 ++++-- .../timeline/session_tab_content/index.tsx | 4 +++- .../timelines/store/timeline/actions.ts | 5 ---- .../timelines/store/timeline/helpers.ts | 24 ++----------------- .../public/timelines/store/timeline/model.ts | 2 -- .../timelines/store/timeline/reducer.ts | 6 ----- .../security_solution/server/ui_settings.ts | 17 +++++++++++++ .../components/process_tree_alerts/styles.ts | 2 +- .../components/process_tree_node/index.tsx | 2 +- x-pack/plugins/session_view/public/types.ts | 2 +- 18 files changed, 93 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index cc64b7e640f1f..ddbc6fcdfb21c 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -181,6 +181,9 @@ export const ENABLE_NEWS_FEED_SETTING = 'securitySolution:enableNewsFeed' as con /** This Kibana Advanced Setting enables the warnings for CCS read permissions */ export const ENABLE_CCS_READ_WARNING_SETTING = 'securitySolution:enableCcsWarning' as const; +/** This Kibana Advanced Setting enables the session view component in the UI */ +export const ENABLE_SESSION_VIEW_PLUGIN = 'securitySolution:enableSessionView' as const; + /** This Kibana Advanced Setting sets the auto refresh interval for the detections all rules table */ export const DEFAULT_RULES_TABLE_REFRESH_SETTING = 'securitySolution:rulesTableRefresh' as const; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 0dd137a2321c6..ec79d8caecd46 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -22,6 +22,7 @@ import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_fe import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import type { EntityType } from '../../../../../timelines/common'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; +import { ENABLE_SESSION_VIEW_PLUGIN } from '../../../../common/constants'; export interface OwnProps { end: string; @@ -79,8 +80,16 @@ const AlertsTableComponent: React.FC = ({ }) => { const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); - const { filterManager } = useKibana().services.data.query; - const ACTION_BUTTON_COUNT = 4; + const { + uiSettings, + data: { + query: { filterManager }, + }, + } = useKibana().services; + + const isSessionViewEnabled = uiSettings.get(ENABLE_SESSION_VIEW_PLUGIN); + + const ACTION_BUTTON_COUNT = isSessionViewEnabled ? 5 : 4; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); @@ -106,7 +115,10 @@ const AlertsTableComponent: React.FC = ({ ); }, [dispatch, filterManager, tGridEnabled, timelineId]); - const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + const leadingControlColumns = useMemo( + () => getDefaultControlColumn(ACTION_BUTTON_COUNT), + [ACTION_BUTTON_COUNT] + ); return ( = }) => { const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); - const ACTION_BUTTON_COUNT = 5; + const { uiSettings } = useKibana().services; + + const isSessionViewEnabled = uiSettings.get(ENABLE_SESSION_VIEW_PLUGIN); + + const ACTION_BUTTON_COUNT = isSessionViewEnabled ? 5 : 4; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); useEffect(() => { @@ -111,7 +117,10 @@ const EventsQueryTabBodyComponent: React.FC = }; }, [deleteQuery]); - const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + const leadingControlColumns = useMemo( + () => getDefaultControlColumn(ACTION_BUTTON_COUNT), + [ACTION_BUTTON_COUNT] + ); return ( <> 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 b4f81e3e5f0e4..b90ccb767e78b 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 @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import type { Filter } from '@kbn/es-query'; -import { APP_ID } from '../../../../common/constants'; +import { APP_ID, ENABLE_SESSION_VIEW_PLUGIN } from '../../../../common/constants'; import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { RowRendererId, TimelineIdLiteral } from '../../../../common/types/timeline'; @@ -104,7 +104,16 @@ export const AlertsTableComponent: React.FC = ({ const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); - const ACTION_BUTTON_COUNT = 5; + const { + uiSettings, + data: { + query: { filterManager }, + }, + } = useKibana().services; + + const isSessionViewEnabled = uiSettings.get(ENABLE_SESSION_VIEW_PLUGIN); + + const ACTION_BUTTON_COUNT = isSessionViewEnabled ? 5 : 4; const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -334,7 +343,6 @@ export const AlertsTableComponent: React.FC = ({ return [...defaultFilters, ...alertStatusFilter]; } }, [defaultFilters, filterGroup]); - const { filterManager } = useKibana().services.data.query; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); @@ -363,7 +371,10 @@ export const AlertsTableComponent: React.FC = ({ ); }, [dispatch, filterManager, tGridEnabled, timelineId]); - const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + const leadingControlColumns = useMemo( + () => getDefaultControlColumn(ACTION_BUTTON_COUNT), + [ACTION_BUTTON_COUNT] + ); const casesPermissions = useGetUserCasesPermissions(); const CasesContext = kibana.services.cases.ui.getCasesContext(); 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..aa1bd17302bd3 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 @@ -36,7 +36,6 @@ import { timelineDefaults } from '../../store/timeline/defaults'; import { isFullScreen } from '../timeline/body/column_headers'; import { updateTimelineGraphEventId, - updateTimelineSessionViewEventId, updateTimelineSessionViewSessionId, } from '../../../timelines/store/timeline/actions'; import { inputsActions } from '../../../common/store/actions'; @@ -147,7 +146,9 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); const sessionViewMain = useMemo(() => { - return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; + return sessionViewId !== null + ? sessionView.getSessionView({ sessionEntityId: sessionViewId }) + : null; }, [sessionView, sessionViewId]); const getStartSelector = useMemo(() => startSelector(), []); @@ -199,7 +200,6 @@ 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]); 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 c783107cee362..8b2e3ccfa106e 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 @@ -25,7 +25,6 @@ import { setActiveTabTimeline, updateTimelineGraphEventId, updateTimelineSessionViewSessionId, - updateTimelineSessionViewEventId, } from '../../../../store/timeline/actions'; import { useGlobalFullScreen, @@ -144,7 +143,6 @@ const ActionsComponent: React.FC = ({ 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]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 12f0d70bca37e..caded6133497d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -24,6 +24,7 @@ import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { CellValueElementProps } from '../cell_rendering'; import { TimelineItem } from '../../../../../common/search_strategy'; +import { ENABLE_SESSION_VIEW_PLUGIN } from '../../../../../common/constants'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; @@ -56,6 +57,7 @@ import { HeaderActions } from '../body/actions/header_actions'; import { getDefaultControlColumn } from '../body/control_columns'; import { Sort } from '../body/sort'; import { Sourcerer } from '../../../../common/components/sourcerer'; +import { useKibana } from '../../../../common/lib/kibana'; const TimelineHeaderContainer = styled.div` margin-top: 6px; @@ -181,7 +183,11 @@ export const EqlTabContentComponent: React.FC = ({ runtimeMappings, selectedPatterns, } = useSourcererDataView(SourcererScopeName.timeline); - const ACTION_BUTTON_COUNT = 5; + const { uiSettings } = useKibana().services; + + const isSessionViewEnabled = uiSettings.get(ENABLE_SESSION_VIEW_PLUGIN); + + const ACTION_BUTTON_COUNT = isSessionViewEnabled ? 6 : 5; const isBlankTimeline: boolean = isEmpty(eqlQuery); @@ -252,7 +258,7 @@ export const EqlTabContentComponent: React.FC = ({ ...x, headerCellRender: HeaderActions, })), - [] + [ACTION_BUTTON_COUNT] ); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index 922ac652d141a..bae18f1dd8b4f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -24,6 +24,8 @@ import { Footer, footerHeight } from '../footer'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { useKibana } from '../../../../common/lib/kibana'; +import { ENABLE_SESSION_VIEW_PLUGIN } from '../../../../../common/constants'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; @@ -125,7 +127,11 @@ export const PinnedTabContentComponent: React.FC = ({ selectedPatterns, } = useSourcererDataView(SourcererScopeName.timeline); const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen(); - const ACTION_BUTTON_COUNT = 5; + const { uiSettings } = useKibana().services; + + const isSessionViewEnabled = uiSettings.get(ENABLE_SESSION_VIEW_PLUGIN); + + const ACTION_BUTTON_COUNT = isSessionViewEnabled ? 6 : 5; const filterQuery = useMemo(() => { if (isEmpty(pinnedEventIds)) { @@ -209,7 +215,7 @@ export const PinnedTabContentComponent: React.FC = ({ ...x, headerCellRender: HeaderActions, })), - [] + [ACTION_BUTTON_COUNT] ); return ( 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 1a6fcbf7c25ba..c2b0492ade42e 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 @@ -45,6 +45,7 @@ import { } from '../../../../../common/types/timeline'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; +import { ENABLE_SESSION_VIEW_PLUGIN } from '../../../../../common/constants'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; import { inputsModel, inputsSelectors, State } from '../../../../common/store'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; @@ -202,8 +203,10 @@ export const QueryTabContentComponent: React.FC = ({ } = useSourcererDataView(SourcererScopeName.timeline); const { uiSettings } = useKibana().services; - const ACTION_BUTTON_COUNT = 6; + const isSessionViewEnabled = uiSettings.get(ENABLE_SESSION_VIEW_PLUGIN); + + const ACTION_BUTTON_COUNT = isSessionViewEnabled ? 6 : 5; const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); const { filterManager: activeFilterManager } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId ?? '') @@ -330,7 +333,7 @@ export const QueryTabContentComponent: React.FC = ({ ...x, headerCellRender: HeaderActions, })), - [] + [ACTION_BUTTON_COUNT] ); return ( 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..e9156a7182c4a 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 @@ -38,7 +38,9 @@ const SessionTabContent: React.FC = ({ timelineId }) => { (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); const sessionViewMain = useMemo(() => { - return sessionViewId !== null ? sessionView.getSessionView(sessionViewId) : null; + return sessionViewId !== null + ? sessionView.getSessionView({ sessionEntityId: sessionViewId }) + : null; }, [sessionView, sessionViewId]); return {sessionViewMain}; 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 3f1159c3db1e7..10403d5404816 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,11 +81,6 @@ 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; 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 1a2c11925bfa3..36b790d451879 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,33 +287,13 @@ 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; + eventId: string | null; timelineById: TimelineById; }): TimelineById => { const timeline = timelineById[id]; @@ -322,7 +302,7 @@ export const updateSessionViewSessionId = ({ ...timelineById, [id]: { ...timeline, - sessionViewSessionId: eventId, + sessionViewId: eventId, }, }; }; 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 735ef7803d07c..73b51453f5e9f 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 @@ -64,7 +64,6 @@ export type TimelineModel = TGridModelForTimeline & { showSaveModal?: boolean; savedQueryId?: string | null; sessionViewId: string | null; - sessionViewSessionId: string | null; /** When true, show the timeline flyover */ show: boolean; /** status: active | draft */ @@ -121,7 +120,6 @@ export type SubsetTimelineModel = Readonly< | '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 d2110d1ff075a..66fe71d058c87 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,7 +44,6 @@ import { updateTimeline, updateTimelineGraphEventId, updateTitleAndDescription, - updateTimelineSessionViewEventId, updateTimelineSessionViewSessionId, toggleModalSaveTimeline, updateEqlOptions, @@ -79,7 +78,6 @@ import { updateGraphEventId, updateFilters, updateTimelineEventType, - updateSessionViewEventId, updateSessionViewSessionId, } from './helpers'; @@ -150,10 +148,6 @@ 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 }), diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index e3457a25aa7c4..6dfc312517008 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -33,6 +33,7 @@ import { NEWS_FEED_URL_SETTING, NEWS_FEED_URL_SETTING_DEFAULT, ENABLE_CCS_READ_WARNING_SETTING, + ENABLE_SESSION_VIEW_PLUGIN, } from '../common/constants'; import { transformConfigSchema } from '../common/transforms/types'; import { ExperimentalFeatures } from '../common/experimental_features'; @@ -232,6 +233,22 @@ export const initUiSettings = ( requiresPageReload: false, schema: schema.boolean(), }, + [ENABLE_SESSION_VIEW_PLUGIN]: { + name: i18n.translate('xpack.securitySolution.uiSettings.enableSessionView', { + defaultMessage: 'Enable Session Viewer Component', + }), + value: true, + description: i18n.translate( + 'xpack.securitySolution.uiSettings.enableSessionViewDescription', + { + defaultMessage: '

Enables the beta Session View component in the UI

', + } + ), + type: 'boolean', + category: [APP_ID], + requiresPageReload: false, + schema: schema.boolean(), + }, // TODO: Remove this check once the experimental flag is removed ...(experimentalFeatures.metricsEntitiesEnabled ? { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts index 3c0ba8c7dc07c..4c6f011ec870b 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts @@ -26,7 +26,7 @@ export const useStyles = () => { maxWidth: 800, maxHeight: 378, overflowY: 'auto', - backgroundColor: colors.mediumShade, + backgroundColor: colors.emptyShade, }; return { 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 387e7a5074699..e22edeeed42cd 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 @@ -192,7 +192,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 && user.id !== parent.user?.id; const interactiveSession = !!tty; const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; const iconTestSubj = hasExec diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index 6cc051bee0795..3fc95e92b9426 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: (props: SessionViewDeps) => JSX.Element; } From ebe4d56c8ece02f44d76b809839b0fcfeaf1e2d0 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 28 Mar 2022 21:12:17 -0400 Subject: [PATCH 11/19] Update tests and types, improve styles around full screen --- .../public/common/mock/global_state.ts | 1 + .../public/common/mock/timeline_results.ts | 2 + .../components/alerts_table/actions.test.tsx | 1 + .../public/hosts/pages/hosts.tsx | 2 +- .../components/graph_overlay/index.test.tsx | 30 ++++++++--- .../components/graph_overlay/index.tsx | 52 +++++++++++++++---- .../timeline/graph_tab_content/index.tsx | 37 +++++++++++-- .../timelines/components/timeline/index.tsx | 12 ++++- .../timeline/session_tab_content/index.tsx | 34 ++++++++++-- .../timeline/tabs_content/index.tsx | 4 +- .../public/components/process_tree/hooks.ts | 2 +- .../public/components/session_view/index.tsx | 2 +- .../public/components/session_view/styles.ts | 8 +-- x-pack/plugins/session_view/public/types.ts | 2 +- .../components/t_grid/integrated/index.tsx | 1 + 15 files changed, 157 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 52f0b1a682097..80b17e78a78a5 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -313,6 +313,7 @@ export const mockGlobalState: State = { end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, + sessionViewId: null, show: false, showCheckboxes: false, pinnedEventIds: {}, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 2de29a8c3acf8..4bbdca8564a8e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2011,6 +2011,7 @@ export const mockTimelineModel: TimelineModel = { savedObjectId: 'ef579e40-jibber-jabber', selectAll: false, selectedEventIds: {}, + sessionViewId: null, show: false, showCheckboxes: false, sort: [ @@ -2132,6 +2133,7 @@ export const defaultTimelineProps: CreateTimelineProps = { savedObjectId: null, selectAll: false, selectedEventIds: {}, + sessionViewId: null, show: false, showCheckboxes: false, sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index b1226e5b59190..63f71dc0d3723 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -228,6 +228,7 @@ describe('alert actions', () => { savedObjectId: null, selectAll: false, selectedEventIds: {}, + sessionViewId: null, show: true, showCheckboxes: false, sort: [ diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 64cd419b85a05..3b57a22d15a6a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -181,7 +181,7 @@ const HostsComponent = () => { return ( <> - {true ? ( + {indicesExist ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index e8d144f07827f..4c860d20262e4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -37,6 +37,24 @@ jest.mock('../../../resolver/view/use_state_syncing_actions'); const useStateSyncingActionsMock = useStateSyncingActions as jest.Mock; jest.mock('../../../resolver/view/use_sync_selected_node'); +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + return { + ...original, + useKibana: () => ({ + services: { + sessionView: { + getSessionView: () =>
, + }, + data: { + search: { + search: jest.fn(), + }, + }, + }, + }), + }; +}); describe('GraphOverlay', () => { const { storage } = createSecuritySolutionStorageMock(); @@ -57,7 +75,7 @@ describe('GraphOverlay', () => { test('it has 100% width when NOT in full screen mode', async () => { const wrapper = mount( - + {}} /> ); @@ -79,7 +97,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + {}} /> ); @@ -110,7 +128,7 @@ describe('GraphOverlay', () => { storage )} > - + {}} /> ); @@ -126,7 +144,7 @@ describe('GraphOverlay', () => { test('it has 100% width when NOT in full screen mode', async () => { const wrapper = mount( - + {}} /> ); @@ -148,7 +166,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + {}} /> ); @@ -189,7 +207,7 @@ describe('GraphOverlay', () => { storage )} > - + {}} /> ); 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 c8b845cf370a7..98fcb281c54e1 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 @@ -92,6 +92,7 @@ interface NavigationProps { timelineId: TimelineId; timelineFullScreen: boolean; toggleFullScreen: () => void; + graphEventId?: string; } const NavigationComponent: React.FC = ({ @@ -101,11 +102,12 @@ const NavigationComponent: React.FC = ({ timelineId, timelineFullScreen, toggleFullScreen, + graphEventId, }) => ( - {i18n.CLOSE_ANALYZER} + {graphEventId ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION} {timelineId !== TimelineId.active && ( @@ -245,16 +247,43 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe ); if (!isInTimeline && sessionViewId !== null) { - return ( - - - - {i18n.CLOSE_SESSION} - - - {sessionViewMain} - - ); + if (fullScreen) { + return ( + + + + + {sessionViewMain} + + ); + } else { + return ( + + + + + + {sessionViewMain} + + + ); + } } else if (fullScreen && !isInTimeline) { return ( @@ -268,6 +297,7 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe timelineId={timelineId} timelineFullScreen={timelineFullScreen} toggleFullScreen={toggleFullScreen} + graphEventId={graphEventId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx index 64d3f01cff265..8dce0c9e0a4bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx @@ -6,27 +6,58 @@ */ import React, { useMemo } from 'react'; - +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; import { timelineSelectors } from '../../../store/timeline'; import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; -import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { GraphOverlay } from '../../graph_overlay'; +import { useDetailPanel } from '../../side_panel/hooks/use_detail_panel'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; interface GraphTabContentProps { timelineId: TimelineId; } +const ScrollableFlexItem = styled(EuiFlexItem)` + ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} + overflow: hidden; +`; + +const VerticalRule = styled.div` + width: 2px; + height: 100%; + background: ${({ theme }) => theme.eui.euiColorLightShade}; +`; + const GraphTabContentComponent: React.FC = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const graphEventId = useShallowEqualSelector( (state) => getTimeline(state, timelineId)?.graphEventId ); + const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ + isFlyoutView: true, + sourcererScope: SourcererScopeName.timeline, + timelineId, + tabType: TimelineTabs.query, + }); + if (!graphEventId) { return null; } - return ; + return ( + <> + + {shouldShowDetailsPanel && ( + <> + + {DetailsPanel} + + )} + + ); }; GraphTabContentComponent.displayName = 'GraphTabContentComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index e4577649bcdca..6ead70b79caac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -74,9 +74,18 @@ const StatefulTimelineComponent: React.FC = ({ savedObjectId, timelineType, description, + sessionViewId, } = useDeepEqualSelector((state) => pick( - ['indexNames', 'dataViewId', 'graphEventId', 'savedObjectId', 'timelineType', 'description'], + [ + 'indexNames', + 'dataViewId', + 'graphEventId', + 'savedObjectId', + 'timelineType', + 'description', + 'sessionViewId', + ], getTimeline(state, timelineId) ?? timelineDefaults ) ); @@ -193,6 +202,7 @@ const StatefulTimelineComponent: React.FC = ({ = ({ timelineId }) => { const { sessionView } = useKibana().services; + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const sessionViewId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId ); + const { setTimelineFullScreen } = useTimelineFullScreen(); + const onCloseOverlay = useCallback(() => { + const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null; + // Since EUI changes these values directly as a side effect, need to add them back on close. + if (isDataGridFullScreen) { + document.body.classList.add('euiDataGrid__restrictBody'); + } else { + setTimelineFullScreen(false); + } + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); + }, [dispatch, timelineId, setTimelineFullScreen]); const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ sourcererScope: SourcererScopeName.timeline, timelineId, @@ -60,7 +81,14 @@ const SessionTabContent: React.FC = ({ timelineId }) => { return ( - {sessionViewMain} + + + + {i18n.CLOSE_SESSION} + + + {sessionViewMain} + {shouldShowDetailsPanel && ( <> 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 8477e9ed136dd..d3f5e2564c1ae 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 @@ -60,6 +60,7 @@ interface BasicTimelineTab { timelineId: TimelineId; timelineType: TimelineType; graphEventId?: string; + sessionViewId?: string | null; timelineDescription: string; } @@ -234,6 +235,7 @@ const TabsContentComponent: React.FC = ({ timelineFullScreen, timelineType, graphEventId, + sessionViewId, timelineDescription, }) => { const dispatch = useDispatch(); @@ -376,7 +378,7 @@ const TabsContentComponent: React.FC = ({ data-test-subj={`timelineTabs-${TimelineTabs.session}`} onClick={setSessionAsActiveTab} isSelected={activeTab === TimelineTabs.session} - disabled={false} + disabled={sessionViewId === null} key={TimelineTabs.session} > {i18n.SESSION_TAB} 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 7b2981aac723f..61483b1211b31 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 @@ -192,7 +192,7 @@ export class ProcessImpl implements Process { // TODO: add these to generator const actionsToFind = [EventAction.fork, EventAction.exec, EventAction.end]; const filtered = events.filter((processEvent) => { - return true; + return actionsToFind.includes(processEvent.event.action); }); // because events is already ordered by @timestamp we take the last event diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index e4b585794d1b0..d3e348401e088 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -131,7 +131,7 @@ export const SessionView = ({ return ( <> - +
{ +export const useStyles = ({ height = '500px' }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { @@ -22,12 +22,12 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const thinBorder = `${border.width.thin} solid ${colors.lightShade}!important`; const processTree: CSSObject = { - height: `${height}px`, + height: `${height}`, position: 'relative', }; const detailPanel: CSSObject = { - height: `${height}px`, + height: `${height}`, borderLeft: thinBorder, borderRight: thinBorder, }; diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index 12e53c8626e12..20525f17682ad 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -16,7 +16,7 @@ export type SessionViewServices = CoreStart & { export interface SessionViewDeps { // the root node of the process tree to render. e.g process.entry.entity_id or process.session_leader.entity_id sessionEntityId: string; - height?: number; + height?: string; // if provided, the session view will jump to and select the provided event if it belongs to the session leader // session view will fetch a page worth of events starting from jumpToEvent as well as a page backwards. jumpToEvent?: ProcessEvent; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 69c04b31fa44b..6f74fd1d2cb37 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -58,6 +58,7 @@ const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` display: flex; flex-direction: column; position: relative; + width: 100%; ${({ $isFullScreen }) => $isFullScreen && From e99a71bef9f56aa3b2fa097a0a548fd5522fe5c9 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 28 Mar 2022 22:45:19 -0400 Subject: [PATCH 12/19] Fix mistake in intl message, clean up tests --- .../components/events_viewer/index.test.tsx | 53 ++---------- .../components/graph_overlay/index.test.tsx | 85 +++++++++++++------ .../timeline/tabs_content/translations.ts | 2 +- 3 files changed, 65 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 9d3a74e8172a2..b59fa0283a512 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -9,9 +9,8 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; -import { waitFor } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; @@ -61,37 +60,27 @@ const testProps = { start: from, }; describe('StatefulEventsViewer', () => { - const mount = useMountAppended(); - (useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]); test('it renders the events viewer', async () => { - const wrapper = mount( + const wrapper = render( ); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.text()).toMatchInlineSnapshot(`"hello grid"`); - }); + expect(wrapper.getByText('hello grid')).toBeTruthy(); }); // InspectButtonContainer controls displaying InspectButton components test('it renders InspectButtonContainer', async () => { - const wrapper = mount( + const wrapper = render( ); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true); - }); + expect(wrapper.findByTestId(`InspectButtonContainer`)).toBeTruthy(); }); test('it closes field editor when unmounted', async () => { @@ -101,42 +90,14 @@ describe('StatefulEventsViewer', () => { return {}; }); - const wrapper = mount( + const { unmount } = render( ); expect(mockCloseEditor).not.toHaveBeenCalled(); - wrapper.unmount(); + unmount(); expect(mockCloseEditor).toHaveBeenCalled(); }); - - test('renders the graph overlay when a graph event id is set', async () => { - const propsWithGraphId = { - ...testProps, - graphEventId: 'test', - }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('overlayContainer').exists()).toBe(true); - }); - - test('renders the graph overlay when a session event id is set', async () => { - const propsWithGraphId = { - ...testProps, - sessionViewId: 'test', - }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('overlayContainer').exists()).toBe(true); - }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index 4c860d20262e4..685b63f2a40e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import { waitFor } from '@testing-library/react'; -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; +import '@testing-library/jest-dom'; import { useGlobalFullScreen, useTimelineFullScreen, @@ -72,20 +72,18 @@ describe('GraphOverlay', () => { }); describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { - test('it has 100% width when NOT in full screen mode', async () => { - const wrapper = mount( + test('it has 100% width when NOT in full screen mode', () => { + const wrapper = render( {}} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', '100%'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('width', '100%'); }); - test('it has a fixed position when in full screen mode', async () => { + test('it has a fixed position when in full screen mode', () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: true, setGlobalFullScreen: jest.fn(), @@ -95,20 +93,18 @@ describe('GraphOverlay', () => { setTimelineFullScreen: jest.fn(), }); - const wrapper = mount( + const wrapper = render( {}} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('position', 'fixed'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('position', 'fixed'); }); test('it gets index pattern from default data view', () => { - mount( + render( { describe('when used in the active timeline', () => { const timelineId = TimelineId.active; - test('it has 100% width when NOT in full screen mode', async () => { - const wrapper = mount( + test('it has 100% width when NOT in full screen mode', () => { + const wrapper = render( {}} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', '100%'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('width', '100%'); }); - test('it has 100% width when the active timeline is in full screen mode', async () => { + test('it has 100% width when the active timeline is in full screen mode', () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: false, setGlobalFullScreen: jest.fn(), @@ -164,20 +158,18 @@ describe('GraphOverlay', () => { setTimelineFullScreen: jest.fn(), }); - const wrapper = mount( + const wrapper = render( {}} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', '100%'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('width', '100%'); }); test('it gets index pattern from Timeline data view', () => { - mount( + render( { expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual(mockIndexNames); }); + + test('it renders session view controls', () => { + (useGlobalFullScreen as jest.Mock).mockReturnValue({ + globalFullScreen: false, + setGlobalFullScreen: jest.fn(), + }); + (useTimelineFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: true, + setTimelineFullScreen: jest.fn(), + }); + + const wrapper = render( + + {}} /> + + ); + + expect(wrapper.findByText('Close Session')).toBeTruthy(); + }); }); }); 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 b116d2f551045..e3a53675389b7 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 @@ -40,7 +40,7 @@ export const PINNED_TAB = i18n.translate( ); export const SESSION_TAB = i18n.translate( - 'pack.securitySolution.timeline.tabs.sessionTabTimelineTitle', + 'xpack.securitySolution.timeline.tabs.sessionTabTimelineTitle', { defaultMessage: 'Session View', } From 9b592b50adb7468c28f298e66f709b7f5339677c Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 29 Mar 2022 01:46:29 -0400 Subject: [PATCH 13/19] Create a hook for session view components and handlers --- .../components/graph_overlay/index.tsx | 99 +++++-------- .../timeline/body/actions/index.tsx | 2 +- .../components/timeline/body/index.tsx | 11 +- .../timeline/session_tab_content/index.tsx | 51 +------ .../use_session_view.test.tsx | 137 ++++++++++++++++++ .../session_tab_content/use_session_view.tsx | 78 ++++++++++ 6 files changed, 263 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx 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 98fcb281c54e1..5d0c721af9839 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 @@ -20,24 +20,16 @@ import styled from 'styled-components'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { - FULL_SCREEN_TOGGLED_CLASS_NAME, - SCROLLING_DISABLED_CLASS_NAME, -} from '../../../../common/constants'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; 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, - updateTimelineSessionViewSessionId, -} from '../../../timelines/store/timeline/actions'; import { inputsActions } from '../../../common/store/actions'; import { Resolver } from '../../../resolver/view'; import { @@ -49,6 +41,7 @@ import * as i18n from './translations'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { sourcererSelectors } from '../../../common/store'; +import { useSessionView } from '../timeline/session_tab_content/use_session_view'; const OverlayContainer = styled.div` display: flex; @@ -103,34 +96,35 @@ const NavigationComponent: React.FC = ({ timelineFullScreen, toggleFullScreen, graphEventId, -}) => ( - - - - {graphEventId ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION} - - - {timelineId !== TimelineId.active && ( +}) => { + return ( + - - - + + {graphEventId ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION} + - )} - -); - + {timelineId !== TimelineId.active && ( + + + + + + )} + + ); +}; NavigationComponent.displayName = 'NavigationComponent'; const Navigation = React.memo(NavigationComponent); @@ -144,18 +138,9 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe 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({ - sessionEntityId: sessionViewId, - loadAlertDetails: openDetailsPanel, - }) - : null; - }, [sessionView, sessionViewId, openDetailsPanel]); const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); @@ -189,25 +174,6 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe ); const isInTimeline = timelineId === TimelineId.active; - const onCloseOverlay = useCallback(() => { - const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null; - // Since EUI changes these values directly as a side effect, need to add them back on close. - if (isDataGridFullScreen) { - if (timelineId === TimelineId.active) { - document.body.classList.add('euiDataGrid__restrictBody'); - } else { - document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody'); - } - } else { - if (timelineId === TimelineId.active) { - setTimelineFullScreen(false); - } else { - setGlobalFullScreen(false); - } - } - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); - dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); - }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]); useEffect(() => { return () => { @@ -246,6 +212,8 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe [defaultDataView.patternList, isInTimeline, timelinePatterns] ); + const { SessionView, onCloseOverlay } = useSessionView({ timelineId }); + if (!isInTimeline && sessionViewId !== null) { if (fullScreen) { return ( @@ -261,7 +229,7 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe graphEventId={graphEventId} /> - {sessionViewMain} + {SessionView} ); } else { @@ -279,7 +247,7 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe graphEventId={graphEventId} /> - {sessionViewMain} + {SessionView} ); @@ -330,6 +298,7 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe timelineId={timelineId} timelineFullScreen={timelineFullScreen} toggleFullScreen={toggleFullScreen} + graphEventId={graphEventId} /> 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 8b2e3ccfa106e..2e41a04aa80e9 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 @@ -132,7 +132,7 @@ const ActionsComponent: React.FC = ({ const entryLeader = useMemo(() => { const { process } = ecsData; const entryLeaderIds = process?.entry_leader?.entity_id; - if (entryLeaderIds !== undefined) { + if (entryLeaderIds !== undefined && entryLeaderIds.length > 0) { return entryLeaderIds[0]; } else { return 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 26fabddc329d5..f891f6824dd80 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 @@ -11,7 +11,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, ConnectedProps, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { APP_ID } from '../../../../../common/constants'; +import { APP_ID, ENABLE_SESSION_VIEW_PLUGIN } from '../../../../../common/constants'; import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; import { FIRST_ARIA_INDEX, @@ -108,7 +108,9 @@ export const BodyComponent = React.memo( const { queryFields, selectAll } = useDeepEqualSelector((state) => getManageTimeline(state, id) ); - const ACTION_BUTTON_COUNT = 6; + const { uiSettings } = useKibana().services; + const isSessionViewEnabled = uiSettings.get(ENABLE_SESSION_VIEW_PLUGIN); + const ACTION_BUTTON_COUNT = isSessionViewEnabled ? 6 : 5; const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { @@ -172,7 +174,10 @@ export const BodyComponent = React.memo( return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); }, [excludedRowRendererIds, rowRenderers]); - const actionsColumnWidth = useMemo(() => getActionsColumnWidth(ACTION_BUTTON_COUNT), []); + const actionsColumnWidth = useMemo( + () => getActionsColumnWidth(ACTION_BUTTON_COUNT), + [ACTION_BUTTON_COUNT] + ); const columnWidths = useMemo( () => 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 7d648d73ab0ea..25a25b125b2d7 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 @@ -5,23 +5,12 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; -import { timelineSelectors } from '../../../store/timeline'; -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 { useDetailPanel } from '../../side_panel/hooks/use_detail_panel'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; -import { - updateTimelineGraphEventId, - updateTimelineSessionViewSessionId, -} from '../../../../timelines/store/timeline/actions'; +import { TimelineId } from '../../../../../common/types/timeline'; import * as i18n from '../../graph_overlay/translations'; +import { useSessionView } from './use_session_view'; const FullWidthFlexGroup = styled(EuiFlexGroup)` margin: 0; @@ -45,39 +34,9 @@ interface Props { } const SessionTabContent: React.FC = ({ timelineId }) => { - const { sessionView } = useKibana().services; - const dispatch = useDispatch(); - - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - - const sessionViewId = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId - ); - const { setTimelineFullScreen } = useTimelineFullScreen(); - const onCloseOverlay = useCallback(() => { - const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null; - // Since EUI changes these values directly as a side effect, need to add them back on close. - if (isDataGridFullScreen) { - document.body.classList.add('euiDataGrid__restrictBody'); - } else { - setTimelineFullScreen(false); - } - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); - dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); - }, [dispatch, timelineId, setTimelineFullScreen]); - const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ - sourcererScope: SourcererScopeName.timeline, + const { SessionView, onCloseOverlay, shouldShowDetailsPanel, DetailsPanel } = useSessionView({ timelineId, - tabType: TimelineTabs.session, }); - const sessionViewMain = useMemo(() => { - return sessionViewId !== null - ? sessionView.getSessionView({ - sessionEntityId: sessionViewId, - loadAlertDetails: openDetailsPanel, - }) - : null; - }, [openDetailsPanel, sessionView, sessionViewId]); return ( @@ -87,7 +46,7 @@ const SessionTabContent: React.FC = ({ timelineId }) => { {i18n.CLOSE_SESSION} - {sessionViewMain} + {SessionView} {shouldShowDetailsPanel && ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx new file mode 100644 index 0000000000000..e585b1cf90fe7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; + +import { renderHook } from '@testing-library/react-hooks'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { mockTimelineModel } from '../../../../common/mock'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + useTimelineFullScreen, + useGlobalFullScreen, +} from '../../../../common/containers/use_full_screen'; +import { useSessionView } from './use_session_view'; + +const mockDispatch = jest.fn(); +jest.mock('../../../../common/hooks/use_selector', () => ({ + useDeepEqualSelector: () => { + return 'test'; + }, + useShallowEqualSelector: () => mockTimelineModel, +})); + +jest.mock('../../../../common/containers/use_full_screen'); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + capabilities: { + siem: { crud_alerts: true, read_alerts: true }, + }, + }, + sessionView: { + getSessionView: jest.fn().mockReturnValue(
), + }, + data: { + search: jest.fn(), + query: jest.fn(), + }, + uiSettings: { + get: jest.fn(), + }, + savedObjects: { + client: {}, + }, + timelines: { + getLastUpdated: jest.fn(), + getLoadingPanel: jest.fn(), + getFieldBrowser: jest.fn(), + getUseDraggableKeyboardWrapper: () => + jest.fn().mockReturnValue({ + onBlur: jest.fn(), + onKeyDown: jest.fn(), + }), + }, + }, + }), + }; +}); +jest.mock('../../side_panel/hooks/use_detail_panel', () => { + return { + useDetailPanel: () => ({ + openDetailsPanel: () => {}, + handleOnDetailsPanelClosed: () => {}, + DetailsPanel: () =>
, + shouldShowDetailsPanel: false, + }), + }; +}); + +describe('useSessionView', () => { + let setTimelineFullScreen: jest.Mock; + let setGlobalFullScreen: jest.Mock; + let kibana: ReturnType; + const Wrapper = memo(({ children }) => { + kibana = useKibana(); + return <>{children}; + }); + Wrapper.displayName = 'Wrapper'; + + beforeEach(() => { + setTimelineFullScreen = jest.fn(); + setGlobalFullScreen = jest.fn(); + (useTimelineFullScreen as jest.Mock).mockImplementation(() => ({ + setTimelineFullScreen, + })); + (useGlobalFullScreen as jest.Mock).mockImplementation(() => ({ + setGlobalFullScreen, + })); + }); + + it('removes the full screen class from the overlay', () => { + renderHook( + () => { + const testProps = { + timelineId: TimelineId.active, + }; + return useSessionView(testProps); + }, + { wrapper: Wrapper } + ); + expect(kibana.services.sessionView.getSessionView).toHaveBeenCalled(); + }); + + it('calls setTimelineFullScreen with false when onCloseOverlay is called and the app is not in full screen mode', () => { + const { result } = renderHook( + () => { + const testProps = { + timelineId: TimelineId.active, + }; + return useSessionView(testProps); + }, + { wrapper: Wrapper } + ); + result.current.onCloseOverlay(); + + expect(setTimelineFullScreen).toBeCalledWith(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx new file mode 100644 index 0000000000000..cb89946c4eaea --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx @@ -0,0 +1,78 @@ +/* + * 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 { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { timelineSelectors } from '../../../store/timeline'; +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 { useDetailPanel } from '../../side_panel/hooks/use_detail_panel'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../../common/constants'; +import { + useTimelineFullScreen, + useGlobalFullScreen, +} from '../../../../common/containers/use_full_screen'; +import { + updateTimelineGraphEventId, + updateTimelineSessionViewSessionId, +} from '../../../../timelines/store/timeline/actions'; + +export const useSessionView = ({ timelineId }: { timelineId: TimelineId }) => { + const { sessionView } = useKibana().services; + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + + const sessionViewId = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId + ); + + const { setGlobalFullScreen } = useGlobalFullScreen(); + + const { setTimelineFullScreen } = useTimelineFullScreen(); + const onCloseOverlay = useCallback(() => { + const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null; + // Since EUI changes these values directly as a side effect, need to add them back on close. + if (isDataGridFullScreen) { + if (timelineId === TimelineId.active) { + document.body.classList.add('euiDataGrid__restrictBody'); + } else { + document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody'); + } + } else { + if (timelineId === TimelineId.active) { + setTimelineFullScreen(false); + } else { + setGlobalFullScreen(false); + } + } + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); + }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]); + const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ + sourcererScope: SourcererScopeName.timeline, + timelineId, + tabType: TimelineTabs.session, + }); + const sessionViewComponent = useMemo(() => { + return sessionViewId !== null + ? sessionView.getSessionView({ + sessionEntityId: sessionViewId, + loadAlertDetails: openDetailsPanel, + }) + : null; + }, [openDetailsPanel, sessionView, sessionViewId]); + + return { + onCloseOverlay, + shouldShowDetailsPanel, + SessionView: sessionViewComponent, + DetailsPanel, + }; +}; From 169e9d0890047f19823c4dd78cb05dcd975062b2 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 29 Mar 2022 04:05:49 -0400 Subject: [PATCH 14/19] Use hook to share logic between tgrid and timeline, clean up css --- .../components/graph_overlay/index.tsx | 144 ++--------------- .../timeline/body/actions/index.tsx | 16 +- .../timeline/session_tab_content/index.tsx | 16 +- .../session_tab_content}/translations.ts | 0 .../use_session_view.test.tsx | 4 +- .../session_tab_content/use_session_view.tsx | 153 ++++++++++++++++-- .../timeline/tabs_content/index.tsx | 18 +-- .../security_solution/server/ui_settings.ts | 4 +- 8 files changed, 187 insertions(+), 168 deletions(-) rename x-pack/plugins/security_solution/public/timelines/components/{graph_overlay => timeline/session_tab_content}/translations.ts (100%) 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 5d0c721af9839..abcaeb7182cde 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 @@ -5,22 +5,10 @@ * 2.0. */ -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiToolTip, - EuiLoadingSpinner, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useEffect } from 'react'; +import React, { useMemo, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiLoadingSpinner } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; - -import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; -import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useGlobalFullScreen, useTimelineFullScreen, @@ -37,7 +25,6 @@ import { startSelector, endSelector, } from '../../../common/components/super_date_picker/selectors'; -import * as i18n from './translations'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { sourcererSelectors } from '../../../common/store'; @@ -63,10 +50,6 @@ const StyledResolver = styled(Resolver)` height: 100%; `; -const FullScreenButtonIcon = styled(EuiButtonIcon)` - margin: 4px 0 4px 0; -`; - const ScrollableFlexItem = styled(EuiFlexItem)` ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} overflow: hidden; @@ -78,61 +61,10 @@ interface GraphOverlayProps { timelineId: TimelineId; } -interface NavigationProps { - fullScreen: boolean; - globalFullScreen: boolean; - onCloseOverlay: () => void; - timelineId: TimelineId; - timelineFullScreen: boolean; - toggleFullScreen: () => void; - graphEventId?: string; -} - -const NavigationComponent: React.FC = ({ - fullScreen, - globalFullScreen, - onCloseOverlay, - timelineId, - timelineFullScreen, - toggleFullScreen, - graphEventId, -}) => { - return ( - - - - {graphEventId ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION} - - - {timelineId !== TimelineId.active && ( - - - - - - )} - - ); -}; -NavigationComponent.displayName = 'NavigationComponent'; - -const Navigation = React.memo(NavigationComponent); - const GraphOverlayComponent: React.FC = ({ timelineId, openDetailsPanel }) => { const dispatch = useDispatch(); - const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); - const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen } = useTimelineFullScreen(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const graphEventId = useDeepEqualSelector( @@ -185,20 +117,6 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe }; }, [dispatch, timelineId]); - const toggleFullScreen = useCallback(() => { - if (timelineId === TimelineId.active) { - setTimelineFullScreen(!timelineFullScreen); - } else { - setGlobalFullScreen(!globalFullScreen); - } - }, [ - timelineId, - setTimelineFullScreen, - timelineFullScreen, - setGlobalFullScreen, - globalFullScreen, - ]); - const getDefaultDataViewSelector = useMemo( () => sourcererSelectors.defaultDataViewSelector(), [] @@ -212,41 +130,23 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe [defaultDataView.patternList, isInTimeline, timelinePatterns] ); - const { SessionView, onCloseOverlay } = useSessionView({ timelineId }); + const { SessionView, Navigation } = useSessionView({ timelineId }); if (!isInTimeline && sessionViewId !== null) { if (fullScreen) { return ( - - - - {SessionView} + + {Navigation} + {SessionView} + ); } else { return ( - - - + {Navigation} {SessionView} @@ -257,17 +157,7 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe - - - + {Navigation} {graphEventId !== undefined ? ( @@ -290,17 +180,7 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe - - - + {Navigation} {graphEventId !== undefined ? ( 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 2e41a04aa80e9..300f000bb4a63 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 @@ -140,11 +140,23 @@ const ActionsComponent: React.FC = ({ }, [ecsData]); const openSessionView = useCallback(() => { + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); + if (timelineId === TimelineId.active) { + if (dataGridIsFullScreen) { + setTimelineFullScreen(true); + } + if (entryLeader !== null) { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session })); + } + } else { + if (dataGridIsFullScreen) { + setGlobalFullScreen(true); + } + } if (entryLeader !== null) { - dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session })); dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: entryLeader })); } - }, [dispatch, timelineId, entryLeader]); + }, [dispatch, timelineId, entryLeader, setGlobalFullScreen, setTimelineFullScreen]); return ( 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 25a25b125b2d7..a12b377fd3a22 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 @@ -6,10 +6,9 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { TimelineId } from '../../../../../common/types/timeline'; -import * as i18n from '../../graph_overlay/translations'; import { useSessionView } from './use_session_view'; const FullWidthFlexGroup = styled(EuiFlexGroup)` @@ -21,6 +20,7 @@ const FullWidthFlexGroup = styled(EuiFlexGroup)` const ScrollableFlexItem = styled(EuiFlexItem)` ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} overflow: hidden; + width: 100%; `; const VerticalRule = styled.div` @@ -34,19 +34,15 @@ interface Props { } const SessionTabContent: React.FC = ({ timelineId }) => { - const { SessionView, onCloseOverlay, shouldShowDetailsPanel, DetailsPanel } = useSessionView({ + const { SessionView, shouldShowDetailsPanel, DetailsPanel, Navigation } = useSessionView({ timelineId, }); return ( - - - - {i18n.CLOSE_SESSION} - - - {SessionView} + + {Navigation} + {SessionView} {shouldShowDetailsPanel && ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts rename to x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/translations.ts diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx index e585b1cf90fe7..5af19205175b2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx @@ -130,8 +130,8 @@ describe('useSessionView', () => { }, { wrapper: Wrapper } ); - result.current.onCloseOverlay(); + const navigation = result.current.Navigation; - expect(setTimelineFullScreen).toBeCalledWith(false); + expect(navigation).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx index cb89946c4eaea..a0554d6889822 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx @@ -5,8 +5,10 @@ * 2.0. */ -import { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; +import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { timelineSelectors } from '../../../store/timeline'; import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; @@ -14,7 +16,13 @@ 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'; -import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../../common/constants'; +import { isFullScreen } from '../body/column_headers'; +import { + SCROLLING_DISABLED_CLASS_NAME, + FULL_SCREEN_TOGGLED_CLASS_NAME, +} from '../../../../../common/constants'; +import { FULL_SCREEN } from '../../timeline/body/column_headers/translations'; +import { EXIT_FULL_SCREEN } from '../../../../common/components/exit_full_screen/translations'; import { useTimelineFullScreen, useGlobalFullScreen, @@ -22,20 +30,76 @@ import { import { updateTimelineGraphEventId, updateTimelineSessionViewSessionId, + setActiveTabTimeline, } from '../../../../timelines/store/timeline/actions'; +import * as i18n from './translations'; + +const FullScreenButtonIcon = styled(EuiButtonIcon)` + margin: 4px 0 4px 0; +`; +interface NavigationProps { + fullScreen: boolean; + globalFullScreen: boolean; + onCloseOverlay: () => void; + timelineId: TimelineId; + timelineFullScreen: boolean; + toggleFullScreen: () => void; + graphEventId?: string; + activeTab: TimelineTabs; +} + +const NavigationComponent: React.FC = ({ + fullScreen, + globalFullScreen, + onCloseOverlay, + timelineId, + timelineFullScreen, + toggleFullScreen, + graphEventId, + activeTab, +}) => { + return ( + + + + {activeTab === TimelineTabs.graph ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION} + + + {timelineId !== TimelineId.active && ( + + + + + + )} + + ); +}; +NavigationComponent.displayName = 'NavigationComponent'; + +const Navigation = React.memo(NavigationComponent); export const useSessionView = ({ timelineId }: { timelineId: TimelineId }) => { const { sessionView } = useKibana().services; const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); - const sessionViewId = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId + const { graphEventId, sessionViewId, activeTab, prevActiveTab } = useDeepEqualSelector( + (state) => getTimeline(state, timelineId) ?? timelineDefaults ); - - const { setGlobalFullScreen } = useGlobalFullScreen(); - - const { setTimelineFullScreen } = useTimelineFullScreen(); const onCloseOverlay = useCallback(() => { const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null; // Since EUI changes these values directly as a side effect, need to add them back on close. @@ -52,9 +116,51 @@ export const useSessionView = ({ timelineId }: { timelineId: TimelineId }) => { setGlobalFullScreen(false); } } - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); - dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); - }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]); + if (timelineId !== TimelineId.active) { + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); + } else { + if (activeTab === TimelineTabs.graph) { + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + if (prevActiveTab === TimelineTabs.session && !sessionViewId) { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query })); + } + } else if (activeTab === TimelineTabs.session) { + dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null })); + if (prevActiveTab === TimelineTabs.graph && !graphEventId) { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query })); + } else { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: prevActiveTab })); + } + } + } + }, [ + dispatch, + timelineId, + setTimelineFullScreen, + setGlobalFullScreen, + activeTab, + prevActiveTab, + graphEventId, + sessionViewId, + ]); + const fullScreen = useMemo( + () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), + [globalFullScreen, timelineId, timelineFullScreen] + ); + const toggleFullScreen = useCallback(() => { + if (timelineId === TimelineId.active) { + setTimelineFullScreen(!timelineFullScreen); + } else { + setGlobalFullScreen(!globalFullScreen); + } + }, [ + timelineId, + setTimelineFullScreen, + timelineFullScreen, + setGlobalFullScreen, + globalFullScreen, + ]); const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ sourcererScope: SourcererScopeName.timeline, timelineId, @@ -69,10 +175,35 @@ export const useSessionView = ({ timelineId }: { timelineId: TimelineId }) => { : null; }, [openDetailsPanel, sessionView, sessionViewId]); + const navigation = useMemo(() => { + return ( + + ); + }, [ + fullScreen, + globalFullScreen, + activeTab, + graphEventId, + onCloseOverlay, + timelineFullScreen, + timelineId, + toggleFullScreen, + ]); + return { onCloseOverlay, shouldShowDetailsPanel, SessionView: sessionViewComponent, DetailsPanel, + Navigation: navigation, }; }; 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 d3f5e2564c1ae..2f5c986829b91 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 @@ -347,6 +347,15 @@ const TabsContentComponent: React.FC = ({ > {i18n.ANALYZER_TAB} + + {i18n.SESSION_TAB} + = ({
)} - - {i18n.SESSION_TAB} - )} diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index 6dfc312517008..538644a09e98e 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -235,13 +235,13 @@ export const initUiSettings = ( }, [ENABLE_SESSION_VIEW_PLUGIN]: { name: i18n.translate('xpack.securitySolution.uiSettings.enableSessionView', { - defaultMessage: 'Enable Session Viewer Component', + defaultMessage: 'Enable Session View Component', }), value: true, description: i18n.translate( 'xpack.securitySolution.uiSettings.enableSessionViewDescription', { - defaultMessage: '

Enables the beta Session View component in the UI

', + defaultMessage: '

Enables the Session View component in the UI

', } ), type: 'boolean', From 545cddd2f527062692d5510b285642462cbe22c6 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 29 Mar 2022 06:46:01 -0400 Subject: [PATCH 15/19] Fix flyout usage with new hook --- .../common/components/events_viewer/index.tsx | 18 +++++------------- .../components/graph_overlay/index.test.tsx | 14 +++++++------- .../components/graph_overlay/index.tsx | 12 +++++++----- .../side_panel/event_details/index.tsx | 4 ++-- .../timeline/graph_tab_content/index.tsx | 12 ++++-------- .../session_tab_content/use_session_view.tsx | 15 +++++++++++++-- 6 files changed, 38 insertions(+), 37 deletions(-) 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 3c25b5abfc3ec..d46ab4b62be68 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,12 +11,7 @@ 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, - TimelineTabs, -} from '../../../../common/types/timeline'; +import { ControlColumnProps, RowRenderer, TimelineId } 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'; @@ -37,7 +32,7 @@ import { useFieldBrowserOptions, FieldEditorActions, } from '../../../timelines/components/fields_browser'; -import { useDetailPanel } from '../../../timelines/components/side_panel/hooks/use_detail_panel'; +import { useSessionView } from '../../../timelines/components/timeline/session_tab_content/use_session_view'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -162,21 +157,18 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; - const { openDetailsPanel, DetailsPanel } = useDetailPanel({ - isFlyoutView: true, + const { DetailsPanel, SessionView, Navigation } = useSessionView({ 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, openDetailsPanel]); + }, [graphEventId, id, sessionViewId, SessionView, Navigation]); const setQuery = useCallback( (inspect, loading, refetch) => { dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index 685b63f2a40e5..e28a8e4f0e412 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -75,7 +75,7 @@ describe('GraphOverlay', () => { test('it has 100% width when NOT in full screen mode', () => { const wrapper = render( - {}} /> + } Navigation={
} /> ); @@ -95,7 +95,7 @@ describe('GraphOverlay', () => { const wrapper = render( - {}} /> + } Navigation={
} /> ); @@ -124,7 +124,7 @@ describe('GraphOverlay', () => { storage )} > - {}} /> + } Navigation={
} /> ); @@ -140,7 +140,7 @@ describe('GraphOverlay', () => { test('it has 100% width when NOT in full screen mode', () => { const wrapper = render( - {}} /> + } Navigation={
} /> ); @@ -160,7 +160,7 @@ describe('GraphOverlay', () => { const wrapper = render( - {}} /> + } Navigation={
} /> ); @@ -199,7 +199,7 @@ describe('GraphOverlay', () => { storage )} > - {}} /> + } Navigation={
} /> ); @@ -236,7 +236,7 @@ describe('GraphOverlay', () => { storage )} > - {}} /> + } Navigation={
} /> ); 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 abcaeb7182cde..694003311e6c8 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,7 +28,6 @@ import { import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { sourcererSelectors } from '../../../common/store'; -import { useSessionView } from '../timeline/session_tab_content/use_session_view'; const OverlayContainer = styled.div` display: flex; @@ -57,11 +56,16 @@ const ScrollableFlexItem = styled(EuiFlexItem)` `; interface GraphOverlayProps { - openDetailsPanel: (eventId?: string, onClose?: () => void) => void; timelineId: TimelineId; + SessionView: JSX.Element | null; + Navigation: JSX.Element | null; } -const GraphOverlayComponent: React.FC = ({ timelineId, openDetailsPanel }) => { +const GraphOverlayComponent: React.FC = ({ + timelineId, + SessionView, + Navigation, +}) => { const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); const { timelineFullScreen } = useTimelineFullScreen(); @@ -130,8 +134,6 @@ const GraphOverlayComponent: React.FC = ({ timelineId, openDe [defaultDataView.patternList, isInTimeline, timelinePatterns] ); - const { SessionView, Navigation } = useSessionView({ timelineId }); - if (!isInTimeline && sessionViewId !== null) { if (fullScreen) { return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 825a81f1984f3..0ca853b84f86e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -193,7 +193,7 @@ const EventDetailsPanelComponent: React.FC = ({ } return isFlyoutView ? ( - <> + {isHostIsolationPanelOpen ? ( backToAlertDetailsLink @@ -249,7 +249,7 @@ const EventDetailsPanelComponent: React.FC = ({ onAddIsolationStatusClick={showHostIsolationPanel} timelineId={timelineId} /> - + ) : ( = ({ timelineId } (state) => getTimeline(state, timelineId)?.graphEventId ); - const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ - isFlyoutView: true, - sourcererScope: SourcererScopeName.timeline, + const { shouldShowDetailsPanel, DetailsPanel, Navigation, SessionView } = useSessionView({ timelineId, - tabType: TimelineTabs.query, }); if (!graphEventId) { @@ -49,7 +45,7 @@ const GraphTabContentComponent: React.FC = ({ timelineId } return ( <> - + {shouldShowDetailsPanel && ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx index a0554d6889822..fc05bf8af8fc5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx @@ -9,6 +9,7 @@ import React, { useMemo, useCallback } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import type { EntityType } from '../../../../../../timelines/common'; import { timelineSelectors } from '../../../store/timeline'; import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; @@ -90,7 +91,13 @@ NavigationComponent.displayName = 'NavigationComponent'; const Navigation = React.memo(NavigationComponent); -export const useSessionView = ({ timelineId }: { timelineId: TimelineId }) => { +export const useSessionView = ({ + timelineId, + entityType, +}: { + timelineId: TimelineId; + entityType?: EntityType; +}) => { const { sessionView } = useKibana().services; const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -162,10 +169,13 @@ export const useSessionView = ({ timelineId }: { timelineId: TimelineId }) => { globalFullScreen, ]); const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({ + isFlyoutView: timelineId !== TimelineId.active, + entityType, sourcererScope: SourcererScopeName.timeline, timelineId, - tabType: TimelineTabs.session, + tabType: timelineId === TimelineId.active ? TimelineTabs.session : TimelineTabs.query, }); + const sessionViewComponent = useMemo(() => { return sessionViewId !== null ? sessionView.getSessionView({ @@ -201,6 +211,7 @@ export const useSessionView = ({ timelineId }: { timelineId: TimelineId }) => { return { onCloseOverlay, + openDetailsPanel, shouldShowDetailsPanel, SessionView: sessionViewComponent, DetailsPanel, From 83502cd0799d102b4e1adc24989fa2ac9cacb36b Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 29 Mar 2022 07:52:09 -0400 Subject: [PATCH 16/19] Update failing snapshot --- .../__snapshots__/index.test.tsx.snap | 598 +++++++++--------- 1 file changed, 308 insertions(+), 290 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 01089552be251..6ea24e5ca57f6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -380,6 +380,313 @@ Array [ runtimeMappings={Object {}} tabType="query" timelineId="test" + > + + +
+ + + +
+ +
+ +
+ + + +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+ +
+ +
+ +
+ +
+ + + + + +
+ , + .c0 { + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + margin-top: 8px; +} + +.c1 .euiFlyoutBody__overflow { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; +} + +.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; + padding: 0 16px 16px; +} + +
+
- , - .c0 { - -webkit-flex: 0 1 auto; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - margin-top: 8px; -} - -.c1 .euiFlyoutBody__overflow { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; -} - -.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; - padding: 0 16px 16px; -} - -
-