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

resolver simulator and click through tests #73310

Merged
merged 32 commits into from
Jul 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public';
import { StartServices } from '../../types';
import { DataAccessLayer } from '../types';
import {
ResolverRelatedEvents,
ResolverTree,
ResolverEntityIndex,
} from '../../../common/endpoint/types';
import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants';

/**
* The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead.
*/
export function dataAccessLayerFactory(
context: KibanaReactContextValue<StartServices>
): DataAccessLayer {
const dataAccessLayer: DataAccessLayer = {
/**
* Used to get non-process related events for a node.
*/
async relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
return context.services.http.get(`/api/endpoint/resolver/${entityID}/events`, {
query: { events: 100 },
});
},
/**
* Used to get descendant and ancestor process events for a node.
*/
async resolverTree(entityID: string, signal: AbortSignal): Promise<ResolverTree> {
return context.services.http.get(`/api/endpoint/resolver/${entityID}`, {
signal,
});
},

/**
* Used to get the default index pattern from the SIEM application.
*/
indexPatterns(): string[] {
return context.services.uiSettings.get(defaultIndexKey);
},

/**
* Used to get the entity_id for an _id.
*/
async entities({
_id,
indices,
signal,
}: {
_id: string;
indices: string[];
signal: AbortSignal;
}): Promise<ResolverEntityIndex> {
return context.services.http.get('/api/endpoint/resolver/entity', {
signal,
query: {
_id,
indices,
},
});
},
};
return dataAccessLayer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 {
ResolverRelatedEvents,
ResolverTree,
ResolverEntityIndex,
} from '../../../../common/endpoint/types';
import { mockEndpointEvent } from '../../store/mocks/endpoint_event';
import { mockTreeWithNoAncestorsAnd2Children } from '../../store/mocks/resolver_tree';
import { DataAccessLayer } from '../../types';

interface Metadata {
/**
* The `_id` of the document being analyzed.
*/
databaseDocumentID: string;
/**
* A record of entityIDs to be used in tests assertions.
*/
entityIDs: {
/**
* The entityID of the node related to the document being analyzed.
*/
origin: 'origin';
/**
* The entityID of the first child of the origin.
*/
firstChild: 'firstChild';
/**
* The entityID of the second child of the origin.
*/
secondChild: 'secondChild';
};
}

/**
* A simple mock dataAccessLayer possible that returns a tree with 0 ancestors and 2 direct children. 1 related event is returned. The parameter to `entities` is ignored.
*/
export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } {
const metadata: Metadata = {
databaseDocumentID: '_id',
entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' },
};
return {
metadata,
dataAccessLayer: {
/**
* Fetch related events for an entity ID
*/
relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
return Promise.resolve({
entityID,
events: [
mockEndpointEvent({
entityID,
name: 'event',
timestamp: 0,
}),
],
nextEvent: null,
});
},

/**
* Fetch a ResolverTree for a entityID
*/
resolverTree(): Promise<ResolverTree> {
return Promise.resolve(
mockTreeWithNoAncestorsAnd2Children({
originID: metadata.entityIDs.origin,
firstChildID: metadata.entityIDs.firstChild,
secondChildID: metadata.entityIDs.secondChild,
})
);
},

/**
* Get an array of index patterns that contain events.
*/
indexPatterns(): string[] {
return ['index pattern'];
},

/**
* Get entities matching a document.
*/
entities(): Promise<ResolverEntityIndex> {
return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]);
},
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) {
}

/**
* ms since unix epoc, based on timestamp.
* ms since Unix epoc, based on timestamp.
* may return NaN if the timestamp wasn't present or was invalid.
*/
export function datetime(passedEvent: ResolverEvent): number | null {
Expand Down Expand Up @@ -85,7 +85,7 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType {
}

/**
* Returns the process event's pid
* Returns the process event's PID
*/
export function uniquePidForProcess(passedEvent: ResolverEvent): string {
if (event.isLegacyEvent(passedEvent)) {
Expand All @@ -96,7 +96,7 @@ export function uniquePidForProcess(passedEvent: ResolverEvent): string {
}

/**
* Returns the pid for the process on the host
* Returns the PID for the process on the host
*/
export function processPid(passedEvent: ResolverEvent): number | undefined {
if (event.isLegacyEvent(passedEvent)) {
Expand All @@ -107,7 +107,7 @@ export function processPid(passedEvent: ResolverEvent): number | undefined {
}

/**
* Returns the process event's parent pid
* Returns the process event's parent PID
*/
export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined {
if (event.isLegacyEvent(passedEvent)) {
Expand All @@ -118,7 +118,7 @@ export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string |
}

/**
* Returns the process event's parent pid
* Returns the process event's parent PID
*/
export function processParentPid(passedEvent: ResolverEvent): number | undefined {
if (event.isLegacyEvent(passedEvent)) {
Expand All @@ -144,12 +144,12 @@ export function processPath(passedEvent: ResolverEvent): string | undefined {
*/
export function userInfoForProcess(
passedEvent: ResolverEvent
): { user?: string; domain?: string } | undefined {
): { name?: string; domain?: string } | undefined {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@james-elastic seems like these values are wrong (and wont work in the UI) in 7.9. not a 100% but we should look into it

Copy link
Contributor

@jonathan-buttner jonathan-buttner Jul 28, 2020

Choose a reason for hiding this comment

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

The change you're proposing is correct. The data that is sent by the endpoint looks like this:

Endpoint process event
{
  ...
"event": {
    "sequence": 81869,
    "ingested": "2020-07-28T18:10:08.032404Z",
    "created": "2020-07-28T18:07:14.30791200Z",
    "kind": "event",
    "module": "endpoint",
    "action": "end",
    "id": "LlVyJJ01mAI5jUhA+++/7VTY",
    "category": [
      "process"
    ],
    "type": [
      "end"
    ],
    "dataset": "endpoint.events.process"
  },
  "dataset": {
    "name": "endpoint.events.process",
    "namespace": "default",
    "type": "logs"
  },
  "user": { <--------------------------------
    "domain": "NT AUTHORITY",
    "name": "LOCAL SERVICE"
  },
  "_index": ".ds-logs-endpoint.events.process-default-000001",
  "_type": "_doc",
  "_id": "ZpqelnMBlglBfzvyECQh",
  "_score": 1
}

The fields that are defined in the mapping are user.id, user.name, and user.domain: https://github.com/elastic/endpoint-package/blob/master/schemas/v1/process/process.yaml#L1260

Copy link
Contributor

Choose a reason for hiding this comment

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

@oatkiller @jonathan-buttner Should pull this (and the other user fix) out into its own PR to get it in the BC today?

return passedEvent.user;
}

/**
* Returns the MD5 hash for the `passedEvent` param, or undefined if it can't be located
* Returns the MD5 hash for the `passedEvent` parameter, or undefined if it can't be located
* @param {ResolverEvent} passedEvent The `ResolverEvent` to get the MD5 value for
* @returns {string | undefined} The MD5 string for the event
*/
Expand All @@ -164,7 +164,7 @@ export function md5HashForProcess(passedEvent: ResolverEvent): string | undefine
/**
* Returns the command line path and arguments used to run the `passedEvent` if any
*
* @param {ResolverEvent} passedEvent The `ResolverEvent` to get the arguemnts value for
* @param {ResolverEvent} passedEvent The `ResolverEvent` to get the arguments value for
* @returns {string | undefined} The arguments (including the path) used to run the process
*/
export function argsForProcess(passedEvent: ResolverEvent): string | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,20 @@

import { createStore, applyMiddleware, Store } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
import { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public';
import { ResolverState } from '../types';
import { StartServices } from '../../types';
import { ResolverState, DataAccessLayer } from '../types';
import { resolverReducer } from './reducer';
import { resolverMiddlewareFactory } from './middleware';
import { ResolverAction } from './actions';

export const storeFactory = (
context?: KibanaReactContextValue<StartServices>
dataAccessLayer: DataAccessLayer
): Store<ResolverState, ResolverAction> => {
const actionsDenylist: Array<ResolverAction['type']> = ['userMovedPointer'];
const composeEnhancers = composeWithDevTools({
name: 'Resolver',
actionsBlacklist: actionsDenylist,
});
const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(context));
const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(dataAccessLayer));

return createStore(resolverReducer, composeEnhancers(middlewareEnhancer));
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,26 @@
*/

import { Dispatch, MiddlewareAPI } from 'redux';
import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public';
import { StartServices } from '../../../types';
import { ResolverState } from '../../types';
import { ResolverState, DataAccessLayer } from '../../types';
import { ResolverRelatedEvents } from '../../../../common/endpoint/types';
import { ResolverTreeFetcher } from './resolver_tree_fetcher';
import { ResolverAction } from '../actions';

type MiddlewareFactory<S = ResolverState> = (
context?: KibanaReactContextValue<StartServices>
dataAccessLayer: DataAccessLayer
) => (
api: MiddlewareAPI<Dispatch<ResolverAction>, S>
) => (next: Dispatch<ResolverAction>) => (action: ResolverAction) => unknown;

/**
* The redux middleware that the app uses to trigger side effects.
* The `redux` middleware that the application uses to trigger side effects.
* All data fetching should be done here.
* For actions that the app triggers directly, use `app` as a prefix for the type.
* For actions that the application triggers directly, use `app` as a prefix for the type.
* For actions that are triggered as a result of server interaction, use `server` as a prefix for the type.
*/
export const resolverMiddlewareFactory: MiddlewareFactory = (context) => {
export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: DataAccessLayer) => {
return (api) => (next) => {
// This cannot work w/o `context`.
if (!context) {
return async (action: ResolverAction) => {
next(action);
};
}
const resolverTreeFetcher = ResolverTreeFetcher(context, api);
const resolverTreeFetcher = ResolverTreeFetcher(dataAccessLayer, api);
return async (action: ResolverAction) => {
next(action);

Expand All @@ -45,12 +37,7 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => {
const entityIdToFetchFor = action.payload;
let result: ResolverRelatedEvents | undefined;
try {
result = await context.services.http.get(
`/api/endpoint/resolver/${entityIdToFetchFor}/events`,
{
query: { events: 100 },
}
);
result = await dataAccessLayer.relatedEvents(entityIdToFetchFor);
} catch {
api.dispatch({
type: 'serverFailedToReturnRelatedEventData',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@
import { Dispatch, MiddlewareAPI } from 'redux';
import { ResolverTree, ResolverEntityIndex } from '../../../../common/endpoint/types';

import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public';
import { ResolverState } from '../../types';
import { ResolverState, DataAccessLayer } from '../../types';
import * as selectors from '../selectors';
import { StartServices } from '../../../types';
import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../../common/constants';
import { ResolverAction } from '../actions';
/**
* A function that handles syncing ResolverTree data w/ the current entity ID.
Expand All @@ -23,7 +20,7 @@ import { ResolverAction } from '../actions';
* This is a factory because it is stateful and keeps that state in closure.
*/
export function ResolverTreeFetcher(
context: KibanaReactContextValue<StartServices>,
dataAccessLayer: DataAccessLayer,
api: MiddlewareAPI<Dispatch<ResolverAction>, ResolverState>
): () => void {
let lastRequestAbortController: AbortController | undefined;
Expand All @@ -48,17 +45,12 @@ export function ResolverTreeFetcher(
payload: databaseDocumentIDToFetch,
});
try {
const indices: string[] = context.services.uiSettings.get(defaultIndexKey);
const matchingEntities: ResolverEntityIndex = await context.services.http.get(
'/api/endpoint/resolver/entity',
{
signal: lastRequestAbortController.signal,
query: {
_id: databaseDocumentIDToFetch,
indices,
},
}
);
const indices: string[] = dataAccessLayer.indexPatterns();
const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({
_id: databaseDocumentIDToFetch,
indices,
signal: lastRequestAbortController.signal,
});
if (matchingEntities.length < 1) {
// If no entity_id could be found for the _id, bail out with a failure.
api.dispatch({
Expand All @@ -68,9 +60,10 @@ export function ResolverTreeFetcher(
return;
}
const entityIDToFetch = matchingEntities[0].entity_id;
result = await context.services.http.get(`/api/endpoint/resolver/${entityIDToFetch}`, {
signal: lastRequestAbortController.signal,
});
result = await dataAccessLayer.resolverTree(
entityIDToFetch,
lastRequestAbortController.signal
);
} catch (error) {
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-AbortError
if (error instanceof DOMException && error.name === 'AbortError') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function mockEndpointEvent({
}: {
entityID: string;
name: string;
parentEntityId: string | undefined;
parentEntityId?: string;
timestamp: number;
lifecycleType?: string;
}): EndpointEvent {
Expand Down
Loading