Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Endpoint] Sample data generator for endpoint app #58936

Merged
merged 31 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
be66b5b
scaffolding and notes.md
Feb 24, 2020
fc0279f
add skeleton event generator to kibana
marshallmain Feb 24, 2020
1655df5
add optional entityID param to generateEvent
marshallmain Feb 25, 2020
9c0b912
add tree generation
marshallmain Feb 25, 2020
52efc27
add tests
marshallmain Feb 28, 2020
5073fdc
working tests
marshallmain Feb 29, 2020
c1bf5d8
fix up tests
marshallmain Mar 2, 2020
69a9221
merge upstream
marshallmain Mar 2, 2020
bdfd9b4
fix linting
marshallmain Mar 2, 2020
027929a
fix event types
marshallmain Mar 2, 2020
c333e87
make process parent types consistent
marshallmain Mar 2, 2020
cf96f10
make generator match types
marshallmain Mar 2, 2020
4bc37d4
move test resolver node out of common types
marshallmain Mar 2, 2020
bf6a055
fix random string generation
marshallmain Mar 2, 2020
f82ef50
merge upstream
marshallmain Mar 3, 2020
8a6bcbf
fix typecheck errors
marshallmain Mar 3, 2020
bfd320f
remove extraneous stuff
marshallmain Mar 3, 2020
9bf2bf3
address PR comments
marshallmain Mar 4, 2020
e1f6df2
add test for full resolver tree
marshallmain Mar 4, 2020
de3b9b7
cleanup
marshallmain Mar 5, 2020
3ead579
make tests clearer
marshallmain Mar 5, 2020
07fa69c
Merge branch 'master' into data-generator
marshallmain Mar 5, 2020
7006c4f
add seedrandom to endpoint plugin. contains DONOTMERGE example code
Mar 5, 2020
d1df9fd
remove robs test
marshallmain Mar 5, 2020
1ee15a0
start replacing random with seedrandom
marshallmain Mar 5, 2020
5f38e3a
Merge branch 'data-generator' of github.com:marshallmain/kibana into …
marshallmain Mar 5, 2020
aa77f8d
use seeded random for uuidv4
marshallmain Mar 6, 2020
ba621f6
separate out IP randomization
marshallmain Mar 6, 2020
041a34f
typecheck fixes
marshallmain Mar 6, 2020
469418e
Merge branch 'master' into data-generator
elasticmachine Mar 9, 2020
3f16642
Merge branch 'master' into data-generator
elasticmachine Mar 10, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions x-pack/plugins/endpoint/common/generate_data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* 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 { EndpointDocGenerator, Event } from './generate_data';

interface Node {
events: Event[];
children: Node[];
parent_entity_id?: string;
}

describe('data generator', () => {
let generator: EndpointDocGenerator;
beforeEach(() => {
generator = new EndpointDocGenerator('seed');
});

it('creates the same documents with same random seed', () => {
const generator1 = new EndpointDocGenerator('seed');
const generator2 = new EndpointDocGenerator('seed');
const timestamp = new Date().getTime();
const metadata1 = generator1.generateEndpointMetadata(timestamp);
const metadata2 = generator2.generateEndpointMetadata(timestamp);
expect(metadata1).toEqual(metadata2);
});

it('creates different documents with different random seeds', () => {
const generator1 = new EndpointDocGenerator('seed');
const generator2 = new EndpointDocGenerator('different seed');
const timestamp = new Date().getTime();
const metadata1 = generator1.generateEndpointMetadata(timestamp);
const metadata2 = generator2.generateEndpointMetadata(timestamp);
expect(metadata1).not.toEqual(metadata2);
});

it('creates endpoint metadata documents', () => {
const timestamp = new Date().getTime();
const metadata = generator.generateEndpointMetadata(timestamp);
expect(metadata['@timestamp']).toEqual(timestamp);
expect(metadata.event.created).toEqual(timestamp);
expect(metadata.endpoint).not.toBeNull();
expect(metadata.agent).not.toBeNull();
expect(metadata.host).not.toBeNull();
});

it('creates alert event documents', () => {
const timestamp = new Date().getTime();
const alert = generator.generateAlert(timestamp);
expect(alert['@timestamp']).toEqual(timestamp);
expect(alert.event.action).not.toBeNull();
expect(alert.endpoint).not.toBeNull();
expect(alert.agent).not.toBeNull();
expect(alert.host).not.toBeNull();
expect(alert.process.entity_id).not.toBeNull();
});

it('creates process event documents', () => {
const timestamp = new Date().getTime();
const processEvent = generator.generateEvent({ timestamp });
expect(processEvent['@timestamp']).toEqual(timestamp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in reference to the later comment about 'snapshot' testing, you could try this instead:

expect(processEvent).toMatchInlineSnapshot()

Save the file, then run the tests (or if you're using --watch, no need.) The test framework will serialize processEvent and drop a serialized version into this file. If the serialization changes, the test will fail. Not the best for every type of test, but in this case it might be worth trying out.

Another thing, if the serialization is really long/big, you can write toMatchSnapshot() instead and the serialization will be saved in a separate file.

One possible downside to using snapshots is that the intent of the assertion can be lost to the reader. In your test here for example, I can tell that the '@timestamp' field should match the timestamp variable, but that might not be obvious if there is just a single snapshot assertion. In this case, you can make the test description longer, like 'it creates process events, using the passed-in timestamp to populate timestamp fields.'

expect(processEvent.event.category).toEqual('process');
expect(processEvent.event.kind).toEqual('event');
expect(processEvent.event.type).toEqual('creation');
expect(processEvent.agent).not.toBeNull();
expect(processEvent.host).not.toBeNull();
expect(processEvent.process.entity_id).not.toBeNull();
});

it('creates other event documents', () => {
const timestamp = new Date().getTime();
const processEvent = generator.generateEvent({ timestamp, eventCategory: 'dns' });
expect(processEvent['@timestamp']).toEqual(timestamp);
expect(processEvent.event.category).toEqual('dns');
expect(processEvent.event.kind).toEqual('event');
expect(processEvent.event.type).toEqual('creation');
expect(processEvent.agent).not.toBeNull();
expect(processEvent.host).not.toBeNull();
expect(processEvent.process.entity_id).not.toBeNull();
});

describe('creates alert ancestor tree', () => {
let events: Event[];

beforeEach(() => {
events = generator.generateAlertEventAncestry(3);
});

it('with n-1 process events', () => {
for (let i = 1; i < events.length - 1; i++) {
expect(events[i].process.parent?.entity_id).toEqual(events[i - 1].process.entity_id);
expect(events[i].event.kind).toEqual('event');
expect(events[i].event.category).toEqual('process');
}
});

it('with a corresponding alert at the end', () => {
// The alert should be last and have the same entity_id as the previous process event
expect(events[events.length - 1].process.entity_id).toEqual(
events[events.length - 2].process.entity_id
);
expect(events[events.length - 1].process.parent?.entity_id).toEqual(
events[events.length - 2].process.parent?.entity_id
);
expect(events[events.length - 1].event.kind).toEqual('alert');
expect(events[events.length - 1].event.category).toEqual('malware');
});
});

function buildResolverTree(events: Event[]): Node {
// First pass we gather up all the events by entity_id
const tree: Record<string, Node> = {};
events.forEach(event => {
if (event.process.entity_id in tree) {
tree[event.process.entity_id].events.push(event);
} else {
tree[event.process.entity_id] = {
events: [event],
children: [],
parent_entity_id: event.process.parent?.entity_id,
};
}
});
// Second pass add child references to each node
for (const value of Object.values(tree)) {
if (value.parent_entity_id) {
tree[value.parent_entity_id].children.push(value);
}
}
// The root node must be first in the array or this fails
return tree[events[0].process.entity_id];
}

function countResolverEvents(rootNode: Node, generations: number): number {
// Start at the root, traverse N levels of the tree and check that we found all nodes
let nodes = [rootNode];
let visitedEvents = 0;
for (let i = 0; i < generations + 1; i++) {
let nextNodes: Node[] = [];
nodes.forEach(node => {
nextNodes = nextNodes.concat(node.children);
visitedEvents += node.events.length;
});
nodes = nextNodes;
}
return visitedEvents;
}

it('creates tree of process children', () => {
const timestamp = new Date().getTime();
const root = generator.generateEvent({ timestamp });
const generations = 2;
const events = generator.generateDescendantsTree(root, generations);
const rootNode = buildResolverTree(events);
const visitedEvents = countResolverEvents(rootNode, generations);
expect(visitedEvents).toEqual(events.length);
});

it('creates full resolver tree', () => {
const alertAncestors = 3;
const generations = 2;
const events = generator.generateFullResolverTree(alertAncestors, generations);
const rootNode = buildResolverTree(events);
const visitedEvents = countResolverEvents(rootNode, alertAncestors + generations);
expect(visitedEvents).toEqual(events.length);
});
});
Loading