Skip to content

Commit

Permalink
[Security Solution][Resolver] Handle disabled process collection (#73592
Browse files Browse the repository at this point in the history
)

* Handling entity ids of empty string

* Tests for entity id being empty

* More comments

* entity test

* Renaming interface

* Removing unneeded test

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
jonathan-buttner and elasticmachine committed Jul 29, 2020
1 parent a5e1720 commit 729726c
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 41 deletions.
20 changes: 10 additions & 10 deletions x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -19,54 +19,54 @@ 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 })),
}),
};

/**
* 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 })),
}),
};

/**
* 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 })),
}),
};

/**
* 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 })),
}),
};

/**
* 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 })),
}),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -242,4 +248,54 @@ 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 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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export const getEventType = (event: Ecs): Omit<EventType, 'all'> => {
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) !== ''
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
field: 'process.entity_id',
},
},
{
bool: {
must_not: {
term: { 'process.entity_id': '' },
},
},
},
],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export abstract class ResolverQuery<T, R = ResolverEvent> 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 };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ export class ChildrenQuery extends ResolverQuery<ResolverEvent[]> {
],
},
},
{
exists: {
field: 'process.entity_id',
},
},
{
bool: {
must_not: {
term: { 'process.entity_id': '' },
},
},
},
{
term: { 'event.category': 'process' },
},
Expand Down
3 changes: 2 additions & 1 deletion x-pack/test/security_solution_endpoint_api_int/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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 expect from '@kbn/expect';
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,
Event,
} from '../../../../plugins/security_solution/common/endpoint/generate_data';
import { InsertedEvents } 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: InsertedEvents;
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<SearchResponse<Event>>({
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;
let childWithEntityID: Event;
let events: Event[];
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
// 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: InsertedEvents;

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);
});
});
});
}
Loading

0 comments on commit 729726c

Please sign in to comment.