[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:
parent
f7ad02d452
commit
ae093e5a7a
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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([]);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -34,6 +34,7 @@ describe('graph controls: when relsover is loaded with an origin node', () => {
|
|||
dataAccessLayer,
|
||||
databaseDocumentID,
|
||||
resolverComponentInstanceID,
|
||||
indices: [],
|
||||
});
|
||||
originEntityID = entityIDs.origin;
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an
|
|||
dataAccessLayer,
|
||||
resolverComponentInstanceID,
|
||||
history: memoryHistory,
|
||||
indices: [],
|
||||
});
|
||||
return simulatorInstance;
|
||||
}
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -82,6 +82,7 @@ const AppRoot = React.memo(
|
|||
<ResolverWithoutProviders
|
||||
databaseDocumentID=""
|
||||
resolverComponentInstanceID="test"
|
||||
indices={[]}
|
||||
/>
|
||||
</Wrapper>
|
||||
</Provider>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
Loading…
Reference in a new issue