From 9424a648e7e60097611aa945f1fe73c670481ff4 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 28 Jul 2020 17:12:20 -0400 Subject: [PATCH 1/6] Handling entity ids of empty string --- .../common/endpoint/schema/resolver.ts | 20 +++--- .../components/timeline/body/helpers.test.ts | 64 ++++++++++++++++++- .../components/timeline/body/helpers.ts | 3 +- .../server/endpoint/routes/resolver/entity.ts | 7 ++ .../endpoint/routes/resolver/queries/base.ts | 3 +- .../routes/resolver/queries/children.ts | 12 ++++ .../apis/index.ts | 3 +- .../apis/resolver/entity_id.ts | 18 ++++++ .../apis/{resolver.ts => resolver/tree.ts} | 14 ++-- .../services/resolver.ts | 63 ++++++++++++------ 10 files changed, 166 insertions(+), 41 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts rename x-pack/test/security_solution_endpoint_api_int/apis/{resolver.ts => resolver/tree.ts} (98%) diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts index c67ad3665d004f..f3e67f84b2880f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; * Used to validate GET requests for a complete resolver tree. */ export const validateTree = { - params: schema.object({ id: schema.string() }), + params: schema.object({ id: schema.string({ minLength: 1 }) }), query: schema.object({ children: schema.number({ defaultValue: 200, min: 0, max: 10000 }), ancestors: schema.number({ defaultValue: 200, min: 0, max: 10000 }), @@ -19,7 +19,7 @@ export const validateTree = { afterEvent: schema.maybe(schema.string()), afterAlert: schema.maybe(schema.string()), afterChild: schema.maybe(schema.string()), - legacyEndpointID: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), }; @@ -27,11 +27,11 @@ export const validateTree = { * Used to validate GET requests for non process events for a specific event. */ export const validateEvents = { - params: schema.object({ id: schema.string() }), + params: schema.object({ id: schema.string({ minLength: 1 }) }), query: schema.object({ events: schema.number({ defaultValue: 1000, min: 1, max: 10000 }), afterEvent: schema.maybe(schema.string()), - legacyEndpointID: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), }; @@ -39,11 +39,11 @@ export const validateEvents = { * Used to validate GET requests for alerts for a specific process. */ export const validateAlerts = { - params: schema.object({ id: schema.string() }), + params: schema.object({ id: schema.string({ minLength: 1 }) }), query: schema.object({ alerts: schema.number({ defaultValue: 1000, min: 1, max: 10000 }), afterAlert: schema.maybe(schema.string()), - legacyEndpointID: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), }; @@ -51,10 +51,10 @@ export const validateAlerts = { * Used to validate GET requests for the ancestors of a process event. */ export const validateAncestry = { - params: schema.object({ id: schema.string() }), + params: schema.object({ id: schema.string({ minLength: 1 }) }), query: schema.object({ ancestors: schema.number({ defaultValue: 200, min: 0, max: 10000 }), - legacyEndpointID: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), }; @@ -62,11 +62,11 @@ export const validateAncestry = { * Used to validate GET requests for children of a specified process event. */ export const validateChildren = { - params: schema.object({ id: schema.string() }), + params: schema.object({ id: schema.string({ minLength: 1 }) }), query: schema.object({ children: schema.number({ defaultValue: 200, min: 1, max: 10000 }), afterChild: schema.maybe(schema.string()), - legacyEndpointID: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts index 8ba1a999e2b2ae..6d657ae00e9555 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts @@ -6,7 +6,13 @@ import { Ecs } from '../../../../graphql/types'; -import { eventHasNotes, eventIsPinned, getPinTooltip, stringifyEvent } from './helpers'; +import { + eventHasNotes, + eventIsPinned, + getPinTooltip, + stringifyEvent, + isInvestigateInResolverActionEnabled, +} from './helpers'; import { TimelineType } from '../../../../../common/types/timeline'; describe('helpers', () => { @@ -242,4 +248,60 @@ describe('helpers', () => { expect(eventIsPinned({ eventId, pinnedEventIds })).toEqual(false); }); }); + + describe('isInvestigateInResolverActionEnabled', () => { + it('returns false if agent.type does not equal endpoint', () => { + const data: Ecs = { _id: '1', agent: { type: ['blah'] } }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns false if agent.type does not have endpoint in first array index', () => { + const data: Ecs = { _id: '1', agent: { type: ['blah', 'endpoint'] } }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns false if process.entity_id is not defined', () => { + const data: Ecs = { _id: '1', agent: { type: ['endpoint'] } }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns false if process.entity_id is an empty array', () => { + const data: Ecs = { _id: '1', agent: { type: ['endpoint'] } }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns true if agent.type has endpoint in first array index', () => { + const data: Ecs = { + _id: '1', + agent: { type: ['endpoint', 'blah'] }, + process: { entity_id: ['5'] }, + }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeTruthy(); + }); + + it('returns false if multiple entity_ids', () => { + const data: Ecs = { + _id: '1', + agent: { type: ['endpoint', 'blah'] }, + process: { entity_id: ['5', '10'] }, + }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns false if entity_id is an empty string', () => { + const data: Ecs = { + _id: '1', + agent: { type: ['endpoint', 'blah'] }, + process: { entity_id: [''] }, + }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 067cea175c99bd..6a5e25632c29ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -106,7 +106,8 @@ export const getEventType = (event: Ecs): Omit => { export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => { return ( get(['agent', 'type', 0], ecsData) === 'endpoint' && - get(['process', 'entity_id'], ecsData)?.length > 0 + get(['process', 'entity_id'], ecsData)?.length === 1 && + get(['process', 'entity_id', 0], ecsData) !== '' ); }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts index ae91201646103f..c79bcda71de9b5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts @@ -70,6 +70,13 @@ export function handleEntities(): RequestHandler implements MSearchQuer } private buildQuery(ids: string | string[]): { query: JsonObject; index: string | string[] } { - const idsArray = ResolverQuery.createIdsArray(ids); + // only accept queries for entity_ids that are not an empty string + const idsArray = ResolverQuery.createIdsArray(ids).filter((id) => id !== ''); if (this.endpointID) { return { query: this.legacyQuery(this.endpointID, idsArray), index: legacyEventIndexPattern }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index 7fd3808662baa7..d99533e23f2c27 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -74,6 +74,18 @@ export class ChildrenQuery extends ResolverQuery { ], }, }, + { + exists: { + field: 'process.entity_id', + }, + }, + { + bool: { + must_not: { + term: { 'process.entity_id': '' }, + }, + }, + }, { term: { 'event.category': 'process' }, }, diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index fb11a7c52fd354..56adc2382e2340 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -26,7 +26,8 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider before(async () => { await ingestManager.setup(); }); - loadTestFile(require.resolve('./resolver')); + loadTestFile(require.resolve('./resolver/entity_id')); + loadTestFile(require.resolve('./resolver/tree')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts new file mode 100644 index 00000000000000..dfbb2f30c70ff0 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const resolver = getService('resolverGenerator'); + + describe('Resolver handling of entity ids', () => { + it('receives 400s for entity_ids that are empty strings', async () => {}); + it('does not find children without a process entity_id', async () => {}); + it('does not query for ancestors that have an empty string for the entity_id', async () => {}); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts similarity index 98% rename from x-pack/test/security_solution_endpoint_api_int/apis/resolver.ts rename to x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 3b515f86c6761d..3527e7e575c996 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -16,12 +16,12 @@ import { LegacyEndpointEvent, ResolverNodeStats, ResolverRelatedAlerts, -} from '../../../plugins/security_solution/common/endpoint/types'; +} from '../../../../plugins/security_solution/common/endpoint/types'; import { parentEntityId, eventId, -} from '../../../plugins/security_solution/common/endpoint/models/event'; -import { FtrProviderContext } from '../ftr_provider_context'; +} from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { FtrProviderContext } from '../../ftr_provider_context'; import { Event, Tree, @@ -29,8 +29,8 @@ import { RelatedEventCategory, RelatedEventInfo, categoryMapping, -} from '../../../plugins/security_solution/common/endpoint/generate_data'; -import { Options, GeneratedTrees } from '../services/resolver'; +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { Options, GeneratedTrees } from '../../services/resolver'; /** * Check that the given lifecycle is in the resolver tree's corresponding map @@ -256,7 +256,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestryArraySize: 2, }; - describe('Resolver', () => { + describe('Resolver tree', () => { before(async () => { await esArchiver.load('endpoint/resolver/api_feature'); resolverTrees = await resolver.createTrees(treeOptions); @@ -264,7 +264,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC tree = resolverTrees.trees[0]; }); after(async () => { - await resolver.deleteTrees(resolverTrees); + await resolver.deleteData(resolverTrees); // this unload is for an endgame-* index so it does not use data streams await esArchiver.unload('endpoint/resolver/api_feature'); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 7f568a2b003140..48a89a91ff2f38 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -7,9 +7,12 @@ import { TreeOptions, Tree, EndpointDocGenerator, + Event, } from '../../../plugins/security_solution/common/endpoint/generate_data'; import { FtrProviderContext } from '../ftr_provider_context'; +const processIndex = 'logs-endpoint.events.process-default'; + /** * Options for build a resolver tree */ @@ -26,17 +29,39 @@ export interface Options extends TreeOptions { */ export interface GeneratedTrees { trees: Tree[]; - eventsIndex: string; - alertsIndex: string; + indices: string[]; +} + +/** + * + * @param + */ +export interface GeneratedEvents { + events: Event[]; + indices: string[]; +} + +interface BulkCreateHeader { + create: { + _index: string; + }; } export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const client = getService('es'); return { + async createEvents(events: Event[], eventsIndex: string = processIndex) { + const body = events.reduce((array: Array, doc) => { + array.push({ create: { _index: eventsIndex } }, doc); + return array; + }, []); + await client.bulk({ body, refresh: true }); + return { events, indices: [eventsIndex] }; + }, async createTrees( options: Options, - eventsIndex: string = 'logs-endpoint.events.process-default', + eventsIndex: string = processIndex, alertsIndex: string = 'logs-endpoint.alerts-default' ): Promise { const seed = options.seed || 'resolver-seed'; @@ -45,7 +70,7 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const numTrees = options.numTrees ?? 1; for (let j = 0; j < numTrees; j++) { const tree = generator.generateTree(options); - const body = tree.allEvents.reduce((array: Array>, doc) => { + const body = tree.allEvents.reduce((array: Array, doc) => { let index = eventsIndex; if (doc.event.kind === 'alert') { index = alertsIndex; @@ -60,23 +85,21 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { await client.bulk({ body, refresh: true }); allTrees.push(tree); } - return { trees: allTrees, eventsIndex, alertsIndex }; + return { trees: allTrees, indices: [eventsIndex, alertsIndex] }; }, - async deleteTrees(trees: GeneratedTrees) { - /** - * The ingest manager handles creating the template for the endpoint's indices. It is using a V2 template - * with data streams. Data streams aren't included in the javascript elasticsearch client in kibana yet so we - * need to do raw requests here. Delete a data stream is slightly different than that of a regular index which - * is why we're using _data_stream here. - */ - await client.transport.request({ - method: 'DELETE', - path: `_data_stream/${trees.eventsIndex}`, - }); - await client.transport.request({ - method: 'DELETE', - path: `_data_stream/${trees.alertsIndex}`, - }); + async deleteData(genData: { indices: string[] }) { + for (const index of genData.indices) { + /** + * The ingest manager handles creating the template for the endpoint's indices. It is using a V2 template + * with data streams. Data streams aren't included in the javascript elasticsearch client in kibana yet so we + * need to do raw requests here. Delete a data stream is slightly different than that of a regular index which + * is why we're using _data_stream here. + */ + await client.transport.request({ + method: 'DELETE', + path: `_data_stream/${index}`, + }); + } }, }; } From 7e3f9d827e505ff7462adbe6fc17b0936bff8219 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 28 Jul 2020 18:55:29 -0400 Subject: [PATCH 2/6] Tests for entity id being empty --- .../apis/resolver/entity_id.ts | 101 +++++++++++++++++- .../services/resolver.ts | 5 +- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index dfbb2f30c70ff0..d72d5af2455ea8 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -3,16 +3,109 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; +import { ResolverTree } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + EndpointDocGenerator, + Event, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { GeneratedEvents } from '../../services/resolver'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const resolver = getService('resolverGenerator'); + const generator = new EndpointDocGenerator('resolver'); describe('Resolver handling of entity ids', () => { - it('receives 400s for entity_ids that are empty strings', async () => {}); - it('does not find children without a process entity_id', async () => {}); - it('does not query for ancestors that have an empty string for the entity_id', async () => {}); + describe('children', () => { + let origin: Event; + let childNoEntityID: Event; + let childWithEntityID: Event; + let events: Event[]; + let genData: GeneratedEvents; + + before(async () => { + // construct a tree with an origin and two direct children. One child will not have an entity_id. That child + // should not be returned by the backend. + origin = generator.generateEvent({ entityID: 'a' }); + childNoEntityID = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + }); + // force it to be empty + childNoEntityID.process.entity_id = ''; + + childWithEntityID = generator.generateEvent({ + entityID: 'b', + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + }); + events = [origin, childNoEntityID, childWithEntityID]; + genData = await resolver.insertEvents(events); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('does not find children without a process entity_id', async () => { + const { body }: { body: ResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process.entity_id}`) + .expect(200); + expect(body.children.childNodes.length).to.be(1); + expect(body.children.childNodes[0].entityID).to.be(childWithEntityID.process.entity_id); + }); + }); + + describe('ancestors', () => { + let origin: Event; + let ancestor1: Event; + let ancestor2: Event; + let ancestorNoEntityID: Event; + let events: Event[]; + let genData: GeneratedEvents; + + before(async () => { + // construct a tree with an origin that has two ancestors. The origin will have an empty string as one of the + // entity_ids in the ancestry array. This is to make sure that the backend will not query for that event. + ancestor2 = generator.generateEvent({ + entityID: '2', + }); + ancestor1 = generator.generateEvent({ + entityID: '1', + parentEntityID: ancestor2.process.entity_id, + ancestry: [ancestor2.process.entity_id], + }); + + // we'll insert an event that doesn't have an entity id so if the backend does search for it, it should be + // returned and our test should fail + ancestorNoEntityID = generator.generateEvent({ + ancestry: [ancestor2.process.entity_id], + }); + ancestorNoEntityID.process.entity_id = ''; + + origin = generator.generateEvent({ + entityID: 'a', + parentEntityID: ancestor1.process.entity_id, + ancestry: ['', ancestor2.process.entity_id], + }); + + events = [origin, ancestor1, ancestor2, ancestorNoEntityID]; + genData = await resolver.insertEvents(events); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('does not query for ancestors that have an empty string for the entity_id', async () => { + const { body }: { body: ResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process.entity_id}`) + .expect(200); + expect(body.ancestry.ancestors.length).to.be(1); + expect(body.ancestry.ancestors[0].entityID).to.be(ancestor2.process.entity_id); + }); + }); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 48a89a91ff2f38..c9ae635cb65b78 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -51,7 +51,10 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const client = getService('es'); return { - async createEvents(events: Event[], eventsIndex: string = processIndex) { + async insertEvents( + events: Event[], + eventsIndex: string = processIndex + ): Promise { const body = events.reduce((array: Array, doc) => { array.push({ create: { _index: eventsIndex } }, doc); return array; From f6f41d254bf3bbb40d21248b2c2747a184b56d29 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 28 Jul 2020 19:00:59 -0400 Subject: [PATCH 3/6] More comments --- .../security_solution_endpoint_api_int/services/resolver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index c9ae635cb65b78..723328525d9dbd 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -33,8 +33,7 @@ export interface GeneratedTrees { } /** - * - * @param + * Structure containing the events inserted into ES and the index they live in */ export interface GeneratedEvents { events: Event[]; From 37b884d9d98df2dd0e1ea831276eaaf44bd3a244 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 28 Jul 2020 20:12:12 -0400 Subject: [PATCH 4/6] entity test --- .../apis/resolver/entity_id.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index d72d5af2455ea8..cdf146f62efca1 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { ResolverTree } from '../../../../plugins/security_solution/common/endpoint/types'; +import { SearchResponse } from 'elasticsearch'; +import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; +import { + ResolverTree, + ResolverEntityIndex, +} from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { EndpointDocGenerator, @@ -15,9 +20,49 @@ import { GeneratedEvents } from '../../services/resolver'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); + const es = getService('es'); const generator = new EndpointDocGenerator('resolver'); describe('Resolver handling of entity ids', () => { + describe('entity api', () => { + let origin: Event; + let genData: GeneratedEvents; + before(async () => { + origin = generator.generateEvent({ parentEntityID: 'a' }); + origin.process.entity_id = ''; + genData = await resolver.insertEvents([origin]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('excludes events that have an empty entity_id field', async () => { + // first lets get the _id of the document using the parent.process.entity_id + // then we'll use the API to search for that specific document + const res = await es.search>({ + index: genData.indices[0], + body: { + query: { + bool: { + filter: [ + { + term: { 'process.parent.entity_id': origin.process.parent!.entity_id }, + }, + ], + }, + }, + }, + }); + const { body }: { body: ResolverEntityIndex } = await supertest.get( + // using the same indices value here twice to force the query parameter to be an array + // for some reason using supertest's query() function doesn't construct a parsable array + `/api/endpoint/resolver/entity?_id=${res.body.hits.hits[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` + ); + expect(body).to.be.empty(); + }); + }); + describe('children', () => { let origin: Event; let childNoEntityID: Event; From 4801974ebf22cb1af1038d43a8fbe3429239608d Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 28 Jul 2020 20:19:59 -0400 Subject: [PATCH 5/6] Renaming interface --- .../apis/resolver/entity_id.ts | 8 ++++---- .../services/resolver.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index cdf146f62efca1..4f2a8013772043 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -15,7 +15,7 @@ import { EndpointDocGenerator, Event, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { GeneratedEvents } from '../../services/resolver'; +import { InsertedEvents } from '../../services/resolver'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -26,7 +26,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC describe('Resolver handling of entity ids', () => { describe('entity api', () => { let origin: Event; - let genData: GeneratedEvents; + let genData: InsertedEvents; before(async () => { origin = generator.generateEvent({ parentEntityID: 'a' }); origin.process.entity_id = ''; @@ -68,7 +68,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC let childNoEntityID: Event; let childWithEntityID: Event; let events: Event[]; - let genData: GeneratedEvents; + let genData: InsertedEvents; before(async () => { // construct a tree with an origin and two direct children. One child will not have an entity_id. That child @@ -109,7 +109,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC let ancestor2: Event; let ancestorNoEntityID: Event; let events: Event[]; - let genData: GeneratedEvents; + let genData: InsertedEvents; before(async () => { // construct a tree with an origin that has two ancestors. The origin will have an empty string as one of the diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 723328525d9dbd..335689b804d5ba 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -35,7 +35,7 @@ export interface GeneratedTrees { /** * Structure containing the events inserted into ES and the index they live in */ -export interface GeneratedEvents { +export interface InsertedEvents { events: Event[]; indices: string[]; } @@ -53,7 +53,7 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { async insertEvents( events: Event[], eventsIndex: string = processIndex - ): Promise { + ): Promise { const body = events.reduce((array: Array, doc) => { array.push({ create: { _index: eventsIndex } }, doc); return array; From 21eee4fa77ce8e85c23cf3b32508789e185cff9e Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 28 Jul 2020 20:25:54 -0400 Subject: [PATCH 6/6] Removing unneeded test --- .../timelines/components/timeline/body/helpers.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts index 6d657ae00e9555..c8adaa891610ad 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts @@ -268,12 +268,6 @@ describe('helpers', () => { expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); }); - it('returns false if process.entity_id is an empty array', () => { - const data: Ecs = { _id: '1', agent: { type: ['endpoint'] } }; - - expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); - }); - it('returns true if agent.type has endpoint in first array index', () => { const data: Ecs = { _id: '1',