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..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[] = []; @@ -105,6 +111,7 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, + sessionViewId, showCheckboxes, sort, } = defaultModel, @@ -155,11 +162,22 @@ 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 { 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, openDetailsPanel]); const setQuery = useCallback( (inspect, loading, refetch) => { dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); @@ -239,14 +257,7 @@ const StatefulEventsViewerComponent: React.FC = ({ })} - + {FlyoutDetailsPanel} ); 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..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 @@ -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,7 +75,14 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` margin: 4px 0 4px 0; `; -interface OwnProps { +const ScrollableFlexItem = styled(EuiFlexItem)` + ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} + overflow: hidden; + width: 100%; +`; + +interface GraphOverlayProps { + openDetailsPanel: (eventId?: string, onClose?: () => void) => void; timelineId: TimelineId; } @@ -122,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(); @@ -131,6 +143,19 @@ 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({ + sessionEntityId: sessionViewId, + loadAlertDetails: openDetailsPanel, + }) + : null; + }, [sessionView, sessionViewId, openDetailsPanel]); + const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []); @@ -180,6 +205,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 +246,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/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/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..299018d96154a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx @@ -0,0 +1,77 @@ +/* + * 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, 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; + width: 100%; + overflow: hidden; +`; + +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}; +`; + +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({ + sessionEntityId: sessionViewId, + loadAlertDetails: openDetailsPanel, + }) + : null; + }, [openDetailsPanel, sessionView, sessionViewId]); + + return ( + + {sessionViewMain} + {shouldShowFlyoutDetailsPanel && ( + <> + + {FlyoutDetailsPanel} + + )} + + ); +}; + +// 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/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/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..a9778d454a226 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: (sessionDeps: SessionViewDeps) => 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/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', } 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',