From 5b64ac8574432974c1c55323df5bdcb7cf9fdfea Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Sat, 13 Nov 2021 15:12:19 -0800 Subject: [PATCH 1/3] session view now loads real data via process.entry.entity_id. some UX treatment around noisy bash forks --- .../plugins/session_view/common/constants.ts | 1 + .../session_view/common/test/mock_data.ts | 26 +-- .../public/components/ProcessTree/index.tsx | 41 ++-- .../components/ProcessTreeNode/index.tsx | 78 ++++---- .../public/components/SessionView/index.tsx | 182 ++++-------------- .../public/components/SessionView/styles.ts | 32 +++ .../components/SessionViewPage/index.tsx | 29 ++- .../public/hooks/use_process_tree.ts | 81 ++++---- .../session_view/server/routes/test_route.ts | 29 +-- 9 files changed, 247 insertions(+), 252 deletions(-) create mode 100644 x-pack/plugins/session_view/public/components/SessionView/styles.ts diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 745ab06d611823..64a60c8858af1d 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -15,3 +15,4 @@ export const INTERNAL_TEST_ROUTE = '/internal/session_view/test_route'; export const INTERNAL_TEST_SAVED_OBJECT_ROUTE = '/internal/session_view/test_saved_object_route'; export const TEST_SAVED_OBJECT = 'session_view_test_saved_object'; +export const PROCESS_EVENTS_PER_PAGE = 2000; diff --git a/x-pack/plugins/session_view/common/test/mock_data.ts b/x-pack/plugins/session_view/common/test/mock_data.ts index b4d249dfea75c6..3a1b57c9cf7648 100644 --- a/x-pack/plugins/session_view/common/test/mock_data.ts +++ b/x-pack/plugins/session_view/common/test/mock_data.ts @@ -5,7 +5,7 @@ * 2.0. */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Action, ProcessEvent } from '../../public/hooks/use_process_tree'; +import { EventAction, ProcessEvent } from '../../public/hooks/use_process_tree'; import uuid from 'uuid'; export const getStart = () => { @@ -14,7 +14,7 @@ export const getStart = () => { event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: ['bash'], @@ -111,7 +111,7 @@ export const getEvent = () => { event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: randomElement.args, @@ -187,7 +187,7 @@ export const getEnd = () => { event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: ['df'], @@ -259,7 +259,7 @@ export const getEnd = () => { event: { kind: 'event', category: 'process', - action: Action.fork, + action: EventAction.fork, }, process: { args: ['df', 'nested'], @@ -333,7 +333,7 @@ export const getEnd = () => { event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: ['df', 'nested'], @@ -407,7 +407,7 @@ export const getEnd = () => { event: { kind: 'event', category: 'process', - action: Action.end, + action: EventAction.end, }, process: { args: ['df', 'nested'], @@ -487,7 +487,7 @@ export const mockData: ProcessEvent[] = [ event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: ['bash'], @@ -561,7 +561,7 @@ export const mockData: ProcessEvent[] = [ event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: ['ls', '-l'], @@ -633,7 +633,7 @@ export const mockData: ProcessEvent[] = [ event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: ['df'], @@ -705,7 +705,7 @@ export const mockData: ProcessEvent[] = [ event: { kind: 'event', category: 'process', - action: Action.fork, + action: EventAction.fork, }, process: { args: ['df', 'nested'], @@ -779,7 +779,7 @@ export const mockData: ProcessEvent[] = [ event: { kind: 'event', category: 'process', - action: Action.exec, + action: EventAction.exec, }, process: { args: ['df', 'nested'], @@ -853,7 +853,7 @@ export const mockData: ProcessEvent[] = [ event: { kind: 'event', category: 'process', - action: Action.end, + action: EventAction.end, }, process: { args: ['df', 'nested'], diff --git a/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx b/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx index 253a80fd717249..1130758234b470 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx +++ b/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx @@ -11,8 +11,8 @@ import { useScroll } from '../../hooks/use_scroll'; import { useStyles } from './styles'; interface ProcessTreeDeps { - // process.entity_id to act as root node - sessionId: string; + // process.entity_id to act as root node (typically a session (or entry session) leader). + sessionEntityId: string; // bi-directional paging support. allows us to load // processes before and after a particular process.entity_id @@ -29,7 +29,7 @@ interface ProcessTreeDeps { } export const ProcessTree = ({ - sessionId, + sessionEntityId, forward, backward, searchQuery, @@ -39,7 +39,7 @@ export const ProcessTree = ({ const styles = useStyles(); const { sessionLeader, orphans, searchResults } = useProcessTree({ - sessionId, + sessionEntityId, forward, backward, searchQuery, @@ -89,10 +89,22 @@ export const ProcessTree = ({ } // find the DOM element for the command which is selected by id - const processEl = scrollerRef.current.querySelector(`[data-id="${process.getEntityID()}"]`); + const processEl = scrollerRef.current.querySelector(`[data-id="${process.id}"]`); if (processEl) { processEl.prepend(selectionAreaEl); + + if (processEl.parentElement) { + const rect = processEl.getBoundingClientRect(); + const elemTop = rect.top; + const elemBottom = rect.bottom; + const containerHeight = processEl.parentElement.offsetHeight; + const isVisible = elemTop >= 0 && elemBottom < containerHeight; + + if (!isVisible) { + processEl.scrollIntoView(); + } + } } }, []); @@ -102,13 +114,8 @@ export const ProcessTree = ({ } }, [selectedProcess, selectProcess]); - // TODO: processes without parents. - // haven't decided whether to just add to session leader - // or some other UX treatment (reparenting to init?) - // eslint-disable-next-line no-console - console.log(orphans); - - // TODO: search input and results navigation + // TODO: bubble the results up to parent component session_view, and show results navigation + // navigating should // eslint-disable-next-line no-console console.log(searchResults); @@ -121,6 +128,16 @@ export const ProcessTree = ({ onProcessSelected={onProcessSelected} /> )} + {orphans.forEach((process) => { + return ( + + ); + })}
); diff --git a/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx b/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx index d91168316105d2..f36754b2934d53 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx +++ b/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx @@ -11,7 +11,7 @@ *2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useEffect, MouseEvent } from 'react'; +import React, { useRef, useLayoutEffect, useState, useEffect, MouseEvent } from 'react'; import { EuiButton, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Process } from '../../hooks/use_process_tree'; @@ -20,6 +20,7 @@ import { useStyles } from './styles'; interface ProcessDeps { process: Process; isSessionLeader?: boolean; + isOrphan?: boolean; depth?: number; onProcessSelected(process: Process): void; } @@ -31,18 +32,34 @@ interface ProcessDeps { export function ProcessTreeNode({ process, isSessionLeader = false, + isOrphan, depth = 0, onProcessSelected, }: ProcessDeps) { const styles = useStyles({ depth }); + const textRef = useRef(null); const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader || process.autoExpand); + const { searchMatched } = process; useEffect(() => { setChildrenExpanded(isSessionLeader || process.autoExpand); }, [isSessionLeader, process.autoExpand]); - const event = process.getLatest(); + useLayoutEffect(() => { + if (searchMatched !== null && textRef && textRef.current) { + const regex = new RegExp(searchMatched); + + const text = textRef.current.innerText; + const html = text.replace(regex, (match) => { + return `${match}`; + }); + + textRef.current.innerHTML = html; + } + }, [searchMatched]); + + const event = process.getDetails(); if (!event) { return null; @@ -64,7 +81,7 @@ export function ProcessTreeNode({ {children.map((child: Process) => { return ( { - const { name, user } = process.getLatest().process; + const { name, executable, user } = process.getDetails().process; const sessionIcon = interactive ? 'consoleApp' : 'compute'; return ( <> - {name} + {name || executable}     @@ -111,44 +128,34 @@ export function ProcessTreeNode({ const template = () => { const { args, + executable, working_directory: workingDirectory, exit_code: exitCode, - } = process.getLatest().process; - const { searchMatched } = process; - - if (searchMatched !== null) { - const regex = new RegExp(searchMatched); - - // TODO: should we allow some form of customization via settings? - let text = `${workingDirectory} ${args.join(' ')}`; - - text = text.replace(regex, (match) => { - return `${match}`; - }); - + } = process.getDetails().process; + if (process.hasExec()) { return ( - <> - {/* eslint-disable-next-line react/no-danger */} - - + + {workingDirectory}  + {args[0]}  + {args.slice(1).join(' ')} + {exitCode && [exit_code: {exitCode}]} + + ); + } else { + return ( + + {executable}  + ); } - - return ( - - {workingDirectory}  - {args[0]}  - {args.slice(1).join(' ')} - {exitCode && [exit_code: {exitCode}]} - - ); }; const renderProcess = () => { return ( {process.isUserEntered() && } - {template()} + {template()} + {isOrphan ? '(orphaned)' : ''} ); }; @@ -166,14 +173,11 @@ export function ProcessTreeNode({ onProcessSelected(process); }; - const id = process.getEntityID(); - - // eslint-disable-next-line - console.log(styles); + const id = process.id; return ( <> -
+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
{isSessionLeader ? renderSessionLeader() : renderProcess()} diff --git a/x-pack/plugins/session_view/public/components/SessionView/index.tsx b/x-pack/plugins/session_view/public/components/SessionView/index.tsx index c5619b0af36a6b..3de2c052023028 100644 --- a/x-pack/plugins/session_view/public/components/SessionView/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionView/index.tsx @@ -5,58 +5,42 @@ * 2.0. */ import React, { useState, useEffect } from 'react'; +import { useQuery } from 'react-query'; +import { EuiSearchBar, EuiSearchBarOnChangeArgs, EuiEmptyPrompt } from '@elastic/eui'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { CoreStart } from '../../../../../../src/core/public'; -import { useQuery, useMutation } from 'react-query'; -import { - EuiSearchBar, - EuiSearchBarOnChangeArgs, - EuiButton, - EuiPage, - EuiPageContent, - EuiPageHeader, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiEmptyPrompt -} from '@elastic/eui'; import { ProcessTree } from '../ProcessTree'; -import { getStart, getEnd, getEvent } from '../../../common/test/mock_data'; -import { Process } from '../../hooks/use_process_tree'; - -import { - INTERNAL_TEST_ROUTE, -} from '../../../common/constants'; +import { Process, ProcessEvent } from '../../hooks/use_process_tree'; +import { useStyles } from './styles'; +import { INTERNAL_TEST_ROUTE } from '../../../common/constants'; interface SessionViewDeps { - sessionId: string; + // the root node of the process tree to render. e.g process.entry.entity_id or process.session.entity_id + sessionEntityId: string; + height?: number; } -interface MockESReturnData { - hits: any[] - length: number +interface ProcessEventResults { + hits: any[]; + length: number; } /** * The main wrapper component for the session view. - * Currently has mock data and only renders the process_tree component * TODO: - * - React query, fetching and paging events by sessionId * - Details panel * - Fullscreen toggle * - Search results navigation * - Settings menu (needs design) */ -export const SessionView = ({ sessionId }: SessionViewDeps) => { +export const SessionView = ({ sessionEntityId, height }: SessionViewDeps) => { const [searchQuery, setSearchQuery] = useState(''); - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [selectedProcess, setSelectedProcess] = useState(null); const { http } = useKibana().services; - const processTreeCSS = ` - height: 300px; - `; + const styles = useStyles({ height }); const onProcessSelected = (process: Process) => { if (selectedProcess !== process) { @@ -72,53 +56,17 @@ export const SessionView = ({ sessionId }: SessionViewDeps) => { } }; - const { - mutate - } = useMutation((insertData) => { - return http.put(INTERNAL_TEST_ROUTE, { - body: JSON.stringify({ - index: 'process_tree', - data: JSON.stringify(insertData) + const { data: getData } = useQuery( + ['process-tree', 'process_tree'], + () => + http.get(INTERNAL_TEST_ROUTE, { + query: { + indexes: ['cmd*', '.siem-signals-*'], + sessionEntityId + }, }) - }); - }); - - const { - data: getData - } = useQuery(['process-tree', 'process_tree'], () => - http.get(INTERNAL_TEST_ROUTE, { - query: { - index: 'process_tree', - }, - }) ); - const { - mutate: deleteMutate - } = useMutation((insertData) => { - return http.delete(INTERNAL_TEST_ROUTE, { - body: JSON.stringify({ - index: 'process_tree' - }) - }); - }); - - const handleMutate = (insertData: any) => { - mutate(insertData, { - onSuccess: () => { - setData([...data, ...insertData]) - } - }); - } - - const handleDelete = () => { - deleteMutate(undefined, { - onSuccess: () => { - setData([]); - } - }) - } - useEffect(() => { if (!getData) { return; @@ -128,83 +76,35 @@ export const SessionView = ({ sessionId }: SessionViewDeps) => { return; } - setData(getData.hits.map((event: any) => event._source)); + setData(getData.hits.map((event: any) => event._source as ProcessEvent)); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [getData]); const renderNoData = () => { - if (data.length) { - return null; - } return ( - No data to render} - body={ -

- Please start by adding some data using the bottons below -

- } + body={

No process events found for this query.

} /> - ) - } + ); + }; - const renderInsertButtons = () => { - return ( - - - handleMutate(getStart())}> - Insert Session Start - - - - handleMutate(getEvent())}> - Insert Command - - - - handleMutate(getEnd())}> - Insert End - - - - - Delete Data - - - - ) + if (!data.length) { + return renderNoData(); } return ( - - - + +
+ - - {renderNoData()} - {!!data.length && - <> - -
- -
- - } - - {renderInsertButtons()} - - +
+ ); }; diff --git a/x-pack/plugins/session_view/public/components/SessionView/styles.ts b/x-pack/plugins/session_view/public/components/SessionView/styles.ts new file mode 100644 index 00000000000000..063d4f81768689 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/SessionView/styles.ts @@ -0,0 +1,32 @@ +/* + * 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 } from 'react'; +// import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +interface StylesDeps { + height: number | undefined; +} + +export const useStyles = ({ height = 500 }: StylesDeps) => { + // const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + // const { colors, border, font, size } = euiTheme; + + const processTree: CSSObject = { + height: height + 'px', + }; + + return { + processTree, + }; + }, []); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx b/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx index 95a371eff50ccf..d4b20c98b94598 100644 --- a/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx @@ -1,13 +1,21 @@ +/* + * 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 from 'react'; import { RouteComponentProps } from 'react-router-dom'; - +import { EuiPage, EuiPageContent, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { CoreStart } from '../../../../../../src/core/public'; import { BASE_PATH } from '../../../common/constants'; import { SessionView } from '../SessionView'; -const testSessionId = '4321'; +// TODO: sourced from local test data. eventually, session list table will pass in process.entry.entity_id +const testRootEntityId = '44f4dece-d963-51c9-bfa3-875c0c8e1ec3'; export const SessionViewPage = (props: RouteComponentProps) => { const { chrome, http } = useKibana().services; @@ -20,8 +28,19 @@ export const SessionViewPage = (props: RouteComponentProps) => { chrome.docTitle.change('Process Tree'); return ( -
- -
+ + + + + + + + ); }; diff --git a/x-pack/plugins/session_view/public/hooks/use_process_tree.ts b/x-pack/plugins/session_view/public/hooks/use_process_tree.ts index d374f4a48ee195..03b3c774a7dee5 100644 --- a/x-pack/plugins/session_view/public/hooks/use_process_tree.ts +++ b/x-pack/plugins/session_view/public/hooks/use_process_tree.ts @@ -4,19 +4,25 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import _ from 'lodash'; import { useState, useEffect } from 'react'; interface UseProcessTreeDeps { - sessionId: string; + sessionEntityId: string; forward: ProcessEvent[]; backward?: ProcessEvent[]; searchQuery?: string; } -export enum Action { +export enum EventKind { + event = 'event', + signal = 'signal', +} + +export enum EventAction { fork = 'fork', exec = 'exec', - end = 'end', + exit = 'exit', output = 'output', } @@ -51,9 +57,9 @@ interface ProcessSelf extends ProcessFields { export interface ProcessEvent { '@timestamp': string; event: { - kind: string; + kind: EventKind; category: string; - action: Action; + action: EventAction; }; host?: { // optional for now (raw agent output doesn't have server identity) @@ -79,62 +85,63 @@ export interface ProcessEvent { } export interface Process { + id: string; // the process entity_id events: ProcessEvent[]; children: Process[]; parent: Process | undefined; autoExpand: boolean; searchMatched: string | null; // either false, or set to searchQuery - getEntityID(): string; hasOutput(): boolean; hasAlerts(): boolean; + hasExec(): boolean; getOutput(): string; - getLatest(): ProcessEvent; + getDetails(): ProcessEvent; isUserEntered(): boolean; getMaxAlertLevel(): number | null; } class ProcessImpl implements Process { + id: string; events: ProcessEvent[]; children: Process[]; parent: Process | undefined; autoExpand: boolean; searchMatched: string | null; - constructor() { + constructor(id: string) { + this.id = id; this.events = []; this.children = []; this.autoExpand = false; this.searchMatched = null; } - getEntityID() { - return this.getLatest().process.entity_id; - } - hasOutput() { // TODO: schema undecided - return !!this.events.find(({ event }) => event.action === Action.output); + return !!this.events.find(({ event }) => event.action === EventAction.output); } hasAlerts() { - // TODO: endpoint alerts schema uncertain (kind = alert comes from ECS) - // endpoint-dev code sets event.action to rule_detection and rule_prevention. - // alerts mechanics at elastic needs a research spike. - return !!this.events.find(({ event }) => event.kind === 'alert'); + return !!this.events.find(({ event }) => event.kind === EventKind.signal); } - getLatest() { - const forksExecsOnly = this.events.filter((event) => { - return [Action.fork, Action.exec, Action.end].includes(event.event.action); - }); + hasExec() { + return !!this.events.find(({ event }) => event.action === EventAction.exec); + } + + hasExited() { + return !!this.events.find(({ event }) => event.action === EventAction.exit); + } - // returns the most recent fork, exec, or exit - return forksExecsOnly[forksExecsOnly.length - 1]; + getDetails() { + const execsForks = this.events.filter(({ event }) => [EventAction.exec, EventAction.fork].includes(event.action)); + + return execsForks[execsForks.length - 1]; } getOutput() { return this.events.reduce((output, event) => { - if (event.event.action === Action.output) { + if (event.event.action === EventAction.output) { output += ''; // TODO: schema unknown } @@ -143,7 +150,7 @@ class ProcessImpl implements Process { } isUserEntered() { - const event = this.getLatest(); + const event = this.getDetails(); const { interactive, pgid, parent } = event.process; return interactive && pgid !== parent.pgid; @@ -160,14 +167,14 @@ type ProcessMap = { }; export const useProcessTree = ({ - sessionId, + sessionEntityId, forward, backward, searchQuery, }: UseProcessTreeDeps) => { // initialize map, as well as a placeholder for session leader process const initializedProcessMap: ProcessMap = { - [sessionId]: new ProcessImpl(), + [sessionEntityId]: new ProcessImpl(sessionEntityId), }; const [processMap, setProcessMap] = useState(initializedProcessMap); @@ -178,15 +185,23 @@ export const useProcessTree = ({ const updateProcessMap = (events: ProcessEvent[]) => { events.forEach((event) => { - let process = processMap[event.process.entity_id]; + const { entity_id: id } = event.process; + let process = processMap[id]; if (!process) { - process = new ProcessImpl(); - processMap[event.process.entity_id] = process; + process = new ProcessImpl(id); + processMap[id] = process; } process.events.push(event); }); + + if (processMap[sessionEntityId].events.length === 0) { + processMap[sessionEntityId].events.push({ + ...events[0], + ...events[0].process.entry + }) + } }; const buildProcessTree = (events: ProcessEvent[], backwardDirection: boolean = false) => { @@ -204,7 +219,7 @@ export const useProcessTree = ({ parentProcess.children.push(process); } } - } else if (!orphans.includes(process)) { + } else if (process.id !== sessionEntityId && !orphans.includes(process)) { // if no parent process, process is probably orphaned orphans.push(process); } @@ -217,7 +232,7 @@ export const useProcessTree = ({ if (searchQuery) { for (const processId of Object.keys(processMap)) { const process = processMap[processId]; - const event = process.getLatest(); + const event = process.getDetails(); const { working_directory: workingDirectory, args } = event.process; // TODO: the text we search is the same as what we render. @@ -289,5 +304,5 @@ export const useProcessTree = ({ }, [searchQuery]); // return the root session leader process, and a list of orphans - return { sessionLeader: processMap[sessionId], orphans, searchResults }; + return { sessionLeader: processMap[sessionEntityId], orphans, searchResults }; }; diff --git a/x-pack/plugins/session_view/server/routes/test_route.ts b/x-pack/plugins/session_view/server/routes/test_route.ts index 2906548f1562a6..9e8563d019427d 100644 --- a/x-pack/plugins/session_view/server/routes/test_route.ts +++ b/x-pack/plugins/session_view/server/routes/test_route.ts @@ -4,11 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { schema } from '@kbn/config-schema'; import uuid from 'uuid'; import { IRouter } from '../../../../../src/core/server'; -import { INTERNAL_TEST_ROUTE } from '../../common/constants'; +import { INTERNAL_TEST_ROUTE, PROCESS_EVENTS_PER_PAGE } from '../../common/constants'; export const registerTestRoute = (router: IRouter) => { router.get( @@ -16,7 +15,8 @@ export const registerTestRoute = (router: IRouter) => { path: INTERNAL_TEST_ROUTE, validate: { query: schema.object({ - index: schema.maybe(schema.string()), + indexes: schema.maybe(schema.arrayOf(schema.string())), + sessionEntityId: schema.maybe(schema.string()), }), }, }, @@ -24,10 +24,17 @@ export const registerTestRoute = (router: IRouter) => { // TODO (Jiawei & Paulo): Evaluate saved objects & ES client const client = context.core.elasticsearch.client.asCurrentUser; - const { index } = request.query; + const { indexes, sessionEntityId } = request.query; const search = await client.search({ - index: [`${index}`] + index: indexes, + query: { + match: { + 'process.entry.entity_id': sessionEntityId, + }, + }, + size: PROCESS_EVENTS_PER_PAGE, + sort: '@timestamp', }); return response.ok({ body: search.body.hits }); @@ -59,7 +66,7 @@ export const registerTestRoute = (router: IRouter) => { timestamp: new Date().toISOString(), }, }); - }) + }); await Promise.all(requests); @@ -76,7 +83,7 @@ export const registerTestRoute = (router: IRouter) => { path: INTERNAL_TEST_ROUTE, validate: { body: schema.object({ - index: schema.string() + index: schema.string(), }), }, }, @@ -84,18 +91,18 @@ export const registerTestRoute = (router: IRouter) => { const { index } = request.body; const client = context.core.elasticsearch.client.asCurrentUser; - await client.deleteByQuery({ + await client.deleteByQuery({ index, body: { query: { match_all: {} }, }, }); - + return response.ok({ body: { message: 'ok!', }, }); - }, - ) + } + ); }; From 3a2408ec4792c712d2d797f6deb8559947d96130 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Sun, 14 Nov 2021 19:43:35 -0800 Subject: [PATCH 2/3] session_view component now has dedicated route to load process events by sessionEntityId. session_view_page will grab latest session id and load it in session_view component --- .../plugins/session_view/common/constants.ts | 3 + .../public/components/SessionView/index.tsx | 4 +- .../components/SessionViewPage/index.tsx | 35 ++++++++-- .../public/components/TestPage/index.tsx | 70 +++++++++---------- .../session_view/server/routes/index.ts | 4 ++ .../server/routes/process_events_route.ts | 41 +++++++++++ .../server/routes/recent_session_route.ts | 39 +++++++++++ .../session_view/server/routes/test_route.ts | 29 +++----- 8 files changed, 164 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/session_view/server/routes/process_events_route.ts create mode 100644 x-pack/plugins/session_view/server/routes/recent_session_route.ts diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 64a60c8858af1d..269e9574bc9d52 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -13,6 +13,9 @@ export const BASE_PATH = '/app/sessionView'; // Internal APIs are recommended to have the INTERNAL- suffix export const INTERNAL_TEST_ROUTE = '/internal/session_view/test_route'; export const INTERNAL_TEST_SAVED_OBJECT_ROUTE = '/internal/session_view/test_saved_object_route'; +export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route'; +export const RECENT_SESSION_ROUTE = '/internal/session_view/recent_session_route'; export const TEST_SAVED_OBJECT = 'session_view_test_saved_object'; + export const PROCESS_EVENTS_PER_PAGE = 2000; diff --git a/x-pack/plugins/session_view/public/components/SessionView/index.tsx b/x-pack/plugins/session_view/public/components/SessionView/index.tsx index 3de2c052023028..3bb23c3da3535c 100644 --- a/x-pack/plugins/session_view/public/components/SessionView/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionView/index.tsx @@ -12,7 +12,7 @@ import { CoreStart } from '../../../../../../src/core/public'; import { ProcessTree } from '../ProcessTree'; import { Process, ProcessEvent } from '../../hooks/use_process_tree'; import { useStyles } from './styles'; -import { INTERNAL_TEST_ROUTE } from '../../../common/constants'; +import { PROCESS_EVENTS_ROUTE } from '../../../common/constants'; interface SessionViewDeps { // the root node of the process tree to render. e.g process.entry.entity_id or process.session.entity_id @@ -59,7 +59,7 @@ export const SessionView = ({ sessionEntityId, height }: SessionViewDeps) => { const { data: getData } = useQuery( ['process-tree', 'process_tree'], () => - http.get(INTERNAL_TEST_ROUTE, { + http.get(PROCESS_EVENTS_ROUTE, { query: { indexes: ['cmd*', '.siem-signals-*'], sessionEntityId diff --git a/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx b/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx index d4b20c98b94598..56908aeb9fbb11 100644 --- a/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx @@ -5,17 +5,20 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { useQuery } from 'react-query'; import { RouteComponentProps } from 'react-router-dom'; import { EuiPage, EuiPageContent, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { CoreStart } from '../../../../../../src/core/public'; -import { BASE_PATH } from '../../../common/constants'; +import { RECENT_SESSION_ROUTE, BASE_PATH } from '../../../common/constants'; import { SessionView } from '../SessionView'; +import { ProcessEvent } from '../../hooks/use_process_tree'; -// TODO: sourced from local test data. eventually, session list table will pass in process.entry.entity_id -const testRootEntityId = '44f4dece-d963-51c9-bfa3-875c0c8e1ec3'; +interface RecentSessionResults { + hits: any[]; +} export const SessionViewPage = (props: RouteComponentProps) => { const { chrome, http } = useKibana().services; @@ -27,6 +30,28 @@ export const SessionViewPage = (props: RouteComponentProps) => { ]); chrome.docTitle.change('Process Tree'); + // loads the entity_id of most recent 'interactive' session + const { data } = useQuery(['recent-session', 'recent_session'], () => + http.get(RECENT_SESSION_ROUTE, { + query: { + indexes: ['cmd*', '.siem-signals*'], + }, + }) + ); + + const [sessionEntityId, setSessionEntityId] = useState(''); + + useEffect(() => { + if (!data) { + return; + } + + if (data.hits.length) { + const event = data.hits[0]._source as ProcessEvent; + setSessionEntityId(event.process.entry.entity_id); + } + }, [data]); + return ( @@ -38,7 +63,7 @@ export const SessionViewPage = (props: RouteComponentProps) => { `} /> - + diff --git a/x-pack/plugins/session_view/public/components/TestPage/index.tsx b/x-pack/plugins/session_view/public/components/TestPage/index.tsx index 61ddb1c0c85d3a..907185730e9c36 100644 --- a/x-pack/plugins/session_view/public/components/TestPage/index.tsx +++ b/x-pack/plugins/session_view/public/components/TestPage/index.tsx @@ -63,29 +63,37 @@ export const TestPage = (props: RouteComponentProps) => { mutate, isLoading, data: putData, - } = useMutation(() => { - return http.put(INTERNAL_TEST_ROUTE, { - body: JSON.stringify({ - index: indexName, - data: JSON.stringify([{ message }]) - }), - }); - }, { onSuccess: () => { - notifications.toasts.addSuccess('Data Added!'); - }}); + } = useMutation( + () => { + return http.put(INTERNAL_TEST_ROUTE, { + body: JSON.stringify({ + index: indexName, + data: JSON.stringify([{ message }]), + }), + }); + }, + { + onSuccess: () => { + notifications.toasts.addSuccess('Data Added!'); + }, + } + ); // An example of using useQuery to hit an internal endpoint via mutation (PUT) - const { - mutate: deleteMutate, - } = useMutation(() => { - return http.delete(INTERNAL_TEST_ROUTE, { - body: JSON.stringify({ - index: indexName - }) - }); - }, { onSuccess: () => { - notifications.toasts.addSuccess('Data Deleted!'); - }}); + const { mutate: deleteMutate } = useMutation( + () => { + return http.delete(INTERNAL_TEST_ROUTE, { + body: JSON.stringify({ + index: indexName, + }), + }); + }, + { + onSuccess: () => { + notifications.toasts.addSuccess('Data Deleted!'); + }, + } + ); const handleInsertData = () => { mutate(); @@ -136,14 +144,12 @@ export const TestPage = (props: RouteComponentProps) => { return ( - @@ -208,20 +214,12 @@ export const TestPage = (props: RouteComponentProps) => {
put network data: - {SOisLoading ? ( -
Loading!
- ) : ( -
{JSON.stringify(SOputData, null, 2)}
- )} + {SOisLoading ?
Loading!
:
{JSON.stringify(SOputData, null, 2)}
}
get network data: - {SOisFetching ? ( -
Loading!
- ) : ( -
{JSON.stringify(SOgetData, null, 2)}
- )} + {SOisFetching ?
Loading!
:
{JSON.stringify(SOgetData, null, 2)}
}
diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts index 2c63f212de58db..02eb764e5ba2b7 100644 --- a/x-pack/plugins/session_view/server/routes/index.ts +++ b/x-pack/plugins/session_view/server/routes/index.ts @@ -8,8 +8,12 @@ import { IRouter } from '../../../../../src/core/server'; import { registerTestRoute } from './test_route'; import { registerTestSavedObjectsRoute } from './test_saved_objects_route'; +import { registerProcessEventsRoute } from './process_events_route'; +import { registerRecentSessionRoute } from './recent_session_route'; export const registerRoutes = (router: IRouter) => { registerTestRoute(router); registerTestSavedObjectsRoute(router); + registerProcessEventsRoute(router); + registerRecentSessionRoute(router); }; diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts new file mode 100644 index 00000000000000..65c04b3fefaad1 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -0,0 +1,41 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from '../../../../../src/core/server'; +import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE } from '../../common/constants'; + +export const registerProcessEventsRoute = (router: IRouter) => { + router.get( + { + path: PROCESS_EVENTS_ROUTE, + validate: { + query: schema.object({ + indexes: schema.maybe(schema.arrayOf(schema.string())), + sessionEntityId: schema.maybe(schema.string()), + }), + }, + }, + async (context, request, response) => { + const client = context.core.elasticsearch.client.asCurrentUser; + + const { indexes, sessionEntityId } = request.query; + + const search = await client.search({ + index: indexes, + query: { + match: { + 'process.entry.entity_id': sessionEntityId, + }, + }, + size: PROCESS_EVENTS_PER_PAGE, + sort: '@timestamp', + }); + + return response.ok({ body: search.body.hits }); + } + ); +}; diff --git a/x-pack/plugins/session_view/server/routes/recent_session_route.ts b/x-pack/plugins/session_view/server/routes/recent_session_route.ts new file mode 100644 index 00000000000000..71a95f3d4d0b95 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/recent_session_route.ts @@ -0,0 +1,39 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from '../../../../../src/core/server'; +import { RECENT_SESSION_ROUTE } from '../../common/constants'; + +export const registerRecentSessionRoute = (router: IRouter) => { + router.get( + { + path: RECENT_SESSION_ROUTE, + validate: { + query: schema.object({ + indexes: schema.maybe(schema.arrayOf(schema.string())), + }), + }, + }, + async (context, request, response) => { + const client = context.core.elasticsearch.client.asCurrentUser; + + const { indexes } = request.query; + + const search = await client.search({ + index: indexes, + query: { + match: { + 'process.entry.interactive': true, + }, + }, + size: 1 + }); + + return response.ok({ body: search.body.hits }); + } + ); +}; diff --git a/x-pack/plugins/session_view/server/routes/test_route.ts b/x-pack/plugins/session_view/server/routes/test_route.ts index 9e8563d019427d..2906548f1562a6 100644 --- a/x-pack/plugins/session_view/server/routes/test_route.ts +++ b/x-pack/plugins/session_view/server/routes/test_route.ts @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { schema } from '@kbn/config-schema'; import uuid from 'uuid'; import { IRouter } from '../../../../../src/core/server'; -import { INTERNAL_TEST_ROUTE, PROCESS_EVENTS_PER_PAGE } from '../../common/constants'; +import { INTERNAL_TEST_ROUTE } from '../../common/constants'; export const registerTestRoute = (router: IRouter) => { router.get( @@ -15,8 +16,7 @@ export const registerTestRoute = (router: IRouter) => { path: INTERNAL_TEST_ROUTE, validate: { query: schema.object({ - indexes: schema.maybe(schema.arrayOf(schema.string())), - sessionEntityId: schema.maybe(schema.string()), + index: schema.maybe(schema.string()), }), }, }, @@ -24,17 +24,10 @@ export const registerTestRoute = (router: IRouter) => { // TODO (Jiawei & Paulo): Evaluate saved objects & ES client const client = context.core.elasticsearch.client.asCurrentUser; - const { indexes, sessionEntityId } = request.query; + const { index } = request.query; const search = await client.search({ - index: indexes, - query: { - match: { - 'process.entry.entity_id': sessionEntityId, - }, - }, - size: PROCESS_EVENTS_PER_PAGE, - sort: '@timestamp', + index: [`${index}`] }); return response.ok({ body: search.body.hits }); @@ -66,7 +59,7 @@ export const registerTestRoute = (router: IRouter) => { timestamp: new Date().toISOString(), }, }); - }); + }) await Promise.all(requests); @@ -83,7 +76,7 @@ export const registerTestRoute = (router: IRouter) => { path: INTERNAL_TEST_ROUTE, validate: { body: schema.object({ - index: schema.string(), + index: schema.string() }), }, }, @@ -91,18 +84,18 @@ export const registerTestRoute = (router: IRouter) => { const { index } = request.body; const client = context.core.elasticsearch.client.asCurrentUser; - await client.deleteByQuery({ + await client.deleteByQuery({ index, body: { query: { match_all: {} }, }, }); - + return response.ok({ body: { message: 'ok!', }, }); - } - ); + }, + ) }; From 5ca9c807c52d56360a3945c9753479aed323860e Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Mon, 15 Nov 2021 11:21:54 -0800 Subject: [PATCH 3/3] addressed PR comments --- .../public/components/ProcessTree/index.tsx | 19 ++++++++++-------- .../components/ProcessTreeNode/index.tsx | 20 ++++++++++++------- .../components/SessionViewPage/index.tsx | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx b/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx index 1130758234b470..e18b480dbf28f9 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx +++ b/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx @@ -89,17 +89,20 @@ export const ProcessTree = ({ } // find the DOM element for the command which is selected by id - const processEl = scrollerRef.current.querySelector(`[data-id="${process.id}"]`); + const processEl = scrollerRef.current.querySelector(`[data-id="${process.id}"]`); if (processEl) { processEl.prepend(selectionAreaEl); - if (processEl.parentElement) { - const rect = processEl.getBoundingClientRect(); - const elemTop = rect.top; - const elemBottom = rect.bottom; - const containerHeight = processEl.parentElement.offsetHeight; - const isVisible = elemTop >= 0 && elemBottom < containerHeight; + const container = processEl.parentElement; + + if (container) { + const cTop = container.scrollTop; + const cBottom = cTop + container.clientHeight; + + const eTop = processEl.offsetTop; + const eBottom = eTop + processEl.clientHeight; + const isVisible = eTop >= cTop && eBottom <= cBottom; if (!isVisible) { processEl.scrollIntoView(); @@ -128,7 +131,7 @@ export const ProcessTree = ({ onProcessSelected={onProcessSelected} /> )} - {orphans.forEach((process) => { + {orphans.map((process) => { return ( { - if (searchMatched !== null && textRef && textRef.current) { + if (searchMatched !== null && textRef.current) { const regex = new RegExp(searchMatched); const text = textRef.current.innerText; @@ -59,13 +59,19 @@ export function ProcessTreeNode({ } }, [searchMatched]); - const event = process.getDetails(); + const processDetails = useMemo(() => { + return process.getDetails(); + }, [process.events.length]); - if (!event) { + const hasExec = useMemo(() => { + return process.hasExec(); + }, [process.events.length]); + + if (!processDetails) { return null; } - const { interactive } = event.process; + const { interactive } = processDetails.process; const renderChildren = () => { const { children } = process; @@ -132,7 +138,7 @@ export function ProcessTreeNode({ working_directory: workingDirectory, exit_code: exitCode, } = process.getDetails().process; - if (process.hasExec()) { + if (hasExec) { return ( {workingDirectory}  @@ -154,7 +160,7 @@ export function ProcessTreeNode({ return ( {process.isUserEntered() && } - {template()} + {template()} {isOrphan ? '(orphaned)' : ''} ); diff --git a/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx b/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx index 56908aeb9fbb11..9c97a1e894b942 100644 --- a/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx @@ -63,7 +63,7 @@ export const SessionViewPage = (props: RouteComponentProps) => { `} /> - + {sessionEntityId && }