Skip to content

Commit

Permalink
Resolver: Display node 75% view submenus (#64121) (#66951)
Browse files Browse the repository at this point in the history
  • Loading branch information
bkimmel authored May 19, 2020
1 parent 48e049e commit 80052f7
Show file tree
Hide file tree
Showing 12 changed files with 768 additions and 49 deletions.
21 changes: 20 additions & 1 deletion x-pack/plugins/endpoint/common/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { LegacyEndpointEvent, ResolverEvent } from '../types';

export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEvent {
Expand Down Expand Up @@ -46,3 +45,23 @@ export function parentEntityId(event: ResolverEvent): string | undefined {
}
return event.process.parent?.entity_id;
}

export function eventType(event: ResolverEvent): string {
// Returning "Process" as a catch-all here because it seems pretty general
let eventCategoryToReturn: string = 'Process';
if (isLegacyEvent(event)) {
const legacyFullType = event.endgame.event_type_full;
if (legacyFullType) {
return legacyFullType;
}
} else {
const eventCategories = event.event.category;
const eventCategory =
typeof eventCategories === 'string' ? eventCategories : eventCategories[0] || '';

if (eventCategory) {
eventCategoryToReturn = eventCategory;
}
}
return eventCategoryToReturn;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ interface AppRequestedResolverData {
readonly type: 'appRequestedResolverData';
}

/**
* The action dispatched when the app requests related event data for one or more
* subjects (whose ids should be included as an array @ `payload`)
*/
interface UserRequestedRelatedEventData {
readonly type: 'userRequestedRelatedEventData';
readonly payload: ResolverEvent;
}

/**
* When the user switches the "active descendant" of the Resolver.
* The "active descendant" (from the point of view of the parent element)
Expand Down Expand Up @@ -77,11 +86,36 @@ interface UserSelectedResolverNode {
};
}

/**
* This action should dispatch to indicate that the user chose to
* focus on examining the related events of a particular ResolverEvent.
* Optionally, this can be bound by a category of related events (e.g. 'file' or 'dns')
*/
interface UserSelectedRelatedEventCategory {
readonly type: 'userSelectedRelatedEventCategory';
readonly payload: {
subject: ResolverEvent;
category?: string;
};
}

/**
* This action should dispatch to indicate that the user chose to focus
* on examining alerts related to a particular ResolverEvent
*/
interface UserSelectedRelatedAlerts {
readonly type: 'userSelectedRelatedAlerts';
readonly payload: ResolverEvent;
}

export type ResolverAction =
| CameraAction
| DataAction
| UserBroughtProcessIntoView
| UserChangedSelectedEvent
| AppRequestedResolverData
| UserFocusedOnResolverNode
| UserSelectedResolverNode;
| UserSelectedResolverNode
| UserRequestedRelatedEventData
| UserSelectedRelatedEventCategory
| UserSelectedRelatedAlerts;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { ResolverEvent } from '../../../../../common/types';
import { RelatedEventDataEntry } from '../../types';

interface ServerReturnedResolverData {
readonly type: 'serverReturnedResolverData';
Expand All @@ -15,4 +16,24 @@ interface ServerFailedToReturnResolverData {
readonly type: 'serverFailedToReturnResolverData';
}

export type DataAction = ServerReturnedResolverData | ServerFailedToReturnResolverData;
/**
* Will occur when a request for related event data is fulfilled by the API.
*/
interface ServerReturnedRelatedEventData {
readonly type: 'serverReturnedRelatedEventData';
readonly payload: Map<ResolverEvent, RelatedEventDataEntry>;
}

/**
* Will occur when a request for related event data is unsuccessful.
*/
interface ServerFailedToReturnRelatedEventData {
readonly type: 'serverFailedToReturnRelatedEventData';
readonly payload: ResolverEvent;
}

export type DataAction =
| ServerReturnedResolverData
| ServerFailedToReturnResolverData
| ServerReturnedRelatedEventData
| ServerFailedToReturnRelatedEventData;
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function initialState(): DataState {
results: [],
isLoading: false,
hasError: false,
resultsEnrichedWithRelatedEventInfo: new Map(),
};
}

Expand All @@ -23,6 +24,26 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
isLoading: false,
hasError: false,
};
} else if (action.type === 'userRequestedRelatedEventData') {
const resolverEvent = action.payload;
const currentStatsMap = new Map(state.resultsEnrichedWithRelatedEventInfo);
/**
* Set the waiting indicator for this event to indicate that related event results are pending.
* It will be replaced by the actual results from the API when they are returned.
*/
currentStatsMap.set(resolverEvent, 'waitingForRelatedEventData');
return { ...state, resultsEnrichedWithRelatedEventInfo: currentStatsMap };
} else if (action.type === 'serverFailedToReturnRelatedEventData') {
const currentStatsMap = new Map(state.resultsEnrichedWithRelatedEventInfo);
const resolverEvent = action.payload;
currentStatsMap.set(resolverEvent, 'error');
return { ...state, resultsEnrichedWithRelatedEventInfo: currentStatsMap };
} else if (action.type === 'serverReturnedRelatedEventData') {
const relatedDataEntries = new Map([
...state.resultsEnrichedWithRelatedEventInfo,
...action.payload,
]);
return { ...state, resultsEnrichedWithRelatedEventInfo: relatedDataEntries };
} else if (action.type === 'appRequestedResolverData') {
return {
...state,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 { Store, createStore } from 'redux';
import { DataAction } from './action';
import { dataReducer } from './reducer';
import {
DataState,
RelatedEventDataEntry,
RelatedEventDataEntryWithStats,
RelatedEventData,
} from '../../types';
import { ResolverEvent } from '../../../../../common/types';
import { relatedEventStats, relatedEvents } from './selectors';

describe('resolver data selectors', () => {
const store: Store<DataState, DataAction> = createStore(dataReducer, undefined);
describe('when related event data is reduced into state with no results', () => {
let relatedEventInfoBeforeAction: RelatedEventData;
beforeEach(() => {
relatedEventInfoBeforeAction = new Map(relatedEvents(store.getState()) || []);
const payload: Map<ResolverEvent, RelatedEventDataEntry> = new Map();
const action: DataAction = { type: 'serverReturnedRelatedEventData', payload };
store.dispatch(action);
});
it('should have the same related info as before the action', () => {
const relatedInfoAfterAction = relatedEvents(store.getState());
expect(relatedInfoAfterAction).toEqual(relatedEventInfoBeforeAction);
});
});
describe('when related event data is reduced into state with 2 dns results', () => {
let mockBaseEvent: ResolverEvent;
beforeEach(() => {
mockBaseEvent = {} as ResolverEvent;
function dnsRelatedEventEntry() {
const fakeEvent = {} as ResolverEvent;
return { relatedEvent: fakeEvent, relatedEventType: 'dns' };
}
const payload: Map<ResolverEvent, RelatedEventDataEntry> = new Map([
[
mockBaseEvent,
{
relatedEvents: [dnsRelatedEventEntry(), dnsRelatedEventEntry()],
},
],
]);
const action: DataAction = { type: 'serverReturnedRelatedEventData', payload };
store.dispatch(action);
});
it('should compile stats reflecting a count of 2 for dns', () => {
const actualStats = relatedEventStats(store.getState());
const statsForFakeEvent = actualStats.get(mockBaseEvent)! as RelatedEventDataEntryWithStats;
expect(statsForFakeEvent.stats).toEqual({ dns: 2 });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
ProcessWithWidthMetadata,
Matrix3,
AdjacentProcessMap,
RelatedEventData,
RelatedEventDataEntryWithStats,
} from '../../types';
import { ResolverEvent } from '../../../../../common/types';
import { Vector2 } from '../../types';
Expand Down Expand Up @@ -405,6 +407,86 @@ export const indexedProcessTree = createSelector(graphableProcesses, function in
return indexedProcessTreeFactory(graphableProcesses);
});

/**
* Process events that will be graphed.
*/
export const relatedEventResults = function(data: DataState) {
return data.resultsEnrichedWithRelatedEventInfo;
};

/**
* This selector compiles the related event data attached in `relatedEventResults`
* into a `RelatedEventData` map of ResolverEvents to statistics about their related events
*/
export const relatedEventStats = createSelector(relatedEventResults, function getRelatedEvents(
/* eslint-disable no-shadow */
relatedEventResults
/* eslint-enable no-shadow */
) {
/* eslint-disable no-shadow */
const relatedEventStats: RelatedEventData = new Map();
/* eslint-enable no-shadow */
if (!relatedEventResults) {
return relatedEventStats;
}

for (const updatedEvent of relatedEventResults.keys()) {
const newStatsEntry = relatedEventResults.get(updatedEvent);
if (newStatsEntry === 'error') {
// If the entry is an error, return it as is
relatedEventStats.set(updatedEvent, newStatsEntry);
continue;
}
if (typeof newStatsEntry === 'object') {
/**
* Otherwise, it should be a valid stats entry.
* Do the work to compile the stats.
* Folowing reduction, this will be a record like
* {DNS: 10, File: 2} etc.
*/
const statsForEntry = newStatsEntry?.relatedEvents.reduce(
(compiledStats: Record<string, number>, relatedEvent: { relatedEventType: string }) => {
compiledStats[relatedEvent.relatedEventType] =
(compiledStats[relatedEvent.relatedEventType] || 0) + 1;
return compiledStats;
},
{}
);

const newRelatedEventStats: RelatedEventDataEntryWithStats = Object.assign(newStatsEntry, {
stats: statsForEntry,
});
relatedEventStats.set(updatedEvent, newRelatedEventStats);
}
}
return relatedEventStats;
});

/**
* This selects `RelatedEventData` maps specifically for graphable processes
*/
export const relatedEvents = createSelector(
graphableProcesses,
relatedEventStats,
function getRelatedEvents(
/* eslint-disable no-shadow */
graphableProcesses,
relatedEventStats
/* eslint-enable no-shadow */
) {
const eventsRelatedByProcess: RelatedEventData = new Map();
/* eslint-disable no-shadow */
return graphableProcesses.reduce((relatedEvents, graphableProcess) => {
/* eslint-enable no-shadow */
const relatedEventDataEntry = relatedEventStats?.get(graphableProcess);
if (relatedEventDataEntry) {
relatedEvents.set(graphableProcess, relatedEventDataEntry);
}
return relatedEvents;
}, eventsRelatedByProcess);
}
);

export const processAdjacencies = createSelector(
indexedProcessTree,
graphableProcesses,
Expand Down
Loading

0 comments on commit 80052f7

Please sign in to comment.