diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index fbeeefe1ab9f264..3de6f08f5e01543 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -12,8 +12,11 @@ import { interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; - readonly events: ResolverEvent[]; - readonly stats: Map; + readonly payload: { + readonly events: Readonly; + readonly stats: Readonly>; + readonly lineageLimits: { readonly children: string | null; readonly ancestors: string | null }; + }; } interface ServerFailedToReturnResolverData { diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts index d120adb72cd81b1..163846e0414dbf7 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts @@ -9,8 +9,13 @@ import { DataAction } from './action'; import { dataReducer } from './reducer'; import { DataState } from '../../types'; import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types'; -import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; +import { + graphableProcesses, + processNodePositionsAndEdgeLineSegments, + limitsReached, +} from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; describe('resolver graph layout', () => { let processA: LegacyEndpointEvent; @@ -114,7 +119,10 @@ describe('resolver graph layout', () => { describe('when rendering no nodes', () => { beforeEach(() => { const events: ResolverEvent[] = []; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it('the graphableProcesses list should only include nothing', () => { @@ -128,7 +136,10 @@ describe('resolver graph layout', () => { describe('when rendering one node', () => { beforeEach(() => { const events = [processA]; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it('the graphableProcesses list should only include nothing', () => { @@ -142,7 +153,10 @@ describe('resolver graph layout', () => { describe('when rendering two nodes, one being the parent of the other', () => { beforeEach(() => { const events = [processA, processB]; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it('the graphableProcesses list should only include nothing', () => { @@ -166,7 +180,10 @@ describe('resolver graph layout', () => { processH, processI, ]; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it("the graphableProcesses list should only include events with 'processCreated' an 'processRan' eventType", () => { @@ -187,3 +204,48 @@ describe('resolver graph layout', () => { }); }); }); + +describe('resolver graph with too much lineage', () => { + let generator: EndpointDocGenerator; + let store: Store; + let allEvents: ResolverEvent[]; + let childrenCursor: string; + let ancestorCursor: string; + + beforeEach(() => { + generator = new EndpointDocGenerator('seed'); + allEvents = generator.generateTree({ ancestors: 1, generations: 2, children: 2 }).allEvents; + childrenCursor = 'aValidChildursor'; + ancestorCursor = 'aValidAncestorCursor'; + store = createStore(dataReducer, undefined); + }); + + describe('should select from state properly', () => { + it('should indicate there are too many ancestors', () => { + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { + events: allEvents, + stats: new Map(), + lineageLimits: { children: childrenCursor, ancestors: ancestorCursor }, + }, + }; + store.dispatch(action); + const { ancestors } = limitsReached(store.getState()); + expect(ancestors).toEqual(true); + }); + it('should indicate there are too many children', () => { + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { + events: allEvents, + stats: new Map(), + lineageLimits: { children: childrenCursor, ancestors: ancestorCursor }, + }, + }; + store.dispatch(action); + const { children } = limitsReached(store.getState()); + expect(children).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index 3e897a91a74c671..a36d43b70b87d74 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -13,6 +13,7 @@ function initialState(): DataState { relatedEventsStats: new Map(), relatedEvents: new Map(), relatedEventsReady: new Map(), + lineageLimits: { children: null, ancestors: null }, isLoading: false, hasError: false, }; @@ -22,8 +23,9 @@ export const dataReducer: Reducer = (state = initialS if (action.type === 'serverReturnedResolverData') { return { ...state, - results: action.events, - relatedEventsStats: action.stats, + results: action.payload.events, + relatedEventsStats: action.payload.stats, + lineageLimits: action.payload.lineageLimits, isLoading: false, hasError: false, }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 2873993cc645f70..ba415e6d83c8d7e 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -529,3 +529,15 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( }; } ); + +/** + * Returns the `children` and `ancestors` limits for the current graph, if any. + * + * @param state {DataState} the DataState from the reducer + */ +export const limitsReached = (state: DataState): { children: boolean; ancestors: boolean } => { + return { + children: state.lineageLimits.children !== null, + ancestors: state.lineageLimits.ancestors !== null, + }; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts index 819ea60e8d57918..343b4e1a14478c6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts @@ -77,6 +77,8 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { } const nodeStats: Map = new Map(); nodeStats.set(entityId, stats); + const lineageLimits = { children: children.nextChild, ancestors: ancestry.nextAncestor }; + const events = [ ...lifecycle, ...getLifecycleEventsAndStats(children.childNodes, nodeStats), @@ -84,8 +86,11 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { ]; api.dispatch({ type: 'serverReturnedResolverData', - events, - stats: nodeStats, + payload: { + events, + stats: nodeStats, + lineageLimits, + }, }); } catch (error) { api.dispatch({ diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index bff30c62864f2e3..3a5c48009e5bb00 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -152,6 +152,15 @@ export const graphableProcesses = composeSelectors( dataSelectors.graphableProcesses ); +/** + * Select the `ancestors` and `children` limits that were reached or exceeded + * during the request for the current tree. + */ +export const lineageLimitsReached = composeSelectors( + dataStateSelector, + dataSelectors.limitsReached +); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index a48f3b59b0f6dc1..f0e401dd2e89300 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -147,9 +147,10 @@ export type CameraState = { */ export interface DataState { readonly results: readonly ResolverEvent[]; - readonly relatedEventsStats: Map; + readonly relatedEventsStats: Readonly>; readonly relatedEvents: Map; readonly relatedEventsReady: Map; + readonly lineageLimits: Readonly<{ children: string | null; ancestors: string | null }>; isLoading: boolean; hasError: boolean; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 8ed9f00d51af8bc..dc7cb9a2ab19918 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -176,8 +176,11 @@ describe('useCamera on an unpainted element', () => { } const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - events, - stats: new Map(), + payload: { + events, + stats: new Map(), + lineageLimits: { children: null, ancestors: null }, + }, }; act(() => { store.dispatch(serverResponseAction);