From e64025795b2fcca99514739d75ff25e638328548 Mon Sep 17 00:00:00 2001 From: Zizhou Wang Date: Tue, 30 Nov 2021 02:34:29 -0500 Subject: [PATCH 1/6] Move process tree types to common folder --- .../common/types/process_tree/index.ts | 204 ++++++++++++++++++ .../public/components/ProcessTree/index.tsx | 3 +- .../components/ProcessTreeAlerts/index.tsx | 2 +- .../components/ProcessTreeNode/index.tsx | 2 +- .../public/components/SessionView/hooks.ts | 2 +- .../public/components/SessionView/index.tsx | 2 +- .../SessionViewDetailPanel/index.tsx | 37 ++-- .../public/hooks/use_process_tree.ts | 199 +---------------- 8 files changed, 233 insertions(+), 218 deletions(-) create mode 100644 x-pack/plugins/session_view/common/types/process_tree/index.ts diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts new file mode 100644 index 0000000000000..c9fd6e21e06b7 --- /dev/null +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -0,0 +1,204 @@ +/* + * 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. + */ + +export enum EventKind { + event = 'event', + signal = 'signal', +} + +export enum EventAction { + fork = 'fork', + exec = 'exec', + exit = 'exit', + output = 'output', +} + +interface EventActionPartition { + fork: ProcessEvent[]; + exec: ProcessEvent[]; + exit: ProcessEvent[]; + output: ProcessEvent[]; +} + +interface User { + id: string; + name: string; +} + +interface ProcessFields { + args: string[]; + args_count: number; + command_line: string; + entity_id: string; + executable: string; + interactive: boolean; + name: string; + working_directory: string; + pid: number; + pgid: number; + user: User; + end?: string; + exit_code?: number; +} + +export interface ProcessSelf extends ProcessFields { + parent: ProcessFields; + session: ProcessFields; + entry: ProcessFields; + last_user_entered?: ProcessFields; +} + +export interface ProcessEvent { + '@timestamp': Date; + event: { + kind: EventKind; + category: string; + action: EventAction; + }; + host?: { + // optional for now (raw agent output doesn't have server identity) + architecture: string; + hostname: string; + id: string; + ip: string; + mac: string; + name: string; + os: { + family: string; + full: string; + kernel: string; + name: string; + platform: string; + type: string; + version: string; + }; + }; + process: ProcessSelf; + kibana?: { + alert: { + uuid: string; + reason: string; + workflow_status: string; + status: string; + original_time: Date; + original_event: { + action: string; + }; + rule: { + category: string; + consumer: string; + description: string; + enabled: boolean; + name: string; + query: string; + risk_score: number; + severity: string; + uuid: string; + }; + }; + }; +} + +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 + hasOutput(): boolean; + hasAlerts(): boolean; + getAlerts(): ProcessEvent[]; + hasExec(): boolean; + getOutput(): string; + getDetails(): ProcessEvent; + isUserEntered(): boolean; + getMaxAlertLevel(): number | null; +} + +export class ProcessImpl implements Process { + id: string; + events: ProcessEvent[]; + children: Process[]; + parent: Process | undefined; + autoExpand: boolean; + searchMatched: string | null; + + constructor(id: string) { + this.id = id; + this.events = []; + this.children = []; + this.autoExpand = false; + this.searchMatched = null; + } + + hasOutput() { + // TODO: schema undecided + return !!this.events.find(({ event }) => event.action === EventAction.output); + } + + hasAlerts() { + return !!this.events.find(({ event }) => event.kind === EventKind.signal); + } + + getAlerts() { + return this.events.filter(({ event }) => event.kind === EventKind.signal); + } + + hasExec() { + return !!this.events.find(({ event }) => event.action === EventAction.exec); + } + + hasExited() { + return !!this.events.find(({ event }) => event.action === EventAction.exit); + } + + getDetails() { + const eventsPartition = this.events.reduce( + (currEventsParition, processEvent) => { + currEventsParition[processEvent.event.action]?.push(processEvent); + return currEventsParition; + }, + Object.values(EventAction).reduce((currActions, action) => { + currActions[action] = [] as ProcessEvent[]; + return currActions; + }, {} as EventActionPartition) + ); + + if (eventsPartition.exec.length) { + return eventsPartition.exec[eventsPartition.exec.length - 1]; + } + + if (eventsPartition.fork.length) { + return eventsPartition.fork[eventsPartition.fork.length - 1]; + } + + return {} as ProcessEvent; + } + + getOutput() { + return this.events.reduce((output, event) => { + if (event.event.action === EventAction.output) { + output += ''; // TODO: schema unknown + } + + return output; + }, ''); + } + + isUserEntered() { + const event = this.getDetails(); + const { interactive, pgid, parent } = event?.process || {}; + + return interactive && pgid !== parent.pgid; + } + + getMaxAlertLevel() { + // TODO: + return null; + } +} 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 74406bd38bf32..382ecee2165aa 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx +++ b/x-pack/plugins/session_view/public/components/ProcessTree/index.tsx @@ -6,7 +6,8 @@ */ import React, { useRef, useLayoutEffect, useCallback } from 'react'; import { ProcessTreeNode } from '../ProcessTreeNode'; -import { useProcessTree, ProcessEvent, Process } from '../../hooks/use_process_tree'; +import { useProcessTree } from '../../hooks/use_process_tree'; +import { ProcessEvent, Process } from '../../../common/types/process_tree'; import { useScroll } from '../../hooks/use_scroll'; import { useStyles } from './styles'; diff --git a/x-pack/plugins/session_view/public/components/ProcessTreeAlerts/index.tsx b/x-pack/plugins/session_view/public/components/ProcessTreeAlerts/index.tsx index f3c71b1f4726f..31cdaa521bbe1 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTreeAlerts/index.tsx +++ b/x-pack/plugins/session_view/public/components/ProcessTreeAlerts/index.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useStyles } from './styles'; -import { ProcessEvent } from '../../hooks/use_process_tree'; +import { ProcessEvent } from '../../../common/types/process_tree'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { CoreStart } from '../../../../../../src/core/public'; 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 33bc259d87a27..9db6d1d69d8ba 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx +++ b/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx @@ -14,7 +14,7 @@ import React, { useMemo, 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'; +import { Process } from '../../../common/types/process_tree'; import { useStyles, ButtonType } from './styles'; import { ProcessTreeAlerts } from '../ProcessTreeAlerts'; diff --git a/x-pack/plugins/session_view/public/components/SessionView/hooks.ts b/x-pack/plugins/session_view/public/components/SessionView/hooks.ts index e290670a76995..3ffbee15986f1 100644 --- a/x-pack/plugins/session_view/public/components/SessionView/hooks.ts +++ b/x-pack/plugins/session_view/public/components/SessionView/hooks.ts @@ -9,7 +9,7 @@ import { useQuery } from 'react-query'; import { EuiSearchBarOnChangeArgs } from '@elastic/eui'; import { CoreStart } from 'kibana/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { ProcessEvent } from '../../hooks/use_process_tree'; +import { ProcessEvent } from '../../../common/types/process_tree'; import { PROCESS_EVENTS_ROUTE } from '../../../common/constants'; interface ProcessEventResults { 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 2e1834d37b6ee..db61b22111c99 100644 --- a/x-pack/plugins/session_view/public/components/SessionView/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionView/index.tsx @@ -16,7 +16,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { SectionLoading } from '../../shared_imports'; import { ProcessTree } from '../ProcessTree'; -import { Process } from '../../hooks/use_process_tree'; +import { Process } from '../../../common/types/process_tree'; import { SessionViewDetailPanel } from '../SessionViewDetailPanel'; import { useStyles } from './styles'; import { diff --git a/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.tsx b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.tsx index 746e6326a8379..098d5e21bc373 100644 --- a/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.tsx @@ -9,7 +9,7 @@ import MonacoEditor from 'react-monaco-editor'; import { partition } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiSplitPanel, EuiTitle, EuiTabs, EuiTab } from '@elastic/eui'; -import { EventKind, Process } from '../../hooks/use_process_tree'; +import { EventKind, Process } from '../../../common/types/process_tree'; import { useStyles } from './styles'; interface SessionViewDetailPanelDeps { @@ -83,7 +83,7 @@ export const SessionViewDetailPanel = ({ const renderSelectedProcessCommandDetail = () => { if (selectedProcess) { return ( -
+
{ if (selectedProcess && selectedProcess.hasAlerts()) { return ( -
+
@@ -147,19 +147,26 @@ export const SessionViewDetailPanel = ({ onAnimationEnd={handleAnimationEnd} > {renderSelectedProcessCommandDetail()} - - - - - - {/* Add session detail */} +
+ + + + + + {/* Add session detail */} +
- - - - - - {/* Add server detail */} +
+ + + + + + {/* Add server detail */} +
{renderSelectedProcessAlertDetail()} 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 8fd3d1dd7400b..b7cad0717fc45 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 @@ -6,6 +6,7 @@ */ import _ from 'lodash'; import { useState, useEffect } from 'react'; +import { EventKind, Process, ProcessImpl, ProcessEvent } from '../../common/types/process_tree'; interface UseProcessTreeDeps { sessionEntityId: string; @@ -14,204 +15,6 @@ interface UseProcessTreeDeps { searchQuery?: string; } -export enum EventKind { - event = 'event', - signal = 'signal', -} - -export enum EventAction { - fork = 'fork', - exec = 'exec', - exit = 'exit', - output = 'output', -} - -interface EventActionPartition { - fork: ProcessEvent[]; - exec: ProcessEvent[]; - exit: ProcessEvent[]; - output: ProcessEvent[]; -} - -interface User { - id: string; - name: string; -} - -interface ProcessFields { - args: string[]; - args_count: number; - command_line: string; - entity_id: string; - executable: string; - interactive: boolean; - name: string; - working_directory: string; - pid: number; - pgid: number; - user: User; - end?: string; - exit_code?: number; -} - -export interface ProcessSelf extends ProcessFields { - parent: ProcessFields; - session: ProcessFields; - entry: ProcessFields; - last_user_entered?: ProcessFields; -} - -export interface ProcessEvent { - '@timestamp': Date; - event: { - kind: EventKind; - category: string; - action: EventAction; - }; - host?: { - // optional for now (raw agent output doesn't have server identity) - architecture: string; - hostname: string; - id: string; - ip: string; - mac: string; - name: string; - os: { - family: string; - full: string; - kernel: string; - name: string; - platform: string; - type: string; - version: string; - }; - }; - process: ProcessSelf; - kibana?: { - alert: { - uuid: string; - reason: string; - workflow_status: string; - status: string; - original_time: Date; - original_event: { - action: string; - }; - rule: { - category: string; - consumer: string; - description: string; - enabled: boolean; - name: string; - query: string; - risk_score: number; - severity: string; - uuid: string; - }; - }; - }; -} - -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 - hasOutput(): boolean; - hasAlerts(): boolean; - getAlerts(): ProcessEvent[]; - hasExec(): boolean; - getOutput(): string; - 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(id: string) { - this.id = id; - this.events = []; - this.children = []; - this.autoExpand = false; - this.searchMatched = null; - } - - hasOutput() { - // TODO: schema undecided - return !!this.events.find(({ event }) => event.action === EventAction.output); - } - - hasAlerts() { - return !!this.events.find(({ event }) => event.kind === EventKind.signal); - } - - getAlerts() { - return this.events.filter(({ event }) => event.kind === EventKind.signal); - } - - hasExec() { - return !!this.events.find(({ event }) => event.action === EventAction.exec); - } - - hasExited() { - return !!this.events.find(({ event }) => event.action === EventAction.exit); - } - - getDetails() { - const eventsPartition = this.events.reduce( - (currEventsParition, processEvent) => { - currEventsParition[processEvent.event.action]?.push(processEvent); - return currEventsParition; - }, - Object.values(EventAction).reduce((currActions, action) => { - currActions[action] = [] as ProcessEvent[]; - return currActions; - }, {} as EventActionPartition) - ); - - if (eventsPartition.exec.length) { - return eventsPartition.exec[eventsPartition.exec.length - 1]; - } - - if (eventsPartition.fork.length) { - return eventsPartition.fork[eventsPartition.fork.length - 1]; - } - - return {} as ProcessEvent; - } - - getOutput() { - return this.events.reduce((output, event) => { - if (event.event.action === EventAction.output) { - output += ''; // TODO: schema unknown - } - - return output; - }, ''); - } - - isUserEntered() { - const event = this.getDetails(); - const { interactive, pgid, parent } = event?.process || {}; - - return interactive && pgid !== parent.pgid; - } - - getMaxAlertLevel() { - // TODO: - return null; - } -} - type ProcessMap = { [key: string]: Process; }; From 042602bf30aead3ef98b277401ea2be2ef0368a4 Mon Sep 17 00:00:00 2001 From: Zizhou Wang Date: Tue, 30 Nov 2021 02:35:23 -0500 Subject: [PATCH 2/6] Write session view detail panel unit tests --- .../constants/session_view_process.mock.ts | 454 ++++++++++++++++++ .../{SessionView.test.tsx => index.test.tsx} | 0 .../SessionViewDetailPanel/index.test.tsx | 73 +++ 3 files changed, 527 insertions(+) create mode 100644 x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts rename x-pack/plugins/session_view/public/components/SessionView/{SessionView.test.tsx => index.test.tsx} (100%) create mode 100644 x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx diff --git a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts new file mode 100644 index 0000000000000..950f173722427 --- /dev/null +++ b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts @@ -0,0 +1,454 @@ +/* + * 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 { Process, ProcessEvent, EventAction, EventKind } from '../../types/process_tree'; + +const mockEvents = [ + { + '@timestamp': new Date('2021-11-23T15:25:04.210Z'), + process: { + pid: 3535, + pgid: 2442, + user: { + name: '', + id: '-1', + }, + executable: '/usr/bin/bash', + interactive: false, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + parent: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + session: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + entry: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + command_line: '', + name: '', + args_count: 0, + args: [], + working_directory: '/home/vagrant', + }, + event: { + action: EventAction.fork, + category: 'process', + kind: EventKind.event, + }, + }, + { + '@timestamp': new Date('2021-11-23T15:25:04.218Z'), + process: { + pid: 3535, + pgid: 3535, + user: { + name: 'vagrant', + id: '1000', + }, + executable: '/usr/bin/vi', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + parent: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + session: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + entry: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + command_line: '', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + event: { + action: EventAction.exec, + category: 'process', + kind: EventKind.event, + }, + }, + { + '@timestamp': new Date('2021-11-23T15:25:05.202Z'), + process: { + pid: 3535, + pgid: 3535, + user: { + name: 'vagrant', + id: '1000', + }, + executable: '/usr/bin/vi', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + parent: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + session: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + entry: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + command_line: '', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + event: { + action: EventAction.exit, + category: 'process', + kind: EventKind.event, + }, + }, +]; + +const mockAlerts = [ + { + kibana: { + alert: { + rule: { + category: 'Custom Query Rule', + consumer: 'siem', + name: 'cmd test alert', + uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf', + enabled: true, + description: 'cmd test alert', + risk_score: 21, + severity: 'low', + query: "process.executable: '/usr/bin/vi'", + }, + status: 'active', + workflow_status: 'open', + reason: 'process event created low alert cmd test alert.', + original_time: new Date('2021-11-23T15:25:04.218Z'), + original_event: { + action: 'exec', + }, + uuid: '6bb22512e0e588d1a2449b61f164b216e366fba2de39e65d002ae734d71a6c38', + }, + }, + '@timestamp': new Date('2021-11-23T15:26:34.859Z'), + process: { + pid: 3535, + pgid: 3535, + user: { + name: 'vagrant', + id: '1000', + }, + executable: '/usr/bin/vi', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + parent: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + session: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + entry: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + }, + command_line: '', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + event: { + action: EventAction.exec, + category: 'process', + kind: EventKind.signal, + }, + }, + { + kibana: { + alert: { + rule: { + category: 'Custom Query Rule', + consumer: 'siem', + name: 'cmd test alert', + uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf', + enabled: true, + description: 'cmd test alert', + risk_score: 21, + severity: 'low', + query: "process.executable: '/usr/bin/vi'", + }, + status: 'active', + workflow_status: 'open', + reason: 'process event created low alert cmd test alert.', + original_time: new Date('2021-11-23T15:25:05.202Z'), + original_event: { + action: 'exit', + }, + uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75', + }, + }, + '@timestamp': new Date('2021-11-23T15:26:34.860Z'), + process: { + pid: 3535, + pgid: 3535, + user: { + name: 'vagrant', + id: '1000', + }, + executable: '/usr/bin/vi', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + parent: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + session: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + entry: { + pid: 2442, + pgid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + command_line: '', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + command_line: '', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + event: { + action: EventAction.exit, + category: 'process', + kind: EventKind.signal, + }, + }, +]; + +export const sessionViewBasicProcessMock: Process = { + id: '3f44d854-fe8d-5666-abc9-9efe7f970b4b', + events: mockEvents, + children: [], + autoExpand: false, + searchMatched: null, + parent: undefined, + hasOutput: () => false, + hasAlerts: () => false, + getAlerts: () => [], + hasExec: () => true, + getOutput: () => '', + getDetails: () => ({} as ProcessEvent), + isUserEntered: () => true, + getMaxAlertLevel: () => null, +}; + +export const sessionViewAlertProcessMock: Process = { + id: '3f44d854-fe8d-5666-abc9-9efe7f970b4b', + events: [...mockEvents, ...mockAlerts], + children: [], + autoExpand: false, + searchMatched: null, + parent: undefined, + hasOutput: () => false, + hasAlerts: () => true, + getAlerts: () => mockEvents, + hasExec: () => true, + getOutput: () => '', + getDetails: () => ({} as ProcessEvent), + isUserEntered: () => true, + getMaxAlertLevel: () => null, +}; diff --git a/x-pack/plugins/session_view/public/components/SessionView/SessionView.test.tsx b/x-pack/plugins/session_view/public/components/SessionView/index.test.tsx similarity index 100% rename from x-pack/plugins/session_view/public/components/SessionView/SessionView.test.tsx rename to x-pack/plugins/session_view/public/components/SessionView/index.test.tsx diff --git a/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx new file mode 100644 index 0000000000000..015424ea13591 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 'jest-canvas-mock'; +import { + sessionViewBasicProcessMock, + sessionViewAlertProcessMock, +} from '../../../common/mocks/constants/session_view_process.mock'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { SessionViewDetailPanel } from './index'; + +describe('SessionView component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When SessionViewDetailPanel is mounted', () => { + it('should show session detail and server detail when no process is selected', async () => { + renderResult = mockedContext.render( + jest.fn()} + /> + ); + // see if loader is present + expect(renderResult.queryByTestId('sessionViewDetailPanelCommandDetail')).toBeNull(); + expect(renderResult.queryByTestId('sessionViewDetailPanelSessionDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelServerDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelAlertDetail')).toBeNull(); + }); + + it('should show command detail when selectedProcess is present', async () => { + renderResult = mockedContext.render( + jest.fn()} + /> + ); + expect(renderResult.queryByTestId('sessionViewDetailPanelCommandDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelSessionDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelServerDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelAlertDetail')).toBeNull(); + }); + + it('should show command and alerts detail if selectedProcess contains a signal event', async () => { + renderResult = mockedContext.render( + jest.fn()} + /> + ); + expect(renderResult.queryByTestId('sessionViewDetailPanelCommandDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelSessionDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelServerDetail')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionViewDetailPanelAlertDetail')).toBeTruthy(); + }); + }); +}); From b481f827f69fbcd2b5a0d77bf58b5a989929369e Mon Sep 17 00:00:00 2001 From: Zizhou Wang Date: Tue, 30 Nov 2021 11:31:14 -0500 Subject: [PATCH 3/6] Include jest canvas mock in jest config --- x-pack/plugins/session_view/jest.config.js | 1 + .../public/components/SessionViewDetailPanel/index.test.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/session_view/jest.config.js b/x-pack/plugins/session_view/jest.config.js index 768dea8ee0182..d35db0d369468 100644 --- a/x-pack/plugins/session_view/jest.config.js +++ b/x-pack/plugins/session_view/jest.config.js @@ -14,4 +14,5 @@ module.exports = { collectCoverageFrom: [ '/x-pack/plugins/session_view/{common,public,server}/**/*.{ts,tsx}', ], + setupFiles: ['jest-canvas-mock'], }; diff --git a/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx index 015424ea13591..aaeed3f8438c2 100644 --- a/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.test.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import 'jest-canvas-mock'; import { sessionViewBasicProcessMock, sessionViewAlertProcessMock, From f2dd95af01d476f49e43de82dad0e6fafa8ade41 Mon Sep 17 00:00:00 2001 From: Zizhou Wang Date: Tue, 30 Nov 2021 12:49:27 -0500 Subject: [PATCH 4/6] Fix build process tree sometimes parent process is undefined --- x-pack/plugins/session_view/public/hooks/use_process_tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b7cad0717fc45..50327093c387e 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 @@ -64,7 +64,7 @@ export const useProcessTree = ({ const buildProcessTree = (events: ProcessEvent[], backwardDirection: boolean = false) => { events.forEach((event) => { const process = processMap[event.process.entity_id]; - const parentProcess = processMap[event.process.parent.entity_id]; + const parentProcess = processMap[event.process.parent?.entity_id]; if (parentProcess) { process.parent = parentProcess; // handy for recursive operations (like auto expand) From f25070782385db0e7ed8f7cd537fe7abe92b942d Mon Sep 17 00:00:00 2001 From: Zizhou Wang Date: Tue, 30 Nov 2021 13:54:03 -0500 Subject: [PATCH 5/6] Extract process nested types for reuse and CR --- .../common/types/process_tree/index.ts | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts index c9fd6e21e06b7..52abfc2b238b2 100644 --- a/x-pack/plugins/session_view/common/types/process_tree/index.ts +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -17,19 +17,19 @@ export enum EventAction { output = 'output', } -interface EventActionPartition { +export interface EventActionPartition { fork: ProcessEvent[]; exec: ProcessEvent[]; exit: ProcessEvent[]; output: ProcessEvent[]; } -interface User { +export interface User { id: string; name: string; } -interface ProcessFields { +export interface ProcessFields { args: string[]; args_count: number; command_line: string; @@ -52,6 +52,48 @@ export interface ProcessSelf extends ProcessFields { last_user_entered?: ProcessFields; } +export interface ProcessEventHost { + architecture: string; + hostname: string; + id: string; + ip: string; + mac: string; + name: string; + os: { + family: string; + full: string; + kernel: string; + name: string; + platform: string; + type: string; + version: string; + }; +} + +export interface ProcessEventAlertRule { + category: string; + consumer: string; + description: string; + enabled: boolean; + name: string; + query: string; + risk_score: number; + severity: string; + uuid: string; +} + +export interface ProcessEventAlert { + uuid: string; + reason: string; + workflow_status: string; + status: string; + original_time: Date; + original_event: { + action: string; + }; + rule: ProcessEventAlertRule; +} + export interface ProcessEvent { '@timestamp': Date; event: { @@ -59,47 +101,11 @@ export interface ProcessEvent { category: string; action: EventAction; }; - host?: { - // optional for now (raw agent output doesn't have server identity) - architecture: string; - hostname: string; - id: string; - ip: string; - mac: string; - name: string; - os: { - family: string; - full: string; - kernel: string; - name: string; - platform: string; - type: string; - version: string; - }; - }; + // optional host for now (raw agent output doesn't have server identity) + host?: ProcessEventHost; process: ProcessSelf; kibana?: { - alert: { - uuid: string; - reason: string; - workflow_status: string; - status: string; - original_time: Date; - original_event: { - action: string; - }; - rule: { - category: string; - consumer: string; - description: string; - enabled: boolean; - name: string; - query: string; - risk_score: number; - severity: string; - uuid: string; - }; - }; + alert: ProcessEventAlert; }; } From 978c93194fb7d38db492b9f8b33306fc977a4300 Mon Sep 17 00:00:00 2001 From: Zizhou Wang Date: Tue, 30 Nov 2021 18:24:26 -0500 Subject: [PATCH 6/6] Move ProcessImpl back to use_process_tree --- .../constants/session_view_process.mock.ts | 25 +++-- .../common/types/process_tree/index.ts | 83 ----------------- .../components/SessionViewPage/index.tsx | 2 +- .../public/hooks/use_process_tree.ts | 91 ++++++++++++++++++- 4 files changed, 103 insertions(+), 98 deletions(-) diff --git a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts index 950f173722427..f99f9f1bfabbc 100644 --- a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts +++ b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts @@ -419,9 +419,9 @@ const mockAlerts = [ }, ]; -export const sessionViewBasicProcessMock: Process = { +const processMock: Process = { id: '3f44d854-fe8d-5666-abc9-9efe7f970b4b', - events: mockEvents, + events: [], children: [], autoExpand: false, searchMatched: null, @@ -429,26 +429,25 @@ export const sessionViewBasicProcessMock: Process = { hasOutput: () => false, hasAlerts: () => false, getAlerts: () => [], - hasExec: () => true, + hasExec: () => false, getOutput: () => '', getDetails: () => ({} as ProcessEvent), - isUserEntered: () => true, + isUserEntered: () => false, getMaxAlertLevel: () => null, }; +export const sessionViewBasicProcessMock: Process = { + ...processMock, + events: mockEvents, + hasExec: () => true, + isUserEntered: () => true, +}; + export const sessionViewAlertProcessMock: Process = { - id: '3f44d854-fe8d-5666-abc9-9efe7f970b4b', + ...processMock, events: [...mockEvents, ...mockAlerts], - children: [], - autoExpand: false, - searchMatched: null, - parent: undefined, - hasOutput: () => false, hasAlerts: () => true, getAlerts: () => mockEvents, hasExec: () => true, - getOutput: () => '', - getDetails: () => ({} as ProcessEvent), isUserEntered: () => true, - getMaxAlertLevel: () => null, }; diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts index 52abfc2b238b2..18fc08feea18c 100644 --- a/x-pack/plugins/session_view/common/types/process_tree/index.ts +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -125,86 +125,3 @@ export interface Process { isUserEntered(): boolean; getMaxAlertLevel(): number | null; } - -export class ProcessImpl implements Process { - id: string; - events: ProcessEvent[]; - children: Process[]; - parent: Process | undefined; - autoExpand: boolean; - searchMatched: string | null; - - constructor(id: string) { - this.id = id; - this.events = []; - this.children = []; - this.autoExpand = false; - this.searchMatched = null; - } - - hasOutput() { - // TODO: schema undecided - return !!this.events.find(({ event }) => event.action === EventAction.output); - } - - hasAlerts() { - return !!this.events.find(({ event }) => event.kind === EventKind.signal); - } - - getAlerts() { - return this.events.filter(({ event }) => event.kind === EventKind.signal); - } - - hasExec() { - return !!this.events.find(({ event }) => event.action === EventAction.exec); - } - - hasExited() { - return !!this.events.find(({ event }) => event.action === EventAction.exit); - } - - getDetails() { - const eventsPartition = this.events.reduce( - (currEventsParition, processEvent) => { - currEventsParition[processEvent.event.action]?.push(processEvent); - return currEventsParition; - }, - Object.values(EventAction).reduce((currActions, action) => { - currActions[action] = [] as ProcessEvent[]; - return currActions; - }, {} as EventActionPartition) - ); - - if (eventsPartition.exec.length) { - return eventsPartition.exec[eventsPartition.exec.length - 1]; - } - - if (eventsPartition.fork.length) { - return eventsPartition.fork[eventsPartition.fork.length - 1]; - } - - return {} as ProcessEvent; - } - - getOutput() { - return this.events.reduce((output, event) => { - if (event.event.action === EventAction.output) { - output += ''; // TODO: schema unknown - } - - return output; - }, ''); - } - - isUserEntered() { - const event = this.getDetails(); - const { interactive, pgid, parent } = event?.process || {}; - - return interactive && pgid !== parent.pgid; - } - - getMaxAlertLevel() { - // TODO: - return null; - } -} 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 9479aa252ede8..eae4ffc20c390 100644 --- a/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionViewPage/index.tsx @@ -14,7 +14,7 @@ import { CoreStart } from '../../../../../../src/core/public'; import { RECENT_SESSION_ROUTE, BASE_PATH } from '../../../common/constants'; import { SessionView } from '../SessionView'; -import { ProcessEvent } from '../../hooks/use_process_tree'; +import { ProcessEvent } from '../../../common/types/process_tree'; interface RecentSessionResults { hits: any[]; 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 50327093c387e..becd2804d6252 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 @@ -6,7 +6,13 @@ */ import _ from 'lodash'; import { useState, useEffect } from 'react'; -import { EventKind, Process, ProcessImpl, ProcessEvent } from '../../common/types/process_tree'; +import { + EventAction, + EventKind, + EventActionPartition, + Process, + ProcessEvent, +} from '../../common/types/process_tree'; interface UseProcessTreeDeps { sessionEntityId: string; @@ -19,6 +25,89 @@ type ProcessMap = { [key: string]: Process; }; +class ProcessImpl implements Process { + id: string; + events: ProcessEvent[]; + children: Process[]; + parent: Process | undefined; + autoExpand: boolean; + searchMatched: string | null; + + constructor(id: string) { + this.id = id; + this.events = []; + this.children = []; + this.autoExpand = false; + this.searchMatched = null; + } + + hasOutput() { + // TODO: schema undecided + return !!this.events.find(({ event }) => event.action === EventAction.output); + } + + hasAlerts() { + return !!this.events.find(({ event }) => event.kind === EventKind.signal); + } + + getAlerts() { + return this.events.filter(({ event }) => event.kind === EventKind.signal); + } + + hasExec() { + return !!this.events.find(({ event }) => event.action === EventAction.exec); + } + + hasExited() { + return !!this.events.find(({ event }) => event.action === EventAction.exit); + } + + getDetails() { + const eventsPartition = this.events.reduce( + (currEventsParition, processEvent) => { + currEventsParition[processEvent.event.action]?.push(processEvent); + return currEventsParition; + }, + Object.values(EventAction).reduce((currActions, action) => { + currActions[action] = [] as ProcessEvent[]; + return currActions; + }, {} as EventActionPartition) + ); + + if (eventsPartition.exec.length) { + return eventsPartition.exec[eventsPartition.exec.length - 1]; + } + + if (eventsPartition.fork.length) { + return eventsPartition.fork[eventsPartition.fork.length - 1]; + } + + return {} as ProcessEvent; + } + + getOutput() { + return this.events.reduce((output, event) => { + if (event.event.action === EventAction.output) { + output += ''; // TODO: schema unknown + } + + return output; + }, ''); + } + + isUserEntered() { + const event = this.getDetails(); + const { interactive, pgid, parent } = event?.process || {}; + + return interactive && pgid !== parent.pgid; + } + + getMaxAlertLevel() { + // TODO: + return null; + } +} + export const useProcessTree = ({ sessionEntityId, forward,