[Security Solution] Resolver retrieve entity id of documents without field mapped (#76562)

* More comments

* Adding tests for mapping without entity_id

* Removing unnecessary comments

* Fixing type errors

* Removing unnecessary import

* Fixups and style

* change 'data' state shape, nesting the tree fetcher data
* rename 'TreeFetcherParameters' from 'DatabaseParameters' to make it
more specific to the API it works on
* fix bug in 'equal' method of 'TreeFetcherParameters'`
* use mockTreeFetcherParameters method in tests that need to specify a
TreeFetcherParameters but when the value isn't relevant to the test
* Hide Resolver if there is no databaseDocumentID
* add doc comments

* Fixing test name and adding comments

* Pulling in roberts test name changes

* [Resolver] Only render resolver once we have a signals index

Co-authored-by: oatkiller <robert.austin@elastic.co>
This commit is contained in:
Jonathan Buttner 2020-09-04 09:24:24 -04:00 committed by GitHub
parent f7ad02d452
commit ae093e5a7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 3887 additions and 262 deletions

View file

@ -12,7 +12,6 @@ import {
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.
@ -38,13 +37,6 @@ export function dataAccessLayerFactory(
});
},
/**
* 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.
*/

View file

@ -12,7 +12,7 @@ import {
import { mockTreeWithNoProcessEvents } from '../../mocks/resolver_tree';
import { DataAccessLayer } from '../../types';
type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities' | 'indexPatterns';
type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities';
interface Metadata<T> {
/**
@ -66,15 +66,6 @@ export function emptifyMock<T>(
: dataAccessLayer.resolverTree(...args);
},
/**
* Get an array of index patterns that contain events.
*/
indexPatterns(...args): string[] {
return dataShouldBeEmpty.includes('indexPatterns')
? []
: dataAccessLayer.indexPatterns(...args);
},
/**
* Get entities matching a document.
*/

View file

@ -78,13 +78,6 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
);
},
/**
* Get an array of index patterns that contain events.
*/
indexPatterns(): string[] {
return ['index pattern'];
},
/**
* Get entities matching a document.
*/

View file

@ -0,0 +1,97 @@
/*
* 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 '../../mocks/endpoint_event';
import { mockTreeWithNoAncestorsAnd2Children } from '../../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 mock DataAccessLayer that will return an origin in two children. The `entity` response will be empty unless
* `awesome_index` is passed in the indices array.
*/
export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
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 entities matching a document.
*/
entities({ indices }): Promise<ResolverEntityIndex> {
// Only return values if the `indices` array contains exactly `'awesome_index'`
if (indices.length === 1 && indices[0] === 'awesome_index') {
return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]);
}
return Promise.resolve([]);
},
},
};
}

View file

@ -76,13 +76,6 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
return Promise.resolve(tree);
},
/**
* Get an array of index patterns that contain events.
*/
indexPatterns(): string[] {
return ['index pattern'];
},
/**
* Get entities matching a document.
*/

View file

@ -105,13 +105,6 @@ export function pausifyMock<T>({
return dataAccessLayer.resolverTree(...args);
},
/**
* Get an array of index patterns that contain events.
*/
indexPatterns(...args): string[] {
return dataAccessLayer.indexPatterns(...args);
},
/**
* Get entities matching a document.
*/

View file

@ -0,0 +1,17 @@
/*
* 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 { TreeFetcherParameters } from '../types';
/**
* A factory for the most basic `TreeFetcherParameters`. Many tests need to provide this even when the values aren't relevant to the test.
*/
export function mockTreeFetcherParameters(): TreeFetcherParameters {
return {
databaseDocumentID: '',
indices: [],
};
}

View file

@ -0,0 +1,38 @@
/*
* 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 { TreeFetcherParameters } from '../types';
import { equal } from './tree_fetcher_parameters';
describe('TreeFetcherParameters#equal:', () => {
const cases: Array<[TreeFetcherParameters, TreeFetcherParameters, boolean]> = [
// different databaseDocumentID
[{ databaseDocumentID: 'a', indices: [] }, { databaseDocumentID: 'b', indices: [] }, false],
// different indices length
[{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'a', indices: [] }, false],
// same indices length, different databaseDocumentID
[{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, false],
// 1 item in `indices`
[{ databaseDocumentID: 'b', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, true],
// 2 item in `indices`
[
{ databaseDocumentID: 'b', indices: ['1', '2'] },
{ databaseDocumentID: 'b', indices: ['1', '2'] },
true,
],
// 2 item in `indices`, but order inversed
[
{ databaseDocumentID: 'b', indices: ['2', '1'] },
{ databaseDocumentID: 'b', indices: ['1', '2'] },
true,
],
];
describe.each(cases)('%p when compared to %p', (first, second, expected) => {
it(`should ${expected ? '' : 'not'}be equal`, () => {
expect(equal(first, second)).toBe(expected);
});
});
});

View file

@ -0,0 +1,40 @@
/*
* 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 { TreeFetcherParameters } from '../types';
/**
* Determine if two instances of `TreeFetcherParameters` are equivalent. Use this to determine if
* a change to a `TreeFetcherParameters` warrants invaliding a request or response.
*/
export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParameters): boolean {
if (!param2) {
return false;
}
if (param1 === param2) {
return true;
}
if (param1.databaseDocumentID !== param2.databaseDocumentID) {
return false;
}
return arraysContainTheSameElements(param1.indices, param2.indices);
}
function arraysContainTheSameElements(first: unknown[], second: unknown[]): boolean {
if (first === second) {
return true;
}
if (first.length !== second.length) {
return false;
}
const firstSet = new Set(first);
for (let index = 0; index < second.length; index++) {
if (!firstSet.has(second[index])) {
return false;
}
}
return true;
}

View file

@ -115,7 +115,7 @@ interface AppReceivedNewExternalProperties {
/**
* the `_id` of an ES document. This defines the origin of the Resolver graph.
*/
databaseDocumentID?: string;
databaseDocumentID: string;
/**
* An ID that uniquely identifies this Resolver instance from other concurrent Resolvers.
*/
@ -125,6 +125,11 @@ interface AppReceivedNewExternalProperties {
* The `search` part of the URL of this page.
*/
locationSearch: string;
/**
* Indices that the backend will use to find the document.
*/
indices: string[];
};
}

View file

@ -5,6 +5,7 @@
*/
import { ResolverRelatedEvents, ResolverTree } from '../../../../common/endpoint/types';
import { TreeFetcherParameters } from '../../types';
interface ServerReturnedResolverData {
readonly type: 'serverReturnedResolverData';
@ -14,9 +15,9 @@ interface ServerReturnedResolverData {
*/
result: ResolverTree;
/**
* The database document ID that was used to fetch the resolver tree
* The database parameters that was used to fetch the resolver tree
*/
databaseDocumentID: string;
parameters: TreeFetcherParameters;
};
}
@ -25,7 +26,7 @@ interface AppRequestedResolverData {
/**
* entity ID used to make the request.
*/
readonly payload: string;
readonly payload: TreeFetcherParameters;
}
interface ServerFailedToReturnResolverData {
@ -33,7 +34,7 @@ interface ServerFailedToReturnResolverData {
/**
* entity ID used to make the failed request
*/
readonly payload: string;
readonly payload: TreeFetcherParameters;
}
interface AppAbortedResolverDataRequest {
@ -41,7 +42,7 @@ interface AppAbortedResolverDataRequest {
/**
* entity ID used to make the aborted request
*/
readonly payload: string;
readonly payload: TreeFetcherParameters;
}
/**

View file

@ -12,6 +12,7 @@ import { DataState } from '../../types';
import { DataAction } from './action';
import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types';
import * as eventModel from '../../../../common/endpoint/models/event';
import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
/**
* Test the data reducer and selector.
@ -27,7 +28,7 @@ describe('Resolver Data Middleware', () => {
type: 'serverReturnedResolverData',
payload: {
result: tree,
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
};
store.dispatch(action);

View file

@ -7,34 +7,53 @@
import { Reducer } from 'redux';
import { DataState } from '../../types';
import { ResolverAction } from '../actions';
import * as treeFetcherParameters from '../../models/tree_fetcher_parameters';
const initialState: DataState = {
relatedEvents: new Map(),
relatedEventsReady: new Map(),
resolverComponentInstanceID: undefined,
tree: {},
};
export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => {
if (action.type === 'appReceivedNewExternalProperties') {
const nextState: DataState = {
...state,
databaseDocumentID: action.payload.databaseDocumentID,
tree: {
...state.tree,
currentParameters: {
databaseDocumentID: action.payload.databaseDocumentID,
indices: action.payload.indices,
},
},
resolverComponentInstanceID: action.payload.resolverComponentInstanceID,
};
return nextState;
} else if (action.type === 'appRequestedResolverData') {
// keep track of what we're requesting, this way we know when to request and when not to.
return {
const nextState: DataState = {
...state,
pendingRequestDatabaseDocumentID: action.payload,
tree: {
...state.tree,
pendingRequestParameters: {
databaseDocumentID: action.payload.databaseDocumentID,
indices: action.payload.indices,
},
},
};
return nextState;
} else if (action.type === 'appAbortedResolverDataRequest') {
if (action.payload === state.pendingRequestDatabaseDocumentID) {
if (treeFetcherParameters.equal(action.payload, state.tree.pendingRequestParameters)) {
// the request we were awaiting was aborted
return {
const nextState: DataState = {
...state,
pendingRequestDatabaseDocumentID: undefined,
tree: {
...state.tree,
pendingRequestParameters: undefined,
},
};
return nextState;
} else {
return state;
}
@ -43,29 +62,35 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
const nextState: DataState = {
...state,
/**
* Store the last received data, as well as the databaseDocumentID it relates to.
*/
lastResponse: {
result: action.payload.result,
databaseDocumentID: action.payload.databaseDocumentID,
successful: true,
},
tree: {
...state.tree,
/**
* Store the last received data, as well as the databaseDocumentID it relates to.
*/
lastResponse: {
result: action.payload.result,
parameters: action.payload.parameters,
successful: true,
},
// This assumes that if we just received something, there is no longer a pending request.
// This cannot model multiple in-flight requests
pendingRequestDatabaseDocumentID: undefined,
// This assumes that if we just received something, there is no longer a pending request.
// This cannot model multiple in-flight requests
pendingRequestParameters: undefined,
},
};
return nextState;
} else if (action.type === 'serverFailedToReturnResolverData') {
/** Only handle this if we are expecting a response */
if (state.pendingRequestDatabaseDocumentID !== undefined) {
if (state.tree.pendingRequestParameters !== undefined) {
const nextState: DataState = {
...state,
pendingRequestDatabaseDocumentID: undefined,
lastResponse: {
databaseDocumentID: state.pendingRequestDatabaseDocumentID,
successful: false,
tree: {
...state.tree,
pendingRequestParameters: undefined,
lastResponse: {
parameters: state.tree.pendingRequestParameters,
successful: false,
},
},
};
return nextState;
@ -76,16 +101,18 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
action.type === 'userRequestedRelatedEventData' ||
action.type === 'appDetectedMissingEventData'
) {
return {
const nextState: DataState = {
...state,
relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload, false]]),
};
return nextState;
} else if (action.type === 'serverReturnedRelatedEventData') {
return {
const nextState: DataState = {
...state,
relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload.entityID, true]]),
relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]),
};
return nextState;
} else {
return state;
}

View file

@ -18,6 +18,7 @@ import {
} from '../../mocks/resolver_tree';
import { uniquePidForProcess } from '../../models/process_event';
import { EndpointEvent } from '../../../../common/endpoint/types';
import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
describe('data state', () => {
let actions: ResolverAction[] = [];
@ -39,29 +40,32 @@ describe('data state', () => {
*/
const viewAsAString = (dataState: DataState) => {
return [
['is loading', selectors.isLoading(dataState)],
['has an error', selectors.hasError(dataState)],
['is loading', selectors.isTreeLoading(dataState)],
['has an error', selectors.hadErrorLoadingTree(dataState)],
['has more children', selectors.hasMoreChildren(dataState)],
['has more ancestors', selectors.hasMoreAncestors(dataState)],
['document to fetch', selectors.databaseDocumentIDToFetch(dataState)],
['requires a pending request to be aborted', selectors.databaseDocumentIDToAbort(dataState)],
['parameters to fetch', selectors.treeParametersToFetch(dataState)],
[
'requires a pending request to be aborted',
selectors.treeRequestParametersToAbort(dataState),
],
]
.map(([message, value]) => `${message}: ${JSON.stringify(value)}`)
.join('\n');
};
it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a document to fetch, or have a pending request that needs to be aborted.`, () => {
it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a request to make, or have a pending request that needs to be aborted.`, () => {
expect(viewAsAString(state())).toMatchInlineSnapshot(`
"is loading: false
has an error: false
has more children: false
has more ancestors: false
document to fetch: null
parameters to fetch: null
requires a pending request to be aborted: null"
`);
});
describe('when there is a databaseDocumentID but no pending request', () => {
describe('when there are parameters to fetch but no pending request', () => {
const databaseDocumentID = 'databaseDocumentID';
const resolverComponentInstanceID = 'resolverComponentInstanceID';
beforeEach(() => {
@ -74,12 +78,13 @@ describe('data state', () => {
// `locationSearch` doesn't matter for this test
locationSearch: '',
indices: [],
},
},
];
});
it('should need to fetch the databaseDocumentID', () => {
expect(selectors.databaseDocumentIDToFetch(state())).toBe(databaseDocumentID);
it('should need to request the tree', () => {
expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(databaseDocumentID);
});
it('should not be loading, have an error, have more children or ancestors, or have a pending request that needs to be aborted.', () => {
expect(viewAsAString(state())).toMatchInlineSnapshot(`
@ -87,39 +92,41 @@ describe('data state', () => {
has an error: false
has more children: false
has more ancestors: false
document to fetch: \\"databaseDocumentID\\"
parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]}
requires a pending request to be aborted: null"
`);
});
});
describe('when there is a pending request but no databaseDocumentID', () => {
describe('when there is a pending request but no current tree fetching parameters', () => {
const databaseDocumentID = 'databaseDocumentID';
beforeEach(() => {
actions = [
{
type: 'appRequestedResolverData',
payload: databaseDocumentID,
payload: { databaseDocumentID, indices: [] },
},
];
});
it('should be loading', () => {
expect(selectors.isLoading(state())).toBe(true);
expect(selectors.isTreeLoading(state())).toBe(true);
});
it('should have a request to abort', () => {
expect(selectors.databaseDocumentIDToAbort(state())).toBe(databaseDocumentID);
expect(selectors.treeRequestParametersToAbort(state())?.databaseDocumentID).toBe(
databaseDocumentID
);
});
it('should not have an error, more children, more ancestors, or a document to fetch.', () => {
it('should not have an error, more children, more ancestors, or request to make.', () => {
expect(viewAsAString(state())).toMatchInlineSnapshot(`
"is loading: true
has an error: false
has more children: false
has more ancestors: false
document to fetch: null
requires a pending request to be aborted: \\"databaseDocumentID\\""
parameters to fetch: null
requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]}"
`);
});
});
describe('when there is a pending request for the current databaseDocumentID', () => {
describe('when there is a pending request that was made using the current parameters', () => {
const databaseDocumentID = 'databaseDocumentID';
const resolverComponentInstanceID = 'resolverComponentInstanceID';
beforeEach(() => {
@ -132,27 +139,28 @@ describe('data state', () => {
// `locationSearch` doesn't matter for this test
locationSearch: '',
indices: [],
},
},
{
type: 'appRequestedResolverData',
payload: databaseDocumentID,
payload: { databaseDocumentID, indices: [] },
},
];
});
it('should be loading', () => {
expect(selectors.isLoading(state())).toBe(true);
expect(selectors.isTreeLoading(state())).toBe(true);
});
it('should not have a request to abort', () => {
expect(selectors.databaseDocumentIDToAbort(state())).toBe(null);
expect(selectors.treeRequestParametersToAbort(state())).toBe(null);
});
it('should not have an error, more children, more ancestors, a document to begin fetching, or a pending request that should be aborted.', () => {
it('should not have an error, more children, more ancestors, a request to make, or a pending request that should be aborted.', () => {
expect(viewAsAString(state())).toMatchInlineSnapshot(`
"is loading: true
has an error: false
has more children: false
has more ancestors: false
document to fetch: null
parameters to fetch: null
requires a pending request to be aborted: null"
`);
});
@ -160,28 +168,28 @@ describe('data state', () => {
beforeEach(() => {
actions.push({
type: 'serverFailedToReturnResolverData',
payload: databaseDocumentID,
payload: { databaseDocumentID, indices: [] },
});
});
it('should not be loading', () => {
expect(selectors.isLoading(state())).toBe(false);
expect(selectors.isTreeLoading(state())).toBe(false);
});
it('should have an error', () => {
expect(selectors.hasError(state())).toBe(true);
expect(selectors.hadErrorLoadingTree(state())).toBe(true);
});
it('should not be loading, have more children, have more ancestors, have a document to fetch, or have a pending request that needs to be aborted.', () => {
it('should not be loading, have more children, have more ancestors, have a request to make, or have a pending request that needs to be aborted.', () => {
expect(viewAsAString(state())).toMatchInlineSnapshot(`
"is loading: false
has an error: true
has more children: false
has more ancestors: false
document to fetch: null
parameters to fetch: null
requires a pending request to be aborted: null"
`);
});
});
});
describe('when there is a pending request for a different databaseDocumentID than the current one', () => {
describe('when there is a pending request that was made with parameters that are different than the current tree fetching parameters', () => {
const firstDatabaseDocumentID = 'first databaseDocumentID';
const secondDatabaseDocumentID = 'second databaseDocumentID';
const resolverComponentInstanceID1 = 'resolverComponentInstanceID1';
@ -196,12 +204,13 @@ describe('data state', () => {
resolverComponentInstanceID: resolverComponentInstanceID1,
// `locationSearch` doesn't matter for this test
locationSearch: '',
indices: [],
},
},
// this happens when the middleware starts the request
{
type: 'appRequestedResolverData',
payload: firstDatabaseDocumentID,
payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] },
},
// receive a different databaseDocumentID. this should cause the middleware to abort the existing request and start a new one
{
@ -211,18 +220,23 @@ describe('data state', () => {
resolverComponentInstanceID: resolverComponentInstanceID2,
// `locationSearch` doesn't matter for this test
locationSearch: '',
indices: [],
},
},
];
});
it('should be loading', () => {
expect(selectors.isLoading(state())).toBe(true);
expect(selectors.isTreeLoading(state())).toBe(true);
});
it('should need to fetch the second databaseDocumentID', () => {
expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID);
it('should need to request the tree using the second set of parameters', () => {
expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(
secondDatabaseDocumentID
);
});
it('should need to abort the request for the databaseDocumentID', () => {
expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID);
expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(
secondDatabaseDocumentID
);
});
it('should use the correct location for the second resolver', () => {
expect(selectors.resolverComponentInstanceID(state())).toBe(resolverComponentInstanceID2);
@ -233,25 +247,27 @@ describe('data state', () => {
has an error: false
has more children: false
has more ancestors: false
document to fetch: \\"second databaseDocumentID\\"
requires a pending request to be aborted: \\"first databaseDocumentID\\""
parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]}
requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"first databaseDocumentID\\",\\"indices\\":[]}"
`);
});
describe('and when the old request was aborted', () => {
beforeEach(() => {
actions.push({
type: 'appAbortedResolverDataRequest',
payload: firstDatabaseDocumentID,
payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] },
});
});
it('should not require a pending request to be aborted', () => {
expect(selectors.databaseDocumentIDToAbort(state())).toBe(null);
expect(selectors.treeRequestParametersToAbort(state())).toBe(null);
});
it('should have a document to fetch', () => {
expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID);
expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(
secondDatabaseDocumentID
);
});
it('should not be loading', () => {
expect(selectors.isLoading(state())).toBe(false);
expect(selectors.isTreeLoading(state())).toBe(false);
});
it('should not have an error, more children, or more ancestors.', () => {
expect(viewAsAString(state())).toMatchInlineSnapshot(`
@ -259,7 +275,7 @@ describe('data state', () => {
has an error: false
has more children: false
has more ancestors: false
document to fetch: \\"second databaseDocumentID\\"
parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]}
requires a pending request to be aborted: null"
`);
});
@ -267,14 +283,14 @@ describe('data state', () => {
beforeEach(() => {
actions.push({
type: 'appRequestedResolverData',
payload: secondDatabaseDocumentID,
payload: { databaseDocumentID: secondDatabaseDocumentID, indices: [] },
});
});
it('should not have a document ID to fetch', () => {
expect(selectors.databaseDocumentIDToFetch(state())).toBe(null);
expect(selectors.treeParametersToFetch(state())).toBe(null);
});
it('should be loading', () => {
expect(selectors.isLoading(state())).toBe(true);
expect(selectors.isTreeLoading(state())).toBe(true);
});
it('should not have an error, more children, more ancestors, or a pending request that needs to be aborted.', () => {
expect(viewAsAString(state())).toMatchInlineSnapshot(`
@ -282,7 +298,7 @@ describe('data state', () => {
has an error: false
has more children: false
has more ancestors: false
document to fetch: null
parameters to fetch: null
requires a pending request to be aborted: null"
`);
});
@ -303,7 +319,7 @@ describe('data state', () => {
secondAncestorID,
}),
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});
@ -331,7 +347,7 @@ describe('data state', () => {
secondAncestorID,
}),
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});
@ -355,7 +371,7 @@ describe('data state', () => {
payload: {
result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }),
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});
@ -386,7 +402,7 @@ describe('data state', () => {
payload: {
result: tree,
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});
@ -417,7 +433,7 @@ describe('data state', () => {
payload: {
result: tree,
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});
@ -433,7 +449,7 @@ describe('data state', () => {
payload: {
result: tree,
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});

View file

@ -15,6 +15,7 @@ import {
AABB,
VisibleEntites,
SectionData,
TreeFetcherParameters,
} from '../../types';
import {
isGraphableProcess,
@ -34,6 +35,7 @@ import {
LegacyEndpointEvent,
} from '../../../../common/endpoint/types';
import * as resolverTreeModel from '../../models/resolver_tree';
import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters';
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
import * as eventModel from '../../../../common/endpoint/models/event';
import * as vector2 from '../../models/vector2';
@ -42,8 +44,18 @@ import { formatDate } from '../../view/panels/panel_content_utilities';
/**
* If there is currently a request.
*/
export function isLoading(state: DataState): boolean {
return state.pendingRequestDatabaseDocumentID !== undefined;
export function isTreeLoading(state: DataState): boolean {
return state.tree.pendingRequestParameters !== undefined;
}
/**
* If a request was made and it threw an error or returned a failure response code.
*/
export function hadErrorLoadingTree(state: DataState): boolean {
if (state.tree.lastResponse) {
return !state.tree.lastResponse.successful;
}
return false;
}
/**
@ -53,27 +65,12 @@ export function resolverComponentInstanceID(state: DataState): string {
return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : '';
}
/**
* If a request was made and it threw an error or returned a failure response code.
*/
export function hasError(state: DataState): boolean {
if (state.lastResponse && state.lastResponse.successful === false) {
return true;
} else {
return false;
}
}
/**
* The last ResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that
* we're currently interested in.
*/
const resolverTreeResponse = (state: DataState): ResolverTree | undefined => {
if (state.lastResponse && state.lastResponse.successful) {
return state.lastResponse.result;
} else {
return undefined;
}
return state.tree.lastResponse?.successful ? state.tree.lastResponse.result : undefined;
};
/**
@ -458,18 +455,24 @@ export const relatedEventInfoByEntityId: (
);
/**
* If we need to fetch, this is the ID to fetch.
* If the tree resource needs to be fetched then these are the parameters that should be used.
*/
export function databaseDocumentIDToFetch(state: DataState): string | null {
// If there is an ID, it must match either the last received version, or the pending version.
// Otherwise, we need to fetch it
// NB: this technique will not allow for refreshing of data.
export function treeParametersToFetch(state: DataState): TreeFetcherParameters | null {
/**
* If there are current tree parameters that don't match the parameters used in the pending request (if there is a pending request) and that don't match the parameters used in the last completed request (if there was a last completed request) then we need to fetch the tree resource using the current parameters.
*/
if (
state.databaseDocumentID !== undefined &&
state.databaseDocumentID !== state.pendingRequestDatabaseDocumentID &&
state.databaseDocumentID !== state.lastResponse?.databaseDocumentID
state.tree.currentParameters !== undefined &&
!treeFetcherParametersModel.equal(
state.tree.currentParameters,
state.tree.lastResponse?.parameters
) &&
!treeFetcherParametersModel.equal(
state.tree.currentParameters,
state.tree.pendingRequestParameters
)
) {
return state.databaseDocumentID;
return state.tree.currentParameters;
} else {
return null;
}
@ -692,15 +695,18 @@ export const nodesAndEdgelines: (
/**
* If there is a pending request that's for a entity ID that doesn't matche the `entityID`, then we should cancel it.
*/
export function databaseDocumentIDToAbort(state: DataState): string | null {
export function treeRequestParametersToAbort(state: DataState): TreeFetcherParameters | null {
/**
* If there is a pending request, and its not for the current databaseDocumentID (even, if the current databaseDocumentID is undefined) then we should abort the request.
* If there is a pending request, and its not for the current parameters (even, if the current parameters are undefined) then we should abort the request.
*/
if (
state.pendingRequestDatabaseDocumentID !== undefined &&
state.pendingRequestDatabaseDocumentID !== state.databaseDocumentID
state.tree.pendingRequestParameters !== undefined &&
!treeFetcherParametersModel.equal(
state.tree.pendingRequestParameters,
state.tree.currentParameters
)
) {
return state.pendingRequestDatabaseDocumentID;
return state.tree.pendingRequestParameters;
} else {
return null;
}

View file

@ -12,6 +12,7 @@ import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/
import { visibleNodesAndEdgeLines } from '../selectors';
import { mockProcessEvent } from '../../models/process_event_test_helpers';
import { mock as mockResolverTree } from '../../models/resolver_tree';
import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
describe('resolver visible entities', () => {
let processA: LegacyEndpointEvent;
@ -112,7 +113,7 @@ describe('resolver visible entities', () => {
];
const action: ResolverAction = {
type: 'serverReturnedResolverData',
payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' },
payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() },
};
const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] };
store.dispatch(action);
@ -140,7 +141,7 @@ describe('resolver visible entities', () => {
];
const action: ResolverAction = {
type: 'serverReturnedResolverData',
payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' },
payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() },
};
const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] };
store.dispatch(action);

View file

@ -28,32 +28,31 @@ export function ResolverTreeFetcher(
// if the entityID changes while
return async () => {
const state = api.getState();
const databaseDocumentIDToFetch = selectors.databaseDocumentIDToFetch(state);
const databaseParameters = selectors.treeParametersToFetch(state);
if (selectors.databaseDocumentIDToAbort(state) && lastRequestAbortController) {
if (selectors.treeRequestParametersToAbort(state) && lastRequestAbortController) {
lastRequestAbortController.abort();
// calling abort will cause an action to be fired
} else if (databaseDocumentIDToFetch !== null) {
} else if (databaseParameters !== null) {
lastRequestAbortController = new AbortController();
let result: ResolverTree | undefined;
// Inform the state that we've made the request. Without this, the middleware will try to make the request again
// immediately.
api.dispatch({
type: 'appRequestedResolverData',
payload: databaseDocumentIDToFetch,
payload: databaseParameters,
});
try {
const indices: string[] = dataAccessLayer.indexPatterns();
const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({
_id: databaseDocumentIDToFetch,
indices,
_id: databaseParameters.databaseDocumentID,
indices: databaseParameters.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({
type: 'serverFailedToReturnResolverData',
payload: databaseDocumentIDToFetch,
payload: databaseParameters,
});
return;
}
@ -67,12 +66,12 @@ export function ResolverTreeFetcher(
if (error instanceof DOMException && error.name === 'AbortError') {
api.dispatch({
type: 'appAbortedResolverDataRequest',
payload: databaseDocumentIDToFetch,
payload: databaseParameters,
});
} else {
api.dispatch({
type: 'serverFailedToReturnResolverData',
payload: databaseDocumentIDToFetch,
payload: databaseParameters,
});
}
}
@ -81,7 +80,7 @@ export function ResolverTreeFetcher(
type: 'serverReturnedResolverData',
payload: {
result,
databaseDocumentID: databaseDocumentIDToFetch,
parameters: databaseParameters,
},
});
}

View file

@ -14,6 +14,7 @@ import {
mockTreeWithNoAncestorsAnd2Children,
} from '../mocks/resolver_tree';
import { SafeResolverEvent } from '../../../common/endpoint/types';
import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters';
describe('resolver selectors', () => {
const actions: ResolverAction[] = [];
@ -43,7 +44,7 @@ describe('resolver selectors', () => {
secondAncestorID,
}),
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});
@ -77,7 +78,7 @@ describe('resolver selectors', () => {
payload: {
result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }),
// this value doesn't matter
databaseDocumentID: '',
parameters: mockTreeFetcherParameters(),
},
});
});

View file

@ -84,14 +84,14 @@ export const layout: (state: ResolverState) => IsometricTaxiLayout = composeSele
/**
* If we need to fetch, this is the entity ID to fetch.
*/
export const databaseDocumentIDToFetch = composeSelectors(
export const treeParametersToFetch = composeSelectors(
dataStateSelector,
dataSelectors.databaseDocumentIDToFetch
dataSelectors.treeParametersToFetch
);
export const databaseDocumentIDToAbort = composeSelectors(
export const treeRequestParametersToAbort = composeSelectors(
dataStateSelector,
dataSelectors.databaseDocumentIDToAbort
dataSelectors.treeRequestParametersToAbort
);
export const resolverComponentInstanceID = composeSelectors(
@ -207,12 +207,15 @@ function uiStateSelector(state: ResolverState) {
/**
* Whether or not the resolver is pending fetching data
*/
export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading);
export const isTreeLoading = composeSelectors(dataStateSelector, dataSelectors.isTreeLoading);
/**
* Whether or not the resolver encountered an error while fetching data
*/
export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError);
export const hadErrorLoadingTree = composeSelectors(
dataStateSelector,
dataSelectors.hadErrorLoadingTree
);
/**
* True if the children cursor is not null

View file

@ -49,6 +49,7 @@ export class Simulator {
dataAccessLayer,
resolverComponentInstanceID,
databaseDocumentID,
indices,
history,
}: {
/**
@ -59,10 +60,14 @@ export class Simulator {
* A string that uniquely identifies this Resolver instance among others mounted in the DOM.
*/
resolverComponentInstanceID: string;
/**
* Indices that the backend would use to find the document ID.
*/
indices: string[];
/**
* a databaseDocumentID to pass to Resolver. Resolver will use this in requests to the mock data layer.
*/
databaseDocumentID?: string;
databaseDocumentID: string;
history?: HistoryPackageHistoryInterface<never>;
}) {
// create the spy middleware (for debugging tests)
@ -99,6 +104,7 @@ export class Simulator {
store={this.store}
coreStart={coreStart}
databaseDocumentID={databaseDocumentID}
indices={indices}
/>
);
}
@ -124,6 +130,20 @@ export class Simulator {
this.wrapper.setProps({ resolverComponentInstanceID: value });
}
/**
* Change the indices (updates the React component props.)
*/
public set indices(value: string[]) {
this.wrapper.setProps({ indices: value });
}
/**
* Get the indices (updates the React component props.)
*/
public get indices(): string[] {
return this.wrapper.prop('indices');
}
/**
* Call this to console.log actions (and state). Use this to debug your tests.
* State and actions aren't exposed otherwise because the tests using this simulator should

View file

@ -99,6 +99,7 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
ref={resolverRef}
databaseDocumentID={props.databaseDocumentID}
resolverComponentInstanceID={props.resolverComponentInstanceID}
indices={props.indices}
/>
</Provider>
</SideEffectContext.Provider>

View file

@ -194,53 +194,68 @@ export interface VisibleEntites {
connectingEdgeLineSegments: EdgeLineSegment[];
}
export interface TreeFetcherParameters {
/**
* The `_id` for an ES document. Used to select a process that we'll show the graph for.
*/
databaseDocumentID: string;
/**
* The indices that the backend will use to search for the document ID.
*/
indices: string[];
}
/**
* State for `data` reducer which handles receiving Resolver data from the back-end.
*/
export interface DataState {
readonly relatedEvents: Map<string, ResolverRelatedEvents>;
readonly relatedEventsReady: Map<string, boolean>;
/**
* The `_id` for an ES document. Used to select a process that we'll show the graph for.
*/
readonly databaseDocumentID?: string;
/**
* The id used for the pending request, if there is one.
*/
readonly pendingRequestDatabaseDocumentID?: string;
readonly tree: {
/**
* The parameters passed from the resolver properties
*/
readonly currentParameters?: TreeFetcherParameters;
/**
* The id used for the pending request, if there is one.
*/
readonly pendingRequestParameters?: TreeFetcherParameters;
/**
* The parameters and response from the last successful request.
*/
readonly lastResponse?: {
/**
* The id used in the request.
*/
readonly parameters: TreeFetcherParameters;
} & (
| {
/**
* If a response with a success code was received, this is `true`.
*/
readonly successful: true;
/**
* The ResolverTree parsed from the response.
*/
readonly result: ResolverTree;
}
| {
/**
* If the request threw an exception or the response had a failure code, this will be false.
*/
readonly successful: false;
}
);
};
/**
* An ID that is used to differentiate this Resolver instance from others concurrently running on the same page.
* Used to prevent collisions in things like query parameters.
*/
readonly resolverComponentInstanceID?: string;
/**
* The parameters and response from the last successful request.
*/
readonly lastResponse?: {
/**
* The id used in the request.
*/
readonly databaseDocumentID: string;
} & (
| {
/**
* If a response with a success code was received, this is `true`.
*/
readonly successful: true;
/**
* The ResolverTree parsed from the response.
*/
readonly result: ResolverTree;
}
| {
/**
* If the request threw an exception or the response had a failure code, this will be false.
*/
readonly successful: false;
}
);
}
/**
@ -494,11 +509,6 @@ export interface DataAccessLayer {
*/
resolverTree: (entityID: string, signal: AbortSignal) => Promise<ResolverTree>;
/**
* Get an array of index patterns that contain events.
*/
indexPatterns: () => string[];
/**
* Get entities matching a document.
*/
@ -524,13 +534,18 @@ export interface ResolverProps {
* The `_id` value of an event in ES.
* Used as the origin of the Resolver graph.
*/
databaseDocumentID?: string;
databaseDocumentID: string;
/**
* An ID that is used to differentiate this Resolver instance from others concurrently running on the same page.
* Used to prevent collisions in things like query parameters.
*/
resolverComponentInstanceID: string;
/**
* Indices that the backend should use to find the originating document.
*/
indices: string[];
}
/**

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { noAncestorsTwoChildenInIndexCalledAwesomeIndex } from '../data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index';
import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children';
import { Simulator } from '../test_utilities/simulator';
// Extend jest with a custom matcher
@ -19,6 +20,62 @@ let entityIDs: { origin: string; firstChild: string; secondChild: string };
// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
const resolverComponentInstanceID = 'resolverComponentInstanceID';
describe("Resolver, when rendered with the `indices` prop set to `[]` and the `databaseDocumentID` prop set to `_id`, and when the document is found in an index called 'awesome_index'", () => {
beforeEach(async () => {
// create a mock data access layer
const {
metadata: dataAccessLayerMetadata,
dataAccessLayer,
} = noAncestorsTwoChildenInIndexCalledAwesomeIndex();
// save a reference to the entity IDs exposed by the mock data layer
entityIDs = dataAccessLayerMetadata.entityIDs;
// save a reference to the `_id` supported by the mock data layer
databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
// create a resolver simulator, using the data access layer and an arbitrary component instance ID
simulator = new Simulator({
databaseDocumentID,
dataAccessLayer,
resolverComponentInstanceID,
indices: [],
});
});
it('should render no processes', async () => {
await expect(
simulator.map(() => ({
processes: simulator.processNodeElements().length,
}))
).toYieldEqualTo({
processes: 0,
});
});
describe("when rerendered with the `indices` prop set to `['awesome_index'`]", () => {
beforeEach(async () => {
simulator.indices = ['awesome_index'];
});
// Combining assertions here for performance. Unfortunately, Enzyme + jsdom + React is slow.
it(`should have 3 nodes, with the entityID's 'origin', 'firstChild', and 'secondChild'. 'origin' should be selected when the simulator has the right indices`, async () => {
await expect(
simulator.map(() => ({
selectedOriginCount: simulator.selectedProcessNode(entityIDs.origin).length,
unselectedFirstChildCount: simulator.unselectedProcessNode(entityIDs.firstChild).length,
unselectedSecondChildCount: simulator.unselectedProcessNode(entityIDs.secondChild).length,
nodePrimaryButtonCount: simulator.testSubject('resolver:node:primary-button').length,
}))
).toYieldEqualTo({
selectedOriginCount: 1,
unselectedFirstChildCount: 1,
unselectedSecondChildCount: 1,
nodePrimaryButtonCount: 3,
});
});
});
});
describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => {
beforeEach(async () => {
// create a mock data access layer
@ -31,7 +88,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
// create a resolver simulator, using the data access layer and an arbitrary component instance ID
simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID });
simulator = new Simulator({
databaseDocumentID,
dataAccessLayer,
resolverComponentInstanceID,
indices: [],
});
});
describe('when it has loaded', () => {
@ -159,7 +221,12 @@ describe('Resolver, when analyzing a tree that has two related events for the or
databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
// create a resolver simulator, using the data access layer and an arbitrary component instance ID
simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID });
simulator = new Simulator({
databaseDocumentID,
dataAccessLayer,
resolverComponentInstanceID,
indices: [],
});
});
describe('when it has loaded', () => {

View file

@ -34,6 +34,7 @@ describe('graph controls: when relsover is loaded with an origin node', () => {
dataAccessLayer,
databaseDocumentID,
resolverComponentInstanceID,
indices: [],
});
originEntityID = entityIDs.origin;

View file

@ -49,6 +49,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an
dataAccessLayer,
resolverComponentInstanceID,
history: memoryHistory,
indices: [],
});
return simulatorInstance;
}

View file

@ -29,7 +29,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
// create a resolver simulator, using the data access layer and an arbitrary component instance ID
simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID });
simulator = new Simulator({
databaseDocumentID,
dataAccessLayer,
resolverComponentInstanceID,
indices: [],
});
});
describe("when the second child node's first button has been clicked", () => {

View file

@ -26,6 +26,7 @@ describe('Resolver: data loading and resolution states', () => {
dataAccessLayer,
databaseDocumentID,
resolverComponentInstanceID,
indices: [],
});
});
@ -56,6 +57,7 @@ describe('Resolver: data loading and resolution states', () => {
dataAccessLayer,
databaseDocumentID,
resolverComponentInstanceID,
indices: [],
});
});
@ -85,6 +87,7 @@ describe('Resolver: data loading and resolution states', () => {
dataAccessLayer,
databaseDocumentID,
resolverComponentInstanceID,
indices: [],
});
});
@ -114,6 +117,7 @@ describe('Resolver: data loading and resolution states', () => {
dataAccessLayer,
databaseDocumentID,
resolverComponentInstanceID,
indices: [],
});
});
@ -145,6 +149,7 @@ describe('Resolver: data loading and resolution states', () => {
dataAccessLayer,
databaseDocumentID,
resolverComponentInstanceID,
indices: [],
});
});

View file

@ -31,7 +31,7 @@ export const ResolverWithoutProviders = React.memo(
* Use `forwardRef` so that the `Simulator` used in testing can access the top level DOM element.
*/
React.forwardRef(function (
{ className, databaseDocumentID, resolverComponentInstanceID }: ResolverProps,
{ className, databaseDocumentID, resolverComponentInstanceID, indices }: ResolverProps,
refToForward
) {
useResolverQueryParamCleaner();
@ -39,7 +39,7 @@ export const ResolverWithoutProviders = React.memo(
* This is responsible for dispatching actions that include any external data.
* `databaseDocumentID`
*/
useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID });
useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, indices });
const { timestamp } = useContext(SideEffectContext);
@ -69,8 +69,8 @@ export const ResolverWithoutProviders = React.memo(
},
[cameraRef, refToForward]
);
const isLoading = useSelector(selectors.isLoading);
const hasError = useSelector(selectors.hasError);
const isLoading = useSelector(selectors.isTreeLoading);
const hasError = useSelector(selectors.hadErrorLoadingTree);
const activeDescendantId = useSelector(selectors.ariaActiveDescendant);
const { colorMap } = useResolverTheme();

View file

@ -20,6 +20,7 @@ import { mock as mockResolverTree } from '../models/resolver_tree';
import { ResolverAction } from '../store/actions';
import { createStore } from 'redux';
import { resolverReducer } from '../store/reducer';
import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters';
describe('useCamera on an unpainted element', () => {
let element: HTMLElement;
@ -181,7 +182,7 @@ describe('useCamera on an unpainted element', () => {
if (tree !== null) {
const serverResponseAction: ResolverAction = {
type: 'serverReturnedResolverData',
payload: { result: tree, databaseDocumentID: '' },
payload: { result: tree, parameters: mockTreeFetcherParameters() },
};
act(() => {
store.dispatch(serverResponseAction);

View file

@ -15,19 +15,21 @@ import { useResolverDispatch } from './use_resolver_dispatch';
export function useStateSyncingActions({
databaseDocumentID,
resolverComponentInstanceID,
indices,
}: {
/**
* The `_id` of an event in ES. Used to determine the origin of the Resolver graph.
*/
databaseDocumentID?: string;
databaseDocumentID: string;
resolverComponentInstanceID: string;
indices: string[];
}) {
const dispatch = useResolverDispatch();
const locationSearch = useLocation().search;
useLayoutEffect(() => {
dispatch({
type: 'appReceivedNewExternalProperties',
payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch },
payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch, indices },
});
}, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch]);
}, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch, indices]);
}

View file

@ -19,7 +19,7 @@ import styled from 'styled-components';
import { FULL_SCREEN } from '../timeline/body/column_headers/translations';
import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations';
import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
import { useFullScreen } from '../../../common/containers/use_full_screen';
import { State } from '../../../common/store';
import { TimelineId, TimelineType } from '../../../../common/types/timeline';
@ -33,6 +33,8 @@ import { Resolver } from '../../../resolver/view';
import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal';
import * as i18n from './translations';
import { useUiSetting$ } from '../../../common/lib/kibana';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
const OverlayContainer = styled.div`
height: 100%;
@ -137,6 +139,16 @@ const GraphOverlayComponent = ({
globalFullScreen,
]);
const { signalIndexName } = useSignalIndex();
const [siemDefaultIndices] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const indices: string[] | null = useMemo(() => {
if (signalIndexName === null) {
return null;
} else {
return [...siemDefaultIndices, signalIndexName];
}
}, [signalIndexName, siemDefaultIndices]);
return (
<OverlayContainer>
<EuiHorizontalRule margin="none" />
@ -178,10 +190,13 @@ const GraphOverlayComponent = ({
</EuiFlexGroup>
<EuiHorizontalRule margin="none" />
<StyledResolver
databaseDocumentID={graphEventId}
resolverComponentInstanceID={currentTimeline.id}
/>
{graphEventId !== undefined && indices !== null && (
<StyledResolver
databaseDocumentID={graphEventId}
resolverComponentInstanceID={currentTimeline.id}
indices={indices}
/>
)}
<AllCasesModal />
</OverlayContainer>
);

View file

@ -18,11 +18,6 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
query: { _id, indices },
} = request;
const siemClient = context.securitySolution!.getAppClient();
const queryIndices = indices;
// if the alert was promoted by a rule it will exist in the signals index so search there too
queryIndices.push(siemClient.getSignalsIndex());
/**
* A safe type for the response based on the semantics of the query.
* We specify _source, asking for `process.entity_id` and we only
@ -36,8 +31,8 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
| [
{
_source: {
process: {
entity_id: string;
process?: {
entity_id?: string;
};
};
}
@ -49,7 +44,7 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
'search',
{
ignoreUnavailable: true,
index: queryIndices,
index: indices,
body: {
// only return process.entity_id
_source: 'process.entity_id',
@ -64,19 +59,6 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
values: _id,
},
},
{
exists: {
// only return documents that have process.entity_id
field: 'process.entity_id',
},
},
{
bool: {
must_not: {
term: { 'process.entity_id': '' },
},
},
},
],
},
},
@ -85,15 +67,13 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
);
const responseBody: ResolverEntityIndex = [];
for (const {
_source: {
// eslint-disable-next-line @typescript-eslint/naming-convention
process: { entity_id },
},
} of queryResponse.hits.hits) {
responseBody.push({
entity_id,
});
for (const hit of queryResponse.hits.hits) {
// check that the field is defined and that is not an empty string
if (hit._source.process?.entity_id) {
responseBody.push({
entity_id: hit._source.process.entity_id,
});
}
}
return response.ok({ body: responseBody });
};

File diff suppressed because it is too large Load diff

View file

@ -82,6 +82,7 @@ const AppRoot = React.memo(
<ResolverWithoutProviders
databaseDocumentID=""
resolverComponentInstanceID="test"
indices={[]}
/>
</Wrapper>
</Provider>

View file

@ -0,0 +1,57 @@
/*
* 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 { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants';
import { ResolverEntityIndex } from '../../../../plugins/security_solution/common/endpoint/types';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('Resolver tests for the entity route', () => {
before(async () => {
await esArchiver.load('endpoint/resolver/signals');
});
after(async () => {
await esArchiver.unload('endpoint/resolver/signals');
});
it('returns an event even if it does not have a mapping for entity_id', async () => {
// this id is from the es archive
const _id = 'fa7eb1546f44fd47d8868be8d74e0082e19f22df493c67a7725457978eb648ab';
const { body }: { body: ResolverEntityIndex } = await supertest.get(
`/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default`
);
expect(body).eql([
{
// this value is from the es archive
entity_id:
'MTIwNWY1NWQtODRkYS00MzkxLWIyNWQtYTNkNGJmNDBmY2E1LTc1NTItMTMyNDM1NDY1MTQuNjI0MjgxMDA=',
},
]);
});
it('does not return an event when it does not have the entity_id field in the document', async () => {
// this id is from the es archive
const _id = 'no-entity-id-field';
const { body }: { body: ResolverEntityIndex } = await supertest.get(
`/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default`
);
expect(body).to.be.empty();
});
it('does not return an event when it does not have the process field in the document', async () => {
// this id is from the es archive
const _id = 'no-process-field';
const { body }: { body: ResolverEntityIndex } = await supertest.get(
`/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default`
);
expect(body).to.be.empty();
});
});
}

View file

@ -10,6 +10,7 @@ export default function (providerContext: FtrProviderContext) {
describe('Resolver tests', () => {
loadTestFile(require.resolve('./entity_id'));
loadTestFile(require.resolve('./entity'));
loadTestFile(require.resolve('./children'));
loadTestFile(require.resolve('./tree'));
loadTestFile(require.resolve('./alerts'));