From 113bc2e61a24087aa8382daf2953f9d890fc832c Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Tue, 22 Oct 2019 13:51:00 -0600 Subject: [PATCH] [SIEM] Additional Endgame Row Renderer Code Coverage (#48722) (#48931) ## [SIEM] Additional Endgame Row Renderer Code Coverage Adds additional unit test coverage for the [Endgame row renderers](https://github.com/elastic/kibana/pull/48277) ### Endgame Event Types / Subtypes Additional unit tests were added for the following Endgame event types / subtypes: * DNS (`dns_event`) - [X] `request_event` * File (FIM) (`file_event`) - [X] `file_create_event` - [X] `file_delete_event` * Network (`network_event`) - [X] `ipv4_connection_accept_event` - [X] `ipv6_connection_accept_event` - [X] `ipv4_disconnect_received_event` - [X] `ipv6_disconnect_received_event` * Security (Authentication) (`security_event`) - [X] `user_logon` - [X] `admin_logon` - [X] `explicit_user_logon` - [X] `user_logoff` * Process (`process_event`) - [X] `creation_event` - [X] `termination_event` ### Non-Endgame Events Additional unit tests for some non-Endgame events were also added, including: * FIM file `created` events * FIM file `deleted` events * Socket `socket_opened` events * Socket `socket_closed` events https://github.com/elastic/ecs-dev/issues/178 --- .../renderers/exit_code_draggable.test.tsx | 87 ++ .../body/renderers/file_draggable.test.tsx | 110 +++ .../body/renderers/file_draggable.tsx | 6 +- .../timeline/body/renderers/helpers.test.tsx | 130 ++- .../timeline/body/renderers/helpers.tsx | 13 +- .../parent_process_draggable.test.tsx | 104 +++ .../renderers/parent_process_draggable.tsx | 14 +- .../body/renderers/process_draggable.test.tsx | 208 ++++- .../body/renderers/process_hash.test.tsx | 89 ++ .../{process.hash.tsx => process_hash.tsx} | 0 .../system/generic_file_details.test.tsx | 517 +++++++++++ .../renderers/system/generic_file_details.tsx | 21 +- .../system/generic_row_renderer.test.tsx | 823 +++++++++++++++++- .../renderers/system/generic_row_renderer.tsx | 4 +- .../renderers/user_host_working_dir.test.tsx | 121 +++ .../siem/public/mock/mock_endgame_ecs_data.ts | 10 +- .../siem/public/mock/mock_timeline_data.ts | 215 ++++- 17 files changed, 2414 insertions(+), 58 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx rename x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/{process.hash.tsx => process_hash.tsx} (100%) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx new file mode 100644 index 0000000000000..86c36187d73fa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../mock'; + +import { ExitCodeDraggable } from './exit_code_draggable'; + +describe('ExitCodeDraggable', () => { + test('it renders the expected text and exit code, when both text and an endgameExitCode are provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('with exit code0'); + }); + + test('it returns an empty string when text is provided, but endgameExitCode is undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('it returns an empty string when text is provided, but endgameExitCode is null', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('it returns an empty string when text is provided, but endgameExitCode is an empty string', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('it renders just the exit code when text is undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('1'); + }); + + test('it renders just the exit code when text is null', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('1'); + }); + + test('it renders just the exit code when text is an empty string', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('1'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx new file mode 100644 index 0000000000000..68acc58972370 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../mock'; + +import { FileDraggable } from './file_draggable'; + +describe('FileDraggable', () => { + test('it prefers fileName and filePath over endgameFileName and endgameFilePath when all of them are provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[fileName]in[filePath]'); + }); + + test('it returns an empty string when none of the files or paths are provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('it renders just the endgameFileName if only endgameFileName is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[endgameFileName]'); + }); + + test('it renders "in endgameFilePath" if only endgameFilePath is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('in[endgameFilePath]'); + }); + + test('it renders just the filename if only fileName is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[fileName]'); + }); + + test('it renders "in filePath" if only filePath is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('in[filePath]'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx index af8876927d235..a1c43f3ecb163 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx @@ -31,8 +31,8 @@ export const FileDraggable = React.memo( return null; } - const fileNameIsKnown = - !isNillEmptyOrNotFinite(fileName) || !isNillEmptyOrNotFinite(endgameFileName); + const filePathIsKnown = + !isNillEmptyOrNotFinite(filePath) || !isNillEmptyOrNotFinite(endgameFilePath); return ( <> @@ -58,7 +58,7 @@ export const FileDraggable = React.memo( ) : null} - {fileNameIsKnown && ( + {filePathIsKnown && ( {i18n.IN} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx index 2cd75344deaf6..98a99cb6e4089 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx @@ -8,7 +8,15 @@ import { cloneDeep } from 'lodash/fp'; import { TimelineNonEcsData } from '../../../../graphql/types'; import { mockTimelineData } from '../../../../mock'; -import { deleteItemIdx, findItem, getValues, isNillEmptyOrNotFinite, showVia } from './helpers'; +import { + deleteItemIdx, + findItem, + getValues, + isFileEvent, + isNillEmptyOrNotFinite, + isProcessStoppedOrTerminationEvent, + showVia, +} from './helpers'; describe('helpers', () => { describe('#deleteItemIdx', () => { @@ -126,66 +134,150 @@ describe('helpers', () => { describe('#isNillEmptyOrNotFinite', () => { test('undefined returns true', () => { - expect(isNillEmptyOrNotFinite(undefined)).toEqual(true); + expect(isNillEmptyOrNotFinite(undefined)).toBe(true); }); test('null returns true', () => { - expect(isNillEmptyOrNotFinite(null)).toEqual(true); + expect(isNillEmptyOrNotFinite(null)).toBe(true); }); test('empty string returns true', () => { - expect(isNillEmptyOrNotFinite('')).toEqual(true); + expect(isNillEmptyOrNotFinite('')).toBe(true); }); test('empty array returns true', () => { - expect(isNillEmptyOrNotFinite([])).toEqual(true); + expect(isNillEmptyOrNotFinite([])).toBe(true); }); test('NaN returns true', () => { - expect(isNillEmptyOrNotFinite(NaN)).toEqual(true); + expect(isNillEmptyOrNotFinite(NaN)).toBe(true); }); test('Infinity returns true', () => { - expect(isNillEmptyOrNotFinite(Infinity)).toEqual(true); + expect(isNillEmptyOrNotFinite(Infinity)).toBe(true); }); test('a single space string returns false', () => { - expect(isNillEmptyOrNotFinite(' ')).toEqual(false); + expect(isNillEmptyOrNotFinite(' ')).toBe(false); }); test('a simple string returns false', () => { - expect(isNillEmptyOrNotFinite('a simple string')).toEqual(false); + expect(isNillEmptyOrNotFinite('a simple string')).toBe(false); }); test('the number 0 returns false', () => { - expect(isNillEmptyOrNotFinite(0)).toEqual(false); + expect(isNillEmptyOrNotFinite(0)).toBe(false); }); test('a non-empty array return false', () => { - expect(isNillEmptyOrNotFinite(['non empty array'])).toEqual(false); + expect(isNillEmptyOrNotFinite(['non empty array'])).toBe(false); }); }); describe('#showVia', () => { test('undefined returns false', () => { - expect(showVia(undefined)).toEqual(false); + expect(showVia(undefined)).toBe(false); }); test('null returns false', () => { - expect(showVia(undefined)).toEqual(false); + expect(showVia(null)).toBe(false); }); - test('empty string false', () => { - expect(showVia('')).toEqual(false); + test('empty string returns false', () => { + expect(showVia('')).toBe(false); }); test('a random string returns false', () => { - expect(showVia('a random string')).toEqual(false); + expect(showVia('a random string')).toBe(false); }); - ['file_create_event', 'created', 'file_delete_event', 'deleted'].forEach(eventAction => { - test(`${eventAction} returns true`, () => { - expect(showVia(eventAction)).toEqual(true); + describe('valid values', () => { + const validValues = ['file_create_event', 'created', 'file_delete_event', 'deleted']; + + validValues.forEach(eventAction => { + test(`${eventAction} returns true`, () => { + expect(showVia(eventAction)).toBe(true); + }); + }); + + validValues.forEach(value => { + const upperCaseValue = value.toUpperCase(); + + test(`${upperCaseValue} (upper case) returns true`, () => { + expect(showVia(upperCaseValue)).toBe(true); + }); + }); + }); + }); + + describe('#isFileEvent', () => { + test('returns true when both eventCategory and eventDataset are file', () => { + expect(isFileEvent({ eventCategory: 'file', eventDataset: 'file' })).toBe(true); + }); + + test('returns false when eventCategory and eventDataset are undefined', () => { + expect(isFileEvent({ eventCategory: undefined, eventDataset: undefined })).toBe(false); + }); + + test('returns false when eventCategory and eventDataset are null', () => { + expect(isFileEvent({ eventCategory: null, eventDataset: null })).toBe(false); + }); + + test('returns false when eventCategory and eventDataset are random values', () => { + expect( + isFileEvent({ eventCategory: 'random category', eventDataset: 'random dataset' }) + ).toBe(false); + }); + + test('returns true when just eventCategory is file', () => { + expect(isFileEvent({ eventCategory: 'file', eventDataset: undefined })).toBe(true); + }); + + test('returns true when just eventDataset is file', () => { + expect(isFileEvent({ eventCategory: null, eventDataset: 'file' })).toBe(true); + }); + + test('returns true when just eventCategory is File with a capitol F', () => { + expect(isFileEvent({ eventCategory: 'File', eventDataset: '' })).toBe(true); + }); + + test('returns true when just eventDataset is File with a capitol F', () => { + expect(isFileEvent({ eventCategory: 'random', eventDataset: 'File' })).toBe(true); + }); + }); + + describe('#isProcessStoppedOrTerminationEvent', () => { + test('returns false when eventAction is undefined', () => { + expect(isProcessStoppedOrTerminationEvent(undefined)).toBe(false); + }); + + test('returns false when eventAction is null', () => { + expect(isProcessStoppedOrTerminationEvent(null)).toBe(false); + }); + + test('returns false when eventAction is an empty string', () => { + expect(isProcessStoppedOrTerminationEvent('')).toBe(false); + }); + + test('returns false when eventAction is a random value', () => { + expect(isProcessStoppedOrTerminationEvent('a random value')).toBe(false); + }); + + describe('valid values', () => { + const validValues = ['process_stopped', 'termination_event']; + + validValues.forEach(value => { + test(`returns true when eventAction is ${value}`, () => { + expect(isProcessStoppedOrTerminationEvent(value)).toBe(true); + }); + }); + + validValues.forEach(value => { + const upperCaseValue = value.toUpperCase(); + + test(`returns true when eventAction is (upper case) ${upperCaseValue}`, () => { + expect(isProcessStoppedOrTerminationEvent(upperCaseValue)).toBe(true); + }); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx index b8e55fd29930e..26aa5cea51ce7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx @@ -43,7 +43,7 @@ export function isNillEmptyOrNotFinite(value: string | number | T[] | null | return isNumber(value) ? !isFinite(value) : isEmpty(value); } -export const isFimEvent = ({ +export const isFileEvent = ({ eventCategory, eventDataset, }: { @@ -53,8 +53,11 @@ export const isFimEvent = ({ (eventCategory != null && eventCategory.toLowerCase() === 'file') || (eventDataset != null && eventDataset.toLowerCase() === 'file'); -export const isProcessStoppedOrTerminationEvent = (eventAction: string | null | undefined) => - eventAction === 'process_stopped' || eventAction === 'termination_event'; +export const isProcessStoppedOrTerminationEvent = ( + eventAction: string | null | undefined +): boolean => ['process_stopped', 'termination_event'].includes(`${eventAction}`.toLowerCase()); -export const showVia = (eventAction: string | null | undefined) => - ['file_create_event', 'created', 'file_delete_event', 'deleted'].includes(`${eventAction}`); +export const showVia = (eventAction: string | null | undefined): boolean => + ['file_create_event', 'created', 'file_delete_event', 'deleted'].includes( + `${eventAction}`.toLowerCase() + ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx new file mode 100644 index 0000000000000..9f6a8676694e4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../mock'; + +import { ParentProcessDraggable } from './parent_process_draggable'; + +describe('ParentProcessDraggable', () => { + test('displays the text, endgameParentProcessName, and processPpid when they are all provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('via parent process[endgameParentProcessName](456)'); + }); + + test('displays nothing when the text is provided, but endgameParentProcessName and processPpid are both undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('displays the text and processPpid when endgameParentProcessName is undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('via parent process(456)'); + }); + + test('displays the processPpid when both endgameParentProcessName and text are undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('(456)'); + }); + + test('displays the text and endgameParentProcessName when processPpid is undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('via parent process[endgameParentProcessName]'); + }); + + test('displays the endgameParentProcessName when both processPpid and text are undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[endgameParentProcessName]'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx index 1423ccad7211e..7cb6c3704a238 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx @@ -8,27 +8,19 @@ import * as React from 'react'; import { DraggableBadge } from '../../../draggables'; -import { - isNillEmptyOrNotFinite, - isProcessStoppedOrTerminationEvent, - TokensFlexItem, -} from './helpers'; +import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; interface Props { contextId: string; endgameParentProcessName: string | null | undefined; - eventAction: string | null | undefined; eventId: string; processPpid: number | undefined | null; text: string | null | undefined; } export const ParentProcessDraggable = React.memo( - ({ contextId, endgameParentProcessName, eventAction, eventId, processPpid, text }) => { - if ( - (isNillEmptyOrNotFinite(endgameParentProcessName) && isNillEmptyOrNotFinite(processPpid)) || - isProcessStoppedOrTerminationEvent(eventAction) - ) { + ({ contextId, endgameParentProcessName, eventId, processPpid, text }) => { + if (isNillEmptyOrNotFinite(endgameParentProcessName) && isNillEmptyOrNotFinite(processPpid)) { return null; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx index 826af00c39011..27b41fa6d5d76 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx @@ -10,7 +10,7 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; -import { ProcessDraggable } from './process_draggable'; +import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable'; describe('ProcessDraggable', () => { describe('rendering', () => { @@ -317,5 +317,211 @@ describe('ProcessDraggable', () => { ); expect(wrapper.text()).toEqual('[process-executable](123)'); }); + + test('it prefers process.name when process.executable and endgame.process_name are also provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[process-name]'); + }); + + test('it falls back to rendering process.executable when process.name is NOT provided, but process.executable and endgame.process_name are provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[process-executable]'); + }); + + test('it falls back to rendering endgame.process_name when process.name and process.executable are NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[endgame-process-name]'); + }); + + test('it prefers process.pid when endgame.pid is also provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('(123)'); + }); + + test('it falls back to rendering endgame.pid when process.pid is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('(999)'); + }); + }); +}); + +describe('ProcessDraggableWithNonExistentProcess', () => { + test('it renders the expected text when all fields are undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it renders the expected text when just endgamePid is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('(999)'); + }); + + test('it renders the expected text when just endgameProcessName is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[endgameProcessName]'); + }); + + test('it renders the expected text when just processExecutable is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[processExecutable]'); + }); + + test('it renders the expected text when just processName is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[processName]'); + }); + + test('it renders the expected text when just processPid is provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('(123)'); + }); + + test('it renders the expected text when all values are provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[processName](123)'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx new file mode 100644 index 0000000000000..35ce52b576d8e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../mock'; + +import { ProcessHash } from './process_hash'; + +describe('ProcessHash', () => { + test('displays the processHashMd5, processHashSha1, and processHashSha256 when they are all provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[processHashSha256][processHashSha1][processHashMd5]'); + }); + + test('displays nothing when processHashMd5, processHashSha1, and processHashSha256 are all undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('displays just processHashMd5 when the other hashes are undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[processHashMd5]'); + }); + + test('displays just processHashSha1 when the other hashes are undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[processHashSha1]'); + }); + + test('displays just processHashSha256 when the other hashes are undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[processHashSha256]'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process.hash.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process.hash.tsx rename to x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx index 91a848c8e02ed..308f80429d57e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -1079,5 +1079,522 @@ describe('SystemGenericFileDetails', () => { '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[arg-1][arg-2][arg-3][process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); + + test('it renders a FileDraggable when endgameFileName and endgameFilePath are provided, but fileName and filePath are NOT provided', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('[endgameFileName]in[endgameFilePath]an unknown process'); + }); + + test('it prefers to render fileName and filePath over endgameFileName and endgameFilePath respectfully when all of those fields are provided', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[fileName]in[filePath]an unknown process'); + }); + + ['file_create_event', 'created', 'file_delete_event', 'deleted'].forEach(eventAction => { + test(`it renders the text "via" when eventAction is ${eventAction}`, () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text().includes('via')).toBe(true); + }); + }); + + test('it does NOT render the text "via" when eventAction is not a whitelisted action', () => { + const eventAction = 'a_non_whitelisted_event_action'; + + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text().includes('via')).toBe(false); + }); + + test('it renders a ParentProcessDraggable when eventAction is NOT "process_stopped" and NOT "termination_event"', () => { + const eventAction = 'something_else'; + + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual( + 'an unknown processvia parent process[endgameParentProcessName](456)' + ); + }); + + test('it does NOT render a ParentProcessDraggable when eventAction is "process_stopped"', () => { + const eventAction = 'process_stopped'; + + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it does NOT render a ParentProcessDraggable when eventAction is "termination_event"', () => { + const eventAction = 'termination_event'; + + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it returns renders the message when showMessage is true', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process[message]'); + }); + + test('it does NOT render the message when showMessage is false', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it renders a ProcessDraggableWithNonExistentProcess when endgamePid and endgameProcessName are provided, but processPid and processName are NOT provided', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[endgameProcessName](789)'); + }); + + test('it prefers to render processName and processPid over endgameProcessName and endgamePid respectfully when all of those fields are provided', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[processName](123)'); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx index 4d87543dd64f2..0e75e8de6f9b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx @@ -17,7 +17,7 @@ import { OverflowField } from '../../../../tables/helpers'; import * as i18n from './translations'; import { NetflowRenderer } from '../netflow'; import { UserHostWorkingDir } from '../user_host_working_dir'; -import { Details, showVia, TokensFlexItem } from '../helpers'; +import { Details, isProcessStoppedOrTerminationEvent, showVia, TokensFlexItem } from '../helpers'; import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; import { Args } from '../args'; import { AuthSsh } from './auth_ssh'; @@ -26,7 +26,7 @@ import { FileDraggable } from '../file_draggable'; import { Package } from './package'; import { Badge } from '../../../../page'; import { ParentProcessDraggable } from '../parent_process_draggable'; -import { ProcessHash } from '../process.hash'; +import { ProcessHash } from '../process_hash'; interface Props { args: string[] | null | undefined; @@ -144,14 +144,15 @@ export const SystemGenericFileLine = pure( eventId={id} text={i18n.WITH_EXIT_CODE} /> - + {!isProcessStoppedOrTerminationEvent(eventAction) && ( + + )} {outcome != null && ( {i18n.WITH_RESULT} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 595e8f0327db8..3f8a726ed44f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -12,12 +12,43 @@ import * as React from 'react'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { Ecs } from '../../../../../graphql/types'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; +import { + mockDnsEvent, + mockFimFileCreatedEvent, + mockFimFileDeletedEvent, + mockSocketClosedEvent, + mockSocketOpenedEvent, + mockTimelineData, + TestProviders, +} from '../../../../../mock'; +import { + mockEndgameAdminLogon, + mockEndgameCreationEvent, + mockEndgameDnsRequest, + mockEndgameExplicitUserLogon, + mockEndgameFileCreateEvent, + mockEndgameFileDeleteEvent, + mockEndgameIpv4ConnectionAcceptEvent, + mockEndgameIpv6ConnectionAcceptEvent, + mockEndgameIpv4DisconnectReceivedEvent, + mockEndgameIpv6DisconnectReceivedEvent, + mockEndgameTerminationEvent, + mockEndgameUserLogoff, + mockEndgameUserLogon, +} from '../../../../../mock/mock_endgame_ecs_data'; import { RowRenderer } from '../row_renderer'; import { + createDnsRowRenderer, + createEndgameProcessRowRenderer, + createFimRowRenderer, createGenericSystemRowRenderer, createGenericFileRowRenderer, + createSecurityEventRowRenderer, + createSocketRowRenderer, } from './generic_row_renderer'; +import * as i18n from './translations'; + +jest.mock('../../../../../lib/settings/use_kibana_ui_setting'); describe('GenericRowRenderer', () => { describe('#createGenericSystemRowRenderer', () => { @@ -144,4 +175,794 @@ describe('GenericRowRenderer', () => { ); }); }); + + describe('#createEndgameProcessRowRenderer', () => { + test('it renders an endgame process creation_event', () => { + const actionName = 'creation_event'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' + ); + }); + + test('it renders an endgame process termination_event', () => { + const actionName = 'termination_event'; + const text = i18n.TERMINATED_PROCESS; + const endgameTerminationEvent = { + ...mockEndgameTerminationEvent, + }; + + const endgameProcessTerminationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessTerminationEventRowRenderer.isInstance(endgameTerminationEvent) && + endgameProcessTerminationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameTerminationEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' + ); + }); + + test('it does NOT render the event if the action name does not match', () => { + const actionName = 'does_not_match'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render the event when the event category is NOT process', () => { + const actionName = 'creation_event'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + event: { + ...mockEndgameCreationEvent.event, + category: ['something_else'], + }, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render the event when both the action name and event category do NOT match', () => { + const actionName = 'does_not_match'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + event: { + ...mockEndgameCreationEvent.event, + category: ['something_else'], + }, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createFimRowRenderer', () => { + test('it renders an endgame file_create_event', () => { + const actionName = 'file_create_event'; + const text = i18n.CREATED_FILE; + const endgameFileCreateEvent = { + ...mockEndgameFileCreateEvent, + }; + + const endgameFileCreateEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && + endgameFileCreateEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileCreateEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' + ); + }); + + test('it renders an endgame file_delete_event', () => { + const actionName = 'file_delete_event'; + const text = i18n.DELETED_FILE; + const endgameFileDeleteEvent = { + ...mockEndgameFileDeleteEvent, + }; + + const endgameFileDeleteEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileDeleteEventRowRenderer.isInstance(endgameFileDeleteEvent) && + endgameFileDeleteEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileDeleteEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' + ); + }); + + test('it renders a FIM (non-endgame) file created event', () => { + const actionName = 'created'; + const text = i18n.CREATED_FILE; + const fimFileCreatedEvent = { + ...mockFimFileCreatedEvent, + }; + + const fileCreatedEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {fileCreatedEventRowRenderer.isInstance(fimFileCreatedEvent) && + fileCreatedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: fimFileCreatedEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children foohostcreated a filein/etc/subgidviaan unknown process' + ); + }); + + test('it renders a FIM (non-endgame) file deleted event', () => { + const actionName = 'deleted'; + const text = i18n.DELETED_FILE; + const fimFileDeletedEvent = { + ...mockFimFileDeletedEvent, + }; + + const fileDeletedEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {fileDeletedEventRowRenderer.isInstance(fimFileDeletedEvent) && + fileDeletedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: fimFileDeletedEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children foohostdeleted a filein/etc/gshadow.lockviaan unknown process' + ); + }); + + test('it does NOT render an event if the action name does not match', () => { + const actionName = 'does_not_match'; + const text = i18n.CREATED_FILE; + const endgameFileCreateEvent = { + ...mockEndgameFileCreateEvent, + }; + + const endgameFileCreateEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && + endgameFileCreateEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileCreateEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render an Endgame file_create_event when category is NOT file', () => { + const actionName = 'file_create_event'; + const text = i18n.CREATED_FILE; + const endgameFileCreateEvent = { + ...mockEndgameFileCreateEvent, + event: { + ...mockEndgameFileCreateEvent.event, + category: ['something_else'], + }, + }; + + const endgameFileCreateEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && + endgameFileCreateEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileCreateEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render a FIM (non-Endgame) file created event when the event dataset is NOT file', () => { + const actionName = 'created'; + const text = i18n.CREATED_FILE; + const fimFileCreatedEvent = { + ...mockFimFileCreatedEvent, + event: { + ...mockEndgameFileCreateEvent.event, + dataset: ['something_else'], + }, + }; + + const fileCreatedEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {fileCreatedEventRowRenderer.isInstance(fimFileCreatedEvent) && + fileCreatedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: fimFileCreatedEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createSocketRowRenderer', () => { + test('it renders an Endgame ipv4_connection_accept_event', () => { + const actionName = 'ipv4_connection_accept_event'; + const text = i18n.ACCEPTED_A_CONNECTION_VIA; + const ipv4ConnectionAcceptEvent = { + ...mockEndgameIpv4ConnectionAcceptEvent, + }; + + const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv4ConnectionAcceptEventRowRenderer.isInstance(ipv4ConnectionAcceptEvent) && + endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv4ConnectionAcceptEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' + ); + }); + + test('it renders an Endgame ipv6_connection_accept_event', () => { + const actionName = 'ipv6_connection_accept_event'; + const text = i18n.ACCEPTED_A_CONNECTION_VIA; + const ipv6ConnectionAcceptEvent = { + ...mockEndgameIpv6ConnectionAcceptEvent, + }; + + const endgameIpv6ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv6ConnectionAcceptEventRowRenderer.isInstance(ipv6ConnectionAcceptEvent) && + endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv6ConnectionAcceptEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' + ); + }); + + test('it renders an Endgame ipv4_disconnect_received_event', () => { + const actionName = 'ipv4_disconnect_received_event'; + const text = i18n.DISCONNECTED_VIA; + const ipv4DisconnectReceivedEvent = { + ...mockEndgameIpv4DisconnectReceivedEvent, + }; + + const endgameIpv4DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv4DisconnectReceivedEventRowRenderer.isInstance(ipv4DisconnectReceivedEvent) && + endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv4DisconnectReceivedEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' + ); + }); + + test('it renders an Endgame ipv6_disconnect_received_event', () => { + const actionName = 'ipv6_disconnect_received_event'; + const text = i18n.DISCONNECTED_VIA; + const ipv6DisconnectReceivedEvent = { + ...mockEndgameIpv6DisconnectReceivedEvent, + }; + + const endgameIpv6DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv6DisconnectReceivedEventRowRenderer.isInstance(ipv6DisconnectReceivedEvent) && + endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv6DisconnectReceivedEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' + ); + }); + + test('it renders a (non-Endgame) socket_opened event', () => { + const actionName = 'socket_opened'; + const text = i18n.SOCKET_OPENED; + const socketOpenedEvent = { + ...mockSocketOpenedEvent, + }; + + const socketOpenedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {socketOpenedEventRowRenderer.isInstance(socketOpenedEvent) && + socketOpenedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: socketOpenedEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' + ); + }); + + test('it renders a (non-Endgame) socket_closed event', () => { + const actionName = 'socket_closed'; + const text = i18n.SOCKET_CLOSED; + const socketClosedEvent = { + ...mockSocketClosedEvent, + }; + + const socketClosedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {socketClosedEventRowRenderer.isInstance(socketClosedEvent) && + socketClosedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: socketClosedEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' + ); + }); + + test('it does NOT render an event if the action name does not match', () => { + const actionName = 'does_not_match'; + const text = i18n.ACCEPTED_A_CONNECTION_VIA; + const ipv4ConnectionAcceptEvent = { + ...mockEndgameIpv4ConnectionAcceptEvent, + }; + + const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv4ConnectionAcceptEventRowRenderer.isInstance(ipv4ConnectionAcceptEvent) && + endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv4ConnectionAcceptEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createSecurityEventRowRenderer', () => { + test('it renders an Endgame user_logon event', () => { + const actionName = 'user_logon'; + const userLogonEvent = { + ...mockEndgameUserLogon, + }; + + const userLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {userLogonEventRowRenderer.isInstance(userLogonEvent) && + userLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: userLogonEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' + ); + }); + + test('it renders an Endgame admin_logon event', () => { + const actionName = 'admin_logon'; + const adminLogonEvent = { + ...mockEndgameAdminLogon, + }; + + const adminLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {adminLogonEventRowRenderer.isInstance(adminLogonEvent) && + adminLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: adminLogonEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' + ); + }); + + test('it renders an Endgame explicit_user_logon event', () => { + const actionName = 'explicit_user_logon'; + const explicitUserLogonEvent = { + ...mockEndgameExplicitUserLogon, + }; + + const explicitUserLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {explicitUserLogonEventRowRenderer.isInstance(explicitUserLogonEvent) && + explicitUserLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: explicitUserLogonEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' + ); + }); + + test('it renders an Endgame user_logoff event', () => { + const actionName = 'user_logoff'; + const userLogoffEvent = { + ...mockEndgameUserLogoff, + }; + + const userLogoffEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {userLogoffEventRowRenderer.isInstance(userLogoffEvent) && + userLogoffEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: userLogoffEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' + ); + }); + + test('it does NOT render an event if the action name does not match', () => { + const actionName = 'does_not_match'; + const userLogonEvent = { + ...mockEndgameUserLogon, + }; + + const userLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {userLogonEventRowRenderer.isInstance(userLogonEvent) && + userLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: userLogonEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createDnsRowRenderer', () => { + test('it renders an Endgame DNS request_event', () => { + const requestEvent = { + ...mockEndgameDnsRequest, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(requestEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: requestEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' + ); + }); + + test('it renders a non-Endgame DNS event', () => { + const dnsEvent = { + ...mockDnsEvent, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(dnsEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: dnsEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'some children iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' + ); + }); + + test('it does NOT render an event if dns.question.type is not provided', () => { + const requestEvent = { + ...mockEndgameDnsRequest, + dns: { + ...mockDnsEvent.dns, + question: { + name: ['lookup.example.com'], + }, + }, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(requestEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: requestEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render an event if dns.question.name is not provided', () => { + const requestEvent = { + ...mockEndgameDnsRequest, + dns: { + ...mockDnsEvent.dns, + question: { + type: ['A'], + }, + }, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(requestEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: requestEvent, + children: {'some children '}, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx index 9000e23ee0d3a..f8930fdca7ba2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { DnsRequestEventDetails } from '../dns/dns_request_event_details'; import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details'; -import { isFimEvent, isNillEmptyOrNotFinite } from '../helpers'; +import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers'; import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { SystemGenericDetails } from './generic_details'; @@ -95,7 +95,7 @@ export const createFimRowRenderer = ({ const category: string | null | undefined = get('event.category[0]', ecs); const dataset: string | null | undefined = get('event.dataset[0]', ecs); return ( - isFimEvent({ eventCategory: category, eventDataset: dataset }) && + isFileEvent({ eventCategory: category, eventDataset: dataset }) && action != null && action.toLowerCase() === actionName ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx index cf03213fc34f3..2ea8c6931f00b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -185,5 +185,126 @@ describe('UserHostWorkingDir', () => { ); expect(wrapper.text()).toEqual('[user-name-123]\\[user-domain-123]@[host-name-123]'); }); + + test('it returns hostName and userName with the default hostNameSeparator "@", when hostNameSeparator is NOT specified as a prop', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[user-name-123]@[host-name-123]'); + }); + + test('it returns hostName and userName with an overridden hostNameSeparator, when hostNameSeparator is specified as a prop', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[user-name-123]custom separator[host-name-123]'); + }); + + test('it renders a draggable `user.domain` field (by default) when userDomain is provided, and userDomainField is NOT specified as a prop', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.find('[data-test-subj="draggable-content-user.domain"]').exists()).toBe(true); + }); + + test('it renders a draggable with an overridden field name when userDomain is provided, and userDomainField is also specified as a prop', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect( + wrapper.find('[data-test-subj="draggable-content-overridden.field.name"]').exists() + ).toBe(true); + }); + + test('it renders a draggable `user.name` field (by default) when userName is provided, and userNameField is NOT specified as a prop', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect(wrapper.find('[data-test-subj="draggable-content-user.name"]').exists()).toBe(true); + }); + + test('it renders a draggable with an overridden field name when userName is provided, and userNameField is also specified as a prop', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + + expect( + wrapper.find('[data-test-subj="draggable-content-overridden.field.name"]').exists() + ).toBe(true); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts b/x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts index 6f58373b508ac..e6eee3d1c1cb1 100644 --- a/x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts +++ b/x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts @@ -147,7 +147,7 @@ export const mockEndgameIpv4ConnectionAcceptEvent: Ecs = { }, timestamp: '1569555676000', network: { - community_id: ['1:PFLzPy11bhJJZCUQhIVPEBoDLG4='], + community_id: ['1:network-community_id'], transport: ['tcp'], }, process: { @@ -193,7 +193,7 @@ export const mockEndgameIpv6ConnectionAcceptEvent: Ecs = { }, timestamp: '1569553566000', network: { - community_id: ['1:bUcbuDIApRZPr7rMsEG5Ngk/ids='], + community_id: ['1:network-community_id'], transport: ['tcp'], }, process: { @@ -253,7 +253,7 @@ export const mockEndgameIpv4DisconnectReceivedEvent: Ecs = { }, destination: { port: [443], - ip: ['54.156.162.53'], + ip: ['10.156.162.53'], bytes: [6193], }, endgame: { @@ -563,8 +563,8 @@ export const mockEndgameTerminationEvent: Ecs = { process: { hash: { md5: ['bd4401441a21bf1abce6404f4231db4d'], - sha1: ['797255e72d5ed5c058d4785950eba7abaa057653]'], - sha256: ['87976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776]'], + sha1: ['797255e72d5ed5c058d4785950eba7abaa057653'], + sha256: ['87976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776'], }, pid: [442384], ppid: [8], diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts b/x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts index 997e3d2704cec..b300053d5f227 100644 --- a/x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts +++ b/x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelineItem } from '../graphql/types'; +import { Ecs, TimelineItem } from '../graphql/types'; export const mockTimelineData: TimelineItem[] = [ { @@ -1394,3 +1394,216 @@ export const mockTimelineData: TimelineItem[] = [ }, }, ]; + +export const mockFimFileCreatedEvent: Ecs = { + _id: 'WuBP4W0BOpWiDweSoYSg', + timestamp: '2019-10-18T23:59:15.091Z', + host: { + architecture: ['x86_64'], + os: { + family: ['debian'], + name: ['Ubuntu'], + kernel: ['4.15.0-1046-gcp'], + platform: ['ubuntu'], + version: ['16.04.6 LTS (Xenial Xerus)'], + }, + id: ['host-id-123'], + name: ['foohost'], + }, + file: { + path: ['/etc/subgid'], + size: [4445], + owner: ['root'], + inode: ['90027'], + ctime: ['2019-10-18T23:59:14.872Z'], + gid: ['0'], + type: ['file'], + mode: ['0644'], + mtime: ['2019-10-18T23:59:14.872Z'], + uid: ['0'], + group: ['root'], + }, + event: { + module: ['file_integrity'], + dataset: ['file'], + action: ['created'], + }, +}; + +export const mockFimFileDeletedEvent: Ecs = { + _id: 'M-BP4W0BOpWiDweSo4cm', + timestamp: '2019-10-18T23:59:16.247Z', + host: { + name: ['foohost'], + os: { + platform: ['ubuntu'], + version: ['16.04.6 LTS (Xenial Xerus)'], + family: ['debian'], + name: ['Ubuntu'], + kernel: ['4.15.0-1046-gcp'], + }, + id: ['host-id-123'], + architecture: ['x86_64'], + }, + event: { + module: ['file_integrity'], + dataset: ['file'], + action: ['deleted'], + }, + file: { + path: ['/etc/gshadow.lock'], + }, +}; + +export const mockSocketOpenedEvent: Ecs = { + _id: 'Vusu4m0BOpWiDweSLkXY', + timestamp: '2019-10-19T04:02:19.473Z', + network: { + direction: ['outbound'], + transport: ['tcp'], + community_id: ['1:network-community_id'], + }, + host: { + name: ['foohost'], + architecture: ['x86_64'], + os: { + platform: ['centos'], + version: ['7 (Core)'], + family: ['redhat'], + name: ['CentOS Linux'], + kernel: ['3.10.0-1062.1.2.el7.x86_64'], + }, + id: ['host-id-123'], + }, + process: { + pid: [2166], + name: ['google_accounts'], + }, + destination: { + ip: ['10.1.2.3'], + port: [80], + }, + user: { + name: ['root'], + }, + source: { + port: [59554], + ip: ['10.4.20.1'], + }, + event: { + action: ['socket_opened'], + module: ['system'], + dataset: ['socket'], + kind: ['event'], + }, + message: [ + 'Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) OPENED by process google_accounts (PID: 2166) and user root (UID: 0)', + ], +}; + +export const mockSocketClosedEvent: Ecs = { + _id: 'V-su4m0BOpWiDweSLkXY', + timestamp: '2019-10-19T04:02:19.473Z', + process: { + pid: [2166], + name: ['google_accounts'], + }, + user: { + name: ['root'], + }, + source: { + port: [59508], + ip: ['10.4.20.1'], + }, + event: { + dataset: ['socket'], + kind: ['event'], + action: ['socket_closed'], + module: ['system'], + }, + message: [ + 'Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) CLOSED by process google_accounts (PID: 2166) and user root (UID: 0)', + ], + network: { + community_id: ['1:network-community_id'], + direction: ['outbound'], + transport: ['tcp'], + }, + destination: { + ip: ['10.1.2.3'], + port: [80], + }, + host: { + name: ['foohost'], + architecture: ['x86_64'], + os: { + version: ['7 (Core)'], + family: ['redhat'], + name: ['CentOS Linux'], + kernel: ['3.10.0-1062.1.2.el7.x86_64'], + platform: ['centos'], + }, + id: ['host-id-123'], + }, +}; + +export const mockDnsEvent: Ecs = { + _id: 'VUTUqm0BgJt5sZM7nd5g', + destination: { + domain: ['ten.one.one.one'], + port: [53], + bytes: [137], + ip: ['10.1.1.1'], + geo: { + continent_name: ['Oceania'], + location: { + lat: [-33.494], + lon: [143.2104], + }, + country_iso_code: ['AU'], + country_name: ['Australia'], + city_name: [''], + }, + }, + host: { + architecture: ['armv7l'], + id: ['host-id'], + os: { + family: ['debian'], + platform: ['raspbian'], + version: ['9 (stretch)'], + name: ['Raspbian GNU/Linux'], + kernel: ['4.19.57-v7+'], + }, + name: ['iot.example.com'], + }, + dns: { + question: { + name: ['lookup.example.com'], + type: ['A'], + }, + response_code: ['NOERROR'], + resolved_ip: ['10.1.2.3'], + }, + timestamp: '2019-10-08T10:05:23.241Z', + network: { + community_id: ['1:network-community_id'], + direction: ['outbound'], + bytes: [177], + transport: ['udp'], + protocol: ['dns'], + }, + event: { + duration: [6937500], + category: ['network_traffic'], + dataset: ['dns'], + kind: ['event'], + end: ['2019-10-08T10:05:23.248Z'], + start: ['2019-10-08T10:05:23.241Z'], + }, + source: { + port: [58732], + bytes: [40], + ip: ['10.9.9.9'], + }, +};