Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display node 75% view submenus #64121

Merged
merged 81 commits into from
May 18, 2020
Merged
Changes from 67 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
c4ab6e2
cleanup: move submenus into a general component
Apr 21, 2020
cfd95e8
remove unnecessary useState import
Apr 21, 2020
2c7b279
referral for help
Apr 22, 2020
5aef930
Use EUISelectable for options list
Apr 23, 2020
8ea107d
adding EUISelectable
Apr 23, 2020
ab58823
user-select off to fix bug Jonny found
Apr 27, 2020
42c9a3b
Revert "user-select off to fix bug Jonny found"
Apr 27, 2020
f166d6d
straighten corners on menu open to match mocks
Apr 27, 2020
6cb887b
adding type information for related events
Apr 27, 2020
36fe414
refine type information for related events
Apr 27, 2020
2429b4b
Node option submenus take waiting prop
Apr 28, 2020
41f4a79
Merge remote-tracking branch 'upstream/master' into resolver/lay-in-s…
Apr 28, 2020
8c454d4
merge upstream
Apr 28, 2020
747cd6e
stub selector for related events
Apr 29, 2020
72665e0
provision component with selected relatedEvents
Apr 29, 2020
a98cd7a
shape reducder
Apr 29, 2020
6e75074
adjust reducer for consistency
Apr 29, 2020
5529b7e
adding request logic to middleware
Apr 30, 2020
29e0018
investigating problems with /events and CLI
Apr 30, 2020
c7c22bf
pulling through selector
May 6, 2020
5631113
test rendering in component
May 6, 2020
e8d48db
cleanup unused
May 6, 2020
01ea4a1
more cleanup
May 6, 2020
c64d990
cleanup action
May 6, 2020
c7edee7
fix submenu options typings
May 6, 2020
2d1b3f2
move utility function off and seal it
May 6, 2020
4ad185d
fix middleware types
May 8, 2020
de039bf
adjust model to return Process
May 8, 2020
ffdb41f
K Qualters review: change action name
May 8, 2020
3a6314b
lint
May 8, 2020
12af60b
Add error handling
May 8, 2020
ecf85db
cleanup reducer
May 8, 2020
84ffd8b
document selector
May 8, 2020
7fc4d0b
adding comments to types
May 8, 2020
8c46844
linting
May 8, 2020
f1863c5
use built-in EUI loading indicator
May 8, 2020
0620a86
linting
May 8, 2020
ea3aa85
node cleanup
May 8, 2020
4842833
add related category select action
May 11, 2020
03237e6
Move submenu to import to clear up event_dot
May 11, 2020
1d09e38
18n format for number
May 11, 2020
7395f6d
add comments to submenu
May 11, 2020
ea0c436
add related alerts action
May 11, 2020
e1d5cc9
Merge branch 'master' into resolver/lay-in-submenu-options
elasticmachine May 11, 2020
211df54
lint selector
May 11, 2020
e2d87f4
D Plumlee review: remove unnecessary bindings to type
May 11, 2020
fe22f98
D Plumlee review: remove comments
May 11, 2020
c0bcc25
add comments to relatedEventOptions breakouts
May 11, 2020
e375fa7
R Austin review: move logic from reducer to selector
May 12, 2020
eaea02e
linting
May 12, 2020
7bb0c30
D Plumlee / R Austin review: i18n for event categories
May 12, 2020
ae14c1e
lint resolver node
May 12, 2020
5a4819c
R Austin Review: adjust middleware, rewrites for clarity, fixing types
May 12, 2020
7b8aa36
R Austin review: Move category display names to view
May 13, 2020
e019815
R Austin review: remove all symbols
May 13, 2020
2d7523a
R Austin review: refactor memos for clarity
May 13, 2020
17dfe3c
add comment
May 13, 2020
2413c93
K Qualters review: destructure arguments
May 13, 2020
d08f414
K Qualters review: add more ECS event types to Map
May 13, 2020
2f90697
add comments for clarification
May 13, 2020
848f659
Merge branch 'master' into resolver/lay-in-submenu-options
elasticmachine May 13, 2020
70fa593
Merge branch 'master' into resolver/lay-in-submenu-options
elasticmachine May 13, 2020
66fc653
D Plumlee review: Display unknown for unmatched categories
May 14, 2020
235ebd5
R Austin review: Call i18n with static values
May 14, 2020
080e112
D Plumlee review: remove reselect call from selector where it's not n…
May 14, 2020
7e03a25
R Austin / J Brown review: Add test coverage for selectors
May 14, 2020
f39f4cd
Merge branch 'master' into resolver/lay-in-submenu-options
elasticmachine May 14, 2020
78b0531
Merge branch 'master' into resolver/lay-in-submenu-options
elasticmachine May 18, 2020
f601477
R Austin review: undo Memos, add comments
May 18, 2020
3d52b8c
Merge remote-tracking branch 'origin/resolver/lay-in-submenu-options'…
May 18, 2020
1985a31
R Austin review: adjust type on submenu for readability
May 18, 2020
91f2bd6
R Austin review: move selectable options to state hook
May 18, 2020
c05de64
R Austin review: simplify type, remove memo, remove inert return
May 18, 2020
5bb0411
R Austin review: add comments, move testing instantiation
May 18, 2020
7e85044
R Austin review: remove unnecessary type check
May 18, 2020
c06da13
R Austin review: direct return
May 18, 2020
e742cce
R Austin review: Add comments to actions
May 18, 2020
fe978f6
R Austin review: change payload for failed action
May 18, 2020
7750c97
R Austin review: More specific related type?
May 18, 2020
9e07c11
R Austin review: Remove Error construction
May 18, 2020
002d631
R Austin review: mark relatedEventInfo non-optional
May 18, 2020
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
@@ -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 {
@@ -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) {
eventCategoryToReturn = legacyFullType;
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
const eventCategories = event.event.category;
const eventCategory =
typeof eventCategories === 'string' ? eventCategories : eventCategories[0] || '';
bkimmel marked this conversation as resolved.
Show resolved Hide resolved

if (eventCategory) {
eventCategoryToReturn = eventCategory;
}
}
return eventCategoryToReturn;
}
Original file line number Diff line number Diff line change
@@ -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)
@@ -77,11 +86,27 @@ interface UserSelectedResolverNode {
};
}

interface UserSelectedRelatedEventCategory {
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
readonly type: 'userSelectedRelatedEventCategory';
readonly payload: {
subject: ResolverEvent;
category: string;
};
}

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
@@ -5,6 +5,7 @@
*/

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

interface ServerReturnedResolverData {
readonly type: 'serverReturnedResolverData';
@@ -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];
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
}

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

@@ -23,6 +24,35 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
isLoading: false,
hasError: false,
};
} else if (action.type === 'userRequestedRelatedEventData') {
const resolverEvent = action.payload;
const statsMap = state.resultsEnrichedWithRelatedEventInfo;
if (statsMap) {
const currentStatsMap = new Map(statsMap);
/**
* 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 };
}
return state;
} else if (action.type === 'serverFailedToReturnRelatedEventData') {
const statsMap = state.resultsEnrichedWithRelatedEventInfo;
if (statsMap) {
const currentStatsMap = new Map(statsMap);
const [resolverEvent] = action.payload;
currentStatsMap.set(resolverEvent, new Error('error requesting related events'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it necessary to set an Error object in redux state? What is the English text in the error object used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an object that indicates there is an Error. The language in the Error could help a user understand why the Error was created. Functionally, it is equivalent to an object - it just has some extra semantics (this is an Error). I'd like to change this to something you like better: Can I just use a normal object with an {errorMessage: ``}? Or a string?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the error message being shown to the user? If not, I think having it in the code is confusing. If so, it should be translated, and it should probably be defined in the view part of the app.

return { ...state, resultsEnrichedWithRelatedEventInfo: currentStatsMap };
}
return state;
} else if (action.type === 'serverReturnedRelatedEventData') {
const statsMap = state.resultsEnrichedWithRelatedEventInfo;
if (statsMap && typeof statsMap?.set === 'function') {
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
const relatedDataEntries = new Map([...statsMap, ...action.payload]);
return { ...state, resultsEnrichedWithRelatedEventInfo: relatedDataEntries };
}
return state;
} else if (action.type === 'appRequestedResolverData') {
return {
...state,
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 } 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', () => {
const relatedEventInfoBeforeAction = new Map(relatedEvents(store.getState()) || []);
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
beforeEach(() => {
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', () => {
const mockBaseEvent = {} as ResolverEvent;
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
beforeEach(() => {
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
@@ -14,6 +14,8 @@ import {
ProcessWithWidthMetadata,
Matrix3,
AdjacentProcessMap,
RelatedEventData,
RelatedEventDataEntryWithStats,
} from '../../types';
import { ResolverEvent } from '../../../../../common/types';
import { Vector2 } from '../../types';
@@ -405,6 +407,77 @@ 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;
};

export const relatedEventStats = createSelector(relatedEventResults, function getRelatedEvents(
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
/* 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 (typeof newStatsEntry === 'object') {
bkimmel marked this conversation as resolved.
Show resolved Hide resolved
// compile stats
if (newStatsEntry instanceof Error) {
relatedEventStats.set(updatedEvent, newStatsEntry);
continue;
}
/**
* 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;
});

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,
Original file line number Diff line number Diff line change
@@ -5,9 +5,10 @@
*/

import { Dispatch, MiddlewareAPI } from 'redux';
import { HttpHandler } from 'kibana/public';
import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public';
import { EndpointPluginServices } from '../../../plugin';
import { ResolverState, ResolverAction } from '../types';
import { ResolverState, ResolverAction, RelatedEventDataEntry } from '../types';
import { ResolverEvent, ResolverNode } from '../../../../common/types';
import * as event from '../../../../common/models/event';

@@ -30,6 +31,33 @@ function flattenEvents(children: ResolverNode[], events: ResolverEvent[] = []):
}, events);
}

type RelatedEventAPIResponse = Error | { events: ResolverEvent[] };
/**
* As the design goal of this stopgap was to prevent saturating the server with /events
* requests, this generator intentionally processes events in serial rather than in parallel.
* @param eventsToFetch
* events to run against the /id/events API
* @param httpGetter
* the HttpHandler to use
*/
async function* getEachRelatedEventsResult(
eventsToFetch: ResolverEvent[],
httpGetter: HttpHandler
): AsyncGenerator<[ResolverEvent, RelatedEventAPIResponse]> {
for (const eventToQueryForRelateds of eventsToFetch) {
const id = event.entityId(eventToQueryForRelateds);
let result: RelatedEventAPIResponse;
try {
result = await httpGetter(`/api/endpoint/resolver/${id}/events`, {
query: { events: 100 },
});
} catch (e) {
result = new Error(`Error fetching related events for entity=${id}`);
}
yield [eventToQueryForRelateds, result];
}
}

export const resolverMiddlewareFactory: MiddlewareFactory = context => {
return api => next => async (action: ResolverAction) => {
next(action);
@@ -78,5 +106,44 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => {
}
}
}

if (action.type === 'userRequestedRelatedEventData') {
if (typeof context !== 'undefined') {
const response: Map<ResolverEvent, RelatedEventDataEntry> = new Map();
for await (const results of getEachRelatedEventsResult(
[action.payload],
context.services.http.get
)) {
/**
* results here will take the shape of
* [event requested , response of event against the /related api]
*/
const [baseEvent, apiResults] = results;
if (apiResults instanceof Error) {
api.dispatch({
type: 'serverFailedToReturnRelatedEventData',
payload: [results[0]],
});
continue;
}

const fetchedResults = apiResults.events;
// pack up the results into response
const relatedEventEntry = fetchedResults.map(relatedEvent => {
return {
relatedEvent,
relatedEventType: event.eventType(relatedEvent),
};
});

response.set(baseEvent, { relatedEvents: relatedEventEntry });
}

api.dispatch({
type: 'serverReturnedRelatedEventData',
payload: response,
});
}
}
};
};
Original file line number Diff line number Diff line change
@@ -60,6 +60,11 @@ export const processAdjacencies = composeSelectors(
dataSelectors.processAdjacencies
);

/**
* Returns a map of `ResolverEvent`s to their related `ResolverEvent`s
*/
export const relatedEvents = composeSelectors(dataStateSelector, dataSelectors.relatedEvents);

/**
* Returns the id of the "current" tree node (fake-focused)
*/
Loading