kibana/x-pack/plugins/endpoint/common/generate_data.ts
Robert Austin 08aa214190
Endpoint: Redux types (#63413)
## Summary

Changes some types around so modifying redux state or actions will cause type errors

* the state and action types aren't using `Immutable` directly. This means that you can instantiate them and mutate them
* the `state` and `action` params received by all reducers will be automatically cast to `Immutable`
* reducers can return either `Immutable` or regular versions of `state`
* some code may be mutating redux state directly, this is being ignored (at least for now)
2020-04-15 11:19:46 -04:00

533 lines
16 KiB
TypeScript

/*
* 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 uuid from 'uuid';
import seedrandom from 'seedrandom';
import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields, PolicyData } from './types';
import { factory as policyFactory } from './models/policy_config';
export type Event = AlertEvent | EndpointEvent;
interface EventOptions {
timestamp?: number;
entityID?: string;
parentEntityID?: string;
eventType?: string;
eventCategory?: string;
processName?: string;
}
const Windows: OSFields[] = [
{
name: 'windows 10.0',
full: 'Windows 10',
version: '10.0',
variant: 'Windows Pro',
},
{
name: 'windows 10.0',
full: 'Windows Server 2016',
version: '10.0',
variant: 'Windows Server',
},
{
name: 'windows 6.2',
full: 'Windows Server 2012',
version: '6.2',
variant: 'Windows Server',
},
{
name: 'windows 6.3',
full: 'Windows Server 2012R2',
version: '6.3',
variant: 'Windows Server Release 2',
},
];
const Linux: OSFields[] = [];
const Mac: OSFields[] = [];
const OS: OSFields[] = [...Windows, ...Mac, ...Linux];
const POLICIES: Array<{ name: string; id: string }> = [
{
name: 'Default',
id: '00000000-0000-0000-0000-000000000000',
},
{
name: 'With Eventing',
id: 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A',
},
];
const FILE_OPERATIONS: string[] = ['creation', 'open', 'rename', 'execution', 'deletion'];
interface EventInfo {
category: string;
/**
* This denotes the `event.type` field for when an event is created, this can be `start` or `creation`
*/
creationType: string;
}
// These are from the v1 schemas and aren't all valid ECS event categories, still in flux
const OTHER_EVENT_CATEGORIES: EventInfo[] = [
{ category: 'driver', creationType: 'start' },
{ category: 'file', creationType: 'creation' },
{ category: 'library', creationType: 'start' },
{ category: 'network', creationType: 'start' },
{ category: 'registry', creationType: 'creation' },
];
interface HostInfo {
elastic: {
agent: {
id: string;
};
};
agent: {
version: string;
id: string;
};
host: HostFields;
endpoint: {
policy: {
id: string;
};
};
}
interface NodeState {
event: Event;
childrenCreated: number;
maxChildren: number;
}
export class EndpointDocGenerator {
commonInfo: HostInfo;
random: seedrandom.prng;
constructor(seed: string | seedrandom.prng = Math.random().toString()) {
if (typeof seed === 'string') {
this.random = seedrandom(seed);
} else {
this.random = seed;
}
this.commonInfo = this.createHostData();
}
/**
* Creates new random IP addresses for the host to simulate new DHCP assignment
*/
public updateHostData() {
this.commonInfo.host.ip = this.randomArray(3, () => this.randomIP());
}
private createHostData(): HostInfo {
return {
agent: {
version: this.randomVersion(),
id: this.seededUUIDv4(),
},
elastic: {
agent: {
id: this.seededUUIDv4(),
},
},
host: {
id: this.seededUUIDv4(),
hostname: this.randomHostname(),
ip: this.randomArray(3, () => this.randomIP()),
mac: this.randomArray(3, () => this.randomMac()),
os: this.randomChoice(OS),
},
endpoint: {
policy: this.randomChoice(POLICIES),
},
};
}
/**
* Creates a host metadata document
* @param ts - Timestamp to put in the event
*/
public generateHostMetadata(ts = new Date().getTime()): HostMetadata {
return {
'@timestamp': ts,
event: {
created: ts,
},
...this.commonInfo,
};
}
/**
* Creates an alert from the simulated host represented by this EndpointDocGenerator
* @param ts - Timestamp to put in the event
* @param entityID - entityID of the originating process
* @param parentEntityID - optional entityID of the parent process, if it exists
*/
public generateAlert(
ts = new Date().getTime(),
entityID = this.randomString(10),
parentEntityID?: string
): AlertEvent {
return {
...this.commonInfo,
'@timestamp': ts,
event: {
action: this.randomChoice(FILE_OPERATIONS),
kind: 'alert',
category: 'malware',
id: this.seededUUIDv4(),
dataset: 'endpoint',
module: 'endpoint',
type: 'creation',
},
file: {
owner: 'SYSTEM',
name: 'fake_malware.exe',
path: 'C:/fake_malware.exe',
accessed: ts,
mtime: ts,
created: ts,
size: 3456,
hash: {
md5: 'fake file md5',
sha1: 'fake file sha1',
sha256: 'fake file sha256',
},
code_signature: {
trusted: false,
subject_name: 'bad signer',
},
malware_classification: {
identifier: 'endpointpe',
score: 1,
threshold: 0.66,
version: '3.0.33',
},
temp_file_path: 'C:/temp/fake_malware.exe',
},
process: {
pid: 2,
name: 'malware writer',
start: ts,
uptime: 0,
user: 'SYSTEM',
entity_id: entityID,
executable: 'C:/malware.exe',
parent: parentEntityID ? { entity_id: parentEntityID, pid: 1 } : undefined,
token: {
domain: 'NT AUTHORITY',
integrity_level: 16384,
integrity_level_name: 'system',
privileges: [
{
description: 'Replace a process level token',
enabled: false,
name: 'SeAssignPrimaryTokenPrivilege',
},
],
sid: 'S-1-5-18',
type: 'tokenPrimary',
user: 'SYSTEM',
},
code_signature: {
trusted: false,
subject_name: 'bad signer',
},
hash: {
md5: 'fake md5',
sha1: 'fake sha1',
sha256: 'fake sha256',
},
},
dll: [
{
pe: {
architecture: 'x64',
imphash: 'c30d230b81c734e82e86e2e2fe01cd01',
},
code_signature: {
subject_name: 'Cybereason Inc',
trusted: true,
},
compile_time: 1534424710,
hash: {
md5: '1f2d082566b0fc5f2c238a5180db7451',
sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d',
sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2',
},
malware_classification: {
identifier: 'Whitelisted',
score: 0,
threshold: 0,
version: '3.0.0',
},
mapped_address: 5362483200,
mapped_size: 0,
path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe',
},
],
};
}
/**
* Creates an event, customized by the options parameter
* @param options - Allows event field values to be specified
*/
public generateEvent(options: EventOptions = {}): EndpointEvent {
return {
'@timestamp': options.timestamp ? options.timestamp : new Date().getTime(),
agent: { ...this.commonInfo.agent, type: 'endpoint' },
ecs: {
version: '1.4.0',
},
event: {
category: options.eventCategory ? options.eventCategory : 'process',
kind: 'event',
type: options.eventType ? options.eventType : 'start',
id: this.seededUUIDv4(),
},
host: this.commonInfo.host,
process: {
entity_id: options.entityID ? options.entityID : this.randomString(10),
parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined,
name: options.processName ? options.processName : 'powershell.exe',
},
};
}
/**
* Generator function that creates the full set of events needed to render resolver.
* The number of nodes grows exponentially with the number of generations and children per node.
* Each node is logically a process, and will have 1 or more process events associated with it.
* @param alertAncestors - number of ancestor generations to create relative to the alert
* @param childGenerations - number of child generations to create relative to the alert
* @param maxChildrenPerNode - maximum number of children for any given node in the tree
* @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree
* @param percentNodesWithRelated - percent of nodes which should have related events
* @param percentChildrenTerminated - percent of nodes which will have process termination events
*/
public *fullResolverTreeGenerator(
alertAncestors?: number,
childGenerations?: number,
maxChildrenPerNode?: number,
relatedEventsPerNode?: number,
percentNodesWithRelated?: number,
percentChildrenTerminated?: number
) {
const ancestry = this.createAlertEventAncestry(alertAncestors);
for (let i = 0; i < ancestry.length; i++) {
yield ancestry[i];
}
// ancestry will always have at least 2 elements, and the second to last element will be the process associated with the alert
yield* this.descendantsTreeGenerator(
ancestry[ancestry.length - 2],
childGenerations,
maxChildrenPerNode,
relatedEventsPerNode,
percentNodesWithRelated,
percentChildrenTerminated
);
}
/**
* Creates an alert event and associated process ancestry. The alert event will always be the last event in the return array.
* @param alertAncestors - number of ancestor generations to create
*/
public createAlertEventAncestry(alertAncestors = 3): Event[] {
const events = [];
const startDate = new Date().getTime();
const root = this.generateEvent({ timestamp: startDate + 1000 });
events.push(root);
let ancestor = root;
for (let i = 0; i < alertAncestors; i++) {
ancestor = this.generateEvent({
timestamp: startDate + 1000 * (i + 1),
parentEntityID: ancestor.process.entity_id,
});
events.push(ancestor);
}
events.push(
this.generateAlert(
startDate + 1000 * alertAncestors,
ancestor.process.entity_id,
ancestor.process.parent?.entity_id
)
);
return events;
}
/**
* Creates the child generations of a process. The number of returned events grows exponentially with generations and maxChildrenPerNode.
* @param root - The process event to use as the root node of the tree
* @param generations - number of child generations to create. The root node is not counted as a generation.
* @param maxChildrenPerNode - maximum number of children for any given node in the tree
* @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree
* @param percentNodesWithRelated - percent of nodes which should have related events
* @param percentChildrenTerminated - percent of nodes which will have process termination events
*/
public *descendantsTreeGenerator(
root: Event,
generations = 2,
maxChildrenPerNode = 2,
relatedEventsPerNode = 3,
percentNodesWithRelated = 100,
percentChildrenTerminated = 100
) {
const rootState: NodeState = {
event: root,
childrenCreated: 0,
maxChildren: this.randomN(maxChildrenPerNode + 1),
};
const lineage: NodeState[] = [rootState];
let timestamp = root['@timestamp'];
while (lineage.length > 0) {
const currentState = lineage[lineage.length - 1];
// If we get to a state node and it has made all the children, move back up a level
if (
currentState.childrenCreated === currentState.maxChildren ||
lineage.length === generations + 1
) {
lineage.pop();
continue;
}
// Otherwise, add a child and any nodes associated with it
currentState.childrenCreated++;
timestamp = timestamp + 1000;
const child = this.generateEvent({
timestamp,
parentEntityID: currentState.event.process.entity_id,
});
lineage.push({
event: child,
childrenCreated: 0,
maxChildren: this.randomN(maxChildrenPerNode + 1),
});
yield child;
let processDuration: number = 6 * 3600;
if (this.randomN(100) < percentChildrenTerminated) {
processDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days)
yield this.generateEvent({
timestamp: timestamp + processDuration * 1000,
entityID: child.process.entity_id,
parentEntityID: child.process.parent?.entity_id,
eventCategory: 'process',
eventType: 'end',
});
}
if (this.randomN(100) < percentNodesWithRelated) {
yield* this.relatedEventsGenerator(child, relatedEventsPerNode, processDuration);
}
}
}
/**
* Creates related events for a process event
* @param node - process event to relate events to by entityID
* @param numRelatedEvents - number of related events to generate
* @param processDuration - maximum number of seconds after process event that related event timestamp can be
*/
public *relatedEventsGenerator(
node: Event,
numRelatedEvents = 10,
processDuration: number = 6 * 3600
) {
for (let i = 0; i < numRelatedEvents; i++) {
const eventInfo = this.randomChoice(OTHER_EVENT_CATEGORIES);
const ts = node['@timestamp'] + this.randomN(processDuration) * 1000;
yield this.generateEvent({
timestamp: ts,
entityID: node.process.entity_id,
parentEntityID: node.process.parent?.entity_id,
eventCategory: eventInfo.category,
eventType: eventInfo.creationType,
});
}
}
/**
* Generates an Ingest `datasource` that includes the Endpoint Policy data
*/
public generatePolicyDatasource(): PolicyData {
return {
id: this.seededUUIDv4(),
name: 'Endpoint Policy',
description: 'Policy to protect the worlds data',
config_id: this.seededUUIDv4(),
enabled: true,
output_id: '',
inputs: [
{
type: 'endpoint',
enabled: true,
streams: [],
config: {
policy: {
value: policyFactory(),
},
},
},
],
namespace: 'default',
package: {
name: 'endpoint',
title: 'Elastic Endpoint',
version: '1.0.0',
},
revision: 1,
};
}
private randomN(n: number): number {
return Math.floor(this.random() * n);
}
private *randomNGenerator(max: number, count: number) {
while (count > 0) {
yield this.randomN(max);
count--;
}
}
private randomArray<T>(lengthLimit: number, generator: () => T): T[] {
const rand = this.randomN(lengthLimit) + 1;
return [...Array(rand).keys()].map(generator);
}
private randomMac(): string {
return [...this.randomNGenerator(255, 6)].map(x => x.toString(16)).join('-');
}
private randomIP(): string {
return [10, ...this.randomNGenerator(255, 3)].map(x => x.toString()).join('.');
}
private randomVersion(): string {
return [6, ...this.randomNGenerator(10, 2)].map(x => x.toString()).join('.');
}
private randomChoice<T>(choices: T[]): T {
return choices[this.randomN(choices.length)];
}
private randomString(length: number): string {
return [...this.randomNGenerator(36, length)].map(x => x.toString(36)).join('');
}
private randomHostname(): string {
return `Host-${this.randomString(10)}`;
}
private seededUUIDv4(): string {
return uuid.v4({ random: [...this.randomNGenerator(255, 16)] });
}
}