diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json
new file mode 100644
index 0000000000000..8efd0eab0eee0
--- /dev/null
+++ b/x-pack/plugins/endpoint/package.json
@@ -0,0 +1,14 @@
+{
+ "author": "Elastic",
+ "name": "endpoint",
+ "version": "0.0.0",
+ "private": true,
+ "license": "Elastic-License",
+ "scripts": {},
+ "dependencies": {
+ "react-redux": "^7.1.0"
+ },
+ "devDependencies": {
+ "@types/react-redux": "^7.1.0"
+ }
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts
new file mode 100644
index 0000000000000..c7f790588a739
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts
@@ -0,0 +1,9 @@
+/*
+ * 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 { CameraAction } from './store/camera';
+import { DataAction } from './store/data';
+
+export type ResolverAction = CameraAction | DataAction;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx
index 55f9fd52f4662..9539162f9cfb6 100644
--- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx
@@ -4,31 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EmbeddableInput,
- IContainer,
- Embeddable,
-} from '../../../../../../src/plugins/embeddable/public';
+import ReactDOM from 'react-dom';
+import React from 'react';
+import { AppRoot } from './view';
+import { storeFactory } from './store';
+import { Embeddable } from '../../../../../../src/plugins/embeddable/public';
export class ResolverEmbeddable extends Embeddable {
public readonly type = 'resolver';
- constructor(initialInput: EmbeddableInput, parent?: IContainer) {
- super(
- // Input state is irrelevant to this embeddable, just pass it along.
- initialInput,
- // Initial output state - this embeddable does not do anything with output, so just
- // pass along an empty object.
- {},
- // Optional parent component, this embeddable can optionally be rendered inside a container.
- parent
- );
- }
+ private lastRenderTarget?: Element;
public render(node: HTMLElement) {
- node.innerHTML = '
Welcome from Resolver
';
+ if (this.lastRenderTarget !== undefined) {
+ ReactDOM.unmountComponentAtNode(this.lastRenderTarget);
+ }
+ this.lastRenderTarget = node;
+ const { store } = storeFactory();
+ ReactDOM.render(, node);
}
public reload(): void {
throw new Error('Method not implemented.');
}
+
+ public destroy(): void {
+ if (this.lastRenderTarget !== undefined) {
+ ReactDOM.unmountComponentAtNode(this.lastRenderTarget);
+ }
+ }
}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts
index aef2e309254ef..f5d1aad93ed57 100644
--- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts
@@ -5,12 +5,12 @@
*/
import { i18n } from '@kbn/i18n';
-import { ResolverEmbeddable } from './';
import {
EmbeddableFactory,
- EmbeddableInput,
IContainer,
+ EmbeddableInput,
} from '../../../../../../src/plugins/embeddable/public';
+import { ResolverEmbeddable } from './embeddable';
export class ResolverEmbeddableFactory extends EmbeddableFactory {
public readonly type = 'resolver';
@@ -20,7 +20,7 @@ export class ResolverEmbeddableFactory extends EmbeddableFactory {
}
public async create(initialInput: EmbeddableInput, parent?: IContainer) {
- return new ResolverEmbeddable(initialInput, parent);
+ return new ResolverEmbeddable(initialInput, {}, parent);
}
public getDisplayName() {
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts
new file mode 100644
index 0000000000000..c59db31c39e82
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+/**
+ * Return `value` unless it is less than `minimum`, in which case return `minimum` or unless it is greater than `maximum`, in which case return `maximum`.
+ */
+export function clamp(value: number, minimum: number, maximum: number) {
+ return Math.max(Math.min(value, maximum), minimum);
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.test.ts
new file mode 100644
index 0000000000000..c0e9f982e23ac
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.test.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { multiply } from './matrix3';
+describe('matrix3', () => {
+ it('can multiply two matrix3s', () => {
+ expect(multiply([1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18])).toEqual([
+ 84,
+ 90,
+ 96,
+ 201,
+ 216,
+ 231,
+ 318,
+ 342,
+ 366,
+ ]);
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts
new file mode 100644
index 0000000000000..0b4a72b9d79a6
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/matrix3.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { Matrix3 } from '../types';
+
+/**
+ * Return a new matrix which is the product of the first and second matrix.
+ */
+export function multiply(
+ [a11, a12, a13, a21, a22, a23, a31, a32, a33]: Matrix3,
+ [b11, b12, b13, b21, b22, b23, b31, b32, b33]: Matrix3
+): Matrix3 {
+ const s11 = a11 * b11 + a12 * b21 + a13 * b31;
+ const s12 = a11 * b12 + a12 * b22 + a13 * b32;
+ const s13 = a11 * b13 + a12 * b23 + a13 * b33;
+
+ const s21 = a21 * b11 + a22 * b21 + a23 * b31;
+ const s22 = a21 * b12 + a22 * b22 + a23 * b32;
+ const s23 = a21 * b13 + a22 * b23 + a23 * b33;
+
+ const s31 = a31 * b11 + a32 * b21 + a33 * b31;
+ const s32 = a31 * b12 + a32 * b22 + a33 * b32;
+ const s33 = a31 * b13 + a32 * b23 + a33 * b33;
+
+ // prettier-ignore
+ return [
+ s11, s12, s13,
+ s21, s22, s23,
+ s31, s32, s33,
+ ];
+}
+
+/**
+ * Return a new matrix which is the sum of the two passed in.
+ */
+export function add(
+ [a11, a12, a13, a21, a22, a23, a31, a32, a33]: Matrix3,
+ [b11, b12, b13, b21, b22, b23, b31, b32, b33]: Matrix3
+): Matrix3 {
+ return [
+ a11 + b11,
+ a12 + b12,
+ a13 + b13,
+
+ a21 + b21,
+ a22 + b22,
+ a23 + b23,
+
+ a31 + b31,
+ a32 + b32,
+ a33 + b33,
+ ];
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts
new file mode 100644
index 0000000000000..3fe6941279bc5
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.test.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { applyMatrix3 } from './vector2';
+import { scalingTransformation } from './transformation';
+
+describe('transforms', () => {
+ it('applying a scale matrix to a vector2 can invert the y value', () => {
+ expect(applyMatrix3([1, 2], scalingTransformation([1, -1]))).toEqual([1, -2]);
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts
new file mode 100644
index 0000000000000..3084ce0eacdb4
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Matrix3, Vector2 } from '../types';
+
+/**
+ * The inverse of `orthographicProjection`.
+ */
+export function inverseOrthographicProjection(
+ top: number,
+ right: number,
+ bottom: number,
+ left: number
+): Matrix3 {
+ const m11 = (right - left) / 2;
+ const m13 = (right + left) / (right - left);
+
+ const m22 = (top - bottom) / 2;
+ const m23 = (top + bottom) / (top - bottom);
+
+ return [m11, 0, m13, 0, m22, m23, 0, 0, 0];
+}
+
+/**
+ * Adjust x, y to be bounded, in scale, of a clipping plane defined by top, right, bottom, left.
+ *
+ * See explanation:
+ * https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix
+ * https://en.wikipedia.org/wiki/Orthographic_projection
+ */
+export function orthographicProjection(
+ top: number,
+ right: number,
+ bottom: number,
+ left: number
+): Matrix3 {
+ const m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds
+ const m13 = -((right + left) / (right - left));
+
+ const m22 = 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds
+ const m23 = -((top + bottom) / (top - bottom));
+
+ return [m11, 0, m13, 0, m22, m23, 0, 0, 0];
+}
+
+/**
+ * Returns a 2D transformation matrix that when applied to a vector will scale the vector by `x` and `y` in their respective axises.
+ * See https://en.wikipedia.org/wiki/Scaling_(geometry)#Matrix_representation
+ */
+export function scalingTransformation([x, y]: Vector2): Matrix3 {
+ // prettier-ignore
+ return [
+ x, 0, 0,
+ 0, y, 0,
+ 0, 0, 0
+ ]
+}
+
+/**
+ * Returns a 2D transformation matrix that when applied to a vector will translate by `x` and `y` in their respective axises.
+ * See https://en.wikipedia.org/wiki/Translation_(geometry)#Matrix_representation
+ */
+export function translationTransformation([x, y]: Vector2): Matrix3 {
+ // prettier-ignore
+ return [
+ 1, 0, x,
+ 0, 1, y,
+ 0, 0, 1
+ ]
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts
new file mode 100644
index 0000000000000..219c58d6efc9c
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/tree_sequencers.ts
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+/**
+ * Sequences a tree, yielding children returned by the `children` function. Sequencing is done in 'depth first preorder' fashion. See https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR)
+ */
+export function* depthFirstPreorder(root: T, children: (parent: T) => T[]): Iterable {
+ const nodesToVisit = [root];
+ while (nodesToVisit.length !== 0) {
+ const currentNode = nodesToVisit.shift();
+ if (currentNode !== undefined) {
+ nodesToVisit.unshift(...(children(currentNode) || []));
+ yield currentNode;
+ }
+ }
+}
+
+/**
+ * Sequences a tree, yielding children returned by the `children` function. Sequencing is done in 'level order' fashion.
+ */
+export function* levelOrder(root: T, children: (parent: T) => T[]): Iterable {
+ let level = [root];
+ while (level.length !== 0) {
+ let nextLevel = [];
+ for (const node of level) {
+ yield node;
+ nextLevel.push(...(children(node) || []));
+ }
+ level = nextLevel;
+ nextLevel = [];
+ }
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts
new file mode 100644
index 0000000000000..3c0681413305e
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { Vector2, Matrix3 } from '../types';
+
+/**
+ * Returns a vector which is the sum of `a` and `b`.
+ */
+export function add(a: Vector2, b: Vector2): Vector2 {
+ return [a[0] + b[0], a[1] + b[1]];
+}
+
+/**
+ * Returns a vector which is the difference of `a` and `b`.
+ */
+export function subtract(a: Vector2, b: Vector2): Vector2 {
+ return [a[0] - b[0], a[1] - b[1]];
+}
+
+/**
+ * Returns a vector which is the quotient of `a` and `b`.
+ */
+export function divide(a: Vector2, b: Vector2): Vector2 {
+ return [a[0] / b[0], a[1] / b[1]];
+}
+
+/**
+ * Returns a vector which is the result of applying a 2D transformation matrix to the provided vector.
+ */
+export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Matrix3): Vector2 {
+ return [x * m11 + y * m12 + m13, x * m21 + y * m22 + m23];
+}
+
+/**
+ * Returns the distance between two vectors
+ */
+export function distance(a: Vector2, b: Vector2) {
+ const [x1, y1] = a;
+ const [x2, y2] = b;
+ return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+}
+
+/**
+ * Returns the angle between two vectors
+ */
+export function angle(a: Vector2, b: Vector2) {
+ const deltaX = b[0] - a[0];
+ const deltaY = b[1] - a[1];
+ return Math.atan2(deltaY, deltaX);
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts
new file mode 100644
index 0000000000000..0eb3505096b4a
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 { uniquePidForProcess, uniqueParentPidForProcess } from './process_event';
+import { IndexedProcessTree, ProcessEvent } from '../types';
+import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers';
+
+/**
+ * Create a new IndexedProcessTree from an array of ProcessEvents
+ */
+export function factory(processes: ProcessEvent[]): IndexedProcessTree {
+ const idToChildren = new Map();
+ const idToValue = new Map();
+
+ for (const process of processes) {
+ idToValue.set(uniquePidForProcess(process), process);
+ const uniqueParentPid = uniqueParentPidForProcess(process);
+ const processChildren = idToChildren.get(uniqueParentPid);
+ if (processChildren) {
+ processChildren.push(process);
+ } else {
+ idToChildren.set(uniqueParentPid, [process]);
+ }
+ }
+
+ return {
+ idToChildren,
+ idToProcess: idToValue,
+ };
+}
+
+/**
+ * Returns an array with any children `ProcessEvent`s of the passed in `process`
+ */
+export function children(tree: IndexedProcessTree, process: ProcessEvent): ProcessEvent[] {
+ const id = uniquePidForProcess(process);
+ const processChildren = tree.idToChildren.get(id);
+ return processChildren === undefined ? [] : processChildren;
+}
+
+/**
+ * Returns the parent ProcessEvent, if any, for the passed in `childProcess`
+ */
+export function parent(
+ tree: IndexedProcessTree,
+ childProcess: ProcessEvent
+): ProcessEvent | undefined {
+ const uniqueParentPid = uniqueParentPidForProcess(childProcess);
+ if (uniqueParentPid === undefined) {
+ return undefined;
+ } else {
+ return tree.idToProcess.get(uniqueParentPid);
+ }
+}
+
+/**
+ * Number of processes in the tree
+ */
+export function size(tree: IndexedProcessTree) {
+ return tree.idToProcess.size;
+}
+
+/**
+ * Return the root process
+ */
+export function root(tree: IndexedProcessTree) {
+ if (size(tree) === 0) {
+ return null;
+ }
+ let current: ProcessEvent = tree.idToProcess.values().next().value;
+ while (parent(tree, current) !== undefined) {
+ current = parent(tree, current)!;
+ }
+ return current;
+}
+
+/**
+ * Yield processes in level order
+ */
+export function* levelOrder(tree: IndexedProcessTree) {
+ const rootNode = root(tree);
+ if (rootNode !== null) {
+ yield* baseLevelOrder(rootNode, children.bind(null, tree));
+ }
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts
new file mode 100644
index 0000000000000..3177671a30001
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { eventType } from './process_event';
+import { ProcessEvent } from '../types';
+import { mockProcessEvent } from './process_event_test_helpers';
+
+describe('process event', () => {
+ describe('eventType', () => {
+ let event: ProcessEvent;
+ beforeEach(() => {
+ event = mockProcessEvent({
+ data_buffer: {
+ node_id: 1,
+ event_type_full: 'process_event',
+ },
+ });
+ });
+ it("returns the right value when the subType is 'creation_event'", () => {
+ event.data_buffer.event_subtype_full = 'creation_event';
+ expect(eventType(event)).toEqual('processCreated');
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts
new file mode 100644
index 0000000000000..c8496b8e6e7a5
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { ProcessEvent } from '../types';
+
+/**
+ * Returns true if the process's eventType is either 'processCreated' or 'processRan'.
+ * Resolver will only render 'graphable' process events.
+ */
+export function isGraphableProcess(event: ProcessEvent) {
+ return eventType(event) === 'processCreated' || eventType(event) === 'processRan';
+}
+
+/**
+ * Returns a custom event type for a process event based on the event's metadata.
+ */
+export function eventType(event: ProcessEvent) {
+ const {
+ data_buffer: { event_type_full: type, event_subtype_full: subType },
+ } = event;
+
+ if (type === 'process_event') {
+ if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') {
+ return 'processCreated';
+ } else if (subType === 'already_running') {
+ return 'processRan';
+ } else if (subType === 'termination_event') {
+ return 'processTerminated';
+ } else {
+ return 'unknownProcessEvent';
+ }
+ } else if (type === 'alert_event') {
+ return 'processCausedAlert';
+ }
+ return 'unknownEvent';
+}
+
+/**
+ * Returns the process event's pid
+ */
+export function uniquePidForProcess(event: ProcessEvent) {
+ return event.data_buffer.node_id;
+}
+
+/**
+ * Returns the process event's parent pid
+ */
+export function uniqueParentPidForProcess(event: ProcessEvent) {
+ return event.data_buffer.source_id;
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts
new file mode 100644
index 0000000000000..67acdbd253f65
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { ProcessEvent } from '../types';
+type DeepPartial = { [K in keyof T]?: DeepPartial };
+
+/**
+ * Creates a mock process event given the 'parts' argument, which can
+ * include all or some process event fields as determined by the ProcessEvent type.
+ * The only field that must be provided is the event's 'node_id' field.
+ * The other fields are populated by the function unless provided in 'parts'
+ */
+export function mockProcessEvent(
+ parts: {
+ data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] };
+ } & DeepPartial
+): ProcessEvent {
+ const { data_buffer: dataBuffer } = parts;
+ return {
+ event_timestamp: 1,
+ event_type: 1,
+ machine_id: '',
+ ...parts,
+ data_buffer: {
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ process_name: '',
+ process_path: '',
+ ...dataBuffer,
+ },
+ };
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts
new file mode 100644
index 0000000000000..b21b79e84f741
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 { Vector2 } from '../../types';
+
+interface UserScaled {
+ readonly type: 'userScaled';
+ /**
+ * A vector who's `x` and `y` component will be the new scaling factors for the projection.
+ */
+ readonly payload: Vector2;
+}
+
+interface UserZoomed {
+ readonly type: 'userZoomed';
+ /**
+ * A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`, pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels.
+ */
+ payload: number;
+}
+
+interface UserSetRasterSize {
+ readonly type: 'userSetRasterSize';
+ /**
+ * The dimensions of the Resolver component in pixels. The Resolver component should not be scrollable itself.
+ */
+ readonly payload: Vector2;
+}
+
+/**
+ * This is currently only used in tests. The 'back to center' button will use this action, and more tests around its behavior will need to be added.
+ */
+interface UserSetPositionOfCamera {
+ readonly type: 'userSetPositionOfCamera';
+ /**
+ * The world transform of the camera
+ */
+ readonly payload: Vector2;
+}
+
+interface UserStartedPanning {
+ readonly type: 'userStartedPanning';
+ /**
+ * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen)
+ * relative to the Resolver component.
+ * Represents a starting position during panning for a pointing device.
+ */
+ readonly payload: Vector2;
+}
+
+interface UserStoppedPanning {
+ readonly type: 'userStoppedPanning';
+}
+
+interface UserMovedPointer {
+ readonly type: 'userMovedPointer';
+ /**
+ * A vector in screen coordinates relative to the Resolver component.
+ * The payload should be contain clientX and clientY minus the client position of the Resolver component.
+ */
+ readonly payload: Vector2;
+}
+
+export type CameraAction =
+ | UserScaled
+ | UserSetRasterSize
+ | UserSetPositionOfCamera
+ | UserStartedPanning
+ | UserStoppedPanning
+ | UserZoomed
+ | UserMovedPointer;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts
new file mode 100644
index 0000000000000..f5e7d73aa10d5
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * The 'camera' in Resolver models the visible area of the Resolver map. Resolver users
+ * can click and drag the Resolver element to pan the map. They can pinch the trackpad
+ * or use Ctrl-MouseWheel to _zoom_, which changes the scale.
+ *
+ * The camera considers the size of Resolver in pixels, and it considers any panning that
+ * has been done, and it considers the scale. With this information it maps points on
+ * the screen to points in Resolver's 'world'. Entities that are mapped in Resolver
+ * are positioned in these unitless 'world' coordinates, and where they show up (if at all)
+ * on the screen is determined by the camera.
+ *
+ * In the future, we may cull entities before rendering them to the DOM. Entities that
+ * would not be in the camera's viewport would be ignored.
+ */
+export { cameraReducer } from './reducer';
+export { CameraAction } from './action';
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts
new file mode 100644
index 0000000000000..3d555b63d8392
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 { Store, createStore } from 'redux';
+import { CameraAction } from './action';
+import { CameraState } from '../../types';
+import { cameraReducer } from './reducer';
+import { inverseProjectionMatrix } from './selectors';
+import { applyMatrix3 } from '../../lib/vector2';
+
+describe('inverseProjectionMatrix', () => {
+ let store: Store;
+ let compare: (worldPosition: [number, number], expectedRasterPosition: [number, number]) => void;
+ beforeEach(() => {
+ store = createStore(cameraReducer, undefined);
+ compare = (rasterPosition: [number, number], expectedWorldPosition: [number, number]) => {
+ const [worldX, worldY] = applyMatrix3(
+ rasterPosition,
+ inverseProjectionMatrix(store.getState())
+ );
+ expect(worldX).toBeCloseTo(expectedWorldPosition[0]);
+ expect(worldY).toBeCloseTo(expectedWorldPosition[1]);
+ };
+ });
+ describe('when the raster size is 300 x 200 pixels', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] };
+ store.dispatch(action);
+ });
+ it('should convert 150,100 in raster space to 0,0 (center) in world space', () => {
+ compare([150, 100], [0, 0]);
+ });
+ it('should convert 150,0 in raster space to 0,100 (top) in world space', () => {
+ compare([150, 0], [0, 100]);
+ });
+ it('should convert 300,0 in raster space to 150,100 (top right) in world space', () => {
+ compare([300, 0], [150, 100]);
+ });
+ it('should convert 300,100 in raster space to 150,0 (right) in world space', () => {
+ compare([300, 100], [150, 0]);
+ });
+ it('should convert 300,200 in raster space to 150,-100 (right bottom) in world space', () => {
+ compare([300, 200], [150, -100]);
+ });
+ it('should convert 150,200 in raster space to 0,-100 (bottom) in world space', () => {
+ compare([150, 200], [0, -100]);
+ });
+ it('should convert 0,200 in raster space to -150,-100 (bottom left) in world space', () => {
+ compare([0, 200], [-150, -100]);
+ });
+ it('should convert 0,100 in raster space to -150,0 (left) in world space', () => {
+ compare([0, 100], [-150, 0]);
+ });
+ it('should convert 0,0 in raster space to -150,100 (top left) in world space', () => {
+ compare([0, 0], [-150, 100]);
+ });
+ describe('when the user has zoomed to 0.5', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userScaled', payload: [0.5, 0.5] };
+ store.dispatch(action);
+ });
+ it('should convert 150, 100 (center) to 0, 0 (center) in world space', () => {
+ compare([150, 100], [0, 0]);
+ });
+ });
+ describe('when the user has panned to the right and up by 50', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] };
+ store.dispatch(action);
+ });
+ it('should convert 100,150 in raster space to 0,0 (center) in world space', () => {
+ compare([100, 150], [0, 0]);
+ });
+ it('should convert 150,100 (center) in raster space to 50,50 (right and up a bit) in world space', () => {
+ compare([150, 100], [50, 50]);
+ });
+ it('should convert 160,210 (center) in raster space to 60,-60 (right and down a bit) in world space', () => {
+ compare([160, 210], [60, -60]);
+ });
+ });
+ describe('when the user has panned to the right by 350 and up by 250', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-350, -250] };
+ store.dispatch(action);
+ });
+ describe('when the user has scaled to 2', () => {
+ // the viewport will only cover half, or 150x100 instead of 300x200
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userScaled', payload: [2, 2] };
+ store.dispatch(action);
+ });
+ // we expect the viewport to be
+ // minX = 350 - (150/2) = 275
+ // maxX = 350 + (150/2) = 425
+ // minY = 250 - (100/2) = 200
+ // maxY = 250 + (100/2) = 300
+ it('should convert 150,100 (center) in raster space to 350,250 in world space', () => {
+ compare([150, 100], [350, 250]);
+ });
+ it('should convert 0,0 (top left) in raster space to 275,300 in world space', () => {
+ compare([0, 0], [275, 300]);
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts
new file mode 100644
index 0000000000000..c09320182e3be
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { Store, createStore } from 'redux';
+import { cameraReducer } from './reducer';
+import { CameraState, Vector2 } from '../../types';
+import { CameraAction } from './action';
+import { translation } from './selectors';
+
+describe('panning interaction', () => {
+ let store: Store;
+ let translationShouldBeCloseTo: (expectedTranslation: Vector2) => void;
+
+ beforeEach(() => {
+ store = createStore(cameraReducer, undefined);
+ translationShouldBeCloseTo = expectedTranslation => {
+ const actualTranslation = translation(store.getState());
+ expect(expectedTranslation[0]).toBeCloseTo(actualTranslation[0]);
+ expect(expectedTranslation[1]).toBeCloseTo(actualTranslation[1]);
+ };
+ });
+ describe('when the raster size is 300 x 200 pixels', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] };
+ store.dispatch(action);
+ });
+ it('should have a translation of 0,0', () => {
+ translationShouldBeCloseTo([0, 0]);
+ });
+ describe('when the user has started panning', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userStartedPanning', payload: [100, 100] };
+ store.dispatch(action);
+ });
+ it('should have a translation of 0,0', () => {
+ translationShouldBeCloseTo([0, 0]);
+ });
+ describe('when the user continues to pan 50px up and to the right', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userMovedPointer', payload: [150, 50] };
+ store.dispatch(action);
+ });
+ it('should have a translation of 50,50', () => {
+ translationShouldBeCloseTo([50, 50]);
+ });
+ describe('when the user then stops panning', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userStoppedPanning' };
+ store.dispatch(action);
+ });
+ it('should have a translation of 50,50', () => {
+ translationShouldBeCloseTo([50, 50]);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts
new file mode 100644
index 0000000000000..025c436a957e8
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts
@@ -0,0 +1,112 @@
+/*
+ * 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 { Store, createStore } from 'redux';
+import { CameraAction } from './action';
+import { CameraState } from '../../types';
+import { cameraReducer } from './reducer';
+import { projectionMatrix } from './selectors';
+import { applyMatrix3 } from '../../lib/vector2';
+
+describe('projectionMatrix', () => {
+ let store: Store;
+ let compare: (worldPosition: [number, number], expectedRasterPosition: [number, number]) => void;
+ beforeEach(() => {
+ store = createStore(cameraReducer, undefined);
+ compare = (worldPosition: [number, number], expectedRasterPosition: [number, number]) => {
+ const [rasterX, rasterY] = applyMatrix3(worldPosition, projectionMatrix(store.getState()));
+ expect(rasterX).toBeCloseTo(expectedRasterPosition[0]);
+ expect(rasterY).toBeCloseTo(expectedRasterPosition[1]);
+ };
+ });
+ describe('when the raster size is 300 x 200 pixels', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] };
+ store.dispatch(action);
+ });
+ it('should convert 0,0 (center) in world space to 150,100 in raster space', () => {
+ compare([0, 0], [150, 100]);
+ });
+ it('should convert 0,100 (top) in world space to 150,0 in raster space', () => {
+ compare([0, 100], [150, 0]);
+ });
+ it('should convert 150,100 (top right) in world space to 300,0 in raster space', () => {
+ compare([150, 100], [300, 0]);
+ });
+ it('should convert 150,0 (right) in world space to 300,100 in raster space', () => {
+ compare([150, 0], [300, 100]);
+ });
+ it('should convert 150,-100 (right bottom) in world space to 300,200 in raster space', () => {
+ compare([150, -100], [300, 200]);
+ });
+ it('should convert 0,-100 (bottom) in world space to 150,200 in raster space', () => {
+ compare([0, -100], [150, 200]);
+ });
+ it('should convert -150,-100 (bottom left) in world space to 0,200 in raster space', () => {
+ compare([-150, -100], [0, 200]);
+ });
+ it('should convert -150,0 (left) in world space to 0,100 in raster space', () => {
+ compare([-150, 0], [0, 100]);
+ });
+ it('should convert -150,100 (top left) in world space to 0,0 in raster space', () => {
+ compare([-150, 100], [0, 0]);
+ });
+ describe('when the user has zoomed to 0.5', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userScaled', payload: [0.5, 0.5] };
+ store.dispatch(action);
+ });
+ it('should convert 0, 0 (center) in world space to 150, 100 (center)', () => {
+ compare([0, 0], [150, 100]);
+ });
+ });
+ describe('when the user has panned to the right and up by 50', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] };
+ store.dispatch(action);
+ });
+ it('should convert 0,0 (center) in world space to 100,150 in raster space', () => {
+ compare([0, 0], [100, 150]);
+ });
+ it('should convert 50,50 (right and up a bit) in world space to 150,100 (center) in raster space', () => {
+ compare([50, 50], [150, 100]);
+ });
+ it('should convert 60,-60 (right and down a bit) in world space to 160,210 (center) in raster space', () => {
+ compare([60, -60], [160, 210]);
+ });
+ });
+ describe('when the user has panned to the right by 350 and up by 250', () => {
+ beforeEach(() => {
+ const action: CameraAction = {
+ type: 'userSetPositionOfCamera',
+ payload: [-350, -250],
+ };
+ store.dispatch(action);
+ });
+ it('should convert 350,250 in world space to 150,100 (center) in raster space', () => {
+ compare([350, 250], [150, 100]);
+ });
+ describe('when the user has scaled to 2', () => {
+ // the viewport will only cover half, or 150x100 instead of 300x200
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userScaled', payload: [2, 2] };
+ store.dispatch(action);
+ });
+ // we expect the viewport to be
+ // minX = 350 - (150/2) = 275
+ // maxX = 350 + (150/2) = 425
+ // minY = 250 - (100/2) = 200
+ // maxY = 250 + (100/2) = 300
+ it('should convert 350,250 in world space to 150,100 (center) in raster space', () => {
+ compare([350, 250], [150, 100]);
+ });
+ it('should convert 275,300 in world space to 0,0 (top left) in raster space', () => {
+ compare([275, 300], [0, 0]);
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts
new file mode 100644
index 0000000000000..457d3904804f2
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts
@@ -0,0 +1,159 @@
+/*
+ * 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 { Reducer } from 'redux';
+import { applyMatrix3, subtract } from '../../lib/vector2';
+import { userIsPanning, translation, projectionMatrix, inverseProjectionMatrix } from './selectors';
+import { clamp } from '../../lib/math';
+
+import { CameraState, ResolverAction } from '../../types';
+
+function initialState(): CameraState {
+ return {
+ scaling: [1, 1] as const,
+ rasterSize: [0, 0] as const,
+ translationNotCountingCurrentPanning: [0, 0] as const,
+ latestFocusedWorldCoordinates: null,
+ };
+}
+
+/**
+ * The minimum allowed value for the camera scale. This is the least scale that we will ever render something at.
+ */
+const minimumScale = 0.1;
+
+/**
+ * The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at.
+ */
+const maximumScale = 6;
+
+export const cameraReducer: Reducer = (
+ state = initialState(),
+ action
+) => {
+ if (action.type === 'userScaled') {
+ /**
+ * Handle the scale being explicitly set, for example by a 'reset zoom' feature, or by a range slider with exact scale values
+ */
+ const [deltaX, deltaY] = action.payload;
+ return {
+ ...state,
+ scaling: [
+ clamp(deltaX, minimumScale, maximumScale),
+ clamp(deltaY, minimumScale, maximumScale),
+ ],
+ };
+ } else if (action.type === 'userZoomed') {
+ /**
+ * When the user zooms we change the scale. Limit the change in scale so that we aren't liable for supporting crazy values (e.g. infinity or negative scale.)
+ */
+ const newScaleX = clamp(state.scaling[0] + action.payload, minimumScale, maximumScale);
+ const newScaleY = clamp(state.scaling[1] + action.payload, minimumScale, maximumScale);
+
+ const stateWithNewScaling: CameraState = {
+ ...state,
+ scaling: [newScaleX, newScaleY],
+ };
+
+ /**
+ * Zooming fundamentally just changes the scale, but that would always zoom in (or out) around the center of the map. The user might be interested in
+ * something else, like a node. If the user has moved their pointer on to the map, we can keep the pointer over the same point in the map by adjusting the
+ * panning when we zoom.
+ *
+ * You can see this in action by moving your pointer over a node that isn't directly in the center of the map and then changing the zoom level. Do it by
+ * using CTRL and the mousewheel, or by pinching the trackpad on a Mac. The node will stay under your mouse cursor and other things in the map will get
+ * nearer or further from the mouse cursor. This lets you keep your context when changing zoom levels.
+ */
+ if (state.latestFocusedWorldCoordinates !== null) {
+ const rasterOfLastFocusedWorldCoordinates = applyMatrix3(
+ state.latestFocusedWorldCoordinates,
+ projectionMatrix(state)
+ );
+ const matrix = inverseProjectionMatrix(stateWithNewScaling);
+ const worldCoordinateThereNow = applyMatrix3(rasterOfLastFocusedWorldCoordinates, matrix);
+ const delta = subtract(worldCoordinateThereNow, state.latestFocusedWorldCoordinates);
+
+ return {
+ ...stateWithNewScaling,
+ translationNotCountingCurrentPanning: [
+ stateWithNewScaling.translationNotCountingCurrentPanning[0] + delta[0],
+ stateWithNewScaling.translationNotCountingCurrentPanning[1] + delta[1],
+ ],
+ };
+ } else {
+ return stateWithNewScaling;
+ }
+ } else if (action.type === 'userSetPositionOfCamera') {
+ /**
+ * Handle the case where the position of the camera is explicitly set, for example by a 'back to center' feature.
+ */
+ return {
+ ...state,
+ translationNotCountingCurrentPanning: action.payload,
+ };
+ } else if (action.type === 'userStartedPanning') {
+ /**
+ * When the user begins panning with a mousedown event we mark the starting position for later comparisons.
+ */
+ return {
+ ...state,
+ panning: {
+ origin: action.payload,
+ currentOffset: action.payload,
+ },
+ };
+ } else if (action.type === 'userStoppedPanning') {
+ /**
+ * When the user stops panning (by letting up on the mouse) we calculate the new translation of the camera.
+ */
+ if (userIsPanning(state)) {
+ return {
+ ...state,
+ translationNotCountingCurrentPanning: translation(state),
+ panning: undefined,
+ };
+ } else {
+ return state;
+ }
+ } else if (action.type === 'userSetRasterSize') {
+ /**
+ * Handle resizes of the Resolver component. We need to know the size in order to convert between screen
+ * and world coordinates.
+ */
+ return {
+ ...state,
+ rasterSize: action.payload,
+ };
+ } else if (action.type === 'userMovedPointer') {
+ const stateWithUpdatedPanning = {
+ ...state,
+ /**
+ * If the user is panning, adjust the panning offset
+ */
+ panning: userIsPanning(state)
+ ? {
+ origin: state.panning ? state.panning.origin : action.payload,
+ currentOffset: action.payload,
+ }
+ : state.panning,
+ };
+ return {
+ ...stateWithUpdatedPanning,
+ /**
+ * keep track of the last world coordinates the user moved over.
+ * When the scale of the projection matrix changes, we adjust the camera's world transform in order
+ * to keep the same point under the pointer.
+ * In order to do this, we need to know the position of the mouse when changing the scale.
+ */
+ latestFocusedWorldCoordinates: applyMatrix3(
+ action.payload,
+ inverseProjectionMatrix(stateWithUpdatedPanning)
+ ),
+ };
+ } else {
+ return state;
+ }
+};
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts
new file mode 100644
index 0000000000000..e0d2062bfc870
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts
@@ -0,0 +1,183 @@
+/*
+ * 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 { Vector2, CameraState, AABB, Matrix3 } from '../../types';
+import { subtract, divide, add, applyMatrix3 } from '../../lib/vector2';
+import { multiply, add as addMatrix } from '../../lib/matrix3';
+import {
+ inverseOrthographicProjection,
+ scalingTransformation,
+ orthographicProjection,
+ translationTransformation,
+} from '../../lib/transformation';
+
+interface ClippingPlanes {
+ renderWidth: number;
+ renderHeight: number;
+ clippingPlaneRight: number;
+ clippingPlaneTop: number;
+ clippingPlaneLeft: number;
+ clippingPlaneBottom: number;
+}
+
+/**
+ * The viewable area in the Resolver map, in world coordinates.
+ */
+export function viewableBoundingBox(state: CameraState): AABB {
+ const { renderWidth, renderHeight } = clippingPlanes(state);
+ const matrix = inverseProjectionMatrix(state);
+ const bottomLeftCorner: Vector2 = [0, renderHeight];
+ const topRightCorner: Vector2 = [renderWidth, 0];
+ return {
+ minimum: applyMatrix3(bottomLeftCorner, matrix),
+ maximum: applyMatrix3(topRightCorner, matrix),
+ };
+}
+
+/**
+ * The 2D clipping planes used for the orthographic projection. See https://en.wikipedia.org/wiki/Orthographic_projection
+ */
+function clippingPlanes(state: CameraState): ClippingPlanes {
+ const renderWidth = state.rasterSize[0];
+ const renderHeight = state.rasterSize[1];
+ const clippingPlaneRight = renderWidth / 2 / state.scaling[0];
+ const clippingPlaneTop = renderHeight / 2 / state.scaling[1];
+
+ return {
+ renderWidth,
+ renderHeight,
+ clippingPlaneRight,
+ clippingPlaneTop,
+ clippingPlaneLeft: -clippingPlaneRight,
+ clippingPlaneBottom: -clippingPlaneTop,
+ };
+}
+
+/**
+ * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates.
+ * See https://en.wikipedia.org/wiki/Orthographic_projection
+ */
+export const projectionMatrix: (state: CameraState) => Matrix3 = state => {
+ const {
+ renderWidth,
+ renderHeight,
+ clippingPlaneRight,
+ clippingPlaneTop,
+ clippingPlaneLeft,
+ clippingPlaneBottom,
+ } = clippingPlanes(state);
+
+ return multiply(
+ // 5. convert from 0->2 to 0->rasterWidth (or height)
+ scalingTransformation([renderWidth / 2, renderHeight / 2]),
+ addMatrix(
+ // 4. add one to change range from -1->1 to 0->2
+ [0, 0, 1, 0, 0, 1, 0, 0, 0],
+ multiply(
+ // 3. invert y since CSS has inverted y
+ scalingTransformation([1, -1]),
+ multiply(
+ // 2. scale to clipping plane
+ orthographicProjection(
+ clippingPlaneTop,
+ clippingPlaneRight,
+ clippingPlaneBottom,
+ clippingPlaneLeft
+ ),
+ // 1. adjust for camera
+ translationTransformation(translation(state))
+ )
+ )
+ )
+ );
+};
+
+/**
+ * The camera has a translation value (not counting any current panning.) This is initialized to (0, 0) and
+ * updating any time panning ends.
+ *
+ * When the user is panning, we keep the initial position of the pointer and the current position of the
+ * pointer. The difference between these values equals the panning vector.
+ *
+ * When the user is panning, the translation of the camera is found by adding the panning vector to the
+ * translationNotCountingCurrentPanning.
+ *
+ * We could update the translation as the user moved the mouse but floating point drift (round-off error) could occur.
+ */
+export function translation(state: CameraState): Vector2 {
+ if (state.panning) {
+ return add(
+ state.translationNotCountingCurrentPanning,
+ divide(subtract(state.panning.currentOffset, state.panning.origin), [
+ state.scaling[0],
+ // Invert `y` since the `.panning` vectors are in screen coordinates and therefore have backwards `y`
+ -state.scaling[1],
+ ])
+ );
+ } else {
+ return state.translationNotCountingCurrentPanning;
+ }
+}
+
+/**
+ * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates.
+ * See https://en.wikipedia.org/wiki/Orthographic_projection
+ */
+export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => {
+ const {
+ renderWidth,
+ renderHeight,
+ clippingPlaneRight,
+ clippingPlaneTop,
+ clippingPlaneLeft,
+ clippingPlaneBottom,
+ } = clippingPlanes(state);
+
+ /* prettier-ignore */
+ const screenToNDC = [
+ 2 / renderWidth, 0, -1,
+ 0, 2 / renderHeight, -1,
+ 0, 0, 0
+ ] as const
+
+ const [translationX, translationY] = translation(state);
+
+ return addMatrix(
+ // 4. Translate for the 'camera'
+ // prettier-ignore
+ [
+ 0, 0, -translationX,
+ 0, 0, -translationY,
+ 0, 0, 0
+ ] as const,
+ multiply(
+ // 3. make values in range of clipping planes
+ inverseOrthographicProjection(
+ clippingPlaneTop,
+ clippingPlaneRight,
+ clippingPlaneBottom,
+ clippingPlaneLeft
+ ),
+ multiply(
+ // 2 Invert Y since CSS has inverted y
+ scalingTransformation([1, -1]),
+ // 1. convert screen coordinates to NDC
+ // e.g. for x-axis, divide by renderWidth then multiply by 2 and subtract by one so the value is in range of -1->1
+ screenToNDC
+ )
+ )
+ );
+};
+
+/**
+ * The scale by which world values are scaled when rendered.
+ */
+export const scale = (state: CameraState): Vector2 => state.scaling;
+
+/**
+ * Whether or not the user is current panning the map.
+ */
+export const userIsPanning = (state: CameraState): boolean => state.panning !== undefined;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts
new file mode 100644
index 0000000000000..fd446c42116a4
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/test_helpers.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { Store } from 'redux';
+import { CameraAction } from './action';
+import { CameraState, Vector2 } from '../../types';
+
+type CameraStore = Store;
+
+/**
+ * Dispatches a 'userScaled' action.
+ */
+export function userScaled(store: CameraStore, scalingValue: [number, number]): void {
+ const action: CameraAction = { type: 'userScaled', payload: scalingValue };
+ store.dispatch(action);
+}
+
+/**
+ * Used to assert that two Vector2s are close to each other (accounting for round-off errors.)
+ */
+export function expectVectorsToBeClose(first: Vector2, second: Vector2): void {
+ expect(first[0]).toBeCloseTo(second[0]);
+ expect(first[1]).toBeCloseTo(second[1]);
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts
new file mode 100644
index 0000000000000..a04ca8376c9b1
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts
@@ -0,0 +1,142 @@
+/*
+ * 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 { CameraAction } from './action';
+import { cameraReducer } from './reducer';
+import { createStore, Store } from 'redux';
+import { CameraState, AABB } from '../../types';
+import { viewableBoundingBox, inverseProjectionMatrix } from './selectors';
+import { userScaled, expectVectorsToBeClose } from './test_helpers';
+import { applyMatrix3 } from '../../lib/vector2';
+
+describe('zooming', () => {
+ let store: Store;
+
+ const cameraShouldBeBoundBy = (expectedViewableBoundingBox: AABB): [string, () => void] => {
+ return [
+ `the camera view should be bound by an AABB with a minimum point of ${expectedViewableBoundingBox.minimum} and a maximum point of ${expectedViewableBoundingBox.maximum}`,
+ () => {
+ const actual = viewableBoundingBox(store.getState());
+ expect(actual.minimum[0]).toBeCloseTo(expectedViewableBoundingBox.minimum[0]);
+ expect(actual.minimum[1]).toBeCloseTo(expectedViewableBoundingBox.minimum[1]);
+ expect(actual.maximum[0]).toBeCloseTo(expectedViewableBoundingBox.maximum[0]);
+ expect(actual.maximum[1]).toBeCloseTo(expectedViewableBoundingBox.maximum[1]);
+ },
+ ];
+ };
+ beforeEach(() => {
+ store = createStore(cameraReducer, undefined);
+ });
+ describe('when the raster size is 300 x 200 pixels', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] };
+ store.dispatch(action);
+ });
+ it(
+ ...cameraShouldBeBoundBy({
+ minimum: [-150, -100],
+ maximum: [150, 100],
+ })
+ );
+ describe('when the user has scaled in to 2x', () => {
+ beforeEach(() => {
+ userScaled(store, [2, 2]);
+ });
+ it(
+ ...cameraShouldBeBoundBy({
+ minimum: [-75, -50],
+ maximum: [75, 50],
+ })
+ );
+ });
+ describe('when the user zooms in by 1 zoom unit', () => {
+ beforeEach(() => {
+ const action: CameraAction = {
+ type: 'userZoomed',
+ payload: 1,
+ };
+ store.dispatch(action);
+ });
+ it(
+ ...cameraShouldBeBoundBy({
+ minimum: [-75, -50],
+ maximum: [75, 50],
+ })
+ );
+ });
+ it('the raster position 200, 50 should map to the world position 50, 50', () => {
+ expectVectorsToBeClose(applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), [
+ 50,
+ 50,
+ ]);
+ });
+ describe('when the user has moved their mouse to the raster position 200, 50', () => {
+ beforeEach(() => {
+ const action: CameraAction = {
+ type: 'userMovedPointer',
+ payload: [200, 50],
+ };
+ store.dispatch(action);
+ });
+ it('should have focused the world position 50, 50', () => {
+ const coords = store.getState().latestFocusedWorldCoordinates;
+ if (coords !== null) {
+ expectVectorsToBeClose(coords, [50, 50]);
+ } else {
+ throw new Error('coords should not have been null');
+ }
+ });
+ describe('when the user zooms in by 0.5 zoom units', () => {
+ beforeEach(() => {
+ const action: CameraAction = {
+ type: 'userZoomed',
+ payload: 0.5,
+ };
+ store.dispatch(action);
+ });
+ it('the raster position 200, 50 should map to the world position 50, 50', () => {
+ expectVectorsToBeClose(
+ applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())),
+ [50, 50]
+ );
+ });
+ });
+ });
+ describe('when the user pans right by 100 pixels', () => {
+ beforeEach(() => {
+ const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-100, 0] };
+ store.dispatch(action);
+ });
+ it(
+ ...cameraShouldBeBoundBy({
+ minimum: [-50, -100],
+ maximum: [250, 100],
+ })
+ );
+ it('should be centered on 100, 0', () => {
+ const worldCenterPoint = applyMatrix3(
+ [150, 100],
+ inverseProjectionMatrix(store.getState())
+ );
+ expect(worldCenterPoint[0]).toBeCloseTo(100);
+ expect(worldCenterPoint[1]).toBeCloseTo(0);
+ });
+ describe('when the user scales to 2x', () => {
+ beforeEach(() => {
+ userScaled(store, [2, 2]);
+ });
+ it('should be centered on 100, 0', () => {
+ const worldCenterPoint = applyMatrix3(
+ [150, 100],
+ inverseProjectionMatrix(store.getState())
+ );
+ expect(worldCenterPoint[0]).toBeCloseTo(100);
+ expect(worldCenterPoint[1]).toBeCloseTo(0);
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap
new file mode 100644
index 0000000000000..261ca7e0a7bba
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap
@@ -0,0 +1,347 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`resolver graph layout when rendering no nodes renders right 1`] = `
+Object {
+ "edgeLineSegments": Array [],
+ "processNodePositions": Map {},
+}
+`;
+
+exports[`resolver graph layout when rendering one node renders right 1`] = `
+Object {
+ "edgeLineSegments": Array [],
+ "processNodePositions": Map {
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 0,
+ "process_name": "",
+ "process_path": "",
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 0,
+ -0.8164965809277259,
+ ],
+ },
+}
+`;
+
+exports[`resolver graph layout when rendering two forks, and one fork has an extra long tine renders right 1`] = `
+Object {
+ "edgeLineSegments": Array [
+ Array [
+ Array [
+ 0,
+ -0.8164965809277259,
+ ],
+ Array [
+ 35.35533905932738,
+ -21.228911104120876,
+ ],
+ ],
+ Array [
+ Array [
+ -35.35533905932738,
+ -62.053740150507174,
+ ],
+ Array [
+ 106.06601717798213,
+ 19.595917942265423,
+ ],
+ ],
+ Array [
+ Array [
+ -35.35533905932738,
+ -62.053740150507174,
+ ],
+ Array [
+ 0,
+ -82.46615467370032,
+ ],
+ ],
+ Array [
+ Array [
+ 106.06601717798213,
+ 19.595917942265423,
+ ],
+ Array [
+ 141.4213562373095,
+ -0.8164965809277259,
+ ],
+ ],
+ Array [
+ Array [
+ 0,
+ -82.46615467370032,
+ ],
+ Array [
+ 35.35533905932738,
+ -102.87856919689347,
+ ],
+ ],
+ Array [
+ Array [
+ 0,
+ -123.2909837200866,
+ ],
+ Array [
+ 70.71067811865476,
+ -82.46615467370032,
+ ],
+ ],
+ Array [
+ Array [
+ 0,
+ -123.2909837200866,
+ ],
+ Array [
+ 35.35533905932738,
+ -143.70339824327976,
+ ],
+ ],
+ Array [
+ Array [
+ 70.71067811865476,
+ -82.46615467370032,
+ ],
+ Array [
+ 106.06601717798213,
+ -102.87856919689347,
+ ],
+ ],
+ Array [
+ Array [
+ 141.4213562373095,
+ -0.8164965809277259,
+ ],
+ Array [
+ 176.7766952966369,
+ -21.22891110412087,
+ ],
+ ],
+ Array [
+ Array [
+ 141.4213562373095,
+ -41.64132562731402,
+ ],
+ Array [
+ 212.13203435596427,
+ -0.8164965809277259,
+ ],
+ ],
+ Array [
+ Array [
+ 141.4213562373095,
+ -41.64132562731402,
+ ],
+ Array [
+ 176.7766952966369,
+ -62.053740150507174,
+ ],
+ ],
+ Array [
+ Array [
+ 212.13203435596427,
+ -0.8164965809277259,
+ ],
+ Array [
+ 247.48737341529164,
+ -21.228911104120883,
+ ],
+ ],
+ Array [
+ Array [
+ 247.48737341529164,
+ -21.228911104120883,
+ ],
+ Array [
+ 318.1980515339464,
+ -62.05374015050717,
+ ],
+ ],
+ ],
+ "processNodePositions": Map {
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 0,
+ "process_name": "",
+ "process_path": "",
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 0,
+ -0.8164965809277259,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "already_running",
+ "event_type_full": "process_event",
+ "node_id": 1,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 0,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 0,
+ -82.46615467370032,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 2,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 0,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 141.4213562373095,
+ -0.8164965809277259,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 3,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 1,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 35.35533905932738,
+ -143.70339824327976,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 4,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 1,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 106.06601717798213,
+ -102.87856919689347,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 5,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 2,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 176.7766952966369,
+ -62.053740150507174,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 6,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 2,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 247.48737341529164,
+ -21.228911104120883,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 7,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 6,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 318.1980515339464,
+ -62.05374015050717,
+ ],
+ },
+}
+`;
+
+exports[`resolver graph layout when rendering two nodes, one being the parent of the other renders right 1`] = `
+Object {
+ "edgeLineSegments": Array [
+ Array [
+ Array [
+ 0,
+ -0.8164965809277259,
+ ],
+ Array [
+ 70.71067811865476,
+ -41.641325627314025,
+ ],
+ ],
+ ],
+ "processNodePositions": Map {
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "creation_event",
+ "event_type_full": "process_event",
+ "node_id": 0,
+ "process_name": "",
+ "process_path": "",
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 0,
+ -0.8164965809277259,
+ ],
+ Object {
+ "data_buffer": Object {
+ "event_subtype_full": "already_running",
+ "event_type_full": "process_event",
+ "node_id": 1,
+ "process_name": "",
+ "process_path": "",
+ "source_id": 0,
+ },
+ "event_timestamp": 1,
+ "event_type": 1,
+ "machine_id": "",
+ } => Array [
+ 70.71067811865476,
+ -41.641325627314025,
+ ],
+ },
+}
+`;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts
new file mode 100644
index 0000000000000..900b9bda571da
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { ProcessEvent } from '../../types';
+
+interface ServerReturnedResolverData {
+ readonly type: 'serverReturnedResolverData';
+ readonly payload: {
+ readonly data: {
+ readonly result: {
+ readonly search_results: readonly ProcessEvent[];
+ };
+ };
+ };
+}
+
+export type DataAction = ServerReturnedResolverData;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts
new file mode 100644
index 0000000000000..fac70433f14b2
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts
@@ -0,0 +1,212 @@
+/*
+ * 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 { Store, createStore } from 'redux';
+import { DataAction } from './action';
+import { dataReducer } from './reducer';
+import { DataState, ProcessEvent } from '../../types';
+import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors';
+import { mockProcessEvent } from '../../models/process_event_test_helpers';
+
+describe('resolver graph layout', () => {
+ let processA: ProcessEvent;
+ let processB: ProcessEvent;
+ let processC: ProcessEvent;
+ let processD: ProcessEvent;
+ let processE: ProcessEvent;
+ let processF: ProcessEvent;
+ let processG: ProcessEvent;
+ let processH: ProcessEvent;
+ let processI: ProcessEvent;
+ let store: Store;
+
+ beforeEach(() => {
+ /*
+ * A
+ * ____|____
+ * | |
+ * B C
+ * ___|___ ___|___
+ * | | | |
+ * D E F G
+ * |
+ * H
+ *
+ */
+ processA = mockProcessEvent({
+ data_buffer: {
+ process_name: '',
+ event_type_full: 'process_event',
+ event_subtype_full: 'creation_event',
+ node_id: 0,
+ },
+ });
+ processB = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'already_running',
+ node_id: 1,
+ source_id: 0,
+ },
+ });
+ processC = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'creation_event',
+ node_id: 2,
+ source_id: 0,
+ },
+ });
+ processD = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'creation_event',
+ node_id: 3,
+ source_id: 1,
+ },
+ });
+ processE = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'creation_event',
+ node_id: 4,
+ source_id: 1,
+ },
+ });
+ processF = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'creation_event',
+ node_id: 5,
+ source_id: 2,
+ },
+ });
+ processG = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'creation_event',
+ node_id: 6,
+ source_id: 2,
+ },
+ });
+ processH = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'creation_event',
+ node_id: 7,
+ source_id: 6,
+ },
+ });
+ processI = mockProcessEvent({
+ data_buffer: {
+ event_type_full: 'process_event',
+ event_subtype_full: 'termination_event',
+ node_id: 8,
+ source_id: 0,
+ },
+ });
+ store = createStore(dataReducer, undefined);
+ });
+ describe('when rendering no nodes', () => {
+ beforeEach(() => {
+ const payload = {
+ data: {
+ result: {
+ search_results: [],
+ },
+ },
+ };
+ const action: DataAction = { type: 'serverReturnedResolverData', payload };
+ store.dispatch(action);
+ });
+ it('the graphableProcesses list should only include nothing', () => {
+ const actual = graphableProcesses(store.getState());
+ expect(actual).toEqual([]);
+ });
+ it('renders right', () => {
+ expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot();
+ });
+ });
+ describe('when rendering one node', () => {
+ beforeEach(() => {
+ const payload = {
+ data: {
+ result: {
+ search_results: [processA],
+ },
+ },
+ };
+ const action: DataAction = { type: 'serverReturnedResolverData', payload };
+ store.dispatch(action);
+ });
+ it('the graphableProcesses list should only include nothing', () => {
+ const actual = graphableProcesses(store.getState());
+ expect(actual).toEqual([processA]);
+ });
+ it('renders right', () => {
+ expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot();
+ });
+ });
+ describe('when rendering two nodes, one being the parent of the other', () => {
+ beforeEach(() => {
+ const payload = {
+ data: {
+ result: {
+ search_results: [processA, processB],
+ },
+ },
+ };
+ const action: DataAction = { type: 'serverReturnedResolverData', payload };
+ store.dispatch(action);
+ });
+ it('the graphableProcesses list should only include nothing', () => {
+ const actual = graphableProcesses(store.getState());
+ expect(actual).toEqual([processA, processB]);
+ });
+ it('renders right', () => {
+ expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot();
+ });
+ });
+ describe('when rendering two forks, and one fork has an extra long tine', () => {
+ beforeEach(() => {
+ const payload = {
+ data: {
+ result: {
+ search_results: [
+ processA,
+ processB,
+ processC,
+ processD,
+ processE,
+ processF,
+ processG,
+ processH,
+ processI,
+ ],
+ },
+ },
+ };
+ const action: DataAction = { type: 'serverReturnedResolverData', payload };
+ store.dispatch(action);
+ });
+ it("the graphableProcesses list should only include events with 'processCreated' an 'processRan' eventType", () => {
+ const actual = graphableProcesses(store.getState());
+ expect(actual).toEqual([
+ processA,
+ processB,
+ processC,
+ processD,
+ processE,
+ processF,
+ processG,
+ processH,
+ ]);
+ });
+ it('renders right', () => {
+ expect(processNodePositionsAndEdgeLineSegments(store.getState())).toMatchSnapshot();
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/index.ts
new file mode 100644
index 0000000000000..8db57c5d9681f
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export { dataReducer } from './reducer';
+export { DataAction } from './action';
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts
new file mode 100644
index 0000000000000..848d814808bac
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { Reducer } from 'redux';
+import { DataState, ResolverAction } from '../../types';
+import { sampleData } from './sample';
+
+function initialState(): DataState {
+ return {
+ results: sampleData.data.result.search_results,
+ };
+}
+
+export const dataReducer: Reducer = (state = initialState(), action) => {
+ if (action.type === 'serverReturnedResolverData') {
+ const {
+ data: {
+ result: { search_results },
+ },
+ } = action.payload;
+ return {
+ ...state,
+ results: search_results,
+ };
+ } else {
+ return state;
+ }
+};
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts
new file mode 100644
index 0000000000000..b0ed9f3554c9b
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/sample.ts
@@ -0,0 +1,1608 @@
+/*
+ * 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 { ProcessEvent } from '../../types';
+
+interface ProcessEventSampleData {
+ data: {
+ result: {
+ search_results: ProcessEvent[];
+ };
+ };
+}
+
+const rawData = {
+ data: {
+ code: 200,
+ result: {
+ alert_id: 'a9834bf5-42c1-4039-83be-08c3ad3232b3',
+ bulk_task_id: null,
+ correlation_id: '7022e509-087e-493d-b02c-d88a206cd993',
+ created_at: '2019-09-24T03:17:36Z',
+ endpoint: {
+ ad_distinguished_name:
+ 'CN=ENDPOINT-W-1-07,OU=Desktops,OU=Workstations,OU=Computers_DEMO,DC=demo,DC=endgamelabs,DC=net',
+ ad_hostname: 'demo.endgamelabs.net',
+ display_operating_system: 'Windows 7 (SP1)',
+ hostname: 'ENDPOINT-W-1-07',
+ id: '39153006-0064-424b-99e9-4e21dcc00c2e',
+ ip_address: '172.31.27.17',
+ mac_address: '00:50:56:b1:b7:7b',
+ name: 'ENDPOINT-W-1-07',
+ operating_system: 'Windows 6.1 Service Pack 1',
+ status: 'monitored',
+ updated_at: '2019-09-24T01:48:47.960649+00:00',
+ },
+ event_logging_search_request_count: 3,
+ family: 'collection',
+ investigation_id: null,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ metadata: {
+ chunk_id: 0,
+ correlation_id: '7022e509-087e-493d-b02c-d88a206cd993',
+ final: true,
+ message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735',
+ os_type: 'windows',
+ priority: 50,
+ result: {
+ local_code: 0,
+ local_msg: 'Success',
+ },
+ semantic_version: '3.52.8',
+ task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735',
+ type: 'collection',
+ },
+ origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735',
+ pagination: {
+ backwards: false,
+ eof: false,
+ page_number: 3,
+ page_offset: 31666,
+ params:
+ 'eyJhbGVydF9pZCI6ICJhOTgzNGJmNS00MmMxLTQwMzktODNiZS0wOGMzYWQzMjMyYjMiLCAidGVtcGxhdGVfZmlsZSI6ICJwcm9jZXNzLWNvbnRleHQubHVhIiwgImNyaXRlcmlhIjogeyJwaWQiOiAxODA4LCAidW5pcXVlX3BpZCI6IDE4OTQzfX0=',
+ remaining_events: 0,
+ },
+ pending_event_logging_search_request: false,
+ results_count: 807,
+ search_results: [
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ _descendant_count: 6,
+ command_line: '',
+ depth: -5,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'already_running',
+ event_type_full: 'process_event',
+ integrity_level: 'system',
+ node_id: 1002,
+ opcode: 3,
+ pid: 4,
+ ppid: 0,
+ process_name: '',
+ process_path: '',
+ serial_event_id: 1002,
+ timestamp: 132137632670000000,
+ timestamp_utc: '2019-09-24 01:47:47Z',
+ unique_pid: 1002,
+ unique_ppid: 1001,
+ user_domain: 'NT AUTHORITY',
+ user_name: 'SYSTEM',
+ user_sid: 'S-1-5-18',
+ },
+ event_timestamp: 132137632670000000,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ _descendant_count: 5,
+ command_line: '\\SystemRoot\\System32\\smss.exe',
+ depth: -4,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'already_running',
+ event_type_full: 'process_event',
+ integrity_level: 'system',
+ md5: '1911a3356fa3f77ccc825ccbac038c2a',
+ node_id: 1003,
+ opcode: 3,
+ original_file_name: 'smss.exe',
+ pid: 244,
+ ppid: 4,
+ process_name: 'smss.exe',
+ process_path: 'C:\\Windows\\System32\\smss.exe',
+ serial_event_id: 1003,
+ sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2',
+ sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 1002,
+ timestamp: 132137632670000000,
+ timestamp_utc: '2019-09-24 01:47:47Z',
+ unique_pid: 1003,
+ unique_ppid: 1002,
+ user_domain: 'NT AUTHORITY',
+ user_name: 'SYSTEM',
+ user_sid: 'S-1-5-18',
+ },
+ event_timestamp: 132137632670000000,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ _descendant_count: 4,
+ authentication_id: 999,
+ command_line: '\\SystemRoot\\System32\\smss.exe 00000000 00000048 ',
+ depth: -3,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'system',
+ md5: '1911a3356fa3f77ccc825ccbac038c2a',
+ node_id: 18643,
+ opcode: 1,
+ original_file_name: 'smss.exe',
+ parent_process_name: 'smss.exe',
+ parent_process_path: 'C:\\Windows\\System32\\smss.exe',
+ pid: 2364,
+ ppid: 244,
+ process_name: 'smss.exe',
+ process_path: 'C:\\Windows\\System32\\smss.exe',
+ serial_event_id: 18643,
+ sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2',
+ sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 1003,
+ timestamp: 132137681960227504,
+ timestamp_utc: '2019-09-24 03:09:56Z',
+ unique_pid: 18643,
+ unique_ppid: 1003,
+ user_domain: 'NT AUTHORITY',
+ user_name: 'SYSTEM',
+ user_sid: 'S-1-5-18',
+ },
+ event_timestamp: 132137681960227504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ _descendant_count: 3,
+ authentication_id: 999,
+ command_line: 'winlogon.exe',
+ depth: -2,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'system',
+ md5: '1151b1baa6f350b1db6598e0fea7c457',
+ node_id: 18645,
+ opcode: 1,
+ original_file_name: 'WINLOGON.EXE',
+ parent_process_name: 'smss.exe',
+ parent_process_path: 'C:\\Windows\\System32\\smss.exe',
+ pid: 3108,
+ ppid: 2364,
+ process_name: 'winlogon.exe',
+ process_path: 'C:\\Windows\\System32\\winlogon.exe',
+ serial_event_id: 18645,
+ sha1: '434856b834baf163c5ea4d26434eeae775a507fb',
+ sha256: 'b1506e0a7e826eff0f5252ef5026070c46e2235438403a9a24d73ee69c0b8a49',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18643,
+ timestamp: 132137681961163504,
+ timestamp_utc: '2019-09-24 03:09:56Z',
+ unique_pid: 18645,
+ unique_ppid: 18643,
+ user_domain: 'NT AUTHORITY',
+ user_name: 'SYSTEM',
+ user_sid: 'S-1-5-18',
+ },
+ event_timestamp: 132137681961163504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ depth: -2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: '1911a3356fa3f77ccc825ccbac038c2a',
+ node_id: 18646,
+ opcode: 2,
+ original_file_name: 'smss.exe',
+ parent_process_name: 'smss.exe',
+ parent_process_path: 'C:\\Windows\\System32\\smss.exe',
+ pid: 2364,
+ ppid: 244,
+ process_name: 'smss.exe',
+ process_path: 'C:\\Windows\\System32\\smss.exe',
+ serial_event_id: 18646,
+ sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2',
+ sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18643,
+ timestamp: 132137681961787504,
+ timestamp_utc: '2019-09-24 03:09:56Z',
+ unique_pid: 18643,
+ unique_ppid: 1003,
+ user_domain: 'NT AUTHORITY',
+ user_name: 'SYSTEM',
+ user_sid: 'S-1-5-18',
+ },
+ event_timestamp: 132137681961787504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ _descendant_count: 1,
+ authentication_id: 4904488,
+ command_line: 'C:\\Windows\\system32\\userinit.exe',
+ depth: -1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'bafe84e637bf7388c96ef48d4d3fdd53',
+ node_id: 18833,
+ opcode: 1,
+ original_file_name: 'USERINIT.EXE',
+ parent_process_name: 'winlogon.exe',
+ parent_process_path: 'C:\\Windows\\System32\\winlogon.exe',
+ pid: 3560,
+ ppid: 3108,
+ process_name: 'userinit.exe',
+ process_path: 'C:\\Windows\\System32\\userinit.exe',
+ serial_event_id: 18833,
+ sha1: '47267f943f060e36604d56c8895a6eece063d9a1',
+ sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18645,
+ timestamp: 132137681981287504,
+ timestamp_utc: '2019-09-24 03:09:58Z',
+ unique_pid: 18833,
+ unique_ppid: 18645,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137681981287504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ _descendant_count: 0,
+ authentication_id: 4904488,
+ command_line: 'C:\\Windows\\Explorer.EXE',
+ depth: 0,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'ac4c51eb24aa95b77f705ab159189e24',
+ node_id: 18943,
+ opcode: 1,
+ origin: true,
+ original_file_name: 'EXPLORER.EXE',
+ parent_process_name: 'userinit.exe',
+ parent_process_path: 'C:\\Windows\\System32\\userinit.exe',
+ pid: 1808,
+ ppid: 3560,
+ process_name: 'explorer.exe',
+ process_path: 'C:\\Windows\\explorer.exe',
+ serial_event_id: 18943,
+ sha1: '4583daf9442880204730fb2c8a060430640494b1',
+ sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18833,
+ timestamp: 132137681985655504,
+ timestamp_utc: '2019-09-24 03:09:58Z',
+ unique_pid: 18943,
+ unique_ppid: 18833,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137681985655504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe" -n vmusr',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: '8dc5ad50587b936f7f616738112bfd2a',
+ node_id: 19545,
+ opcode: 1,
+ original_file_name: 'vmtoolsd.exe',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3596,
+ ppid: 1808,
+ process_name: 'vmtoolsd.exe',
+ process_path: 'C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe',
+ serial_event_id: 19545,
+ sha1: '04479ea30943ec471a6a5ca4c0dc74b5ff496e9f',
+ sha256: 'd6d9f041da6f724bf69f48bbee3bf41295a0ed4dca715b1908c5f35bc8034d53',
+ signature_signer: 'VMware, Inc.',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137681999539504,
+ timestamp_utc: '2019-09-24 03:09:59Z',
+ unique_pid: 19545,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137681999539504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ depth: 0,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: 'bafe84e637bf7388c96ef48d4d3fdd53',
+ node_id: 20261,
+ opcode: 2,
+ original_file_name: 'USERINIT.EXE',
+ parent_process_name: 'winlogon.exe',
+ parent_process_path: 'C:\\Windows\\System32\\winlogon.exe',
+ pid: 3560,
+ ppid: 3108,
+ process_name: 'userinit.exe',
+ process_path: 'C:\\Windows\\System32\\userinit.exe',
+ serial_event_id: 20261,
+ sha1: '47267f943f060e36604d56c8895a6eece063d9a1',
+ sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18833,
+ timestamp: 132137682277819504,
+ timestamp_utc: '2019-09-24 03:10:27Z',
+ unique_pid: 18833,
+ unique_ppid: 18645,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137682277819504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Windows\\explorer.exe" ',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'ac4c51eb24aa95b77f705ab159189e24',
+ node_id: 20303,
+ opcode: 1,
+ original_file_name: 'EXPLORER.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3124,
+ ppid: 1808,
+ process_name: 'explorer.exe',
+ process_path: 'C:\\Windows\\explorer.exe',
+ serial_event_id: 20303,
+ sha1: '4583daf9442880204730fb2c8a060430640494b1',
+ sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137682603979504,
+ timestamp_utc: '2019-09-24 03:11:00Z',
+ unique_pid: 20303,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137682603979504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 1,
+ md5: 'ac4c51eb24aa95b77f705ab159189e24',
+ node_id: 20310,
+ opcode: 2,
+ original_file_name: 'EXPLORER.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3124,
+ ppid: 1808,
+ process_name: 'explorer.exe',
+ process_path: 'C:\\Windows\\explorer.exe',
+ serial_event_id: 20310,
+ sha1: '4583daf9442880204730fb2c8a060430640494b1',
+ sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 20303,
+ timestamp: 132137682604229504,
+ timestamp_utc: '2019-09-24 03:11:00Z',
+ unique_pid: 20303,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137682604229504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Windows\\explorer.exe" ',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'ac4c51eb24aa95b77f705ab159189e24',
+ node_id: 20455,
+ opcode: 1,
+ original_file_name: 'EXPLORER.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3084,
+ ppid: 1808,
+ process_name: 'explorer.exe',
+ process_path: 'C:\\Windows\\explorer.exe',
+ serial_event_id: 20455,
+ sha1: '4583daf9442880204730fb2c8a060430640494b1',
+ sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137682773669504,
+ timestamp_utc: '2019-09-24 03:11:17Z',
+ unique_pid: 20455,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137682773669504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 1,
+ md5: 'ac4c51eb24aa95b77f705ab159189e24',
+ node_id: 20462,
+ opcode: 2,
+ original_file_name: 'EXPLORER.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3084,
+ ppid: 1808,
+ process_name: 'explorer.exe',
+ process_path: 'C:\\Windows\\explorer.exe',
+ serial_event_id: 20462,
+ sha1: '4583daf9442880204730fb2c8a060430640494b1',
+ sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 20455,
+ timestamp: 132137682774259504,
+ timestamp_utc: '2019-09-24 03:11:17Z',
+ unique_pid: 20455,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137682774259504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Windows\\System32\\cmd.exe" ',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: '5746bd7e255dd6a8afa06f7c42c1ba41',
+ node_id: 21120,
+ opcode: 1,
+ original_file_name: 'Cmd.Exe',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3280,
+ ppid: 1808,
+ process_name: 'cmd.exe',
+ process_path: 'C:\\Windows\\System32\\cmd.exe',
+ serial_event_id: 21120,
+ sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8',
+ sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137682997939504,
+ timestamp_utc: '2019-09-24 03:11:39Z',
+ unique_pid: 21120,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137682997939504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Windows\\explorer.exe" ',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'ac4c51eb24aa95b77f705ab159189e24',
+ node_id: 21166,
+ opcode: 1,
+ original_file_name: 'EXPLORER.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3548,
+ ppid: 1808,
+ process_name: 'explorer.exe',
+ process_path: 'C:\\Windows\\explorer.exe',
+ serial_event_id: 21166,
+ sha1: '4583daf9442880204730fb2c8a060430640494b1',
+ sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137683166079504,
+ timestamp_utc: '2019-09-24 03:11:56Z',
+ unique_pid: 21166,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683166079504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 1,
+ md5: 'ac4c51eb24aa95b77f705ab159189e24',
+ node_id: 21173,
+ opcode: 2,
+ original_file_name: 'EXPLORER.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3548,
+ ppid: 1808,
+ process_name: 'explorer.exe',
+ process_path: 'C:\\Windows\\explorer.exe',
+ serial_event_id: 21173,
+ sha1: '4583daf9442880204730fb2c8a060430640494b1',
+ sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 21166,
+ timestamp: 132137683166729504,
+ timestamp_utc: '2019-09-24 03:11:56Z',
+ unique_pid: 21166,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683166729504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: '743b91619fbfee3c3e173ba5a17b1290',
+ node_id: 21480,
+ opcode: 1,
+ original_file_name: '',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 4060,
+ ppid: 1808,
+ process_name: 'python.exe',
+ process_path: 'C:\\Python27\\python.exe',
+ serial_event_id: 21480,
+ sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620',
+ sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32',
+ signature_signer: '',
+ signature_status: 'noSignature',
+ source_id: 18943,
+ timestamp: 132137683493349504,
+ timestamp_utc: '2019-09-24 03:12:29Z',
+ unique_pid: 21480,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683493349504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: '743b91619fbfee3c3e173ba5a17b1290',
+ node_id: 21500,
+ opcode: 2,
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 4060,
+ ppid: 1808,
+ process_name: 'python.exe',
+ process_path: 'C:\\Python27\\python.exe',
+ serial_event_id: 21500,
+ sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620',
+ sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32',
+ signature_status: 'noSignature',
+ source_id: 21480,
+ timestamp: 132137683493889504,
+ timestamp_utc: '2019-09-24 03:12:29Z',
+ unique_pid: 21480,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683493889504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ',
+ depth: 2,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: '743b91619fbfee3c3e173ba5a17b1290',
+ node_id: 21539,
+ opcode: 1,
+ original_file_name: '',
+ parent_process_name: 'cmd.exe',
+ parent_process_path: 'C:\\Windows\\System32\\cmd.exe',
+ pid: 2888,
+ ppid: 3280,
+ process_name: 'python.exe',
+ process_path: 'C:\\Python27\\python.exe',
+ serial_event_id: 21539,
+ sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620',
+ sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32',
+ signature_signer: '',
+ signature_status: 'noSignature',
+ source_id: 21120,
+ timestamp: 132137683555889504,
+ timestamp_utc: '2019-09-24 03:12:35Z',
+ unique_pid: 21539,
+ unique_ppid: 21120,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683555889504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 3,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: '743b91619fbfee3c3e173ba5a17b1290',
+ node_id: 21540,
+ opcode: 2,
+ parent_process_name: 'cmd.exe',
+ parent_process_path: 'C:\\Windows\\System32\\cmd.exe',
+ pid: 2888,
+ ppid: 3280,
+ process_name: 'python.exe',
+ process_path: 'C:\\Python27\\python.exe',
+ serial_event_id: 21540,
+ sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620',
+ sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32',
+ signature_status: 'noSignature',
+ source_id: 21539,
+ timestamp: 132137683556159504,
+ timestamp_utc: '2019-09-24 03:12:35Z',
+ unique_pid: 21539,
+ unique_ppid: 21120,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683556159504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ depth: 2,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'c29675ce0750f73225bf05d03080dfb2',
+ node_id: 21634,
+ opcode: 1,
+ original_file_name: '',
+ parent_process_name: 'cmd.exe',
+ parent_process_path: 'C:\\Windows\\System32\\cmd.exe',
+ pid: 3996,
+ ppid: 3280,
+ process_name: 'fakenet.exe',
+ process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ serial_event_id: 21634,
+ sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb',
+ sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303',
+ signature_signer: '',
+ signature_status: 'noSignature',
+ source_id: 21120,
+ timestamp: 132137683921669504,
+ timestamp_utc: '2019-09-24 03:13:12Z',
+ unique_pid: 21634,
+ unique_ppid: 21120,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683921669504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ depth: 3,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'c29675ce0750f73225bf05d03080dfb2',
+ node_id: 21669,
+ opcode: 1,
+ original_file_name: '',
+ parent_process_name: 'fakenet.exe',
+ parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ pid: 184,
+ ppid: 3996,
+ process_name: 'fakenet.exe',
+ process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ serial_event_id: 21669,
+ sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb',
+ sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303',
+ signature_signer: '',
+ signature_status: 'noSignature',
+ source_id: 21634,
+ timestamp: 132137683923819504,
+ timestamp_utc: '2019-09-24 03:13:12Z',
+ unique_pid: 21669,
+ unique_ppid: 21634,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683923819504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 4,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 1,
+ md5: 'c29675ce0750f73225bf05d03080dfb2',
+ node_id: 21679,
+ opcode: 2,
+ parent_process_name: 'fakenet.exe',
+ parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ pid: 184,
+ ppid: 3996,
+ process_name: 'fakenet.exe',
+ process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ serial_event_id: 21679,
+ sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb',
+ sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303',
+ signature_status: 'noSignature',
+ source_id: 21669,
+ timestamp: 132137683931089504,
+ timestamp_utc: '2019-09-24 03:13:13Z',
+ unique_pid: 21669,
+ unique_ppid: 21634,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683931089504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 3,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 1,
+ md5: 'c29675ce0750f73225bf05d03080dfb2',
+ node_id: 21694,
+ opcode: 2,
+ parent_process_name: 'cmd.exe',
+ parent_process_path: 'C:\\Windows\\System32\\cmd.exe',
+ pid: 3996,
+ ppid: 3280,
+ process_name: 'fakenet.exe',
+ process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ serial_event_id: 21694,
+ sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb',
+ sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303',
+ signature_status: 'noSignature',
+ source_id: 21634,
+ timestamp: 132137683931569504,
+ timestamp_utc: '2019-09-24 03:13:13Z',
+ unique_pid: 21634,
+ unique_ppid: 21120,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137683931569504,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line:
+ '"C:\\Windows\\system32\\NOTEPAD.EXE" C:\\tmp\\fakenet1.4.3\\configs\\default.ini',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'f2c7bb8acc97f92e987a2d4087d021b1',
+ node_id: 21769,
+ opcode: 1,
+ original_file_name: 'NOTEPAD.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 2492,
+ ppid: 1808,
+ process_name: 'notepad.exe',
+ process_path: 'C:\\Windows\\System32\\notepad.exe',
+ serial_event_id: 21769,
+ sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29',
+ sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137684112851830,
+ timestamp_utc: '2019-09-24 03:13:31Z',
+ unique_pid: 21769,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137684112851830,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: 'f2c7bb8acc97f92e987a2d4087d021b1',
+ node_id: 21794,
+ opcode: 2,
+ original_file_name: 'NOTEPAD.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 2492,
+ ppid: 1808,
+ process_name: 'notepad.exe',
+ process_path: 'C:\\Windows\\System32\\notepad.exe',
+ serial_event_id: 21794,
+ sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29',
+ sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 21769,
+ timestamp: 132137684131573702,
+ timestamp_utc: '2019-09-24 03:13:33Z',
+ unique_pid: 21769,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137684131573702,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: 'fakenet.exe',
+ depth: 2,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'c29675ce0750f73225bf05d03080dfb2',
+ node_id: 21890,
+ opcode: 1,
+ original_file_name: '',
+ parent_process_name: 'cmd.exe',
+ parent_process_path: 'C:\\Windows\\System32\\cmd.exe',
+ pid: 1060,
+ ppid: 3280,
+ process_name: 'fakenet.exe',
+ process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ serial_event_id: 21890,
+ sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb',
+ sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303',
+ signature_signer: '',
+ signature_status: 'noSignature',
+ source_id: 21120,
+ timestamp: 132137684579848525,
+ timestamp_utc: '2019-09-24 03:14:17Z',
+ unique_pid: 21890,
+ unique_ppid: 21120,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137684579848525,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: 'fakenet.exe',
+ depth: 3,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'c29675ce0750f73225bf05d03080dfb2',
+ node_id: 21924,
+ opcode: 1,
+ original_file_name: '',
+ parent_process_name: 'fakenet.exe',
+ parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ pid: 4024,
+ ppid: 1060,
+ process_name: 'fakenet.exe',
+ process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe',
+ serial_event_id: 21924,
+ sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb',
+ sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303',
+ signature_signer: '',
+ signature_status: 'noSignature',
+ source_id: 21890,
+ timestamp: 132137684580468587,
+ timestamp_utc: '2019-09-24 03:14:18Z',
+ unique_pid: 21924,
+ unique_ppid: 21890,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137684580468587,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Windows\\System32\\cmd.exe" ',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: '5746bd7e255dd6a8afa06f7c42c1ba41',
+ node_id: 22238,
+ opcode: 1,
+ original_file_name: 'Cmd.Exe',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 3328,
+ ppid: 1808,
+ process_name: 'cmd.exe',
+ process_path: 'C:\\Windows\\System32\\cmd.exe',
+ serial_event_id: 22238,
+ sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8',
+ sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137684944024939,
+ timestamp_utc: '2019-09-24 03:14:54Z',
+ unique_pid: 22238,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137684944024939,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ attack_references: [
+ {
+ tactics: ['Privilege Escalation', 'Execution', 'Persistence'],
+ technique_id: 'T1053',
+ technique_name: 'Scheduled Task',
+ },
+ ],
+ authentication_id: 4904488,
+ command_line: 'SCHTASKS /CREATE /SC MINUTE /TN "Windiws" /TR "C:\\tmp\\scheduler.bat"',
+ depth: 2,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc',
+ node_id: 22376,
+ opcode: 1,
+ original_file_name: 'sctasks.exe',
+ parent_process_name: 'cmd.exe',
+ parent_process_path: 'C:\\Windows\\System32\\cmd.exe',
+ pid: 2864,
+ ppid: 3328,
+ process_name: 'schtasks.exe',
+ process_path: 'C:\\Windows\\System32\\schtasks.exe',
+ serial_event_id: 22376,
+ sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92',
+ sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 22238,
+ timestamp: 132137685249385472,
+ timestamp_utc: '2019-09-24 03:15:24Z',
+ unique_pid: 22376,
+ unique_ppid: 22238,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137685249385472,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 3,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc',
+ node_id: 22384,
+ opcode: 2,
+ original_file_name: 'sctasks.exe',
+ parent_process_name: 'cmd.exe',
+ parent_process_path: 'C:\\Windows\\System32\\cmd.exe',
+ pid: 2864,
+ ppid: 3328,
+ process_name: 'schtasks.exe',
+ process_path: 'C:\\Windows\\System32\\schtasks.exe',
+ serial_event_id: 22384,
+ sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92',
+ sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 22376,
+ timestamp: 132137685251515685,
+ timestamp_utc: '2019-09-24 03:15:25Z',
+ unique_pid: 22376,
+ unique_ppid: 22238,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137685251515685,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ authentication_id: 4904488,
+ command_line: '"C:\\Windows\\System32\\NOTEPAD.EXE" C:\\tmp\\scheduler.bat',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'f2c7bb8acc97f92e987a2d4087d021b1',
+ node_id: 22448,
+ opcode: 1,
+ original_file_name: 'NOTEPAD.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 4048,
+ ppid: 1808,
+ process_name: 'notepad.exe',
+ process_path: 'C:\\Windows\\System32\\notepad.exe',
+ serial_event_id: 22448,
+ sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29',
+ sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137685448755407,
+ timestamp_utc: '2019-09-24 03:15:44Z',
+ unique_pid: 22448,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137685448755407,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: 'f2c7bb8acc97f92e987a2d4087d021b1',
+ node_id: 22464,
+ opcode: 2,
+ original_file_name: 'NOTEPAD.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 4048,
+ ppid: 1808,
+ process_name: 'notepad.exe',
+ process_path: 'C:\\Windows\\System32\\notepad.exe',
+ serial_event_id: 22464,
+ sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29',
+ sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 22448,
+ timestamp: 132137685516752206,
+ timestamp_utc: '2019-09-24 03:15:51Z',
+ unique_pid: 22448,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137685516752206,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ attack_references: [
+ {
+ tactics: ['Execution'],
+ technique_id: 'T1085',
+ technique_name: 'Rundll32',
+ },
+ ],
+ authentication_id: 4904488,
+ command_line:
+ '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\XLS_no_email_Upcoming Events February 2018.xls\\cb85072e6ca66a29cb0b73659a0fe5ba2456d9ba0b52e3a4c89e86549bc6e2c7.xls',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'dd81d91ff3b0763c392422865c9ac12e',
+ node_id: 22799,
+ opcode: 1,
+ original_file_name: 'RUNDLL32.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 2864,
+ ppid: 1808,
+ process_name: 'rundll32.exe',
+ process_path: 'C:\\Windows\\System32\\rundll32.exe',
+ serial_event_id: 22799,
+ sha1: '963b55acc8c566876364716d5aafa353995812a8',
+ sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137686572217742,
+ timestamp_utc: '2019-09-24 03:17:37Z',
+ unique_pid: 22799,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137686572217742,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: 'dd81d91ff3b0763c392422865c9ac12e',
+ node_id: 22805,
+ opcode: 2,
+ original_file_name: 'RUNDLL32.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 2864,
+ ppid: 1808,
+ process_name: 'rundll32.exe',
+ process_path: 'C:\\Windows\\System32\\rundll32.exe',
+ serial_event_id: 22805,
+ sha1: '963b55acc8c566876364716d5aafa353995812a8',
+ sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 22799,
+ timestamp: 132137686585839104,
+ timestamp_utc: '2019-09-24 03:17:38Z',
+ unique_pid: 22799,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137686585839104,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ attack_references: [
+ {
+ tactics: ['Execution'],
+ technique_id: 'T1085',
+ technique_name: 'Rundll32',
+ },
+ ],
+ authentication_id: 4904488,
+ command_line:
+ '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\Upcoming Defense events February 2018.eml',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'dd81d91ff3b0763c392422865c9ac12e',
+ node_id: 22933,
+ opcode: 1,
+ original_file_name: 'RUNDLL32.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 1864,
+ ppid: 1808,
+ process_name: 'rundll32.exe',
+ process_path: 'C:\\Windows\\System32\\rundll32.exe',
+ serial_event_id: 22933,
+ sha1: '963b55acc8c566876364716d5aafa353995812a8',
+ sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137686702740793,
+ timestamp_utc: '2019-09-24 03:17:50Z',
+ unique_pid: 22933,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137686702740793,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: 'dd81d91ff3b0763c392422865c9ac12e',
+ node_id: 22945,
+ opcode: 2,
+ original_file_name: 'RUNDLL32.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 1864,
+ ppid: 1808,
+ process_name: 'rundll32.exe',
+ process_path: 'C:\\Windows\\System32\\rundll32.exe',
+ serial_event_id: 22945,
+ sha1: '963b55acc8c566876364716d5aafa353995812a8',
+ sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 22933,
+ timestamp: 132137686718432362,
+ timestamp_utc: '2019-09-24 03:17:51Z',
+ unique_pid: 22933,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137686718432362,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ attack_references: [
+ {
+ tactics: ['Execution'],
+ technique_id: 'T1085',
+ technique_name: 'Rundll32',
+ },
+ ],
+ authentication_id: 4904488,
+ command_line:
+ '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\Users\\Administrator\\AppData\\Roaming\\Microsoft\\Windows\\SendTo\\Mail Recipient.MAPIMail',
+ depth: 1,
+ elevated: true,
+ elevation_type: 'default',
+ event_subtype_full: 'creation_event',
+ event_type_full: 'process_event',
+ integrity_level: 'high',
+ md5: 'dd81d91ff3b0763c392422865c9ac12e',
+ node_id: 27050,
+ opcode: 1,
+ original_file_name: 'RUNDLL32.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 568,
+ ppid: 1808,
+ process_name: 'rundll32.exe',
+ process_path: 'C:\\Windows\\System32\\rundll32.exe',
+ serial_event_id: 27050,
+ sha1: '963b55acc8c566876364716d5aafa353995812a8',
+ sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 18943,
+ timestamp: 132137686926723189,
+ timestamp_utc: '2019-09-24 03:18:12Z',
+ unique_pid: 27050,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137686926723189,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ {
+ collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56',
+ data_buffer: {
+ depth: 2,
+ event_subtype_full: 'termination_event',
+ event_type_full: 'process_event',
+ exit_code: 0,
+ md5: 'dd81d91ff3b0763c392422865c9ac12e',
+ node_id: 27053,
+ opcode: 2,
+ original_file_name: 'RUNDLL32.EXE',
+ parent_process_name: 'explorer.exe',
+ parent_process_path: 'C:\\Windows\\explorer.exe',
+ pid: 568,
+ ppid: 1808,
+ process_name: 'rundll32.exe',
+ process_path: 'C:\\Windows\\System32\\rundll32.exe',
+ serial_event_id: 27053,
+ sha1: '963b55acc8c566876364716d5aafa353995812a8',
+ sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9',
+ signature_signer: 'Microsoft Windows',
+ signature_status: 'trusted',
+ source_id: 27050,
+ timestamp: 132137686939784495,
+ timestamp_utc: '2019-09-24 03:18:13Z',
+ unique_pid: 27050,
+ unique_ppid: 18943,
+ user_domain: 'ENDPOINT-W-1-07',
+ user_name: 'vagrant',
+ user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001',
+ },
+ event_timestamp: 132137686939784495,
+ event_type: 4,
+ machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34',
+ serial_event_id: 0,
+ },
+ ],
+ status: 'success',
+ task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735',
+ total_events_searched: 7730,
+ type: 'eventLoggingSearchResponse',
+ },
+ },
+ metadata: {
+ count: 39,
+ next: null,
+ next_url: null,
+ per_page: '4000',
+ previous_url: null,
+ timestamp: '2019-12-18T19:31:27.565110',
+ },
+};
+
+export const sampleData: ProcessEventSampleData = rawData as ProcessEventSampleData;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts
new file mode 100644
index 0000000000000..745bd125c151d
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts
@@ -0,0 +1,436 @@
+/*
+ * 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 { createSelector } from 'reselect';
+import {
+ DataState,
+ ProcessEvent,
+ IndexedProcessTree,
+ ProcessWidths,
+ ProcessPositions,
+ EdgeLineSegment,
+ ProcessWithWidthMetadata,
+ Matrix3,
+} from '../../types';
+import { Vector2 } from '../../types';
+import { add as vector2Add, applyMatrix3 } from '../../lib/vector2';
+import { isGraphableProcess } from '../../models/process_event';
+import {
+ factory as indexedProcessTreeFactory,
+ children as indexedProcessTreeChildren,
+ parent as indexedProcessTreeParent,
+ size,
+ levelOrder,
+} from '../../models/indexed_process_tree';
+
+const unit = 100;
+const distanceBetweenNodesInUnits = 1;
+
+/**
+ * An isometric projection is a method for representing three dimensional objects in 2 dimensions.
+ * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection.
+ * In our case, we obtain the isometric projection by rotating the objects 45 degrees in the plane of the screen
+ * and arctan(1/sqrt(2)) (~35.3 degrees) through the horizontal axis.
+ *
+ * A rotation by 45 degrees in the plane of the screen is given by:
+ * [ sqrt(2)/2 -sqrt(2)/2 0
+ * sqrt(2)/2 sqrt(2)/2 0
+ * 0 0 1]
+ *
+ * A rotation by arctan(1/sqrt(2)) through the horizantal axis is given by:
+ * [ 1 0 0
+ * 0 sqrt(3)/3 -sqrt(6)/3
+ * 0 sqrt(6)/3 sqrt(3)/3]
+ *
+ * We can multiply both of these matrices to get the final transformation below.
+ */
+/* prettier-ignore */
+const isometricTransformMatrix: Matrix3 = [
+ Math.sqrt(2) / 2, -(Math.sqrt(2) / 2), 0,
+ Math.sqrt(6) / 6, Math.sqrt(6) / 6, -(Math.sqrt(6) / 3),
+ 0, 0, 1,
+]
+
+/**
+ * The distance in pixels (at scale 1) between nodes. Change this to space out nodes more
+ */
+export const distanceBetweenNodes = distanceBetweenNodesInUnits * unit;
+
+export function graphableProcesses(state: DataState) {
+ return state.results.filter(isGraphableProcess);
+}
+
+/**
+ * In laying out the graph, we precalculate the 'width' of each subtree. The 'width' of the subtree is determined by its
+ * descedants and the rule that each process node must be at least 1 unit apart. Enforcing that all nodes are at least
+ * 1 unit apart on the x axis makes it easy to prevent the UI components from overlapping. There will always be space.
+ *
+ * Example widths:
+ *
+ * A and B each have a width of 0
+ *
+ * A
+ * |
+ * B
+ *
+ * A has a width of 1. B and C have a width of 0.
+ * B and C must be 1 unit apart, so the A subtree has a width of 1.
+ *
+ * A
+ * ____|____
+ * | |
+ * B C
+ *
+ *
+ * D, E, F, G, H all have a width of 0.
+ * B has a width of 1 since D->E must be 1 unit apart.
+ * Similarly, C has a width of 1 since F->G must be 1 unit apart.
+ * A has width of 3, since B has a width of 1, and C has a width of 1, and E->F must be at least
+ * 1 unit apart.
+ * A
+ * ____|____
+ * | |
+ * B C
+ * ___|___ ___|___
+ * | | | |
+ * D E F G
+ * |
+ * H
+ *
+ */
+function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths {
+ const widths = new Map();
+
+ if (size(indexedProcessTree) === 0) {
+ return widths;
+ }
+
+ const processesInReverseLevelOrder = [...levelOrder(indexedProcessTree)].reverse();
+
+ for (const process of processesInReverseLevelOrder) {
+ const children = indexedProcessTreeChildren(indexedProcessTree, process);
+
+ const sumOfWidthOfChildren = function sumOfWidthOfChildren() {
+ return children.reduce(function sum(currentValue, child) {
+ /**
+ * `widths.get` will always return a number in this case.
+ * This loop sequences a tree in reverse level order. Width values are set for each node.
+ * Therefore a parent can always find a width for its children, since all of its children
+ * will have been handled already.
+ */
+ return currentValue + widths.get(child)!;
+ }, 0);
+ };
+
+ const width = sumOfWidthOfChildren() + Math.max(0, children.length - 1) * distanceBetweenNodes;
+ widths.set(process, width);
+ }
+
+ return widths;
+}
+
+function processEdgeLineSegments(
+ indexedProcessTree: IndexedProcessTree,
+ widths: ProcessWidths,
+ positions: ProcessPositions
+): EdgeLineSegment[] {
+ const edgeLineSegments: EdgeLineSegment[] = [];
+ for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) {
+ /**
+ * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it
+ */
+ if (metadata.parent === null) {
+ continue;
+ }
+ const { process, parent, parentWidth } = metadata;
+ const position = positions.get(process);
+ const parentPosition = positions.get(parent);
+
+ if (position === undefined || parentPosition === undefined) {
+ /**
+ * All positions have been precalculated, so if any are missing, it's an error. This will never happen.
+ */
+ throw new Error();
+ }
+
+ /**
+ * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line
+ */
+ const midwayY = parentPosition[1] + (position[1] - parentPosition[1]) / 2;
+
+ /**
+ * When drawing edge lines between a parent and children (when there are multiple children) we draw a pitchfork type
+ * design. The 'midway' line, runs along the x axis and joins all the children with a single descendant line from the parent.
+ * See the ascii diagram below. The underscore characters would be the midway line.
+ *
+ * A
+ * ____|____
+ * | |
+ * B C
+ */
+ const lineFromProcessToMidwayLine: EdgeLineSegment = [[position[0], midwayY], position];
+
+ const siblings = indexedProcessTreeChildren(indexedProcessTree, parent);
+ const isFirstChild = process === siblings[0];
+
+ if (metadata.isOnlyChild) {
+ // add a single line segment directly from parent to child. We don't do the 'pitchfork' in this case.
+ edgeLineSegments.push([parentPosition, position]);
+ } else if (isFirstChild) {
+ /**
+ * If the parent has multiple children, we draw the 'midway' line, and the line from the
+ * parent to the midway line, while handling the first child.
+ *
+ * Consider A the parent, and B the first child. We would draw somemthing like what's in the below diagram. The line from the
+ * midway line to C would be drawn when we handle C.
+ *
+ * A
+ * ____|____
+ * |
+ * B C
+ */
+ const { firstChildWidth, lastChildWidth } = metadata;
+
+ const lineFromParentToMidwayLine: EdgeLineSegment = [
+ parentPosition,
+ [parentPosition[0], midwayY],
+ ];
+
+ const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2;
+
+ const minX = parentWidth / -2 + firstChildWidth / 2;
+ const maxX = minX + widthOfMidline;
+
+ const midwayLine: EdgeLineSegment = [
+ [
+ // Position line relative to the parent's x component
+ parentPosition[0] + minX,
+ midwayY,
+ ],
+ [
+ // Position line relative to the parent's x component
+ parentPosition[0] + maxX,
+ midwayY,
+ ],
+ ];
+
+ edgeLineSegments.push(
+ /* line from parent to midway line */
+ lineFromParentToMidwayLine,
+ midwayLine,
+ lineFromProcessToMidwayLine
+ );
+ } else {
+ // If this isn't the first child, it must have siblings (the first of which drew the midway line and line
+ // from the parent to the midway line
+ edgeLineSegments.push(lineFromProcessToMidwayLine);
+ }
+ }
+ return edgeLineSegments;
+}
+
+function* levelOrderWithWidths(
+ tree: IndexedProcessTree,
+ widths: ProcessWidths
+): Iterable {
+ for (const process of levelOrder(tree)) {
+ const parent = indexedProcessTreeParent(tree, process);
+ const width = widths.get(process);
+
+ if (width === undefined) {
+ /**
+ * All widths have been precalcluated, so this will not happen.
+ */
+ throw new Error();
+ }
+
+ /** If the parent is undefined, we are processing the root. */
+ if (parent === undefined) {
+ yield {
+ process,
+ width,
+ parent: null,
+ parentWidth: null,
+ isOnlyChild: null,
+ firstChildWidth: null,
+ lastChildWidth: null,
+ };
+ } else {
+ const parentWidth = widths.get(parent);
+
+ if (parentWidth === undefined) {
+ /**
+ * All widths have been precalcluated, so this will not happen.
+ */
+ throw new Error();
+ }
+
+ const metadata: Partial = {
+ process,
+ width,
+ parent,
+ parentWidth,
+ };
+
+ const siblings = indexedProcessTreeChildren(tree, parent);
+ if (siblings.length === 1) {
+ metadata.isOnlyChild = true;
+ metadata.lastChildWidth = width;
+ metadata.firstChildWidth = width;
+ } else {
+ const firstChildWidth = widths.get(siblings[0]);
+ const lastChildWidth = widths.get(siblings[0]);
+ if (firstChildWidth === undefined || lastChildWidth === undefined) {
+ /**
+ * All widths have been precalcluated, so this will not happen.
+ */
+ throw new Error();
+ }
+ metadata.isOnlyChild = false;
+ metadata.firstChildWidth = firstChildWidth;
+ metadata.lastChildWidth = lastChildWidth;
+ }
+
+ yield metadata as ProcessWithWidthMetadata;
+ }
+ }
+}
+
+function processPositions(
+ indexedProcessTree: IndexedProcessTree,
+ widths: ProcessWidths
+): ProcessPositions {
+ const positions = new Map();
+ /**
+ * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent.
+ * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and
+ * reset the counters.
+ */
+ let lastProcessedParentNode: ProcessEvent | undefined;
+ /**
+ * Nodes are positioned relative to their siblings. We walk this in level order, so we handle
+ * children left -> right.
+ *
+ * The width of preceding siblings is used to left align the node.
+ * The number of preceding siblings is important because each sibling must be 1 unit apart
+ * on the x axis.
+ */
+ let numberOfPrecedingSiblings = 0;
+ let runningWidthOfPrecedingSiblings = 0;
+
+ for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) {
+ // Handle root node
+ if (metadata.parent === null) {
+ const { process } = metadata;
+ /**
+ * Place the root node at (0, 0) for now.
+ */
+ positions.set(process, [0, 0]);
+ } else {
+ const { process, parent, width, parentWidth } = metadata;
+
+ // Reinit counters when parent changes
+ if (lastProcessedParentNode !== parent) {
+ numberOfPrecedingSiblings = 0;
+ runningWidthOfPrecedingSiblings = 0;
+
+ // keep track of this so we know when to reinitialize
+ lastProcessedParentNode = parent;
+ }
+
+ const parentPosition = positions.get(parent);
+
+ if (parentPosition === undefined) {
+ /**
+ * Since this algorithm populates the `positions` map in level order,
+ * the parent node will have been processed already and the parent position
+ * will always be available.
+ *
+ * This will never happen.
+ */
+ throw new Error();
+ }
+
+ /**
+ * The x 'offset' is added to the x value of the parent to determine the position of the node.
+ * We add `parentWidth / -2` in order to align the left side of this node with the left side of its parent.
+ * We add `numberOfPrecedingSiblings * distanceBetweenNodes` in order to keep each node 1 apart on the x axis.
+ * We add `runningWidthOfPrecedingSiblings` so that we don't overlap with our preceding siblings. We stack em up.
+ * We add `width / 2` so that we center the node horizontally (in case it has non-0 width.)
+ */
+ const xOffset =
+ parentWidth / -2 +
+ numberOfPrecedingSiblings * distanceBetweenNodes +
+ runningWidthOfPrecedingSiblings +
+ width / 2;
+
+ /**
+ * The y axis gains `-distanceBetweenNodes` as we move down the screen 1 unit at a time.
+ */
+ const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition);
+
+ positions.set(process, position);
+
+ numberOfPrecedingSiblings += 1;
+ runningWidthOfPrecedingSiblings += width;
+ }
+ }
+
+ return positions;
+}
+
+export const processNodePositionsAndEdgeLineSegments = createSelector(
+ graphableProcesses,
+ function processNodePositionsAndEdgeLineSegments(
+ /* eslint-disable no-shadow */
+ graphableProcesses
+ /* eslint-enable no-shadow */
+ ) {
+ /**
+ * Index the tree, creating maps from id -> node and id -> children
+ */
+ const indexedProcessTree = indexedProcessTreeFactory(graphableProcesses);
+ /**
+ * Walk the tree in reverse level order, calculating the 'width' of subtrees.
+ */
+ const widths = widthsOfProcessSubtrees(indexedProcessTree);
+
+ /**
+ * Walk the tree in level order. Using the precalculated widths, calculate the position of nodes.
+ * Nodes are positioned relative to their parents and preceding siblings.
+ */
+ const positions = processPositions(indexedProcessTree, widths);
+
+ /**
+ * With the widths and positions precalculated, we calculate edge line segments (arrays of vector2s)
+ * which connect them in a 'pitchfork' design.
+ */
+ const edgeLineSegments = processEdgeLineSegments(indexedProcessTree, widths, positions);
+
+ /**
+ * Transform the positions of nodes and edges so they seem like they are on an isometric grid.
+ */
+ const transformedEdgeLineSegments: EdgeLineSegment[] = [];
+ const transformedPositions = new Map();
+
+ for (const [processEvent, position] of positions) {
+ transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix));
+ }
+
+ for (const edgeLineSegment of edgeLineSegments) {
+ const transformedSegment = [];
+ for (const point of edgeLineSegment) {
+ transformedSegment.push(applyMatrix3(point, isometricTransformMatrix));
+ }
+ transformedEdgeLineSegments.push(transformedSegment);
+ }
+
+ return {
+ processNodePositions: transformedPositions,
+ edgeLineSegments: transformedEdgeLineSegments,
+ };
+ }
+);
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts
new file mode 100644
index 0000000000000..d043453a8e4cd
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { createStore, StoreEnhancer } from 'redux';
+import { ResolverAction } from '../types';
+import { resolverReducer } from './reducer';
+
+export const storeFactory = () => {
+ /**
+ * Redux Devtools extension exposes itself via a property on the global object.
+ * This interface can be used to cast `window` to a type that may expose Redux Devtools.
+ */
+ interface SomethingThatMightHaveReduxDevTools {
+ __REDUX_DEVTOOLS_EXTENSION__?: (options?: PartialReduxDevToolsOptions) => StoreEnhancer;
+ }
+
+ /**
+ * Some of the options that can be passed when configuring Redux Devtools.
+ */
+ interface PartialReduxDevToolsOptions {
+ /**
+ * A name for this store
+ */
+ name?: string;
+ /**
+ * A list of action types to ignore. This is used to ignore high frequency events created by a mousemove handler
+ */
+ actionsBlacklist?: readonly string[];
+ }
+ const windowWhichMightHaveReduxDevTools = window as SomethingThatMightHaveReduxDevTools;
+ // Make sure blacklisted action types are valid
+ const actionsBlacklist: ReadonlyArray = ['userMovedPointer'];
+ const store = createStore(
+ resolverReducer,
+ windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__ &&
+ windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__({
+ name: 'Resolver',
+ actionsBlacklist,
+ })
+ );
+ return {
+ store,
+ };
+};
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts
new file mode 100644
index 0000000000000..97ab51cbd6dea
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { Reducer, combineReducers } from 'redux';
+import { cameraReducer } from './camera/reducer';
+import { dataReducer } from './data/reducer';
+import { ResolverState, ResolverAction } from '../types';
+
+export const resolverReducer: Reducer = combineReducers({
+ camera: cameraReducer,
+ data: dataReducer,
+});
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts
new file mode 100644
index 0000000000000..30adf17203096
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 cameraSelectors from './camera/selectors';
+import * as dataSelectors from './data/selectors';
+import { ResolverState } from '../types';
+
+/**
+ * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates.
+ * See https://en.wikipedia.org/wiki/Orthographic_projection
+ */
+export const projectionMatrix = composeSelectors(
+ cameraStateSelector,
+ cameraSelectors.projectionMatrix
+);
+
+/**
+ * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates.
+ * See https://en.wikipedia.org/wiki/Orthographic_projection
+ */
+export const inverseProjectionMatrix = composeSelectors(
+ cameraStateSelector,
+ cameraSelectors.inverseProjectionMatrix
+);
+
+/**
+ * The scale by which world values are scaled when rendered.
+ */
+export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale);
+
+/**
+ * Whether or not the user is current panning the map.
+ */
+export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelectors.userIsPanning);
+
+export const processNodePositionsAndEdgeLineSegments = composeSelectors(
+ dataStateSelector,
+ dataSelectors.processNodePositionsAndEdgeLineSegments
+);
+
+/**
+ * Returns the camera state from within ResolverState
+ */
+function cameraStateSelector(state: ResolverState) {
+ return state.camera;
+}
+
+/**
+ * Returns the data state from within ResolverState
+ */
+function dataStateSelector(state: ResolverState) {
+ return state.data;
+}
+
+/**
+ * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a
+ * concern-specific selector. `selector` should return the concern-specific state.
+ */
+function composeSelectors(
+ selector: (state: OuterState) => InnerState,
+ secondSelector: (state: InnerState) => ReturnValue
+): (state: OuterState) => ReturnValue {
+ return state => secondSelector(selector(state));
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
new file mode 100644
index 0000000000000..ed7eb79d621fc
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
@@ -0,0 +1,184 @@
+/*
+ * 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.
+ */
+
+export { ResolverAction } from './actions';
+
+/**
+ * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`.
+ */
+export interface ResolverState {
+ /**
+ * Contains the state of the camera. This includes panning interactions, transform, and projection.
+ */
+ readonly camera: CameraState;
+
+ /**
+ * Contains the state associated with event data (process events and possibly other event types).
+ */
+ readonly data: DataState;
+}
+
+interface PanningState {
+ /**
+ * Screen coordinate vector representing the starting point when panning.
+ */
+ readonly origin: Vector2;
+
+ /**
+ * Screen coordinate vector representing the current point when panning.
+ */
+ readonly currentOffset: Vector2;
+}
+
+/**
+ * Redux state for the virtual 'camera' used by Resolver.
+ */
+export interface CameraState {
+ /**
+ * Contains the starting and current position of the pointer when the user is panning the map.
+ */
+ readonly panning?: PanningState;
+
+ /**
+ * Scales the coordinate system, used for zooming.
+ */
+ readonly scaling: Vector2;
+
+ /**
+ * The size (in pixels) of the Resolver component.
+ */
+ readonly rasterSize: Vector2;
+
+ /**
+ * The camera world transform not counting any change from panning. When panning finishes, this value is updated to account for it.
+ * Use the `transform` selector to get the transform adjusted for panning.
+ */
+ readonly translationNotCountingCurrentPanning: Vector2;
+
+ /**
+ * The world coordinates that the pointing device was last over. This is used during mousewheel zoom.
+ */
+ readonly latestFocusedWorldCoordinates: Vector2 | null;
+}
+
+/**
+ * State for `data` reducer which handles receiving Resolver data from the backend.
+ */
+export interface DataState {
+ readonly results: readonly ProcessEvent[];
+}
+
+export type Vector2 = readonly [number, number];
+
+export type Vector3 = readonly [number, number, number];
+
+/**
+ * A rectangle with sides that align with the `x` and `y` axises.
+ */
+export interface AABB {
+ /**
+ * Vector who's `x` component is the _left_ side of the AABB and who's `y` component is the _bottom_ side of the AABB.
+ **/
+ readonly minimum: Vector2;
+ /**
+ * Vector who's `x` component is the _right_ side of the AABB and who's `y` component is the _bottom_ side of the AABB.
+ **/
+ readonly maximum: Vector2;
+}
+
+/**
+ * A 2D transformation matrix in row-major order.
+ */
+export type Matrix3 = readonly [
+ number,
+ number,
+ number,
+ number,
+ number,
+ number,
+ number,
+ number,
+ number
+];
+
+type eventSubtypeFull =
+ | 'creation_event'
+ | 'fork_event'
+ | 'exec_event'
+ | 'already_running'
+ | 'termination_event';
+
+type eventTypeFull = 'process_event';
+
+/**
+ * The 'events' which contain process data and are used to model Resolver.
+ */
+export interface ProcessEvent {
+ readonly event_timestamp: number;
+ readonly event_type: number;
+ readonly machine_id: string;
+ readonly data_buffer: {
+ event_subtype_full: eventSubtypeFull;
+ event_type_full: eventTypeFull;
+ node_id: number;
+ source_id?: number;
+ process_name: string;
+ process_path: string;
+ };
+}
+
+/**
+ * A represention of a process tree with indices for O(1) access to children and values by id.
+ */
+export interface IndexedProcessTree {
+ /**
+ * Map of ID to a process's children
+ */
+ idToChildren: Map;
+ /**
+ * Map of ID to process
+ */
+ idToProcess: Map;
+}
+
+/**
+ * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees`
+ */
+export type ProcessWidths = Map;
+/**
+ * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions`
+ */
+export type ProcessPositions = Map;
+/**
+ * An array of vectors2 forming an polyline. Used to connect process nodes in the graph.
+ */
+export type EdgeLineSegment = Vector2[];
+
+/**
+ * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph.
+ */
+export type ProcessWithWidthMetadata = {
+ process: ProcessEvent;
+ width: number;
+} & (
+ | {
+ parent: ProcessEvent;
+ parentWidth: number;
+ isOnlyChild: boolean;
+ firstChildWidth: number;
+ lastChildWidth: number;
+ }
+ | {
+ parent: null;
+ /* Without a parent, there is no parent width */
+ parentWidth: null;
+ /* Without a parent, we can't be an only child */
+ isOnlyChild: null;
+ /** If there is no parent, there are no siblings */
+ lastChildWidth: null;
+ firstChildWidth: null;
+ }
+);
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx
new file mode 100644
index 0000000000000..cdecd3e02bde1
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 React from 'react';
+import styled from 'styled-components';
+import { useSelector } from 'react-redux';
+import { applyMatrix3, distance, angle } from '../lib/vector2';
+import { Vector2 } from '../types';
+import * as selectors from '../store/selectors';
+
+/**
+ * A placeholder line segment view that connects process nodes.
+ */
+export const EdgeLine = styled(
+ React.memo(
+ ({
+ className,
+ startPosition,
+ endPosition,
+ }: {
+ /**
+ * A className string provided by `styled`
+ */
+ className?: string;
+ /**
+ * The postion of first point in the line segment. In 'world' coordinates.
+ */
+ startPosition: Vector2;
+ /**
+ * The postion of second point in the line segment. In 'world' coordinates.
+ */
+ endPosition: Vector2;
+ }) => {
+ /**
+ * Convert the start and end positions, which are in 'world' coordinates,
+ * to `left` and `top` css values.
+ */
+ const projectionMatrix = useSelector(selectors.projectionMatrix);
+ const screenStart = applyMatrix3(startPosition, projectionMatrix);
+ const screenEnd = applyMatrix3(endPosition, projectionMatrix);
+
+ /**
+ * We render the line using a short, long, `div` element. The length of this `div`
+ * should be the same as the distance between the start and end points.
+ */
+ const length = distance(screenStart, screenEnd);
+
+ const style = {
+ left: screenStart[0] + 'px',
+ top: screenStart[1] + 'px',
+ width: length + 'px',
+ /**
+ * Transform from the left of the div, as the left side of the `div` is positioned
+ * at the start point of the line segment.
+ */
+ transformOrigin: 'top left',
+ /**
+ * Translate the `div` in the y axis to accomodate for the height of the `div`.
+ * Also rotate the `div` in the z axis so that it's angle matches the angle
+ * between the start and end points.
+ */
+ transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`,
+ };
+ return ;
+ }
+ )
+)`
+ position: absolute;
+ height: 3px;
+ background-color: #d4d4d4;
+ color: #333333;
+`;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx
new file mode 100644
index 0000000000000..2c5c60440522d
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx
@@ -0,0 +1,162 @@
+/*
+ * 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 React, { useCallback, useState, useEffect } from 'react';
+import { Store } from 'redux';
+import { Provider, useSelector, useDispatch } from 'react-redux';
+import styled from 'styled-components';
+import { ResolverState, ResolverAction } from '../types';
+import * as selectors from '../store/selectors';
+import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect';
+import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler';
+import { ProcessEventDot } from './process_event_dot';
+import { EdgeLine } from './edge_line';
+
+export const AppRoot = React.memo(({ store }: { store: Store }) => {
+ return (
+
+
+
+ );
+});
+
+const Resolver = styled(
+ React.memo(({ className }: { className?: string }) => {
+ const dispatch: (action: ResolverAction) => unknown = useDispatch();
+
+ const { processNodePositions, edgeLineSegments } = useSelector(
+ selectors.processNodePositionsAndEdgeLineSegments
+ );
+
+ const [ref, setRef] = useState(null);
+
+ const userIsPanning = useSelector(selectors.userIsPanning);
+
+ const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect();
+
+ const relativeCoordinatesFromMouseEvent = useCallback(
+ (event: { clientX: number; clientY: number }): null | [number, number] => {
+ if (elementBoundingClientRect === null) {
+ return null;
+ }
+ return [
+ event.clientX - elementBoundingClientRect.x,
+ event.clientY - elementBoundingClientRect.y,
+ ];
+ },
+ [elementBoundingClientRect]
+ );
+
+ useEffect(() => {
+ if (elementBoundingClientRect !== null) {
+ dispatch({
+ type: 'userSetRasterSize',
+ payload: [elementBoundingClientRect.width, elementBoundingClientRect.height],
+ });
+ }
+ }, [dispatch, elementBoundingClientRect]);
+
+ const handleMouseDown = useCallback(
+ (event: React.MouseEvent) => {
+ const maybeCoordinates = relativeCoordinatesFromMouseEvent(event);
+ if (maybeCoordinates !== null) {
+ dispatch({
+ type: 'userStartedPanning',
+ payload: maybeCoordinates,
+ });
+ }
+ },
+ [dispatch, relativeCoordinatesFromMouseEvent]
+ );
+
+ const handleMouseMove = useCallback(
+ (event: MouseEvent) => {
+ const maybeCoordinates = relativeCoordinatesFromMouseEvent(event);
+ if (maybeCoordinates) {
+ dispatch({
+ type: 'userMovedPointer',
+ payload: maybeCoordinates,
+ });
+ }
+ },
+ [dispatch, relativeCoordinatesFromMouseEvent]
+ );
+
+ const handleMouseUp = useCallback(() => {
+ if (userIsPanning) {
+ dispatch({
+ type: 'userStoppedPanning',
+ });
+ }
+ }, [dispatch, userIsPanning]);
+
+ const handleWheel = useCallback(
+ (event: WheelEvent) => {
+ // we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height
+ if (
+ elementBoundingClientRect !== null &&
+ event.ctrlKey &&
+ event.deltaY !== 0 &&
+ event.deltaMode === 0
+ ) {
+ event.preventDefault();
+ dispatch({
+ type: 'userZoomed',
+ payload: (-2 * event.deltaY) / elementBoundingClientRect.height,
+ });
+ }
+ },
+ [elementBoundingClientRect, dispatch]
+ );
+
+ useEffect(() => {
+ window.addEventListener('mouseup', handleMouseUp, { passive: true });
+ return () => {
+ window.removeEventListener('mouseup', handleMouseUp);
+ };
+ }, [handleMouseUp]);
+
+ useEffect(() => {
+ window.addEventListener('mousemove', handleMouseMove, { passive: true });
+ return () => {
+ window.removeEventListener('mousemove', handleMouseMove);
+ };
+ }, [handleMouseMove]);
+
+ const refCallback = useCallback(
+ (node: null | HTMLDivElement) => {
+ setRef(node);
+ clientRectCallback(node);
+ },
+ [clientRectCallback]
+ );
+
+ useNonPassiveWheelHandler(handleWheel, ref);
+
+ return (
+
+ {Array.from(processNodePositions).map(([processEvent, position], index) => (
+
+ ))}
+ {edgeLineSegments.map(([startPosition, endPosition], index) => (
+
+ ))}
+
+ );
+ })
+)`
+ /**
+ * Take up all availble space
+ */
+ display: flex;
+ flex-grow: 1;
+ position: relative;
+`;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx
new file mode 100644
index 0000000000000..5c3a253d619ef
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx
@@ -0,0 +1,72 @@
+/*
+ * 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 React from 'react';
+import styled from 'styled-components';
+import { useSelector } from 'react-redux';
+import { applyMatrix3 } from '../lib/vector2';
+import { Vector2, ProcessEvent } from '../types';
+import * as selectors from '../store/selectors';
+
+/**
+ * A placeholder view for a process node.
+ */
+export const ProcessEventDot = styled(
+ React.memo(
+ ({
+ className,
+ position,
+ event,
+ }: {
+ /**
+ * A `className` string provided by `styled`
+ */
+ className?: string;
+ /**
+ * The positon of the process node, in 'world' coordinates.
+ */
+ position: Vector2;
+ /**
+ * An event which contains details about the process node.
+ */
+ event: ProcessEvent;
+ }) => {
+ /**
+ * Convert the position, which is in 'world' coordinates, to screen coordinates.
+ */
+ const projectionMatrix = useSelector(selectors.projectionMatrix);
+ const [left, top] = applyMatrix3(position, projectionMatrix);
+ const style = {
+ left: (left - 20).toString() + 'px',
+ top: (top - 20).toString() + 'px',
+ };
+ return (
+
+ name: {event.data_buffer.process_name}
+
+ x: {position[0]}
+
+ y: {position[1]}
+
+ );
+ }
+ )
+)`
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ text-align: left;
+ font-size: 10px;
+ /**
+ * Give the element a button-like appearance.
+ */
+ user-select: none;
+ border: 1px solid black;
+ box-sizing: border-box;
+ border-radius: 10%;
+ padding: 4px;
+ white-space: nowrap;
+`;
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx
new file mode 100644
index 0000000000000..5f13995de1c2a
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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 { useCallback, useState, useEffect, useRef } from 'react';
+import ResizeObserver from 'resize-observer-polyfill';
+
+/**
+ * Returns a nullable DOMRect and a ref callback. Pass the refCallback to the
+ * `ref` property of a native element and this hook will return a DOMRect for
+ * it by calling `getBoundingClientRect`. This hook will observe the element
+ * with a resize observer and call getBoundingClientRect again after resizes.
+ *
+ * Note that the changes to the position of the element aren't automatically
+ * tracked. So if the element's position moves for some reason, be sure to
+ * handle that.
+ */
+export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] {
+ const [rect, setRect] = useState(null);
+ const nodeRef = useRef(null);
+ const ref = useCallback((node: Element | null) => {
+ nodeRef.current = node;
+ if (node !== null) {
+ setRect(node.getBoundingClientRect());
+ }
+ }, []);
+ useEffect(() => {
+ if (nodeRef.current !== null) {
+ const resizeObserver = new ResizeObserver(entries => {
+ if (nodeRef.current !== null && nodeRef.current === entries[0].target) {
+ setRect(nodeRef.current.getBoundingClientRect());
+ }
+ });
+ resizeObserver.observe(nodeRef.current);
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }
+ }, [nodeRef]);
+ return [rect, ref];
+}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx
new file mode 100644
index 0000000000000..a0738bcf4d14c
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { useEffect } from 'react';
+/**
+ * Register an event handler directly on `elementRef` for the `wheel` event, with no options
+ * React sets native event listeners on the `window` and calls provided handlers via event propagation.
+ * As of Chrome 73, `'wheel'` events on `window` are automatically treated as 'passive'.
+ * If you don't need to call `event.preventDefault` then you should use regular React event handling instead.
+ */
+export function useNonPassiveWheelHandler(
+ handler: (event: WheelEvent) => void,
+ elementRef: HTMLElement | null
+) {
+ useEffect(() => {
+ if (elementRef !== null) {
+ elementRef.addEventListener('wheel', handler);
+ return () => {
+ elementRef.removeEventListener('wheel', handler);
+ };
+ }
+ }, [elementRef, handler]);
+}
diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts
index 02514cc974af0..355364253b2a5 100644
--- a/x-pack/plugins/endpoint/public/plugin.ts
+++ b/x-pack/plugins/endpoint/public/plugin.ts
@@ -26,7 +26,6 @@ export class EndpointPlugin
EndpointPluginStartDependencies
> {
public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) {
- const resolverEmbeddableFactory = new ResolverEmbeddableFactory();
core.application.register({
id: 'endpoint',
title: i18n.translate('xpack.endpoint.pluginTitle', {
@@ -39,6 +38,8 @@ export class EndpointPlugin
},
});
+ const resolverEmbeddableFactory = new ResolverEmbeddableFactory();
+
plugins.embeddable.registerEmbeddableFactory(
resolverEmbeddableFactory.type,
resolverEmbeddableFactory
diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
index 98baad6a18411..55fd436de40a1 100644
--- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
+++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
@@ -10,6 +10,7 @@ import { AppMountParameters } from 'kibana/public';
import { I18nProvider } from '@kbn/i18n/react';
import { IEmbeddable } from 'src/plugins/embeddable/public';
import { useEffect } from 'react';
+import styled from 'styled-components';
/**
* This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle.
@@ -18,6 +19,12 @@ export function renderApp(
{ element }: AppMountParameters,
embeddable: Promise
) {
+ /**
+ * The application DOM node should take all available space.
+ */
+ element.style.display = 'flex';
+ element.style.flexGrow = '1';
+
ReactDOM.render(
@@ -30,34 +37,89 @@ export function renderApp(
};
}
-const AppRoot = React.memo(
- ({ embeddable: embeddablePromise }: { embeddable: Promise }) => {
- const [embeddable, setEmbeddable] = React.useState(undefined);
- const [renderTarget, setRenderTarget] = React.useState(null);
+const AppRoot = styled(
+ React.memo(
+ ({
+ embeddable: embeddablePromise,
+ className,
+ }: {
+ /**
+ * A promise which resolves to the Resolver embeddable.
+ */
+ embeddable: Promise;
+ /**
+ * A `className` string provided by `styled`
+ */
+ className?: string;
+ }) => {
+ /**
+ * This state holds the reference to the embeddable, once resolved.
+ */
+ const [embeddable, setEmbeddable] = React.useState(undefined);
+ /**
+ * This state holds the reference to the DOM node that will contain the embeddable.
+ */
+ const [renderTarget, setRenderTarget] = React.useState(null);
+
+ /**
+ * Keep component state with the Resolver embeddable.
+ *
+ * If the reference to the embeddablePromise changes, we ignore the stale promise.
+ */
+ useEffect(() => {
+ /**
+ * A promise rejection function that will prevent a stale embeddable promise from being resolved
+ * as the current eembeddable.
+ *
+ * If the embeddablePromise itself changes before the old one is resolved, we cancel and restart this effect.
+ */
+ let cleanUp;
- useEffect(() => {
- let cleanUp;
- Promise.race([
- new Promise((_resolve, reject) => {
+ const cleanupPromise = new Promise((_resolve, reject) => {
cleanUp = reject;
- }),
- embeddablePromise,
- ]).then(value => {
- setEmbeddable(value);
- });
-
- return cleanUp;
- }, [embeddablePromise]);
-
- useEffect(() => {
- if (embeddable && renderTarget) {
- embeddable.render(renderTarget);
- return () => {
- embeddable.destroy();
- };
- }
- }, [embeddable, renderTarget]);
-
- return ;
- }
-);
+ });
+
+ /**
+ * Either set the embeddable in state, or cancel and restart this process.
+ */
+ Promise.race([cleanupPromise, embeddablePromise]).then(value => {
+ setEmbeddable(value);
+ });
+
+ /**
+ * If `embeddablePromise` is changed, the cleanup function is run.
+ */
+ return cleanUp;
+ }, [embeddablePromise]);
+
+ /**
+ * Render the eembeddable into the DOM node.
+ */
+ useEffect(() => {
+ if (embeddable && renderTarget) {
+ embeddable.render(renderTarget);
+ /**
+ * If the embeddable or DOM node changes then destroy the old embeddable.
+ */
+ return () => {
+ embeddable.destroy();
+ };
+ }
+ }, [embeddable, renderTarget]);
+
+ return (
+
+ );
+ }
+ )
+)`
+ /**
+ * Take all available space.
+ */
+ display: flex;
+ flex-grow: 1;
+`;
diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts
index f063271f4b5dd..9252825ea1107 100644
--- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts
+++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts
@@ -26,6 +26,12 @@ export class ResolverTestPlugin
private resolveEmbeddable!: (
value: IEmbeddable | undefined | PromiseLike | undefined
) => void;
+ /**
+ * We register our application during the `setup` phase, but the embeddable
+ * plugin API is not available until the `start` phase. In order to access
+ * the embeddable API from our application, we pass a Promise to the application
+ * which we resolve during the `start` phase.
+ */
private embeddablePromise: Promise = new Promise<
IEmbeddable | undefined
>(resolve => {
@@ -39,6 +45,9 @@ export class ResolverTestPlugin
}),
mount: async (_context, params) => {
const { renderApp } = await import('./applications/resolver_test');
+ /**
+ * Pass a promise which resolves to the Resolver embeddable.
+ */
return renderApp(params, this.embeddablePromise);
},
});
@@ -47,6 +56,9 @@ export class ResolverTestPlugin
public start(...args: [unknown, { embeddable: IEmbeddableStart }]) {
const [, plugins] = args;
const factory = plugins.embeddable.getEmbeddableFactory('resolver');
+ /**
+ * Provide the Resolver embeddable to the application
+ */
this.resolveEmbeddable(factory.create({ id: 'test basic render' }));
}
public stop() {}
diff --git a/yarn.lock b/yarn.lock
index dfce24ad77d2e..f8ae804f52e54 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4069,6 +4069,16 @@
"@types/react" "*"
redux "^4.0.0"
+"@types/react-redux@^7.1.0":
+ version "7.1.5"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.5.tgz#c7a528d538969250347aa53c52241051cf886bd3"
+ integrity sha512-ZoNGQMDxh5ENY7PzU7MVonxDzS1l/EWiy8nUhDqxFqUZn4ovboCyvk4Djf68x6COb7vhGTKjyjxHxtFdAA5sUA==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
+
"@types/react-resize-detector@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/react-resize-detector/-/react-resize-detector-4.0.1.tgz#cc8f012f5957e4826e69b8d2afd59baadcac556c"