Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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