x-pack/solutions/security/plugins/security_solution/common/endpoint/generate_data.ts (1,650 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable max-classes-per-file */
import type seedrandom from 'seedrandom';
import { assertNever } from '@kbn/std';
import type {
GetAgentPoliciesResponseItem,
GetPackagesResponse,
GetInfoResponse,
EsAssetReference,
KibanaAssetReference,
AssetsGroupedByServiceByType,
} from '@kbn/fleet-plugin/common';
import { agentPolicyStatuses } from '@kbn/fleet-plugin/common';
import { clone } from 'lodash';
import { EndpointMetadataGenerator } from './data_generators/endpoint_metadata_generator';
import type {
AlertEvent,
DataStream,
HostMetadata,
HostMetadataInterface,
HostPolicyResponse,
PolicyData,
SafeEndpointEvent,
} from './types';
import { HostPolicyResponseActionStatus } from './types';
import {
ancestryArray,
entityIDSafeVersion,
parentEntityIDSafeVersion,
processNameSafeVersion,
timestampSafeVersion,
} from './models/event';
import { firstNonNullValue } from './models/ecs_safety_helpers';
import type { EventOptions } from './types/generator';
import { BaseDataGenerator } from './data_generators/base_data_generator';
import type { PartialEndpointPolicyData } from './data_generators/fleet_package_policy_generator';
import { FleetPackagePolicyGenerator } from './data_generators/fleet_package_policy_generator';
export type Event = AlertEvent | SafeEndpointEvent;
/**
* This value indicates the limit for the size of the ancestry array. The endpoint currently saves up to 20 values
* in its messages. To simulate a limit on the array size I'm using 2 here so that we can't rely on there being a large
* number like 20. The ancestry array contains entity_ids for the ancestors of a particular process.
*
* The array has a special format. The entity_ids towards the beginning of the array are closer ancestors and the
* values towards the end of the array are more distant ancestors (grandparents). Therefore
* ancestry_array[0] == process.parent.entity_id and ancestry_array[1] == process.parent.parent.entity_id
*/
export const ANCESTRY_LIMIT: number = 2;
const POLICY_RESPONSE_STATUSES: HostPolicyResponseActionStatus[] = [
HostPolicyResponseActionStatus.success,
HostPolicyResponseActionStatus.failure,
HostPolicyResponseActionStatus.warning,
HostPolicyResponseActionStatus.unsupported,
];
const APPLIED_POLICIES: Array<HostMetadataInterface['Endpoint']['policy']['applied']> = [
{
name: 'Default',
id: '00000000-0000-0000-0000-000000000000',
status: HostPolicyResponseActionStatus.success,
endpoint_policy_version: 1,
version: 3,
},
{
name: 'With Eventing',
id: 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A',
status: HostPolicyResponseActionStatus.success,
endpoint_policy_version: 3,
version: 5,
},
{
name: 'Detect Malware Only',
id: '47d7965d-6869-478b-bd9c-fb0d2bb3959f',
status: HostPolicyResponseActionStatus.success,
endpoint_policy_version: 4,
version: 9,
},
];
const FILE_OPERATIONS: string[] = ['creation', 'open', 'rename', 'execution', 'deletion'];
interface EventInfo {
category: string | string[];
/**
* This denotes the `event.type` field for when an event is created, this can be `start` or `creation`
*/
creationType: string;
}
/**
* The valid ecs categories.
*/
export enum ECSCategory {
Driver = 'driver',
File = 'file',
Network = 'network',
/**
* Registry has not been added to ecs yet.
*/
Registry = 'registry',
Authentication = 'authentication',
Session = 'session',
}
/**
* High level categories for related events. These specify the type of related events that should be generated.
*/
export enum RelatedEventCategory {
/**
* The Random category allows the related event categories to be chosen randomly
*/
Random = 'random',
Driver = 'driver',
File = 'file',
Network = 'network',
Registry = 'registry',
/**
* Security isn't an actual category but defines a type of related event to be created.
*/
Security = 'security',
}
/**
* This map defines the relationship between a higher level event type defined by the RelatedEventCategory enums and
* the ECS categories that is should map to. This should only be used for tests that need to determine the exact
* ecs categories that were created based on the related event information passed to the generator.
*/
export const categoryMapping: Record<RelatedEventCategory, ECSCategory | ECSCategory[] | ''> = {
[RelatedEventCategory.Security]: [ECSCategory.Authentication, ECSCategory.Session],
[RelatedEventCategory.Driver]: ECSCategory.Driver,
[RelatedEventCategory.File]: ECSCategory.File,
[RelatedEventCategory.Network]: ECSCategory.Network,
[RelatedEventCategory.Registry]: ECSCategory.Registry,
/**
* Random is only used by the generator to indicate that it should randomly choose the event information when generating
* related events. It does not map to a specific ecs category.
*/
[RelatedEventCategory.Random]: '',
};
/**
* The related event category and number of events that should be generated.
*/
export interface RelatedEventInfo {
category: RelatedEventCategory;
count: number;
}
// These are from the v1 schemas and aren't all valid ECS event categories, still in flux
const OTHER_EVENT_CATEGORIES: Record<
Exclude<RelatedEventCategory, RelatedEventCategory.Random>,
EventInfo
> = {
[RelatedEventCategory.Security]: {
category: categoryMapping[RelatedEventCategory.Security],
creationType: 'start',
},
[RelatedEventCategory.Driver]: {
category: categoryMapping[RelatedEventCategory.Driver],
creationType: 'start',
},
[RelatedEventCategory.File]: {
category: categoryMapping[RelatedEventCategory.File],
creationType: 'creation',
},
[RelatedEventCategory.Network]: {
category: categoryMapping[RelatedEventCategory.Network],
creationType: 'start',
},
[RelatedEventCategory.Registry]: {
category: categoryMapping[RelatedEventCategory.Registry],
creationType: 'creation',
},
};
type CommonHostInfo = Pick<HostMetadataInterface, 'elastic' | 'agent' | 'host' | 'Endpoint'>;
interface NodeState {
event: Event;
childrenCreated: number;
maxChildren: number;
}
/**
* The Tree and TreeNode interfaces define structures to make testing of resolver functionality easier. The `generateTree`
* method builds a `Tree` structures which organizes the different parts of the resolver tree. Maps are used to allow
* tests to quickly verify if the node they retrieved from ES was actually created by the generator or if there is an
* issue with the implementation. The `Tree` structure serves as a source of truth for queries to ES. The entire Tree
* is stored in memory so it can be quickly accessed by the tests. The resolver api_integration tests currently leverage
* these structures for verifying that its implementation is returning the correct documents from ES and structuring
* the response correctly.
*/
/**
* Defines the fields for each node in the tree.
*/
export interface TreeNode {
/**
* The entity_id for the node
*/
id: string;
lifecycle: Event[];
relatedEvents: Event[];
relatedAlerts: Event[];
}
/**
* A resolver tree that makes accessing specific nodes easier for tests.
*/
export interface Tree {
/**
* Children grouped by the parent's ID
*/
childrenByParent: Map<string, Map<string, TreeNode>>;
/**
* Map of entity_id to node
*/
children: Map<string, TreeNode>;
/**
* An array of levels of the children, that doesn't include the origin or any ancestors
* childrenLevels[0] are the direct children of the origin node. The next level would be those children's descendants
*/
childrenLevels: Array<Map<string, TreeNode>>;
/**
* Map of entity_id to node
*/
ancestry: Map<string, TreeNode>;
origin: TreeNode;
/**
* All events from children, ancestry, origin, and the alert in a single array
*/
allEvents: Event[];
startTime: Date;
endTime: Date;
agentId: string;
}
export interface TreeOptions {
/**
* The value in ancestors does not include the origin/root node
*/
ancestors?: number;
generations?: number;
children?: number;
relatedEvents?: RelatedEventInfo[] | number;
/**
* If true then the related events will be created with timestamps that preserve the
* generation order, meaning the first event will always have a timestamp number less
* than the next related event
*/
relatedEventsOrdered?: boolean;
relatedAlerts?: number;
percentWithRelated?: number;
percentTerminated?: number;
alwaysGenMaxChildrenPerNode?: boolean;
ancestryArraySize?: number;
eventsDataStream?: DataStream;
alertsDataStream?: DataStream;
sessionEntryLeader?: string;
}
type TreeOptionDefaults = Required<TreeOptions>;
/**
* This function provides defaults for fields that are not specified in the options
*
* @param options tree options for defining the structure of the tree
*/
export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults {
return {
ancestors: options?.ancestors ?? 3,
generations: options?.generations ?? 2,
children: options?.children ?? 2,
relatedEvents: options?.relatedEvents ?? 5,
relatedEventsOrdered: options?.relatedEventsOrdered ?? false,
relatedAlerts: options?.relatedAlerts ?? 3,
sessionEntryLeader: options?.sessionEntryLeader ?? '',
percentWithRelated: options?.percentWithRelated ?? 30,
percentTerminated: options?.percentTerminated ?? 100,
alwaysGenMaxChildrenPerNode: options?.alwaysGenMaxChildrenPerNode ?? false,
ancestryArraySize: options?.ancestryArraySize ?? ANCESTRY_LIMIT,
eventsDataStream: options?.eventsDataStream ?? eventsDefaultDataStream,
alertsDataStream: options?.alertsDataStream ?? alertsDefaultDataStream,
};
}
const metadataDefaultDataStream = () => ({
type: 'metrics',
dataset: 'endpoint.metadata',
namespace: 'default',
});
const policyDefaultDataStream = () => ({
type: 'metrics',
dataset: 'endpoint.policy',
namespace: 'default',
});
const eventsDefaultDataStream = {
type: 'logs',
dataset: 'endpoint.events.process',
namespace: 'default',
};
enum AlertTypes {
MALWARE = 'MALWARE',
MEMORY_SIGNATURE = 'MEMORY_SIGNATURE',
MEMORY_SHELLCODE = 'MEMORY_SHELLCODE',
BEHAVIOR = 'BEHAVIOR',
}
const alertsDefaultDataStream = {
type: 'logs',
dataset: 'endpoint.alerts',
namespace: 'default',
};
/**
* Generator to create various ElasticSearch documents that are normally streamed by the Endpoint.
*
* NOTE: this generator currently reuses certain data (ex. `this.commonInfo`) across several
* documents, thus use caution if manipulating/mutating value in the generated data
* (ex. in tests). Individual standalone generators exist, whose generated data does not
* contain shared data structures.
*/
export class EndpointDocGenerator extends BaseDataGenerator {
/**
* DO NOT ACCESS THIS PROPERTY DIRECTORY.
* Should only be accessed from the `getter/setter` property for `commonInfo` defined further
* below.
* @deprecated (just to ensure that its obvious not to access it directory)
*/
_commonInfo: CommonHostInfo;
sequence: number = 0;
private readonly metadataGenerator: EndpointMetadataGenerator;
/**
* The EndpointDocGenerator parameters
*
* @param seed either a string to seed the random number generator or a random number generator function
* @param MetadataGenerator
*/
constructor(
seed: string | seedrandom.prng = Math.random().toString(),
MetadataGenerator: typeof EndpointMetadataGenerator = EndpointMetadataGenerator
) {
super(seed);
this.metadataGenerator = new MetadataGenerator(seed);
this._commonInfo = this.createHostData();
}
/**
* Get a custom `EndpointDocGenerator` subclass that customizes certain fields based on input arguments
*/
public static custom({
CustomMetadataGenerator,
}: Partial<{
CustomMetadataGenerator: typeof EndpointMetadataGenerator;
}> = {}): typeof EndpointDocGenerator {
return class extends EndpointDocGenerator {
constructor(...options: ConstructorParameters<typeof EndpointDocGenerator>) {
if (CustomMetadataGenerator) {
options[1] = CustomMetadataGenerator;
}
super(...options);
}
};
}
// Ensure that `this.commonInfo` is returned cloned data
protected get commonInfo() {
return clone(this._commonInfo);
}
protected set commonInfo(newInfo) {
this._commonInfo = newInfo;
}
/**
* Creates new random IP addresses for the host to simulate new DHCP assignment
*/
public updateHostData() {
const newInfo = this.commonInfo;
newInfo.host.ip = this.randomArray(3, () => this.randomIP());
this.commonInfo = newInfo;
}
/**
* Updates the current Host common record applied Policy to a different one from the list
* of random choices and gives it a random policy response status.
*
*/
public updateHostPolicyData({
excludeInitialPolicy = false,
}: Partial<{
/** Excludes the initial policy id (non-existent) that endpoint reports when it first is installed */
excludeInitialPolicy: boolean;
}> = {}) {
const newInfo = this.commonInfo;
newInfo.Endpoint.policy.applied = this.randomChoice(
excludeInitialPolicy
? APPLIED_POLICIES.filter(({ id }) => id !== '00000000-0000-0000-0000-000000000000')
: APPLIED_POLICIES
);
newInfo.Endpoint.policy.applied.status = this.randomChoice(POLICY_RESPONSE_STATUSES);
this.commonInfo = newInfo;
}
/**
* Update the common host metadata - essentially creating an entire new endpoint metadata record
* when the `.generateHostMetadata()` is subsequently called
*/
public updateCommonInfo() {
this.commonInfo = this.createHostData();
}
/**
* Parses an index and returns the data stream fields extracted from the index.
*
* @param index the index name to parse into the data stream parts
*/
public static createDataStreamFromIndex(index: string): DataStream {
// e.g. logs-endpoint.events.network-default
const parts = index.split('-');
return {
type: parts[0], // logs
dataset: parts[1], // endpoint.events.network
namespace: parts[2], // default
};
}
private createHostData(): CommonHostInfo {
const { agent, elastic, host, Endpoint } = this.metadataGenerator.generate({
Endpoint: {
policy: { applied: this.randomChoice(APPLIED_POLICIES) },
},
});
return { agent, elastic, host, Endpoint };
}
/**
* Creates a host metadata document
* @param ts - Timestamp to put in the event
* @param metadataDataStream the values to populate the data_stream fields when generating metadata documents
*/
public generateHostMetadata(
ts = new Date().getTime(),
metadataDataStream = metadataDefaultDataStream()
): HostMetadata {
return clone(
this.metadataGenerator.generate({
'@timestamp': ts,
data_stream: metadataDataStream,
...this.commonInfo,
})
);
}
/**
* Creates a malware 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
* @param ancestry - an array of ancestors for the generated alert
* @param alertsDataStream the values to populate the data_stream fields when generating alert documents
*/
public generateMalwareAlert({
ts = new Date().getTime(),
sessionEntryLeader = this.randomString(10),
entityID = this.randomString(10),
parentEntityID,
ancestry = [],
alertsDataStream = alertsDefaultDataStream,
}: {
ts?: number;
sessionEntryLeader?: string;
entityID?: string;
parentEntityID?: string;
ancestry?: string[];
alertsDataStream?: DataStream;
} = {}): AlertEvent {
return {
...this.commonInfo,
data_stream: alertsDataStream,
'@timestamp': ts,
ecs: {
version: '1.4.0',
},
event: {
action: this.randomChoice(FILE_OPERATIONS),
kind: 'alert',
category: 'malware',
code: 'malicious_file',
id: this.seededUUIDv4(),
dataset: 'endpoint',
module: 'endpoint',
type: 'creation',
sequence: this.sequence++,
},
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',
},
Ext: {
code_signature: [
{
trusted: false,
subject_name: 'bad signer',
},
{
trusted: true,
subject_name: 'a good signer',
},
],
malware_classification: {
identifier: 'endpointpe',
score: 1,
threshold: 0.66,
version: '3.0.33',
},
temp_file_path: 'C:/temp/fake_malware.exe',
quarantine_result: true,
quarantine_message: 'fake quarantine message',
},
},
process: {
pid: 2,
name: 'malware writer',
start: ts,
uptime: 0,
entity_id: entityID,
executable: 'C:/malware.exe',
parent: parentEntityID ? { entity_id: parentEntityID, pid: 1 } : undefined,
hash: {
md5: 'fake md5',
sha1: 'fake sha1',
sha256: 'fake sha256',
},
entry_leader: {
entity_id: sessionEntryLeader,
name: 'fake entry',
pid: Math.floor(Math.random() * 1000),
start: [new Date(0).toISOString()],
},
session_leader: {
entity_id: sessionEntryLeader,
name: 'fake session',
pid: Math.floor(Math.random() * 1000),
},
group_leader: {
entity_id: sessionEntryLeader,
name: 'fake leader',
pid: Math.floor(Math.random() * 1000),
},
Ext: {
ancestry,
code_signature: [
{
trusted: false,
subject_name: 'bad signer',
},
],
user: 'SYSTEM',
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',
},
},
},
dll: this.getAlertsDefaultDll(),
user: {
domain: this.randomString(10),
name: this.randomString(10),
},
};
}
/**
* Creates a memory 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
* @param ancestry - an array of ancestors for the generated alert
* @param alertsDataStream the values to populate the data_stream fields when generating alert documents
*/
public generateMemoryAlert({
ts = new Date().getTime(),
sessionEntryLeader = this.randomString(10),
entityID = this.randomString(10),
parentEntityID,
ancestry = [],
alertsDataStream = alertsDefaultDataStream,
alertType,
}: {
ts?: number;
sessionEntryLeader?: string;
entityID?: string;
parentEntityID?: string;
ancestry?: string[];
alertsDataStream?: DataStream;
alertType?: AlertTypes;
} = {}): AlertEvent {
const processName = this.randomProcessName();
const isShellcode = alertType === AlertTypes.MEMORY_SHELLCODE;
const newAlert: AlertEvent = {
...this.commonInfo,
data_stream: alertsDataStream,
'@timestamp': ts,
ecs: {
version: '1.6.0',
},
// disabling naming-convention to accommodate external field
// eslint-disable-next-line @typescript-eslint/naming-convention
Memory_protection: {
feature: isShellcode ? 'shellcode_thread' : 'signature',
self_injection: true,
},
event: {
action: 'start',
kind: 'alert',
category: 'malware',
code: isShellcode ? 'shellcode_thread' : 'memory_signature',
id: this.seededUUIDv4(),
dataset: 'endpoint',
module: 'endpoint',
type: 'info',
sequence: this.sequence++,
},
file: {},
process: {
pid: 2,
name: processName,
start: ts,
uptime: 0,
entity_id: entityID,
executable: `C:/fake/${processName}`,
parent: parentEntityID ? { entity_id: parentEntityID, pid: 1 } : undefined,
hash: {
md5: 'fake md5',
sha1: 'fake sha1',
sha256: 'fake sha256',
},
entry_leader: {
entity_id: sessionEntryLeader,
name: 'fake entry',
pid: Math.floor(Math.random() * 1000),
},
session_leader: {
entity_id: sessionEntryLeader,
name: 'fake session',
pid: Math.floor(Math.random() * 1000),
},
group_leader: {
entity_id: sessionEntryLeader,
name: 'fake leader',
pid: Math.floor(Math.random() * 1000),
},
Ext: {
ancestry,
code_signature: [
{
trusted: false,
subject_name: 'bad signer',
},
],
user: 'SYSTEM',
token: {
integrity_level_name: 'high',
},
malware_signature: {
all_names: 'Windows.Trojan.FakeAgent',
identifier: 'diagnostic-malware-signature-v1-fake',
},
},
},
dll: this.getAlertsDefaultDll(),
user: {
domain: this.randomString(10),
name: this.randomString(10),
},
};
// shellcode_thread memory alert have an additional process field
if (isShellcode) {
newAlert.Target = {
process: {
thread: {
Ext: {
start_address_allocation_offset: 0,
start_address_bytes_disasm_hash: 'a disam hash',
start_address_details: {
allocation_type: 'PRIVATE',
allocation_size: 4000,
region_size: 4000,
region_protection: 'RWX',
memory_pe: {
imphash: 'a hash',
},
},
},
},
},
};
}
return newAlert;
}
/**
* 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
* @param ancestry - an array of ancestors for the generated alert
* @param alertsDataStream the values to populate the data_stream fields when generating alert documents
*/
public generateAlert({
ts = new Date().getTime(),
entityID = this.randomString(10),
sessionEntryLeader,
parentEntityID,
ancestry = [],
alertsDataStream = alertsDefaultDataStream,
}: {
ts?: number;
entityID?: string;
sessionEntryLeader?: string;
parentEntityID?: string;
ancestry?: string[];
alertsDataStream?: DataStream;
} = {}): AlertEvent {
const alertType = this.randomChoice(Object.values(AlertTypes));
switch (alertType) {
case AlertTypes.MALWARE:
return this.generateMalwareAlert({
ts,
sessionEntryLeader,
entityID,
parentEntityID,
ancestry,
alertsDataStream,
});
case AlertTypes.MEMORY_SIGNATURE:
case AlertTypes.MEMORY_SHELLCODE:
return this.generateMemoryAlert({
ts,
entityID,
sessionEntryLeader,
parentEntityID,
ancestry,
alertsDataStream,
alertType,
});
case AlertTypes.BEHAVIOR:
return this.generateBehaviorAlert({
ts,
sessionEntryLeader,
entityID,
parentEntityID,
ancestry,
alertsDataStream,
});
default:
return assertNever(alertType);
}
}
/**
* Creates a memory 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
* @param ancestry - an array of ancestors for the generated alert
* @param alertsDataStream the values to populate the data_stream fields when generating alert documents
*/
public generateBehaviorAlert({
ts = new Date().getTime(),
sessionEntryLeader = this.randomString(10),
entityID = this.randomString(10),
parentEntityID,
ancestry = [],
alertsDataStream = alertsDefaultDataStream,
}: {
ts?: number;
sessionEntryLeader?: string;
entityID?: string;
parentEntityID?: string;
ancestry?: string[];
alertsDataStream?: DataStream;
} = {}): AlertEvent {
const processName = this.randomProcessName();
const newAlert: AlertEvent = {
...this.commonInfo,
data_stream: alertsDataStream,
'@timestamp': ts,
ecs: {
version: '1.6.0',
},
rule: {
id: this.randomUUID(),
description: 'Behavior rule description',
},
event: {
action: 'rule_detection',
kind: 'alert',
category: 'behavior',
code: 'behavior',
id: this.seededUUIDv4(),
dataset: 'endpoint.diagnostic.collection',
module: 'endpoint',
type: 'info',
sequence: this.sequence++,
},
file: {
name: 'fake_behavior.exe',
path: 'C:/fake_behavior.exe',
},
destination: {
port: 443,
ip: this.randomIP(),
},
source: {
port: 59406,
ip: this.randomIP(),
},
network: {
transport: 'tcp',
type: 'ipv4',
direction: 'outgoing',
},
registry: {
path: 'HKEY_USERS\\S-1-5-21-2460036010-3910878774-3458087990-1001\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\chrome',
value: processName,
data: {
strings: `C:/fake_behavior/${processName}`,
},
},
process: {
pid: 2,
name: processName,
entity_id: entityID,
executable: `C:/fake_behavior/${processName}`,
code_signature: {
status: 'trusted',
subject_name: 'Microsoft Windows',
},
entry_leader: {
entity_id: sessionEntryLeader,
name: 'fake entry',
pid: Math.floor(Math.random() * 1000),
},
session_leader: {
entity_id: sessionEntryLeader,
name: 'fake session',
pid: Math.floor(Math.random() * 1000),
},
group_leader: {
entity_id: sessionEntryLeader,
name: 'fake leader',
pid: Math.floor(Math.random() * 1000),
},
parent: parentEntityID
? {
entity_id: parentEntityID,
pid: 1,
}
: undefined,
Ext: {
ancestry,
code_signature: [
{
trusted: false,
subject_name: 'bad signer',
},
{
trusted: true,
subject_name: 'good signer',
},
],
user: 'SYSTEM',
token: {
integrity_level_name: 'high',
elevation_level: 'full',
},
},
},
dll: this.getAlertsDefaultDll(),
user: {
domain: this.randomString(10),
name: this.randomString(10),
},
};
return newAlert;
}
/**
* Returns the default DLLs used in alerts
*/
private getAlertsDefaultDll() {
return {
pe: {
architecture: 'x64',
},
code_signature: {
subject_name: 'Cybereason Inc',
trusted: true,
},
hash: {
md5: '1f2d082566b0fc5f2c238a5180db7451',
sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d',
sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2',
},
path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe',
Ext: {
compile_time: 1534424710,
mapped_address: 5362483200,
mapped_size: 0,
malware_classification: {
identifier: 'Whitelisted',
score: 0,
threshold: 0,
version: '3.0.0',
},
},
};
}
/**
* Creates an event, customized by the options parameter
* @param options - Allows event field values to be specified
*/
public generateEvent(options: EventOptions = {}): Event {
// this will default to an empty array for the ancestry field if options.ancestry isn't included
const ancestry: string[] =
options.ancestry?.slice(0, options?.ancestryArrayLimit ?? ANCESTRY_LIMIT) ?? [];
const processName = options.processName ? options.processName : this.randomProcessName();
const sessionEntryLeader = options.sessionEntryLeader
? options.sessionEntryLeader
: this.randomString(10);
const userName = this.randomString(10);
const detailRecordForEventType =
options.extensions ||
((eventCategory) => {
if (eventCategory === 'registry') {
return { registry: { key: `HKLM/Windows/Software/${this.randomString(5)}` } };
}
if (eventCategory === 'network') {
return {
network: {
direction: this.randomChoice(['inbound', 'outbound']),
forwarded_ip: `${this.randomIP()}`,
},
};
}
if (eventCategory === 'file') {
return { file: { path: 'C:\\My Documents\\business\\January\\processName' } };
}
if (eventCategory === 'dns') {
return { dns: { question: { name: `${this.randomIP()}` } } };
}
return {};
})(options.eventCategory);
return {
data_stream: options?.eventsDataStream ?? eventsDefaultDataStream,
'@timestamp': options.timestamp ? options.timestamp : new Date().getTime(),
agent: { ...this.commonInfo.agent, type: 'endpoint' },
ecs: {
version: '1.4.0',
},
...detailRecordForEventType,
event: {
category: options.eventCategory ? options.eventCategory : ['process'],
outcome: options.eventCategory?.includes('authentication')
? this.randomChoice(['success', 'failure'])
: '',
kind: 'event',
type: options.eventType ? options.eventType : ['start'],
id: this.seededUUIDv4(),
sequence: this.sequence++,
},
host: this.commonInfo.host,
process: {
pid:
'pid' in options && typeof options.pid !== 'undefined' ? options.pid : this.randomN(5000),
executable: `C:\\${processName}`,
args: [`"C:\\${processName}"`, `--${this.randomString(3)}`],
working_directory: `/home/${userName}/`,
code_signature: {
status: 'trusted',
subject_name: 'Microsoft',
},
hash: { md5: this.seededUUIDv4() },
entity_id: options.entityID ? options.entityID : this.randomString(10),
entry_leader: {
entity_id: sessionEntryLeader,
name: 'fake entry',
pid: Math.floor(Math.random() * 1000),
start: [new Date(0).toISOString()],
},
session_leader: {
entity_id: sessionEntryLeader,
name: 'fake session',
pid: Math.floor(Math.random() * 1000),
},
group_leader: {
entity_id: sessionEntryLeader,
name: 'fake leader',
pid: Math.floor(Math.random() * 1000),
},
parent: options.parentEntityID
? {
entity_id: options.parentEntityID,
pid:
'parentPid' in options && typeof options.parentPid !== 'undefined'
? options.parentPid
: this.randomN(5000),
}
: undefined,
name: processName,
// simulate a finite ancestry array size, the endpoint limits the ancestry array to 20 entries we'll use
// 2 so that the backend can handle that case
Ext: {
ancestry,
},
},
user: {
domain: this.randomString(10),
name: userName,
},
};
}
private static getStartEndTimes(events: Event[]): { startTime: Date; endTime: Date } {
let startTime: number;
let endTime: number;
if (events.length > 0) {
startTime = timestampSafeVersion(events[0]) ?? new Date().getTime();
endTime = startTime;
} else {
startTime = new Date().getTime();
endTime = startTime;
}
for (const event of events) {
const eventTimestamp = timestampSafeVersion(event);
if (eventTimestamp !== undefined) {
if (eventTimestamp < startTime) {
startTime = eventTimestamp;
}
if (eventTimestamp > endTime) {
endTime = eventTimestamp;
}
}
}
return {
startTime: new Date(startTime),
endTime: new Date(endTime),
};
}
/**
* This generates a full resolver tree and keeps the entire tree in memory. This is useful for tests that want
* to compare results from elasticsearch with the actual events created by this generator. Because all the events
* are stored in memory do not use this function to generate large trees.
*
* @param options - options for the layout of the tree, like how many children, generations, and ancestry
* @returns a Tree structure that makes accessing specific events easier
*/
public generateTree(options: TreeOptions = {}): Tree {
const optionsWithDef = getTreeOptionsWithDef(options);
const addEventToMap = (nodeMap: Map<string, TreeNode>, event: Event) => {
const nodeID = entityIDSafeVersion(event);
if (!nodeID) {
return nodeMap;
}
// if a node already exists for the entity_id we'll use that one, otherwise let's create a new empty node
// and add the event to the right array.
let node = nodeMap.get(nodeID);
if (!node) {
node = { id: nodeID, lifecycle: [], relatedEvents: [], relatedAlerts: [] };
}
// place the event in the right array depending on its category
if (firstNonNullValue(event.event?.kind) === 'event') {
if (firstNonNullValue(event.event?.category) === 'process') {
node.lifecycle.push(event);
} else {
node.relatedEvents.push(event);
}
} else if (firstNonNullValue(event.event?.kind) === 'alert') {
node.relatedAlerts.push(event);
}
return nodeMap.set(nodeID, node);
};
const groupNodesByParent = (children: Map<string, TreeNode>) => {
const nodesByParent: Map<string, Map<string, TreeNode>> = new Map();
for (const node of children.values()) {
const parentID = parentEntityIDSafeVersion(node.lifecycle[0]);
if (parentID) {
let groupedNodes = nodesByParent.get(parentID);
if (!groupedNodes) {
groupedNodes = new Map();
nodesByParent.set(parentID, groupedNodes);
}
groupedNodes.set(node.id, node);
}
}
return nodesByParent;
};
const createLevels = (
childrenByParent: Map<string, Map<string, TreeNode>>,
levels: Array<Map<string, TreeNode>>,
currentNodes: Map<string, TreeNode> | undefined
): Array<Map<string, TreeNode>> => {
if (!currentNodes || currentNodes.size === 0) {
return levels;
}
levels.push(currentNodes);
const nextLevel: Map<string, TreeNode> = new Map();
for (const node of currentNodes.values()) {
const children = childrenByParent.get(node.id);
if (children) {
for (const child of children.values()) {
nextLevel.set(child.id, child);
}
}
}
return createLevels(childrenByParent, levels, nextLevel);
};
const ancestry = this.createAlertEventAncestry(optionsWithDef);
// create a mapping of entity_id -> {lifecycle, related events, and related alerts}
const ancestryNodes: Map<string, TreeNode> = ancestry.reduce(addEventToMap, new Map());
const alert = ancestry[ancestry.length - 1];
const alertEntityID = entityIDSafeVersion(alert);
if (!alertEntityID) {
throw Error("could not find the originating alert's entity id");
}
const origin = ancestryNodes.get(alertEntityID);
if (!origin) {
throw Error(`could not find origin while building tree: ${alertEntityID}`);
}
const children = Array.from(this.descendantsTreeGenerator(alert, optionsWithDef));
const childrenNodes: Map<string, TreeNode> = children.reduce(addEventToMap, new Map());
const childrenByParent = groupNodesByParent(childrenNodes);
const levels = createLevels(childrenByParent, [], childrenByParent.get(origin.id));
const allEvents = [...ancestry, ...children];
const { startTime, endTime } = EndpointDocGenerator.getStartEndTimes(allEvents);
return {
childrenByParent,
children: childrenNodes,
ancestry: ancestryNodes,
allEvents,
origin,
childrenLevels: levels,
startTime,
endTime,
agentId: this.commonInfo.agent.id,
};
}
/**
* Wrapper generator for fullResolverTreeGenerator to make it easier to quickly stream
* many resolver trees to Elasticsearch.
* @param numAlerts - number of alerts to generate
* @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 relatedAlertsPerNode - number of alerts to generate for each node, if this is 0 an alert will still be generated for the origin node
* @param percentNodesWithRelated - percent of nodes which should have related events
* @param percentTerminated - percent of nodes which will have process termination events
* @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children
*/
public *alertsGenerator(numAlerts: number, options: TreeOptions = {}) {
const opts = getTreeOptionsWithDef(options);
for (let i = 0; i < numAlerts; i++) {
// 1 session per resolver tree
const sessionEntryLeader = this.randomString(10);
yield* this.fullResolverTreeGenerator({ ...opts, sessionEntryLeader });
}
}
/**
* 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 - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node
* or a number which defines the number of related events and will default to random categories
* @param relatedAlertsPerNode - number of alerts to generate for each node, if this is 0 an alert will still be generated for the origin node
* @param percentNodesWithRelated - percent of nodes which should have related events
* @param percentTerminated - percent of nodes which will have process termination events
* @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children
*/
public *fullResolverTreeGenerator(options: TreeOptions = {}) {
const opts = getTreeOptionsWithDef(options);
const ancestry = this.createAlertEventAncestry(opts);
for (let i = 0; i < ancestry.length; i++) {
yield ancestry[i];
}
// ancestry will always have at least 2 elements, and the last element will be the alert
yield* this.descendantsTreeGenerator(ancestry[ancestry.length - 1], opts);
}
/**
* 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
* @param relatedEventsPerNode - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node
* or a number which defines the number of related events and will default to random categories
* @param relatedAlertsPerNode - number of alerts to generate for each node, if this is 0 an alert will still be generated for the origin node
* @param pctWithRelated - percent of ancestors that will have related events and alerts
* @param pctWithTerminated - percent of ancestors that will have termination events
*/
public createAlertEventAncestry(options: TreeOptions = {}): Event[] {
const opts = getTreeOptionsWithDef(options);
const events = [];
const startDate = new Date().getTime();
const root = this.generateEvent({
timestamp: startDate + 1000,
entityID: opts.sessionEntryLeader,
sessionEntryLeader: opts.sessionEntryLeader,
eventsDataStream: opts.eventsDataStream,
});
events.push(root);
let ancestor = root;
let timestamp = (timestampSafeVersion(root) ?? 0) + 1000;
const addRelatedAlerts = (
node: Event,
alertsPerNode: number,
secBeforeAlert: number,
eventList: Event[]
) => {
for (const relatedAlert of this.relatedAlertsGenerator({
node,
relatedAlerts: alertsPerNode,
alertCreationTime: secBeforeAlert,
sessionEntryLeader: opts.sessionEntryLeader,
alertsDataStream: opts.alertsDataStream,
})) {
eventList.push(relatedAlert);
}
};
const addRelatedEvents = (node: Event, secBeforeEvent: number, eventList: Event[]) => {
for (const relatedEvent of this.relatedEventsGenerator({
node,
relatedEvents: opts.relatedEvents,
processDuration: secBeforeEvent,
ordered: opts.relatedEventsOrdered,
sessionEntryLeader: opts.sessionEntryLeader,
eventsDataStream: opts.eventsDataStream,
})) {
eventList.push(relatedEvent);
}
};
// generate related alerts for root
const processDuration: number = 6 * 3600;
if (this.randomN(100) < opts.percentWithRelated) {
addRelatedEvents(ancestor, processDuration, events);
addRelatedAlerts(ancestor, opts.relatedAlerts, processDuration, events);
}
// generate the termination event for the root
if (this.randomN(100) < opts.percentTerminated) {
const termProcessDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days)
events.push(
this.generateEvent({
timestamp: timestamp + termProcessDuration * 1000,
entityID: entityIDSafeVersion(root),
parentEntityID: parentEntityIDSafeVersion(root),
sessionEntryLeader: opts.sessionEntryLeader,
eventCategory: ['process'],
eventType: ['end'],
eventsDataStream: opts.eventsDataStream,
processName: processNameSafeVersion(root),
})
);
}
for (let i = 0; i < opts.ancestors; i++) {
const ancestorEntityID = entityIDSafeVersion(ancestor);
const ancestry: string[] = [];
if (ancestorEntityID) {
ancestry.push(ancestorEntityID);
}
ancestry.push(...(ancestryArray(ancestor) ?? []));
ancestor = this.generateEvent({
timestamp,
parentEntityID: entityIDSafeVersion(ancestor),
sessionEntryLeader: opts.sessionEntryLeader,
// add the parent to the ancestry array
ancestry,
ancestryArrayLimit: opts.ancestryArraySize,
parentPid: firstNonNullValue(ancestor.process?.pid),
pid: this.randomN(5000),
eventsDataStream: opts.eventsDataStream,
});
events.push(ancestor);
timestamp = timestamp + 1000;
if (this.randomN(100) < opts.percentTerminated) {
const termProcessDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days)
events.push(
this.generateEvent({
timestamp: timestamp + termProcessDuration * 1000,
entityID: entityIDSafeVersion(ancestor),
parentEntityID: parentEntityIDSafeVersion(ancestor),
sessionEntryLeader: opts.sessionEntryLeader,
eventCategory: ['process'],
eventType: ['end'],
ancestry: ancestryArray(ancestor),
ancestryArrayLimit: opts.ancestryArraySize,
eventsDataStream: opts.eventsDataStream,
processName: processNameSafeVersion(ancestor),
})
);
}
// generate related alerts for ancestor
if (this.randomN(100) < opts.percentWithRelated) {
addRelatedEvents(ancestor, processDuration, events);
let numAlertsPerNode = opts.relatedAlerts;
// if this is the last ancestor, create one less related alert so that we have a uniform amount of related alerts
// for each node. The last alert at the end of this function should always be created even if the related alerts
// amount is 0
if (i === opts.ancestors - 1) {
numAlertsPerNode -= 1;
}
addRelatedAlerts(ancestor, numAlertsPerNode, processDuration, events);
}
}
timestamp = timestamp + 1000;
events.push(
this.generateAlert({
ts: timestamp,
entityID: entityIDSafeVersion(ancestor),
parentEntityID: parentEntityIDSafeVersion(ancestor),
sessionEntryLeader: opts.sessionEntryLeader,
ancestry: ancestryArray(ancestor),
alertsDataStream: opts.alertsDataStream,
})
);
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 - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node
* or a number which defines the number of related events and will default to random categories
* @param percentNodesWithRelated - percent of nodes which should have related events
* @param percentChildrenTerminated - percent of nodes which will have process termination events
* @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children
*/
public *descendantsTreeGenerator(root: Event, options: TreeOptions = {}) {
const opts = getTreeOptionsWithDef(options);
let maxChildren = this.randomN(opts.children + 1);
if (opts.alwaysGenMaxChildrenPerNode) {
maxChildren = opts.children;
}
const rootState: NodeState = {
event: root,
childrenCreated: 0,
maxChildren,
};
const lineage: NodeState[] = [rootState];
let timestamp = timestampSafeVersion(root) ?? 0;
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 === opts.generations + 1
) {
lineage.pop();
// eslint-disable-next-line no-continue
continue;
}
// Otherwise, add a child and any nodes associated with it
currentState.childrenCreated++;
timestamp = timestamp + 1000;
const currentStateEntityID = entityIDSafeVersion(currentState.event);
const ancestry: string[] = [];
if (currentStateEntityID) {
ancestry.push(currentStateEntityID);
}
ancestry.push(...(ancestryArray(currentState.event) ?? []));
const child = this.generateEvent({
timestamp,
parentEntityID: currentStateEntityID,
sessionEntryLeader: opts.sessionEntryLeader,
ancestry,
ancestryArrayLimit: opts.ancestryArraySize,
eventsDataStream: opts.eventsDataStream,
});
maxChildren = this.randomN(opts.children + 1);
if (opts.alwaysGenMaxChildrenPerNode) {
maxChildren = opts.children;
}
lineage.push({
event: child,
childrenCreated: 0,
maxChildren,
});
yield child;
let processDuration: number = 6 * 3600;
if (this.randomN(100) < opts.percentTerminated) {
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: entityIDSafeVersion(child),
sessionEntryLeader: opts.sessionEntryLeader,
parentEntityID: parentEntityIDSafeVersion(child),
eventCategory: ['process'],
eventType: ['end'],
ancestry,
ancestryArrayLimit: opts.ancestryArraySize,
eventsDataStream: opts.eventsDataStream,
});
}
if (this.randomN(100) < opts.percentWithRelated) {
yield* this.relatedEventsGenerator({
node: child,
relatedEvents: opts.relatedEvents,
sessionEntryLeader: opts.sessionEntryLeader,
processDuration,
ordered: opts.relatedEventsOrdered,
eventsDataStream: opts.eventsDataStream,
});
yield* this.relatedAlertsGenerator({
node: child,
relatedAlerts: opts.relatedAlerts,
sessionEntryLeader: opts.sessionEntryLeader,
alertCreationTime: processDuration,
alertsDataStream: opts.alertsDataStream,
});
}
}
}
/**
* Creates related events for a process event
* @param node - process event to relate events to by entityID
* @param relatedEvents - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node
* or a number which defines the number of related events and will default to random categories
* @param processDuration - maximum number of seconds after process event that related event timestamp can be
* @param ordered - if true the events will have an increasing timestamp, otherwise their timestamp will be random but
* guaranteed to be greater than or equal to the originating event
*/
public *relatedEventsGenerator({
node,
relatedEvents = 10,
processDuration = 6 * 3600,
ordered = false,
sessionEntryLeader,
eventsDataStream = eventsDefaultDataStream,
}: {
node: Event;
relatedEvents?: RelatedEventInfo[] | number;
processDuration?: number;
sessionEntryLeader: string;
ordered?: boolean;
eventsDataStream?: DataStream;
}) {
let relatedEventsInfo: RelatedEventInfo[];
const nodeTimestamp = timestampSafeVersion(node) ?? 0;
let ts = nodeTimestamp + 1;
if (typeof relatedEvents === 'number') {
relatedEventsInfo = [{ category: RelatedEventCategory.Random, count: relatedEvents }];
} else {
relatedEventsInfo = relatedEvents;
}
for (const event of relatedEventsInfo) {
let eventInfo: EventInfo;
for (let i = 0; i < event.count; i++) {
if (event.category === RelatedEventCategory.Random) {
eventInfo = this.randomChoice(Object.values(OTHER_EVENT_CATEGORIES));
} else {
eventInfo = OTHER_EVENT_CATEGORIES[event.category];
}
if (ordered) {
ts += this.randomN(processDuration) * 1000;
} else {
ts = nodeTimestamp + this.randomN(processDuration) * 1000;
}
yield this.generateEvent({
timestamp: ts,
entityID: entityIDSafeVersion(node),
sessionEntryLeader,
parentEntityID: parentEntityIDSafeVersion(node),
eventCategory: eventInfo.category,
eventType: eventInfo.creationType,
ancestry: ancestryArray(node),
eventsDataStream,
});
}
}
}
/**
* Creates related alerts for a process event
* @param node - process event to relate alerts to by entityID
* @param relatedAlerts - number which defines the number of related alerts to create
* @param alertCreationTime - maximum number of seconds after process event that related alert timestamp can be
*/
public *relatedAlertsGenerator({
node,
relatedAlerts = 3,
alertCreationTime = 6 * 3600,
alertsDataStream = alertsDefaultDataStream,
sessionEntryLeader,
}: {
node: Event;
relatedAlerts: number;
alertCreationTime: number;
alertsDataStream: DataStream;
sessionEntryLeader: string;
}) {
for (let i = 0; i < relatedAlerts; i++) {
const ts = (timestampSafeVersion(node) ?? 0) + this.randomN(alertCreationTime) * 1000;
yield this.generateAlert({
ts,
entityID: entityIDSafeVersion(node),
sessionEntryLeader,
parentEntityID: parentEntityIDSafeVersion(node),
ancestry: ancestryArray(node),
alertsDataStream,
});
}
}
/**
* Generates a Fleet `package policy` that includes the Endpoint Policy data
*/
public generatePolicyPackagePolicy({
seed,
overrides,
}: {
seed?: string;
overrides?: PartialEndpointPolicyData;
} = {}): PolicyData {
return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy(overrides);
}
/**
* Generate an Agent Policy (Fleet)
*/
public generateAgentPolicy(): GetAgentPoliciesResponseItem {
// FIXME: remove and use new FleetPackagePolicyGenerator (#2262)
return {
id: this.seededUUIDv4(),
name: 'Agent Policy',
status: agentPolicyStatuses.Active,
description: 'Some description',
namespace: 'default',
is_managed: false,
monitoring_enabled: ['logs', 'metrics'],
revision: 2,
updated_at: '2020-07-22T16:36:49.196Z',
updated_by: 'elastic',
agents: 0,
is_protected: false,
};
}
/**
* Generate a Fleet EPM Package for Endpoint
*/
public generateEpmPackage(): GetPackagesResponse['items'][0] {
return {
id: this.seededUUIDv4(),
name: 'endpoint',
title: 'Elastic Endpoint',
version: '0.5.0',
description: 'This is the Elastic Endpoint package.',
type: 'integration',
download: '/epr/endpoint/endpoint-0.5.0.tar.gz',
path: '/package/endpoint/0.5.0',
icons: [
{
path: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg',
src: '/img/logo-endpoint-64-color.svg',
size: '16x16',
type: 'image/svg+xml',
},
],
status: 'installed',
release: 'ga',
savedObject: {
type: 'epm-packages',
id: 'endpoint',
attributes: {
installed_kibana: [
{ id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' },
{ id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' },
] as KibanaAssetReference[],
installed_es: [
{ id: 'logs-endpoint.alerts', type: 'index_template' },
{ id: 'events-endpoint', type: 'index_template' },
{ id: 'logs-endpoint.events.file', type: 'index_template' },
{ id: 'logs-endpoint.events.library', type: 'index_template' },
{ id: 'metrics-endpoint.metadata', type: 'index_template' },
{ id: 'metrics-endpoint.metadata_mirror', type: 'index_template' },
{ id: 'logs-endpoint.events.network', type: 'index_template' },
{ id: 'metrics-endpoint.policy', type: 'index_template' },
{ id: 'logs-endpoint.events.process', type: 'index_template' },
{ id: 'logs-endpoint.events.registry', type: 'index_template' },
{ id: 'logs-endpoint.events.security', type: 'index_template' },
{ id: 'metrics-endpoint.telemetry', type: 'index_template' },
] as EsAssetReference[],
package_assets: [],
es_index_patterns: {
alerts: 'logs-endpoint.alerts-*',
events: 'events-endpoint-*',
file: 'logs-endpoint.events.file-*',
library: 'logs-endpoint.events.library-*',
metadata: 'metrics-endpoint.metadata-*',
metadata_mirror: 'metrics-endpoint.metadata_mirror-*',
network: 'logs-endpoint.events.network-*',
policy: 'metrics-endpoint.policy-*',
process: 'logs-endpoint.events.process-*',
registry: 'logs-endpoint.events.registry-*',
security: 'logs-endpoint.events.security-*',
telemetry: 'metrics-endpoint.telemetry-*',
},
name: 'endpoint',
version: '0.5.0',
internal: false,
install_version: '0.5.0',
install_status: 'installed',
install_started_at: '2020-06-24T14:41:23.098Z',
install_source: 'registry',
keep_policies_up_to_date: false,
verification_status: 'unknown',
},
references: [],
updated_at: '2020-06-24T14:41:23.098Z',
version: 'Wzc0LDFd',
},
};
}
/**
* Generate a Fleet EPM Package for Endpoint
*/
public generateEpmPackageInfo(): GetInfoResponse['item'] {
return {
name: 'endpoint',
title: 'Elastic Endpoint',
version: '0.5.0',
description: 'This is the Elastic Endpoint package.',
type: 'integration',
download: '/epr/endpoint/endpoint-0.5.0.tar.gz',
path: '/package/endpoint/0.5.0',
format_version: '',
owner: { github: '' },
latestVersion: '',
assets: {} as AssetsGroupedByServiceByType,
icons: [
{
path: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg',
src: '/img/logo-endpoint-64-color.svg',
size: '16x16',
type: 'image/svg+xml',
},
],
status: 'installed',
release: 'ga',
savedObject: {
type: 'epm-packages',
id: 'endpoint',
attributes: {
installed_kibana: [
{ id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' },
{ id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
{ id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' },
] as KibanaAssetReference[],
installed_es: [
{ id: 'logs-endpoint.alerts', type: 'index_template' },
{ id: 'events-endpoint', type: 'index_template' },
{ id: 'logs-endpoint.events.file', type: 'index_template' },
{ id: 'logs-endpoint.events.library', type: 'index_template' },
{ id: 'metrics-endpoint.metadata', type: 'index_template' },
{ id: 'metrics-endpoint.metadata_mirror', type: 'index_template' },
{ id: 'logs-endpoint.events.network', type: 'index_template' },
{ id: 'metrics-endpoint.policy', type: 'index_template' },
{ id: 'logs-endpoint.events.process', type: 'index_template' },
{ id: 'logs-endpoint.events.registry', type: 'index_template' },
{ id: 'logs-endpoint.events.security', type: 'index_template' },
{ id: 'metrics-endpoint.telemetry', type: 'index_template' },
] as EsAssetReference[],
package_assets: [],
es_index_patterns: {
alerts: 'logs-endpoint.alerts-*',
events: 'events-endpoint-*',
file: 'logs-endpoint.events.file-*',
library: 'logs-endpoint.events.library-*',
metadata: 'metrics-endpoint.metadata-*',
metadata_mirror: 'metrics-endpoint.metadata_mirror-*',
network: 'logs-endpoint.events.network-*',
policy: 'metrics-endpoint.policy-*',
process: 'logs-endpoint.events.process-*',
registry: 'logs-endpoint.events.registry-*',
security: 'logs-endpoint.events.security-*',
telemetry: 'metrics-endpoint.telemetry-*',
},
name: 'endpoint',
version: '0.5.0',
internal: false,
install_version: '0.5.0',
install_status: 'installed',
install_started_at: '2020-06-24T14:41:23.098Z',
install_source: 'registry',
keep_policies_up_to_date: false,
verification_status: 'unknown',
},
references: [],
updated_at: '2020-06-24T14:41:23.098Z',
version: 'Wzc0LDFd',
},
};
}
/**
* Generates a Host Policy response message
*/
public generatePolicyResponse({
ts = new Date().getTime(),
allStatus,
policyDataStream = policyDefaultDataStream(),
}: {
ts?: number;
allStatus?: HostPolicyResponseActionStatus;
policyDataStream?: DataStream;
} = {}): HostPolicyResponse {
const policyVersion = this.randomN(10);
const status = () => {
return allStatus || this.randomHostPolicyResponseActionStatus();
};
return {
data_stream: policyDataStream,
'@timestamp': ts,
agent: {
id: this.commonInfo.agent.id,
version: '1.0.0-local.20200416.0',
},
elastic: {
agent: {
id: this.commonInfo.elastic.agent.id,
},
},
ecs: {
version: '1.4.0',
},
host: this.commonInfo.host,
Endpoint: {
policy: {
applied: {
actions: [
{
name: 'configure_elasticsearch_connection',
message: 'elasticsearch comes configured successfully',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'configure_kernel',
message: 'Failed to configure kernel',
status: HostPolicyResponseActionStatus.failure,
},
{
name: 'configure_logging',
message: 'Successfully configured logging',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'configure_malware',
message: 'Unexpected error configuring malware',
status: HostPolicyResponseActionStatus.failure,
},
{
name: 'connect_kernel',
message: 'Successfully initialized minifilter',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'detect_file_open_events',
message: 'Successfully stopped file open event reporting',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'detect_file_write_events',
message: 'Failed to stop file write event reporting',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'detect_image_load_events',
message: 'Successfully started image load event reporting',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'detect_process_events',
message: 'Successfully started process event reporting',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'download_global_artifacts',
message: 'Failed to download EXE model',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'load_config',
message: 'Successfully parsed configuration',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'load_malware_model',
message: 'Error deserializing EXE model; no valid malware model installed',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'read_elasticsearch_config',
message: 'Successfully read Elasticsearch configuration',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'read_events_config',
message: 'Successfully read events configuration',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'read_kernel_config',
message: 'Succesfully read kernel configuration',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'read_logging_config',
message: 'Field (logging.debugview) not found in config',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'read_malware_config',
message: 'Successfully read malware detect configuration',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'workflow',
message: 'Failed to apply a portion of the configuration (kernel)',
status: HostPolicyResponseActionStatus.unsupported,
},
{
name: 'download_model',
message: 'Failed to apply a portion of the configuration (kernel)',
status: HostPolicyResponseActionStatus.success,
},
{
name: 'ingest_events_config',
message: 'Failed to apply a portion of the configuration (kernel)',
status: HostPolicyResponseActionStatus.success,
},
],
id: this.commonInfo.Endpoint.policy.applied.id,
response: {
configurations: {
events: {
concerned_actions: ['download_model'],
status: status(),
},
logging: {
concerned_actions: this.randomHostPolicyResponseActionNames(),
status: status(),
},
malware: {
concerned_actions: this.randomHostPolicyResponseActionNames(),
status: status(),
},
streaming: {
concerned_actions: this.randomHostPolicyResponseActionNames(),
status: status(),
},
},
// TODO:PT refactor to use EndpointPolicyResponse Generator
} as HostPolicyResponse['Endpoint']['policy']['applied']['response'],
artifacts: {
global: {
version: '1.4.0',
identifiers: [
{
name: 'endpointpe-model',
sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
},
],
},
user: {
version: '1.4.0',
identifiers: [
{
name: 'user-model',
sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
},
],
},
},
status: this.commonInfo.Endpoint.policy.applied.status,
version: policyVersion,
name: this.commonInfo.Endpoint.policy.applied.name,
endpoint_policy_version:
this.commonInfo.Endpoint.policy.applied.endpoint_policy_version,
},
},
},
event: {
created: ts,
id: this.seededUUIDv4(),
kind: 'state',
category: ['host'],
type: ['change'],
module: 'endpoint',
action: 'endpoint_policy_response',
dataset: 'endpoint.policy',
},
};
}
private randomHostPolicyResponseActionNames(): string[] {
return this.randomArray(this.randomN(8), () =>
this.randomChoice([
'load_config',
'workflow',
'download_global_artifacts',
'configure_malware',
'read_malware_config',
'load_malware_model',
'read_kernel_config',
'configure_kernel',
'detect_process_events',
'detect_file_write_events',
'detect_file_open_events',
'detect_image_load_events',
'connect_kernel',
])
);
}
private randomHostPolicyResponseActionStatus(): HostPolicyResponseActionStatus {
return this.randomChoice([
HostPolicyResponseActionStatus.failure,
HostPolicyResponseActionStatus.success,
HostPolicyResponseActionStatus.warning,
HostPolicyResponseActionStatus.unsupported,
]);
}
/** Return a random fake process name */
private randomProcessName(): string {
return this.randomChoice(fakeProcessNames);
}
public randomIP(): string {
return super.randomIP();
}
}
const fakeProcessNames = [
'lsass.exe',
'notepad.exe',
'mimikatz.exe',
'powershell.exe',
'iexlorer.exe',
'explorer.exe',
];