-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Endpoint] Recursive resolver children #61914
Changes from 34 commits
42e9f93
228ed62
d3d82a7
4d248bc
58009e5
3b35b1a
a36ef94
63ad7eb
2b4ce9f
d686f17
0731e88
dd4ac56
51fcea6
35d0631
b4a5a6a
8899993
c4bcbbb
50d5d35
e5e1c96
1d6cd24
e50dc02
139c4f2
7fdf029
3e9fa09
63f13a7
0f5963d
307318d
eed8ca3
e5a3a48
b822069
288aadd
24abb1f
8321e99
9b9f46e
7f47791
8ebf221
a607144
b47f270
b430f18
5725da1
3ae0c85
8262af9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { EndpointEvent, LegacyEndpointEvent } from '../types'; | ||
import { EndpointEvent, LegacyEndpointEvent, ResolverEvent } from '../types'; | ||
|
||
export function isLegacyEvent( | ||
event: EndpointEvent | LegacyEndpointEvent | ||
|
@@ -22,10 +22,33 @@ export function eventTimestamp( | |
} | ||
} | ||
|
||
/** TODO, seems wrong */ | ||
export function eventName(event: EndpointEvent | LegacyEndpointEvent): string { | ||
if (isLegacyEvent(event)) { | ||
return event.endgame.process_name ? event.endgame.process_name : ''; | ||
} else { | ||
return event.process.name; | ||
} | ||
} | ||
|
||
export function eventId(event: ResolverEvent) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind moving these methods that take "ResolverEvent" to a file called "resolver_event" |
||
if (isLegacyEvent(event)) { | ||
return String(event.endgame.serial_event_id); | ||
} | ||
return event.event.id; | ||
} | ||
|
||
export function entityId(event: ResolverEvent) { | ||
if (isLegacyEvent(event)) { | ||
return String(event.endgame.unique_pid); | ||
} | ||
return event.process.entity_id; | ||
} | ||
|
||
export function parentEntityId(event: ResolverEvent) { | ||
if (isLegacyEvent(event)) { | ||
const ppid = event.endgame.unique_ppid; | ||
return String(ppid); // if unique_ppid is undefined return undefined | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❔ This will return the string "undefined" if ppid is undefined, right? Maybe put quotes around There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we shouldn't cast undefined to a string. Could you add an explicit return type here? |
||
} | ||
return event.process.parent?.entity_id; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,29 @@ export class EndpointAppConstants { | |
static MAX_LONG_INT = '9223372036854775807'; // 2^63-1 | ||
} | ||
|
||
export interface NodeStats { | ||
totalEvents: number; | ||
totalAlerts: number; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this info? Isn't it expensive to get a count (esp. an exact count?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think the info would useful when display totals in the UI.
I think this would be useful for displaying counts, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❔ Does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jonathan-buttner if we showed the user some total, we would need to explain what the total is. What is this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds like we're probably going to remove the stats stuff for now, but I think what this is trying to communicate is
|
||
} | ||
|
||
export interface NodePagination { | ||
nextChild?: string | null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❔ The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ Note to self to replace/merge some of the things in adjacencyMap with this so the meaning doesn't diverge There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jonathan-buttner @kqualters-elastic if there is special meaning to these interface fields, can we document it here? Also, can you clarify what "you've reached the last page" means? Data could still be coming in right? So unless we know that there will never be any more data, we will probably need to assume that there is more. Let me know if I'm wrong or unclear. I'm writing everything on a tablet and it's tedious 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO ask about this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @oatkiller Yeah that's a fair point that we won't ever know for sure that there isn't more data because we could have been received some since they last time we searched, but it's probably still useful to indicate that we know for sure that there is more data available right? For example if we only retrieve a portion of the children for a node, by indicating that we know there are more children we could display an arrow or plus sign or something in the UI indicating to the user that there is definitely more children to retrieve. |
||
nextEvent?: string | null; | ||
nextAncestor?: string | null; | ||
nextAlert?: string | null; | ||
} | ||
|
||
export interface Node { | ||
id: string; | ||
children: Node[]; | ||
events: ResolverEvent[]; | ||
lifecycle: ResolverEvent[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how's this different than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎗️ |
||
ancestors?: Node[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❔ Are these in any particular order like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, you're correct, |
||
parent?: Node | null; | ||
pagination: NodePagination; | ||
stats?: NodeStats; | ||
} | ||
|
||
export interface AlertResultList { | ||
/** | ||
* The alerts restricted by page size. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,5 +33,6 @@ export const AlertDetailResolver = styled( | |
width: 100%; | ||
display: flex; | ||
flex-grow: 1; | ||
min-height: 500px; | ||
/* gross demo hack */ | ||
min-height: calc(100vh - 505px); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❔ Did you check this on laptop-sized monitors? Seems like it would get squishy, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that 505px is the static height of the alert detail content above it, not sure what choice we have really. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is an improvement, but the long term solution should involve flexbox or something |
||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,26 +8,26 @@ import { Dispatch, MiddlewareAPI } from 'redux'; | |
import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; | ||
import { EndpointPluginServices } from '../../../plugin'; | ||
import { ResolverState, ResolverAction } from '../types'; | ||
import { ResolverEvent } from '../../../../common/types'; | ||
import { ResolverEvent, Node } from '../../../../common/types'; | ||
import * as event from '../../../../common/models/event'; | ||
|
||
type MiddlewareFactory<S = ResolverState> = ( | ||
context?: KibanaReactContextValue<EndpointPluginServices> | ||
) => ( | ||
api: MiddlewareAPI<Dispatch<ResolverAction>, S> | ||
) => (next: Dispatch<ResolverAction>) => (action: ResolverAction) => unknown; | ||
interface Lifecycle { | ||
lifecycle: ResolverEvent[]; | ||
} | ||
type ChildResponse = [Lifecycle]; | ||
|
||
function flattenEvents(events: ChildResponse): ResolverEvent[] { | ||
return events | ||
.map((child: Lifecycle) => child.lifecycle) | ||
.reduce( | ||
(accumulator: ResolverEvent[], value: ResolverEvent[]) => accumulator.concat(value), | ||
[] | ||
); | ||
function flattenEvents(children: Node[], events: ResolverEvent[] = []): ResolverEvent[] { | ||
return children.reduce((flattenedEvents, currentNode) => { | ||
if (currentNode.lifecycle && currentNode.lifecycle.length > 0) { | ||
flattenedEvents.push(...currentNode.lifecycle); | ||
} | ||
if (currentNode.children && currentNode.children.length > 0) { | ||
return flattenEvents(currentNode.children, events); | ||
} else { | ||
return flattenedEvents; | ||
} | ||
}, events); | ||
} | ||
|
||
export const resolverMiddlewareFactory: MiddlewareFactory = context => { | ||
|
@@ -39,53 +39,38 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => { | |
*/ | ||
if (context?.services.http && action.payload.selectedEvent) { | ||
api.dispatch({ type: 'appRequestedResolverData' }); | ||
let response = []; | ||
let lifecycle: ResolverEvent[]; | ||
let childEvents: ResolverEvent[]; | ||
let relatedEvents: ResolverEvent[]; | ||
let children = []; | ||
const ancestors: ResolverEvent[] = []; | ||
const maxAncestors = 5; | ||
if (event.isLegacyEvent(action.payload.selectedEvent)) { | ||
const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; | ||
const legacyEndpointID = action.payload.selectedEvent?.agent?.id; | ||
[{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ | ||
context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { | ||
query: { legacyEndpointID }, | ||
}), | ||
context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { | ||
query: { legacyEndpointID }, | ||
}), | ||
context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { | ||
query: { legacyEndpointID }, | ||
}), | ||
]); | ||
childEvents = children.length > 0 ? flattenEvents(children) : []; | ||
} else { | ||
const uniquePid = action.payload.selectedEvent.process.entity_id; | ||
const ppid = action.payload.selectedEvent.process.parent?.entity_id; | ||
async function getAncestors(pid: string | undefined) { | ||
if (ancestors.length < maxAncestors && pid !== undefined) { | ||
const parent = await context?.services.http.get(`/api/endpoint/resolver/${pid}`); | ||
ancestors.push(parent.lifecycle[0]); | ||
if (parent.lifecycle[0].process?.parent?.entity_id) { | ||
await getAncestors(parent.lifecycle[0].process.parent.entity_id); | ||
} | ||
} | ||
try { | ||
let lifecycle: ResolverEvent[]; | ||
let children: Node[]; | ||
let ancestors: Node[]; | ||
if (event.isLegacyEvent(action.payload.selectedEvent)) { | ||
const entityId = action.payload.selectedEvent?.endgame?.unique_pid; | ||
const legacyEndpointID = action.payload.selectedEvent?.agent?.id; | ||
[{ lifecycle, children, ancestors }] = await Promise.all([ | ||
context.services.http.get(`/api/endpoint/resolver/${entityId}`, { | ||
query: { legacyEndpointID }, | ||
}), | ||
]); | ||
} else { | ||
const entityId = action.payload.selectedEvent.process.entity_id; | ||
[{ lifecycle, children, ancestors }] = await Promise.all([ | ||
context.services.http.get(`/api/endpoint/resolver/${entityId}`), | ||
]); | ||
} | ||
[{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ | ||
context.services.http.get(`/api/endpoint/resolver/${uniquePid}`), | ||
context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`), | ||
context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`), | ||
getAncestors(ppid), | ||
]); | ||
const response: ResolverEvent[] = [ | ||
...lifecycle, | ||
...flattenEvents(children), | ||
...flattenEvents(ancestors), | ||
]; | ||
api.dispatch({ | ||
type: 'serverReturnedResolverData', | ||
payload: { data: { result: { search_results: response } } }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for the nesting? |
||
}); | ||
} catch (error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error could be any runtime error originating in the try. Consider moving the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thoughts on this? @kqualters-elastic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the http request succeeds but something else in this try/catch fails, the response is not something expected and is effectively the same as a network error. |
||
api.dispatch({ | ||
type: 'serverFailedToReturnResolverData', | ||
}); | ||
} | ||
childEvents = children.length > 0 ? flattenEvents(children) : []; | ||
response = [...lifecycle, ...childEvents, ...relatedEvents, ...ancestors]; | ||
api.dispatch({ | ||
type: 'serverReturnedResolverData', | ||
payload: { data: { result: { search_results: response } } }, | ||
}); | ||
} | ||
} | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ | |
*/ | ||
|
||
import React, { memo } from 'react'; | ||
import { saturate } from 'polished'; | ||
import { saturate, lighten } from 'polished'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 I think these assets were swapped out for new ones from Creative a while back. I think you can safely jettison the changes from this file. They really aren't related to the rest of this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎗️ remove from defs on upstream merge |
||
|
||
import { | ||
htmlIdGenerator, | ||
|
@@ -87,6 +87,8 @@ const idGenerator = htmlIdGenerator(); | |
* Ids of paint servers to be referenced by fill and stroke attributes | ||
*/ | ||
export const PaintServerIds = { | ||
runningProcess: idGenerator('psRunningProcess'), | ||
runningTrigger: idGenerator('psRunningTrigger'), | ||
runningProcessCube: idGenerator('psRunningProcessCube'), | ||
runningTriggerCube: idGenerator('psRunningTriggerCube'), | ||
terminatedProcessCube: idGenerator('psTerminatedProcessCube'), | ||
|
@@ -99,6 +101,46 @@ export const PaintServerIds = { | |
*/ | ||
const PaintServers = memo(() => ( | ||
<> | ||
<linearGradient | ||
id={PaintServerIds.runningProcess} | ||
x1="0" | ||
y1="0" | ||
x2="1" | ||
y2="0" | ||
spreadMethod="reflect" | ||
gradientUnits="objectBoundingBox" | ||
> | ||
<stop | ||
offset="0%" | ||
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningProcessStart))} | ||
stopOpacity="1" | ||
/> | ||
<stop | ||
offset="100%" | ||
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningProcessEnd))} | ||
stopOpacity="1" | ||
/> | ||
</linearGradient> | ||
<linearGradient | ||
id={PaintServerIds.runningTrigger} | ||
x1="0" | ||
y1="0" | ||
x2="1" | ||
y2="0" | ||
spreadMethod="reflect" | ||
gradientUnits="objectBoundingBox" | ||
> | ||
<stop | ||
offset="0%" | ||
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningTriggerStart))} | ||
stopOpacity="1" | ||
/> | ||
<stop | ||
offset="100%" | ||
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningTriggerEnd))} | ||
stopOpacity="1" | ||
/> | ||
</linearGradient> | ||
<linearGradient | ||
id={PaintServerIds.terminatedProcessCube} | ||
x1="-381.23752" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -59,6 +59,7 @@ export const Resolver = styled( | |
|
||
const { projectionMatrix, ref, onMouseDown } = useCamera(); | ||
const isLoading = useSelector(selectors.isLoading); | ||
const hasError = useSelector(selectors.hasError); | ||
const activeDescendantId = useSelector(selectors.uiActiveDescendantId); | ||
|
||
useLayoutEffect(() => { | ||
|
@@ -74,6 +75,10 @@ export const Resolver = styled( | |
<div className="loading-container"> | ||
<EuiLoadingSpinner size="xl" /> | ||
</div> | ||
) : hasError ? ( | ||
<div className="loading-container"> | ||
<div>error occured fetching data</div> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i18n reminder |
||
</div> | ||
) : ( | ||
<StyledResolverContainer | ||
className="resolver-graph kbn-resetFocusState" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, how come you think this is wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This came from @oatkiller 's commit, but I think it's alluding to the fact that this is an "event name" only for process event types, and not necessarily for other event types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup