mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Resolver] Refactoring panel view (#77928)
* Moved `descriptiveName` from the 'common' event model into the panel view. It is now a component. Each type of event has its own translation string. Translation placeholders have more specific names. * Reorganized 'breadcrumb' components. * Use safer types many places * Add `useLinkProps` hook. It takes `PanelViewAndParameters` and returns `onClick` and `href`. Remove a bunch of copy-pasted code that did the same. * Add new common event methods to safely expose fields that were being read directly (`processPID`, `userName`, `userDomain`, `parentPID`, `md5HashForProcess`, `argsForProcess` * Removed 'primaryEventCategory' from the event model. * Removed the 'aggregate' total count concept from the panel * The mock data access layer calle no_ancestors_two_children now has related events. This will allow the click through to test all panels and it will allow the resolver test plugin to view all panels. * The `mockEndpointEvent` factory can now return events of any type instead of just process events. * Several mocks that were using unsafe casting now return the correct types. The unsafe casting was fine for testing but it made refactoring difficult because typescript couldn't find issues. * The mock helper function `withRelatedEventsOnOrigin` now takes the related events to add to the origin instead of an array describing events to be created. * The data state's `tree` field was optional but the initial state incorrectly set it to an invalid object. Now code checks for the presence of a tree object. * Added a selector called `eventByID` which is used to get the event shown in the event detail panel. This will be replaced with an API call in the near future. * Added a selector called `relatedEventCountByType` which finds the count of related events for a type from the `byCategory` structure returned from the API. We should consider changing this as it requires metaprogramming as it is. * Created a new middleware 'fetcher' to fetch related events. This is a stop-gap implementation that we expect to replace before release. * Removed the action called `appDetectedNewIdFromQueryParams`. Use `appReceivedNewExternal...` instead. * Added the first simulator test for a graph node. It checks that the origin node has 'Analyzed Event' in the label. * Added a new panel test that navigates to the nodeEvents panel view and verifies the items in the list. * Added a new panel component called 'Breadcrumbs'. * Fixed an issue where the CubeForProcess component was using `0 0 100% 100%` in the `viewBox` attribute. * The logic that calculates the 'entries' to show when viewing the details of an event was moved into a separate function and unit tested. It is called `deepObjectEntries`. * The code that shows the name of an event is now a component called `DescriptiveName`. It has an enzyme test. Each event type has its own `i18n` string which includes more descriptive placeholders. I'm not sure, but I think this will make it possible for translators to provide better contextual formatting around the values. * Refactored most panel views. They have loading components and breadcrumb components. Links are moved to their own components, allowing them to call `useLinkProps`. * Introduced a hook called `useLinkProps` which combines the `relativeHref` selector with the `useNavigateOrReplace` hook. * Removed the hook called `useRelatedEventDetailNavigation`. Use `useLinkProps` instead. * Move various styled-components into `styles` modules. * The graph node label wasn't translating 'Analyzed Event'. It now does so using a `select` expression in the ICU message. * Renamed a method on the common event model from `getAncestryAsArray` to `ancestry` for consistency. It no longer takes `undefined` for the event it operates on. * Some translations were removed due to code de-duping.
This commit is contained in:
parent
0cf3bf2731
commit
35a6a230cd
58 changed files with 1696 additions and 1944 deletions
|
@ -4,8 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EndpointDocGenerator } from '../generate_data';
|
||||
import { descriptiveName, isProcessRunning } from './event';
|
||||
import { ResolverEvent, SafeResolverEvent } from '../types';
|
||||
import { isProcessRunning } from './event';
|
||||
import { SafeResolverEvent } from '../types';
|
||||
|
||||
describe('Generated documents', () => {
|
||||
let generator: EndpointDocGenerator;
|
||||
|
@ -13,50 +13,6 @@ describe('Generated documents', () => {
|
|||
generator = new EndpointDocGenerator('seed');
|
||||
});
|
||||
|
||||
describe('Event descriptive names', () => {
|
||||
it('returns the right name for a registry event', () => {
|
||||
const extensions = { registry: { key: `HKLM/Windows/Software/abc` } };
|
||||
const event = generator.generateEvent({ eventCategory: 'registry', extensions });
|
||||
// casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies
|
||||
// on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast.
|
||||
expect(descriptiveName(event as ResolverEvent)).toEqual({
|
||||
subject: `HKLM/Windows/Software/abc`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the right name for a network event', () => {
|
||||
const randomIP = `${generator.randomIP()}`;
|
||||
const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } };
|
||||
const event = generator.generateEvent({ eventCategory: 'network', extensions });
|
||||
// casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies
|
||||
// on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast.
|
||||
expect(descriptiveName(event as ResolverEvent)).toEqual({
|
||||
subject: `${randomIP}`,
|
||||
descriptor: 'outbound',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the right name for a file event', () => {
|
||||
const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } };
|
||||
const event = generator.generateEvent({ eventCategory: 'file', extensions });
|
||||
// casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies
|
||||
// on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast.
|
||||
expect(descriptiveName(event as ResolverEvent)).toEqual({
|
||||
subject: 'C:\\My Documents\\business\\January\\processName',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the right name for a dns event', () => {
|
||||
const extensions = { dns: { question: { name: `${generator.randomIP()}` } } };
|
||||
const event = generator.generateEvent({ eventCategory: 'dns', extensions });
|
||||
// casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies
|
||||
// on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast.
|
||||
expect(descriptiveName(event as ResolverEvent)).toEqual({
|
||||
subject: extensions.dns.question.name,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Process running events', () => {
|
||||
it('is a running event when event.type is a string', () => {
|
||||
const event: SafeResolverEvent = generator.generateEvent({
|
||||
|
|
|
@ -104,11 +104,14 @@ export function timestampAsDateSafeVersion(event: TimestampFields): Date | undef
|
|||
}
|
||||
}
|
||||
|
||||
export function eventTimestamp(event: ResolverEvent): string | undefined | number {
|
||||
return event['@timestamp'];
|
||||
export function eventTimestamp(event: SafeResolverEvent): string | undefined | number {
|
||||
return firstNonNullValue(event['@timestamp']);
|
||||
}
|
||||
|
||||
export function eventName(event: ResolverEvent): string {
|
||||
/**
|
||||
* Find the name of the related process.
|
||||
*/
|
||||
export function processName(event: ResolverEvent): string {
|
||||
if (isLegacyEvent(event)) {
|
||||
return event.endgame.process_name ? event.endgame.process_name : '';
|
||||
} else {
|
||||
|
@ -116,6 +119,58 @@ export function eventName(event: ResolverEvent): string {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First non-null value in the `user.name` field.
|
||||
*/
|
||||
export function userName(event: SafeResolverEvent): string | undefined {
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
return undefined;
|
||||
} else {
|
||||
return firstNonNullValue(event.user?.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the process event's parent PID
|
||||
*/
|
||||
export function parentPID(event: SafeResolverEvent): number | undefined {
|
||||
return firstNonNullValue(
|
||||
isLegacyEventSafeVersion(event) ? event.endgame.ppid : event.process?.parent?.pid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* First non-null value for the `process.hash.md5` field.
|
||||
*/
|
||||
export function md5HashForProcess(event: SafeResolverEvent): string | undefined {
|
||||
return firstNonNullValue(isLegacyEventSafeVersion(event) ? undefined : event.process?.hash?.md5);
|
||||
}
|
||||
|
||||
/**
|
||||
* First non-null value for the `event.process.args` field.
|
||||
*/
|
||||
export function argsForProcess(event: SafeResolverEvent): string | undefined {
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
// There is not currently a key for this on Legacy event types
|
||||
return undefined;
|
||||
}
|
||||
return firstNonNullValue(event.process?.args);
|
||||
}
|
||||
|
||||
/**
|
||||
* First non-null value in the `user.name` field.
|
||||
*/
|
||||
export function userDomain(event: SafeResolverEvent): string | undefined {
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
return undefined;
|
||||
} else {
|
||||
return firstNonNullValue(event.user?.domain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the name of the related process.
|
||||
*/
|
||||
export function processNameSafeVersion(event: SafeResolverEvent): string | undefined {
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
return firstNonNullValue(event.endgame.process_name);
|
||||
|
@ -124,11 +179,10 @@ export function processNameSafeVersion(event: SafeResolverEvent): string | undef
|
|||
}
|
||||
}
|
||||
|
||||
export function eventId(event: ResolverEvent): number | undefined | string {
|
||||
if (isLegacyEvent(event)) {
|
||||
return event.endgame.serial_event_id;
|
||||
}
|
||||
return event.event.id;
|
||||
export function eventID(event: SafeResolverEvent): number | undefined | string {
|
||||
return firstNonNullValue(
|
||||
isLegacyEventSafeVersion(event) ? event.endgame.serial_event_id : event.event?.id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -275,18 +329,14 @@ export function ancestryArray(event: AncestryArrayFields): string[] | undefined
|
|||
/**
|
||||
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
|
||||
*/
|
||||
type GetAncestryArrayFields = AncestryArrayFields & ParentEntityIDFields;
|
||||
type AncestryFields = AncestryArrayFields & ParentEntityIDFields;
|
||||
|
||||
/**
|
||||
* Returns an array of strings representing the ancestry for a process.
|
||||
*
|
||||
* @param event an ES document
|
||||
*/
|
||||
export function getAncestryAsArray(event: GetAncestryArrayFields | undefined): string[] {
|
||||
if (!event) {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function ancestry(event: AncestryFields): string[] {
|
||||
const ancestors = ancestryArray(event);
|
||||
if (ancestors) {
|
||||
return ancestors;
|
||||
|
@ -300,35 +350,13 @@ export function getAncestryAsArray(event: GetAncestryArrayFields | undefined): s
|
|||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param event The event to get the category for
|
||||
*/
|
||||
export function primaryEventCategory(event: ResolverEvent): string | undefined {
|
||||
if (isLegacyEvent(event)) {
|
||||
const legacyFullType = event.endgame.event_type_full;
|
||||
if (legacyFullType) {
|
||||
return legacyFullType;
|
||||
}
|
||||
} else {
|
||||
const eventCategories = event.event.category;
|
||||
const category = typeof eventCategories === 'string' ? eventCategories : eventCategories[0];
|
||||
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param event The event to get the full ECS category for
|
||||
*/
|
||||
export function allEventCategories(event: ResolverEvent): string | string[] | undefined {
|
||||
if (isLegacyEvent(event)) {
|
||||
const legacyFullType = event.endgame.event_type_full;
|
||||
if (legacyFullType) {
|
||||
return legacyFullType;
|
||||
}
|
||||
} else {
|
||||
return event.event.category;
|
||||
}
|
||||
export function eventCategory(event: SafeResolverEvent): string[] {
|
||||
return values(
|
||||
isLegacyEventSafeVersion(event) ? event.endgame.event_type_full : event.event?.category
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -336,71 +364,19 @@ export function allEventCategories(event: ResolverEvent): string | string[] | un
|
|||
* see: https://www.elastic.co/guide/en/ecs/current/ecs-event.html
|
||||
* @param event The ResolverEvent to get the ecs type for
|
||||
*/
|
||||
export function ecsEventType(event: ResolverEvent): Array<string | undefined> {
|
||||
if (isLegacyEvent(event)) {
|
||||
return [event.endgame.event_subtype_full];
|
||||
}
|
||||
return typeof event.event.type === 'string' ? [event.event.type] : event.event.type;
|
||||
export function eventType(event: SafeResolverEvent): string[] {
|
||||
return values(
|
||||
isLegacyEventSafeVersion(event) ? event.endgame.event_subtype_full : event.event?.type
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* #Descriptive Names For Related Events:
|
||||
*
|
||||
* The following section provides facilities for deriving **Descriptive Names** for ECS-compliant event data.
|
||||
* There are drawbacks to trying to do this: It *will* require ongoing maintenance. It presents temptations to overarticulate.
|
||||
* On balance, however, it seems that the benefit of giving the user some form of information they can recognize & scan outweighs the drawbacks.
|
||||
* event.kind as an array.
|
||||
*/
|
||||
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
|
||||
/**
|
||||
* Based on the ECS category of the event, attempt to provide a more descriptive name
|
||||
* (e.g. the `event.registry.key` for `registry` or the `dns.question.name` for `dns`, etc.).
|
||||
* This function returns the data in the form of `{subject, descriptor}` where `subject` will
|
||||
* tend to be the more distinctive term (e.g. 137.213.212.7 for a network event) and the
|
||||
* `descriptor` can be used to present more useful/meaningful view (e.g. `inbound 137.213.212.7`
|
||||
* in the example above).
|
||||
* see: https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html
|
||||
* @param event The ResolverEvent to get the descriptive name for
|
||||
* @returns { descriptiveName } An attempt at providing a readable name to the user
|
||||
*/
|
||||
export function descriptiveName(event: ResolverEvent): { subject: string; descriptor?: string } {
|
||||
if (isLegacyEvent(event)) {
|
||||
return { subject: eventName(event) };
|
||||
export function eventKind(event: SafeResolverEvent): string[] {
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
return [];
|
||||
} else {
|
||||
return values(event.event?.kind);
|
||||
}
|
||||
|
||||
// To be somewhat defensive, we'll check for the presence of these.
|
||||
const partialEvent: DeepPartial<ResolverEvent> = event;
|
||||
|
||||
/**
|
||||
* This list of attempts can be expanded/adjusted as the underlying model changes over time:
|
||||
*/
|
||||
|
||||
// Stable, per ECS 1.5: https://www.elastic.co/guide/en/ecs/current/ecs-allowed-values-event-category.html
|
||||
|
||||
if (partialEvent.network?.forwarded_ip) {
|
||||
return {
|
||||
subject: String(partialEvent.network?.forwarded_ip),
|
||||
descriptor: String(partialEvent.network?.direction),
|
||||
};
|
||||
}
|
||||
|
||||
if (partialEvent.file?.path) {
|
||||
return {
|
||||
subject: String(partialEvent.file?.path),
|
||||
};
|
||||
}
|
||||
|
||||
// Extended categories (per ECS 1.5):
|
||||
const pathOrKey = partialEvent.registry?.path || partialEvent.registry?.key;
|
||||
if (pathOrKey) {
|
||||
return {
|
||||
subject: String(pathOrKey),
|
||||
};
|
||||
}
|
||||
|
||||
if (partialEvent.dns?.question?.name) {
|
||||
return { subject: String(partialEvent.dns?.question?.name) };
|
||||
}
|
||||
|
||||
// Fall back on entityId if we can't fish a more descriptive name out.
|
||||
return { subject: entityId(event) };
|
||||
}
|
||||
|
|
|
@ -183,7 +183,7 @@ export interface ResolverTree {
|
|||
relatedEvents: Omit<ResolverRelatedEvents, 'entityID'>;
|
||||
relatedAlerts: Omit<ResolverRelatedAlerts, 'entityID'>;
|
||||
ancestry: ResolverAncestry;
|
||||
lifecycle: ResolverEvent[];
|
||||
lifecycle: SafeResolverEvent[];
|
||||
stats: ResolverNodeStats;
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,7 @@ export interface SafeResolverTree {
|
|||
*/
|
||||
export interface ResolverLifecycleNode {
|
||||
entityID: string;
|
||||
lifecycle: ResolverEvent[];
|
||||
lifecycle: SafeResolverEvent[];
|
||||
/**
|
||||
* stats are only set when the entire tree is being fetched
|
||||
*/
|
||||
|
@ -263,7 +263,7 @@ export interface SafeResolverAncestry {
|
|||
*/
|
||||
export interface ResolverRelatedEvents {
|
||||
entityID: string;
|
||||
events: ResolverEvent[];
|
||||
events: SafeResolverEvent[];
|
||||
nextEvent: string | null;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
ResolverTree,
|
||||
ResolverEntityIndex,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { mockEndpointEvent } from '../../mocks/endpoint_event';
|
||||
import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree';
|
||||
import { DataAccessLayer } from '../../types';
|
||||
|
||||
|
@ -54,13 +53,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
|
|||
relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
|
||||
return Promise.resolve({
|
||||
entityID,
|
||||
events: [
|
||||
mockEndpointEvent({
|
||||
entityID,
|
||||
name: 'event',
|
||||
timestamp: 0,
|
||||
}),
|
||||
],
|
||||
events: [],
|
||||
nextEvent: null,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -61,7 +61,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
|
|||
events: [
|
||||
mockEndpointEvent({
|
||||
entityID,
|
||||
name: 'event',
|
||||
processName: 'event',
|
||||
timestamp: 0,
|
||||
}),
|
||||
],
|
||||
|
|
|
@ -66,7 +66,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
|
|||
entityID,
|
||||
events,
|
||||
nextEvent: null,
|
||||
} as ResolverRelatedEvents);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Provider } from 'react-redux';
|
|||
import { ResolverPluginSetup } from './types';
|
||||
import { resolverStoreFactory } from './store/index';
|
||||
import { ResolverWithoutProviders } from './view/resolver_without_providers';
|
||||
import { noAncestorsTwoChildren } from './data_access_layer/mocks/no_ancestors_two_children';
|
||||
import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from './data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin';
|
||||
|
||||
/**
|
||||
* These exports are used by the plugin 'resolverTest' defined in x-pack's plugin_functional suite.
|
||||
|
@ -23,7 +23,7 @@ export function resolverPluginSetup(): ResolverPluginSetup {
|
|||
ResolverWithoutProviders,
|
||||
mocks: {
|
||||
dataAccessLayer: {
|
||||
noAncestorsTwoChildren,
|
||||
noAncestorsTwoChildrenWithRelatedEventsOnOrigin,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,31 +4,36 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EndpointEvent } from '../../../common/endpoint/types';
|
||||
import { SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
|
||||
/**
|
||||
* Simple mock endpoint event that works for tree layouts.
|
||||
*/
|
||||
export function mockEndpointEvent({
|
||||
entityID,
|
||||
name,
|
||||
parentEntityId,
|
||||
timestamp,
|
||||
lifecycleType,
|
||||
processName = 'process name',
|
||||
parentEntityID,
|
||||
timestamp = 0,
|
||||
eventType = 'start',
|
||||
eventCategory = 'process',
|
||||
pid = 0,
|
||||
eventID = 'event id',
|
||||
}: {
|
||||
entityID: string;
|
||||
name: string;
|
||||
parentEntityId?: string;
|
||||
timestamp: number;
|
||||
lifecycleType?: string;
|
||||
processName?: string;
|
||||
parentEntityID?: string;
|
||||
timestamp?: number;
|
||||
eventType?: string;
|
||||
eventCategory?: string;
|
||||
pid?: number;
|
||||
}): EndpointEvent {
|
||||
eventID?: string;
|
||||
}): SafeResolverEvent {
|
||||
return {
|
||||
'@timestamp': timestamp,
|
||||
event: {
|
||||
type: lifecycleType ? lifecycleType : 'start',
|
||||
category: 'process',
|
||||
type: eventType,
|
||||
category: eventCategory,
|
||||
id: eventID,
|
||||
},
|
||||
agent: {
|
||||
id: 'agent.id',
|
||||
|
@ -46,15 +51,15 @@ export function mockEndpointEvent({
|
|||
entity_id: entityID,
|
||||
executable: 'executable',
|
||||
args: 'args',
|
||||
name,
|
||||
name: processName,
|
||||
pid,
|
||||
hash: {
|
||||
md5: 'hash.md5',
|
||||
},
|
||||
parent: {
|
||||
pid: 0,
|
||||
entity_id: parentEntityId,
|
||||
entity_id: parentEntityID,
|
||||
},
|
||||
},
|
||||
} as EndpointEvent;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
|
||||
import { mockEndpointEvent } from './endpoint_event';
|
||||
import { ResolverTree, ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import * as eventModel from '../../../common/endpoint/models/event';
|
||||
|
||||
export function mockTreeWith2AncestorsAndNoChildren({
|
||||
originID,
|
||||
|
@ -16,34 +17,42 @@ export function mockTreeWith2AncestorsAndNoChildren({
|
|||
firstAncestorID: string;
|
||||
originID: string;
|
||||
}): ResolverTree {
|
||||
const secondAncestor: ResolverEvent = mockEndpointEvent({
|
||||
const secondAncestor: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: secondAncestorID,
|
||||
name: 'a',
|
||||
parentEntityId: 'none',
|
||||
processName: 'a',
|
||||
parentEntityID: 'none',
|
||||
timestamp: 0,
|
||||
});
|
||||
const firstAncestor: ResolverEvent = mockEndpointEvent({
|
||||
const firstAncestor: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: firstAncestorID,
|
||||
name: 'b',
|
||||
parentEntityId: secondAncestorID,
|
||||
processName: 'b',
|
||||
parentEntityID: secondAncestorID,
|
||||
timestamp: 1,
|
||||
});
|
||||
const originEvent: ResolverEvent = mockEndpointEvent({
|
||||
const originEvent: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: originID,
|
||||
name: 'c',
|
||||
parentEntityId: firstAncestorID,
|
||||
processName: 'c',
|
||||
parentEntityID: firstAncestorID,
|
||||
timestamp: 2,
|
||||
});
|
||||
return ({
|
||||
return {
|
||||
entityID: originID,
|
||||
children: {
|
||||
childNodes: [],
|
||||
nextChild: null,
|
||||
},
|
||||
ancestry: {
|
||||
ancestors: [{ lifecycle: [secondAncestor] }, { lifecycle: [firstAncestor] }],
|
||||
nextAncestor: null,
|
||||
ancestors: [
|
||||
{ entityID: secondAncestorID, lifecycle: [secondAncestor] },
|
||||
{ entityID: firstAncestorID, lifecycle: [firstAncestor] },
|
||||
],
|
||||
},
|
||||
lifecycle: [originEvent],
|
||||
} as unknown) as ResolverTree;
|
||||
relatedEvents: { events: [], nextEvent: null },
|
||||
relatedAlerts: { alerts: [], nextAlert: null },
|
||||
stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
export function mockTreeWithAllProcessesTerminated({
|
||||
|
@ -55,44 +64,44 @@ export function mockTreeWithAllProcessesTerminated({
|
|||
firstAncestorID: string;
|
||||
originID: string;
|
||||
}): ResolverTree {
|
||||
const secondAncestor: ResolverEvent = mockEndpointEvent({
|
||||
const secondAncestor: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: secondAncestorID,
|
||||
name: 'a',
|
||||
parentEntityId: 'none',
|
||||
processName: 'a',
|
||||
parentEntityID: 'none',
|
||||
timestamp: 0,
|
||||
});
|
||||
const firstAncestor: ResolverEvent = mockEndpointEvent({
|
||||
const firstAncestor: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: firstAncestorID,
|
||||
name: 'b',
|
||||
parentEntityId: secondAncestorID,
|
||||
processName: 'b',
|
||||
parentEntityID: secondAncestorID,
|
||||
timestamp: 1,
|
||||
});
|
||||
const originEvent: ResolverEvent = mockEndpointEvent({
|
||||
const originEvent: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: originID,
|
||||
name: 'c',
|
||||
parentEntityId: firstAncestorID,
|
||||
processName: 'c',
|
||||
parentEntityID: firstAncestorID,
|
||||
timestamp: 2,
|
||||
});
|
||||
const secondAncestorTermination: ResolverEvent = mockEndpointEvent({
|
||||
const secondAncestorTermination: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: secondAncestorID,
|
||||
name: 'a',
|
||||
parentEntityId: 'none',
|
||||
processName: 'a',
|
||||
parentEntityID: 'none',
|
||||
timestamp: 0,
|
||||
lifecycleType: 'end',
|
||||
eventType: 'end',
|
||||
});
|
||||
const firstAncestorTermination: ResolverEvent = mockEndpointEvent({
|
||||
const firstAncestorTermination: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: firstAncestorID,
|
||||
name: 'b',
|
||||
parentEntityId: secondAncestorID,
|
||||
processName: 'b',
|
||||
parentEntityID: secondAncestorID,
|
||||
timestamp: 1,
|
||||
lifecycleType: 'end',
|
||||
eventType: 'end',
|
||||
});
|
||||
const originEventTermination: ResolverEvent = mockEndpointEvent({
|
||||
const originEventTermination: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: originID,
|
||||
name: 'c',
|
||||
parentEntityId: firstAncestorID,
|
||||
processName: 'c',
|
||||
parentEntityID: firstAncestorID,
|
||||
timestamp: 2,
|
||||
lifecycleType: 'end',
|
||||
eventType: 'end',
|
||||
});
|
||||
return ({
|
||||
entityID: originID,
|
||||
|
@ -109,26 +118,10 @@ export function mockTreeWithAllProcessesTerminated({
|
|||
} as unknown) as ResolverTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* A valid category for a related event. E.g. "registry", "network", "file"
|
||||
*/
|
||||
type RelatedEventCategory = string;
|
||||
/**
|
||||
* A valid type for a related event. E.g. "start", "end", "access"
|
||||
*/
|
||||
type RelatedEventType = string;
|
||||
|
||||
/**
|
||||
* Add/replace related event info (on origin node) for any mock ResolverTree
|
||||
*
|
||||
* @param treeToAddRelatedEventsTo the ResolverTree to modify
|
||||
* @param relatedEventsToAddByCategoryAndType Iterable of `[category, type]` pairs describing related events. e.g. [['dns','info'],['registry','access']]
|
||||
*/
|
||||
function withRelatedEventsOnOrigin(
|
||||
treeToAddRelatedEventsTo: ResolverTree,
|
||||
relatedEventsToAddByCategoryAndType: Iterable<[RelatedEventCategory, RelatedEventType]>
|
||||
): ResolverTree {
|
||||
const events: SafeResolverEvent[] = [];
|
||||
function withRelatedEventsOnOrigin(tree: ResolverTree, events: SafeResolverEvent[]): ResolverTree {
|
||||
const byCategory: Record<string, number> = {};
|
||||
const stats = {
|
||||
totalAlerts: 0,
|
||||
|
@ -137,29 +130,19 @@ function withRelatedEventsOnOrigin(
|
|||
byCategory,
|
||||
},
|
||||
};
|
||||
for (const [category, type] of relatedEventsToAddByCategoryAndType) {
|
||||
events.push({
|
||||
'@timestamp': 1,
|
||||
event: {
|
||||
kind: 'event',
|
||||
type,
|
||||
category,
|
||||
id: 'xyz',
|
||||
},
|
||||
process: {
|
||||
entity_id: treeToAddRelatedEventsTo.entityID,
|
||||
},
|
||||
});
|
||||
for (const event of events) {
|
||||
stats.events.total++;
|
||||
stats.events.byCategory[category] = stats.events.byCategory[category]
|
||||
? stats.events.byCategory[category] + 1
|
||||
: 1;
|
||||
for (const category of eventModel.eventCategory(event)) {
|
||||
stats.events.byCategory[category] = stats.events.byCategory[category]
|
||||
? stats.events.byCategory[category] + 1
|
||||
: 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...treeToAddRelatedEventsTo,
|
||||
...tree,
|
||||
stats,
|
||||
relatedEvents: {
|
||||
events: events as ResolverEvent[],
|
||||
events,
|
||||
nextEvent: null,
|
||||
},
|
||||
};
|
||||
|
@ -174,38 +157,46 @@ export function mockTreeWithNoAncestorsAnd2Children({
|
|||
firstChildID: string;
|
||||
secondChildID: string;
|
||||
}): ResolverTree {
|
||||
const origin: ResolverEvent = mockEndpointEvent({
|
||||
const origin: SafeResolverEvent = mockEndpointEvent({
|
||||
pid: 0,
|
||||
entityID: originID,
|
||||
name: 'c.ext',
|
||||
parentEntityId: 'none',
|
||||
processName: 'c.ext',
|
||||
parentEntityID: 'none',
|
||||
timestamp: 0,
|
||||
});
|
||||
const firstChild: ResolverEvent = mockEndpointEvent({
|
||||
const firstChild: SafeResolverEvent = mockEndpointEvent({
|
||||
pid: 1,
|
||||
entityID: firstChildID,
|
||||
name: 'd',
|
||||
parentEntityId: originID,
|
||||
processName: 'd',
|
||||
parentEntityID: originID,
|
||||
timestamp: 1,
|
||||
});
|
||||
const secondChild: ResolverEvent = mockEndpointEvent({
|
||||
const secondChild: SafeResolverEvent = mockEndpointEvent({
|
||||
pid: 2,
|
||||
entityID: secondChildID,
|
||||
name: 'e',
|
||||
parentEntityId: originID,
|
||||
processName: 'e',
|
||||
parentEntityID: originID,
|
||||
timestamp: 2,
|
||||
});
|
||||
|
||||
return ({
|
||||
return {
|
||||
entityID: originID,
|
||||
children: {
|
||||
childNodes: [{ lifecycle: [firstChild] }, { lifecycle: [secondChild] }],
|
||||
childNodes: [
|
||||
{ entityID: firstChildID, lifecycle: [firstChild] },
|
||||
{ entityID: secondChildID, lifecycle: [secondChild] },
|
||||
],
|
||||
nextChild: null,
|
||||
},
|
||||
ancestry: {
|
||||
ancestors: [],
|
||||
nextAncestor: null,
|
||||
},
|
||||
lifecycle: [origin],
|
||||
} as unknown) as ResolverTree;
|
||||
relatedEvents: { events: [], nextEvent: null },
|
||||
relatedAlerts: { alerts: [], nextAlert: null },
|
||||
stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -222,52 +213,52 @@ export function mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents
|
|||
firstChildID: string;
|
||||
secondChildID: string;
|
||||
}): ResolverTree {
|
||||
const ancestor: ResolverEvent = mockEndpointEvent({
|
||||
const ancestor: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: ancestorID,
|
||||
name: ancestorID,
|
||||
processName: ancestorID,
|
||||
timestamp: 1,
|
||||
parentEntityId: undefined,
|
||||
parentEntityID: undefined,
|
||||
});
|
||||
const ancestorClone: ResolverEvent = mockEndpointEvent({
|
||||
const ancestorClone: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: ancestorID,
|
||||
name: ancestorID,
|
||||
processName: ancestorID,
|
||||
timestamp: 1,
|
||||
parentEntityId: undefined,
|
||||
parentEntityID: undefined,
|
||||
});
|
||||
const origin: ResolverEvent = mockEndpointEvent({
|
||||
const origin: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: originID,
|
||||
name: originID,
|
||||
parentEntityId: ancestorID,
|
||||
processName: originID,
|
||||
parentEntityID: ancestorID,
|
||||
timestamp: 0,
|
||||
});
|
||||
const originClone: ResolverEvent = mockEndpointEvent({
|
||||
const originClone: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: originID,
|
||||
name: originID,
|
||||
parentEntityId: ancestorID,
|
||||
processName: originID,
|
||||
parentEntityID: ancestorID,
|
||||
timestamp: 0,
|
||||
});
|
||||
const firstChild: ResolverEvent = mockEndpointEvent({
|
||||
const firstChild: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: firstChildID,
|
||||
name: firstChildID,
|
||||
parentEntityId: originID,
|
||||
processName: firstChildID,
|
||||
parentEntityID: originID,
|
||||
timestamp: 1,
|
||||
});
|
||||
const firstChildClone: ResolverEvent = mockEndpointEvent({
|
||||
const firstChildClone: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: firstChildID,
|
||||
name: firstChildID,
|
||||
parentEntityId: originID,
|
||||
processName: firstChildID,
|
||||
parentEntityID: originID,
|
||||
timestamp: 1,
|
||||
});
|
||||
const secondChild: ResolverEvent = mockEndpointEvent({
|
||||
const secondChild: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: secondChildID,
|
||||
name: secondChildID,
|
||||
parentEntityId: originID,
|
||||
processName: secondChildID,
|
||||
parentEntityID: originID,
|
||||
timestamp: 2,
|
||||
});
|
||||
const secondChildClone: ResolverEvent = mockEndpointEvent({
|
||||
const secondChildClone: SafeResolverEvent = mockEndpointEvent({
|
||||
entityID: secondChildID,
|
||||
name: secondChildID,
|
||||
parentEntityId: originID,
|
||||
processName: secondChildID,
|
||||
parentEntityID: originID,
|
||||
timestamp: 2,
|
||||
});
|
||||
|
||||
|
@ -330,9 +321,22 @@ export function mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({
|
|||
firstChildID,
|
||||
secondChildID,
|
||||
});
|
||||
const withRelatedEvents: Array<[string, string]> = [
|
||||
['registry', 'access'],
|
||||
['registry', 'access'],
|
||||
const parentEntityID = eventModel.parentEntityIDSafeVersion(baseTree.lifecycle[0]);
|
||||
const relatedEvents = [
|
||||
mockEndpointEvent({
|
||||
entityID: originID,
|
||||
parentEntityID,
|
||||
eventID: 'first related event',
|
||||
eventType: 'access',
|
||||
eventCategory: 'registry',
|
||||
}),
|
||||
mockEndpointEvent({
|
||||
entityID: originID,
|
||||
parentEntityID,
|
||||
eventID: 'second related event',
|
||||
eventType: 'access',
|
||||
eventCategory: 'registry',
|
||||
}),
|
||||
];
|
||||
return withRelatedEventsOnOrigin(baseTree, withRelatedEvents);
|
||||
return withRelatedEventsOnOrigin(baseTree, relatedEvents);
|
||||
}
|
||||
|
|
|
@ -6,11 +6,7 @@
|
|||
import { eventType, orderByTime, userInfoForProcess } from './process_event';
|
||||
|
||||
import { mockProcessEvent } from './process_event_test_helpers';
|
||||
import {
|
||||
LegacyEndpointEvent,
|
||||
ResolverEvent,
|
||||
SafeResolverEvent,
|
||||
} from '../../../common/endpoint/types';
|
||||
import { LegacyEndpointEvent, SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
|
||||
describe('process event', () => {
|
||||
describe('eventType', () => {
|
||||
|
@ -45,7 +41,7 @@ describe('process event', () => {
|
|||
});
|
||||
});
|
||||
describe('orderByTime', () => {
|
||||
let mock: (time: number, eventID: string) => ResolverEvent;
|
||||
let mock: (time: number, eventID: string) => SafeResolverEvent;
|
||||
let events: SafeResolverEvent[];
|
||||
beforeEach(() => {
|
||||
mock = (time, eventID) => {
|
||||
|
@ -54,20 +50,20 @@ describe('process event', () => {
|
|||
event: {
|
||||
id: eventID,
|
||||
},
|
||||
} as ResolverEvent;
|
||||
};
|
||||
};
|
||||
// 2 events each for numbers -1, 0, 1, and NaN
|
||||
// each event has a unique id, a through h
|
||||
// order is arbitrary
|
||||
events = [
|
||||
mock(-1, 'a') as SafeResolverEvent,
|
||||
mock(0, 'c') as SafeResolverEvent,
|
||||
mock(1, 'e') as SafeResolverEvent,
|
||||
mock(NaN, 'g') as SafeResolverEvent,
|
||||
mock(-1, 'b') as SafeResolverEvent,
|
||||
mock(0, 'd') as SafeResolverEvent,
|
||||
mock(1, 'f') as SafeResolverEvent,
|
||||
mock(NaN, 'h') as SafeResolverEvent,
|
||||
mock(-1, 'a'),
|
||||
mock(0, 'c'),
|
||||
mock(1, 'e'),
|
||||
mock(NaN, 'g'),
|
||||
mock(-1, 'b'),
|
||||
mock(0, 'd'),
|
||||
mock(1, 'f'),
|
||||
mock(NaN, 'h'),
|
||||
];
|
||||
});
|
||||
it('sorts events as expected', () => {
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as event from '../../../common/endpoint/models/event';
|
||||
import { firstNonNullValue } from '../../../common/endpoint/models/ecs_safety_helpers';
|
||||
|
||||
import * as eventModel from '../../../common/endpoint/models/event';
|
||||
import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { ResolverProcessType } from '../types';
|
||||
|
||||
|
@ -12,19 +14,11 @@ import { ResolverProcessType } from '../types';
|
|||
* Returns true if the process's eventType is either 'processCreated' or 'processRan'.
|
||||
* Resolver will only render 'graphable' process events.
|
||||
*/
|
||||
export function isGraphableProcess(passedEvent: ResolverEvent) {
|
||||
export function isGraphableProcess(passedEvent: SafeResolverEvent) {
|
||||
return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan';
|
||||
}
|
||||
|
||||
function isValue(field: string | string[], value: string) {
|
||||
if (field instanceof Array) {
|
||||
return field.length === 1 && field[0] === value;
|
||||
} else {
|
||||
return field === value;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTerminatedProcess(passedEvent: ResolverEvent) {
|
||||
export function isTerminatedProcess(passedEvent: SafeResolverEvent) {
|
||||
return eventType(passedEvent) === 'processTerminated';
|
||||
}
|
||||
|
||||
|
@ -33,7 +27,7 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) {
|
|||
* may return NaN if the timestamp wasn't present or was invalid.
|
||||
*/
|
||||
export function datetime(passedEvent: SafeResolverEvent): number | null {
|
||||
const timestamp = event.timestampSafeVersion(passedEvent);
|
||||
const timestamp = eventModel.timestampSafeVersion(passedEvent);
|
||||
|
||||
const time = timestamp === undefined ? 0 : new Date(timestamp).getTime();
|
||||
|
||||
|
@ -44,8 +38,8 @@ export function datetime(passedEvent: SafeResolverEvent): number | null {
|
|||
/**
|
||||
* Returns a custom event type for a process event based on the event's metadata.
|
||||
*/
|
||||
export function eventType(passedEvent: ResolverEvent): ResolverProcessType {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
export function eventType(passedEvent: SafeResolverEvent): ResolverProcessType {
|
||||
if (eventModel.isLegacyEventSafeVersion(passedEvent)) {
|
||||
const {
|
||||
endgame: { event_type_full: type, event_subtype_full: subType },
|
||||
} = passedEvent;
|
||||
|
@ -64,20 +58,20 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType {
|
|||
return 'processCausedAlert';
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
event: { type, category, kind },
|
||||
} = passedEvent;
|
||||
if (isValue(category, 'process')) {
|
||||
if (isValue(type, 'start') || isValue(type, 'change') || isValue(type, 'creation')) {
|
||||
const type = new Set(eventModel.eventType(passedEvent));
|
||||
const category = new Set(eventModel.eventCategory(passedEvent));
|
||||
const kind = new Set(eventModel.eventKind(passedEvent));
|
||||
if (category.has('process')) {
|
||||
if (type.has('start') || type.has('change') || type.has('creation')) {
|
||||
return 'processCreated';
|
||||
} else if (isValue(type, 'info')) {
|
||||
} else if (type.has('info')) {
|
||||
return 'processRan';
|
||||
} else if (isValue(type, 'end')) {
|
||||
} else if (type.has('end')) {
|
||||
return 'processTerminated';
|
||||
} else {
|
||||
return 'unknownProcessEvent';
|
||||
}
|
||||
} else if (kind === 'alert') {
|
||||
} else if (kind.has('alert')) {
|
||||
return 'processCausedAlert';
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +82,7 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType {
|
|||
* Returns the process event's PID
|
||||
*/
|
||||
export function uniquePidForProcess(passedEvent: ResolverEvent): string {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
if (eventModel.isLegacyEvent(passedEvent)) {
|
||||
return String(passedEvent.endgame.unique_pid);
|
||||
} else {
|
||||
return passedEvent.process.entity_id;
|
||||
|
@ -98,45 +92,32 @@ export function uniquePidForProcess(passedEvent: ResolverEvent): string {
|
|||
/**
|
||||
* Returns the PID for the process on the host
|
||||
*/
|
||||
export function processPid(passedEvent: ResolverEvent): number | undefined {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
return passedEvent.endgame.pid;
|
||||
} else {
|
||||
return passedEvent.process.pid;
|
||||
}
|
||||
export function processPID(event: SafeResolverEvent): number | undefined {
|
||||
return firstNonNullValue(
|
||||
eventModel.isLegacyEventSafeVersion(event) ? event.endgame.pid : event.process?.pid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the process event's parent PID
|
||||
*/
|
||||
export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
if (eventModel.isLegacyEvent(passedEvent)) {
|
||||
return String(passedEvent.endgame.unique_ppid);
|
||||
} else {
|
||||
return passedEvent.process.parent?.entity_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the process event's parent PID
|
||||
*/
|
||||
export function processParentPid(passedEvent: ResolverEvent): number | undefined {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
return passedEvent.endgame.ppid;
|
||||
} else {
|
||||
return passedEvent.process.parent?.pid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the process event's path on its host
|
||||
*/
|
||||
export function processPath(passedEvent: ResolverEvent): string | undefined {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
return passedEvent.endgame.process_path;
|
||||
} else {
|
||||
return passedEvent.process.executable;
|
||||
}
|
||||
export function processPath(passedEvent: SafeResolverEvent): string | undefined {
|
||||
return firstNonNullValue(
|
||||
eventModel.isLegacyEventSafeVersion(passedEvent)
|
||||
? passedEvent.endgame.process_path
|
||||
: passedEvent.process?.executable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -148,19 +129,6 @@ export function userInfoForProcess(
|
|||
return passedEvent.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MD5 hash for the `passedEvent` parameter, or undefined if it can't be located
|
||||
* @param {ResolverEvent} passedEvent The `ResolverEvent` to get the MD5 value for
|
||||
* @returns {string | undefined} The MD5 string for the event
|
||||
*/
|
||||
export function md5HashForProcess(passedEvent: ResolverEvent): string | undefined {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
// There is not currently a key for this on Legacy event types
|
||||
return undefined;
|
||||
}
|
||||
return passedEvent?.process?.hash?.md5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the command line path and arguments used to run the `passedEvent` if any
|
||||
*
|
||||
|
@ -168,7 +136,7 @@ export function md5HashForProcess(passedEvent: ResolverEvent): string | undefine
|
|||
* @returns {string | undefined} The arguments (including the path) used to run the process
|
||||
*/
|
||||
export function argsForProcess(passedEvent: ResolverEvent): string | undefined {
|
||||
if (event.isLegacyEvent(passedEvent)) {
|
||||
if (eventModel.isLegacyEvent(passedEvent)) {
|
||||
// There is not currently a key for this on Legacy event types
|
||||
return undefined;
|
||||
}
|
||||
|
@ -184,8 +152,8 @@ export function orderByTime(first: SafeResolverEvent, second: SafeResolverEvent)
|
|||
|
||||
if (firstDatetime === secondDatetime) {
|
||||
// break ties using an arbitrary (stable) comparison of `eventId` (which should be unique)
|
||||
return String(event.eventIDSafeVersion(first)).localeCompare(
|
||||
String(event.eventIDSafeVersion(second))
|
||||
return String(eventModel.eventIDSafeVersion(first)).localeCompare(
|
||||
String(eventModel.eventIDSafeVersion(second))
|
||||
);
|
||||
} else if (firstDatetime === null || secondDatetime === null) {
|
||||
// sort `null`'s as higher than numbers
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
import {
|
||||
ResolverTree,
|
||||
ResolverEvent,
|
||||
ResolverNodeStats,
|
||||
ResolverLifecycleNode,
|
||||
ResolverChildNode,
|
||||
SafeResolverEvent,
|
||||
} from '../../../common/endpoint/types';
|
||||
import { uniquePidForProcess } from './process_event';
|
||||
import * as eventModel from '../../../common/endpoint/models/event';
|
||||
|
||||
/**
|
||||
* ResolverTree is a type returned by the server.
|
||||
|
@ -29,7 +29,7 @@ function lifecycleNodes(tree: ResolverTree): ResolverLifecycleNode[] {
|
|||
* All the process events
|
||||
*/
|
||||
export function lifecycleEvents(tree: ResolverTree) {
|
||||
const events: ResolverEvent[] = [...tree.lifecycle];
|
||||
const events: SafeResolverEvent[] = [...tree.lifecycle];
|
||||
for (const { lifecycle } of tree.children.childNodes) {
|
||||
events.push(...lifecycle);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export function mock({
|
|||
/**
|
||||
* Events represented by the ResolverTree.
|
||||
*/
|
||||
events: ResolverEvent[];
|
||||
events: SafeResolverEvent[];
|
||||
children?: ResolverChildNode[];
|
||||
/**
|
||||
* Optionally provide cursors for the 'children' and 'ancestry' edges.
|
||||
|
@ -77,8 +77,12 @@ export function mock({
|
|||
return null;
|
||||
}
|
||||
const first = events[0];
|
||||
const entityID = eventModel.entityIDSafeVersion(first);
|
||||
if (!entityID) {
|
||||
throw new Error('first mock event must include an entityID.');
|
||||
}
|
||||
return {
|
||||
entityID: uniquePidForProcess(first),
|
||||
entityID,
|
||||
// Required
|
||||
children: {
|
||||
childNodes: children,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { CameraAction } from './camera';
|
||||
import { ResolverEvent } from '../../../common/endpoint/types';
|
||||
import { SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { DataAction } from './data/action';
|
||||
|
||||
/**
|
||||
|
@ -16,25 +16,7 @@ interface UserBroughtProcessIntoView {
|
|||
/**
|
||||
* Used to identify the process node that should be brought into view.
|
||||
*/
|
||||
readonly process: ResolverEvent;
|
||||
/**
|
||||
* The time (since epoch in milliseconds) when the action was dispatched.
|
||||
*/
|
||||
readonly time: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When an examination of query params in the UI indicates that state needs to
|
||||
* be updated to reflect the new selection
|
||||
*/
|
||||
interface AppDetectedNewIdFromQueryParams {
|
||||
readonly type: 'appDetectedNewIdFromQueryParams';
|
||||
readonly payload: {
|
||||
/**
|
||||
* Used to identify the process the process that should be synced with state.
|
||||
*/
|
||||
readonly process: ResolverEvent;
|
||||
readonly process: SafeResolverEvent;
|
||||
/**
|
||||
* The time (since epoch in milliseconds) when the action was dispatched.
|
||||
*/
|
||||
|
@ -51,15 +33,6 @@ interface UserRequestedRelatedEventData {
|
|||
readonly payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The action dispatched when the app requests related event data for one
|
||||
* subject (whose entity_id should be included as `payload`)
|
||||
*/
|
||||
interface AppDetectedMissingEventData {
|
||||
readonly type: 'appDetectedMissingEventData';
|
||||
readonly payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user switches the "active descendant" of the Resolver.
|
||||
* The "active descendant" (from the point of view of the parent element)
|
||||
|
@ -127,6 +100,4 @@ export type ResolverAction =
|
|||
| UserBroughtProcessIntoView
|
||||
| UserFocusedOnResolverNode
|
||||
| UserSelectedResolverNode
|
||||
| UserRequestedRelatedEventData
|
||||
| AppDetectedNewIdFromQueryParams
|
||||
| AppDetectedMissingEventData;
|
||||
| UserRequestedRelatedEventData;
|
||||
|
|
|
@ -45,14 +45,6 @@ interface AppAbortedResolverDataRequest {
|
|||
readonly payload: TreeFetcherParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will occur when a request for related event data is unsuccessful.
|
||||
*/
|
||||
interface ServerFailedToReturnRelatedEventData {
|
||||
readonly type: 'serverFailedToReturnRelatedEventData';
|
||||
readonly payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When related events are returned from the server
|
||||
*/
|
||||
|
@ -64,7 +56,6 @@ interface ServerReturnedRelatedEventData {
|
|||
export type DataAction =
|
||||
| ServerReturnedResolverData
|
||||
| ServerFailedToReturnResolverData
|
||||
| ServerFailedToReturnRelatedEventData
|
||||
| ServerReturnedRelatedEventData
|
||||
| AppRequestedResolverData
|
||||
| AppAbortedResolverDataRequest;
|
||||
|
|
|
@ -10,8 +10,7 @@ import { dataReducer } from './reducer';
|
|||
import * as selectors from './selectors';
|
||||
import { DataState } from '../../types';
|
||||
import { DataAction } from './action';
|
||||
import { ResolverChildNode, ResolverEvent, ResolverTree } from '../../../../common/endpoint/types';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types';
|
||||
import { values } from '../../../../common/endpoint/models/ecs_safety_helpers';
|
||||
import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
|
||||
|
||||
|
@ -43,7 +42,7 @@ describe('Resolver Data Middleware', () => {
|
|||
const tree = mockResolverTree({
|
||||
// Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with
|
||||
// a lot of the frontend functions. So casting it back to the unsafe type for now.
|
||||
events: baseTree.allEvents as ResolverEvent[],
|
||||
events: baseTree.allEvents,
|
||||
cursors: {
|
||||
childrenNextChild: 'aValidChildCursor',
|
||||
ancestryNextAncestor: 'aValidAncestorCursor',
|
||||
|
@ -61,9 +60,6 @@ describe('Resolver Data Middleware', () => {
|
|||
|
||||
describe('when data was received with stats mocked for the first child node', () => {
|
||||
let firstChildNodeInTree: TreeNode;
|
||||
let eventStatsForFirstChildNode: { total: number; byCategory: Record<string, number> };
|
||||
let categoryToOverCount: string;
|
||||
let aggregateCategoryTotalForFirstChildNode: number;
|
||||
let tree: ResolverTree;
|
||||
|
||||
/**
|
||||
|
@ -73,13 +69,7 @@ describe('Resolver Data Middleware', () => {
|
|||
*/
|
||||
|
||||
beforeEach(() => {
|
||||
({
|
||||
tree,
|
||||
firstChildNodeInTree,
|
||||
eventStatsForFirstChildNode,
|
||||
categoryToOverCount,
|
||||
aggregateCategoryTotalForFirstChildNode,
|
||||
} = mockedTree());
|
||||
({ tree, firstChildNodeInTree } = mockedTree());
|
||||
if (tree) {
|
||||
dispatchTree(tree);
|
||||
}
|
||||
|
@ -94,7 +84,7 @@ describe('Resolver Data Middleware', () => {
|
|||
entityID: firstChildNodeInTree.id,
|
||||
// Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with
|
||||
// a lot of the frontend functions. So casting it back to the unsafe type for now.
|
||||
events: firstChildNodeInTree.relatedEvents as ResolverEvent[],
|
||||
events: firstChildNodeInTree.relatedEvents,
|
||||
nextEvent: null,
|
||||
},
|
||||
};
|
||||
|
@ -108,121 +98,6 @@ describe('Resolver Data Middleware', () => {
|
|||
|
||||
expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents);
|
||||
});
|
||||
it('should indicate the correct related event count for each category', () => {
|
||||
const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
|
||||
const displayCountsForCategory = selectedRelatedInfo(firstChildNodeInTree.id)
|
||||
?.numberActuallyDisplayedForCategory!;
|
||||
|
||||
const eventCategoriesForNode: string[] = Object.keys(
|
||||
eventStatsForFirstChildNode.byCategory
|
||||
);
|
||||
|
||||
for (const eventCategory of eventCategoriesForNode) {
|
||||
expect(`${eventCategory}:${displayCountsForCategory(eventCategory)}`).toBe(
|
||||
`${eventCategory}:${eventStatsForFirstChildNode.byCategory[eventCategory]}`
|
||||
);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* The general approach reflected here is to _avoid_ showing a limit warning - even if we hit
|
||||
* the overall related event limit - as long as the number in our category matches what the stats
|
||||
* say we have. E.g. If the stats say you have 20 dns events, and we receive 20 dns events, we
|
||||
* don't need to display a limit warning for that, even if we hit some overall event limit of e.g. 100
|
||||
* while we were fetching the 20.
|
||||
*/
|
||||
it('should not indicate the limit has been exceeded because the number of related events received for the category is greater or equal to the stats count', () => {
|
||||
const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
|
||||
const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id)
|
||||
?.shouldShowLimitForCategory!;
|
||||
for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) {
|
||||
expect(shouldShowLimit(typeCounted)).toBe(false);
|
||||
}
|
||||
});
|
||||
it('should not indicate that there are any related events missing because the number of related events received for the category is greater or equal to the stats count', () => {
|
||||
const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
|
||||
const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id)
|
||||
?.numberNotDisplayedForCategory!;
|
||||
for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) {
|
||||
expect(notDisplayed(typeCounted)).toBe(0);
|
||||
}
|
||||
});
|
||||
it('should return an overall correct count for the number of related events', () => {
|
||||
const aggregateTotalByEntityId = selectors.relatedEventAggregateTotalByEntityId(
|
||||
store.getState()
|
||||
);
|
||||
const countForId = aggregateTotalByEntityId(firstChildNodeInTree.id);
|
||||
expect(countForId).toBe(aggregateCategoryTotalForFirstChildNode);
|
||||
});
|
||||
});
|
||||
describe('when data was received and stats show more related events than the API can provide', () => {
|
||||
beforeEach(() => {
|
||||
// Add 1 to the stats for an event category so that the selectors think we are missing data.
|
||||
// This mutates `tree`, and then we re-dispatch it
|
||||
eventStatsForFirstChildNode.byCategory[categoryToOverCount] =
|
||||
eventStatsForFirstChildNode.byCategory[categoryToOverCount] + 1;
|
||||
|
||||
if (tree) {
|
||||
dispatchTree(tree);
|
||||
const relatedAction: DataAction = {
|
||||
type: 'serverReturnedRelatedEventData',
|
||||
payload: {
|
||||
entityID: firstChildNodeInTree.id,
|
||||
// Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with
|
||||
// a lot of the frontend functions. So casting it back to the unsafe type for now.
|
||||
events: firstChildNodeInTree.relatedEvents as ResolverEvent[],
|
||||
nextEvent: 'aValidNextEventCursor',
|
||||
},
|
||||
};
|
||||
store.dispatch(relatedAction);
|
||||
}
|
||||
});
|
||||
it('should have the correct related events', () => {
|
||||
const selectedEventsByEntityId = selectors.relatedEventsByEntityId(store.getState());
|
||||
const selectedEventsForFirstChildNode = selectedEventsByEntityId.get(
|
||||
firstChildNodeInTree.id
|
||||
)!.events;
|
||||
|
||||
expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents);
|
||||
});
|
||||
it('should return related events for the category equal to the number of events of that type provided', () => {
|
||||
const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState());
|
||||
const relatedEventsForOvercountedCategory = relatedEventsByCategory(
|
||||
firstChildNodeInTree.id
|
||||
)(categoryToOverCount);
|
||||
expect(relatedEventsForOvercountedCategory.length).toBe(
|
||||
eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1
|
||||
);
|
||||
});
|
||||
it('should return the correct related event detail metadata for a given related event', () => {
|
||||
const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState());
|
||||
const someRelatedEventForTheFirstChild = relatedEventsByCategory(firstChildNodeInTree.id)(
|
||||
categoryToOverCount
|
||||
)[0];
|
||||
const relatedEventID = eventModel.eventId(someRelatedEventForTheFirstChild)!;
|
||||
const relatedDisplayInfo = selectors.relatedEventDisplayInfoByEntityAndSelfID(
|
||||
store.getState()
|
||||
)(firstChildNodeInTree.id, relatedEventID);
|
||||
const [, countOfSameType, , sectionData] = relatedDisplayInfo;
|
||||
const hostEntries = sectionData.filter((section) => {
|
||||
return section.sectionTitle === 'host';
|
||||
})[0].entries;
|
||||
expect(hostEntries).toContainEqual({ title: 'os.platform', description: 'Windows' });
|
||||
expect(countOfSameType).toBe(
|
||||
eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1
|
||||
);
|
||||
});
|
||||
it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => {
|
||||
const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
|
||||
const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id)
|
||||
?.shouldShowLimitForCategory!;
|
||||
expect(shouldShowLimit(categoryToOverCount)).toBe(true);
|
||||
});
|
||||
it('should indicate that there are related events missing because the number of related events received for the category is less than what the stats count said it would be', () => {
|
||||
const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
|
||||
const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id)
|
||||
?.numberNotDisplayedForCategory!;
|
||||
expect(notDisplayed(categoryToOverCount)).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -241,7 +116,7 @@ function mockedTree() {
|
|||
const tree = mockResolverTree({
|
||||
// Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with
|
||||
// a lot of the frontend functions. So casting it back to the unsafe type for now.
|
||||
events: baseTree.allEvents as ResolverEvent[],
|
||||
events: baseTree.allEvents,
|
||||
/**
|
||||
* Calculate children from the ResolverTree response using the children of the `Tree` we generated using the Resolver data generator code.
|
||||
* Compile (and attach) stats to the first child node.
|
||||
|
@ -255,7 +130,7 @@ function mockedTree() {
|
|||
const childNode: Partial<ResolverChildNode> = {};
|
||||
// Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with
|
||||
// a lot of the frontend functions. So casting it back to the unsafe type for now.
|
||||
childNode.lifecycle = node.lifecycle as ResolverEvent[];
|
||||
childNode.lifecycle = node.lifecycle;
|
||||
|
||||
// `TreeNode` has `id` which is the same as `entityID`.
|
||||
// The `ResolverChildNode` calls the entityID as `entityID`.
|
||||
|
@ -281,8 +156,6 @@ function mockedTree() {
|
|||
return {
|
||||
tree: tree!,
|
||||
firstChildNodeInTree,
|
||||
eventStatsForFirstChildNode: statsResults.eventStats,
|
||||
aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal,
|
||||
categoryToOverCount: statsResults.firstCategory,
|
||||
};
|
||||
}
|
||||
|
@ -309,7 +182,6 @@ function compileStatsForChild(
|
|||
};
|
||||
/** The category of the first event. */
|
||||
firstCategory: string;
|
||||
aggregateCategoryTotal: number;
|
||||
} {
|
||||
const totalRelatedEvents = node.relatedEvents.length;
|
||||
// For the purposes of testing, we pick one category to fake an extra event for
|
||||
|
@ -317,12 +189,6 @@ function compileStatsForChild(
|
|||
|
||||
let firstCategory: string | undefined;
|
||||
|
||||
// This is the "aggregate total" which is displayed to users as the total count
|
||||
// of related events for the node. It is tallied by incrementing for every discrete
|
||||
// event.category in an event.category array (or just 1 for a plain string). E.g. two events
|
||||
// categories 'file' and ['dns','network'] would have an `aggregate total` of 3.
|
||||
let aggregateCategoryTotal: number = 0;
|
||||
|
||||
const compiledStats = node.relatedEvents.reduce(
|
||||
(counts: Record<string, number>, relatedEvent) => {
|
||||
// get an array of categories regardless of whether category is a string or string[]
|
||||
|
@ -336,7 +202,6 @@ function compileStatsForChild(
|
|||
|
||||
// Increment the count of events with this category
|
||||
counts[category] = counts[category] ? counts[category] + 1 : 1;
|
||||
aggregateCategoryTotal++;
|
||||
}
|
||||
return counts;
|
||||
},
|
||||
|
@ -354,6 +219,5 @@ function compileStatsForChild(
|
|||
byCategory: compiledStats,
|
||||
},
|
||||
firstCategory,
|
||||
aggregateCategoryTotal,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,9 +11,7 @@ import * as treeFetcherParameters from '../../models/tree_fetcher_parameters';
|
|||
|
||||
const initialState: DataState = {
|
||||
relatedEvents: new Map(),
|
||||
relatedEventsReady: new Map(),
|
||||
resolverComponentInstanceID: undefined,
|
||||
tree: {},
|
||||
};
|
||||
|
||||
export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => {
|
||||
|
@ -44,7 +42,7 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
|
|||
};
|
||||
return nextState;
|
||||
} else if (action.type === 'appAbortedResolverDataRequest') {
|
||||
if (treeFetcherParameters.equal(action.payload, state.tree.pendingRequestParameters)) {
|
||||
if (treeFetcherParameters.equal(action.payload, state.tree?.pendingRequestParameters)) {
|
||||
// the request we were awaiting was aborted
|
||||
const nextState: DataState = {
|
||||
...state,
|
||||
|
@ -81,7 +79,7 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
|
|||
return nextState;
|
||||
} else if (action.type === 'serverFailedToReturnResolverData') {
|
||||
/** Only handle this if we are expecting a response */
|
||||
if (state.tree.pendingRequestParameters !== undefined) {
|
||||
if (state.tree?.pendingRequestParameters !== undefined) {
|
||||
const nextState: DataState = {
|
||||
...state,
|
||||
tree: {
|
||||
|
@ -97,19 +95,9 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
|
|||
} else {
|
||||
return state;
|
||||
}
|
||||
} else if (
|
||||
action.type === 'userRequestedRelatedEventData' ||
|
||||
action.type === 'appDetectedMissingEventData'
|
||||
) {
|
||||
const nextState: DataState = {
|
||||
...state,
|
||||
relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload, false]]),
|
||||
};
|
||||
return nextState;
|
||||
} else if (action.type === 'serverReturnedRelatedEventData') {
|
||||
const nextState: DataState = {
|
||||
...state,
|
||||
relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload.entityID, true]]),
|
||||
relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]),
|
||||
};
|
||||
return nextState;
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
mockTreeWithAllProcessesTerminated,
|
||||
mockTreeWithNoProcessEvents,
|
||||
} from '../../mocks/resolver_tree';
|
||||
import { uniquePidForProcess } from '../../models/process_event';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import { EndpointEvent } from '../../../../common/endpoint/types';
|
||||
import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
|
||||
|
||||
|
@ -411,7 +411,7 @@ describe('data state', () => {
|
|||
expect(graphables.length).toBe(3);
|
||||
for (const event of graphables) {
|
||||
expect(() => {
|
||||
selectors.ariaFlowtoCandidate(state())(uniquePidForProcess(event));
|
||||
selectors.ariaFlowtoCandidate(state())(eventModel.entityIDSafeVersion(event)!);
|
||||
}).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -14,46 +14,36 @@ import {
|
|||
IndexedProcessNode,
|
||||
AABB,
|
||||
VisibleEntites,
|
||||
SectionData,
|
||||
TreeFetcherParameters,
|
||||
} from '../../types';
|
||||
import {
|
||||
isGraphableProcess,
|
||||
isTerminatedProcess,
|
||||
uniquePidForProcess,
|
||||
uniqueParentPidForProcess,
|
||||
} from '../../models/process_event';
|
||||
import { isGraphableProcess, isTerminatedProcess } from '../../models/process_event';
|
||||
import * as indexedProcessTreeModel from '../../models/indexed_process_tree';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
|
||||
import {
|
||||
ResolverEvent,
|
||||
ResolverTree,
|
||||
ResolverNodeStats,
|
||||
ResolverRelatedEvents,
|
||||
SafeResolverEvent,
|
||||
EndpointEvent,
|
||||
LegacyEndpointEvent,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import * as resolverTreeModel from '../../models/resolver_tree';
|
||||
import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters';
|
||||
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import * as vector2 from '../../models/vector2';
|
||||
import { formatDate } from '../../view/panels/panel_content_utilities';
|
||||
|
||||
/**
|
||||
* If there is currently a request.
|
||||
*/
|
||||
export function isTreeLoading(state: DataState): boolean {
|
||||
return state.tree.pendingRequestParameters !== undefined;
|
||||
return state.tree?.pendingRequestParameters !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a request was made and it threw an error or returned a failure response code.
|
||||
*/
|
||||
export function hadErrorLoadingTree(state: DataState): boolean {
|
||||
if (state.tree.lastResponse) {
|
||||
return !state.tree.lastResponse.successful;
|
||||
if (state.tree?.lastResponse) {
|
||||
return !state.tree?.lastResponse.successful;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -70,7 +60,7 @@ export function resolverComponentInstanceID(state: DataState): string {
|
|||
* we're currently interested in.
|
||||
*/
|
||||
const resolverTreeResponse = (state: DataState): ResolverTree | undefined => {
|
||||
return state.tree.lastResponse?.successful ? state.tree.lastResponse.result : undefined;
|
||||
return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -102,7 +92,7 @@ export const terminatedProcesses = createSelector(resolverTreeResponse, function
|
|||
.lifecycleEvents(tree)
|
||||
.filter(isTerminatedProcess)
|
||||
.map((terminatedEvent) => {
|
||||
return uniquePidForProcess(terminatedEvent);
|
||||
return eventModel.entityIDSafeVersion(terminatedEvent);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -115,8 +105,8 @@ export const isProcessTerminated = createSelector(terminatedProcesses, function
|
|||
terminatedProcesses
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
return (entityId: string) => {
|
||||
return terminatedProcesses.has(entityId);
|
||||
return (entityID: string) => {
|
||||
return terminatedProcesses.has(entityID);
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -125,12 +115,14 @@ export const isProcessTerminated = createSelector(terminatedProcesses, function
|
|||
*/
|
||||
export const graphableProcesses = createSelector(resolverTreeResponse, function (tree?) {
|
||||
// Keep track of the last process event (in array order) for each entity ID
|
||||
const events: Map<string, ResolverEvent> = new Map();
|
||||
const events: Map<string, SafeResolverEvent> = new Map();
|
||||
if (tree) {
|
||||
for (const event of resolverTreeModel.lifecycleEvents(tree)) {
|
||||
if (isGraphableProcess(event)) {
|
||||
const entityID = uniquePidForProcess(event);
|
||||
events.set(entityID, event);
|
||||
const entityID = eventModel.entityIDSafeVersion(event);
|
||||
if (entityID !== undefined) {
|
||||
events.set(entityID, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...events.values()];
|
||||
|
@ -147,7 +139,7 @@ export const tree = createSelector(graphableProcesses, function indexedTree(
|
|||
graphableProcesses
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
return indexedProcessTreeModel.factory(graphableProcesses as SafeResolverEvent[]);
|
||||
return indexedProcessTreeModel.factory(graphableProcesses);
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -169,24 +161,18 @@ export const relatedEventsStats: (
|
|||
);
|
||||
|
||||
/**
|
||||
* This returns the "aggregate total" for related events, tallied as the sum
|
||||
* of their individual `event.category`s. E.g. a [DNS, Network] would count as two
|
||||
* towards the aggregate total.
|
||||
* The total number of events related to a node.
|
||||
*/
|
||||
export const relatedEventAggregateTotalByEntityId: (
|
||||
export const relatedEventTotalCount: (
|
||||
state: DataState
|
||||
) => (entityId: string) => number = createSelector(relatedEventsStats, (relatedStats) => {
|
||||
return (entityId) => {
|
||||
const statsForEntity = relatedStats(entityId);
|
||||
if (statsForEntity === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return Object.values(statsForEntity?.events?.byCategory || {}).reduce(
|
||||
(sum, val) => sum + val,
|
||||
0
|
||||
);
|
||||
};
|
||||
});
|
||||
) => (entityID: string) => number | undefined = createSelector(
|
||||
relatedEventsStats,
|
||||
(relatedStats) => {
|
||||
return (entityID) => {
|
||||
return relatedStats(entityID)?.events?.total;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* returns a map of entity_ids to related event data.
|
||||
|
@ -197,98 +183,36 @@ export function relatedEventsByEntityId(data: DataState): Map<string, ResolverRe
|
|||
}
|
||||
|
||||
/**
|
||||
* A helper function to turn objects into EuiDescriptionList entries.
|
||||
* This reflects the strategy of more or less "dumping" metadata for related processes
|
||||
* in description lists with little/no 'prettification'. This has the obvious drawback of
|
||||
* data perhaps appearing inscrutable/daunting, but the benefit of presenting these fields
|
||||
* to the user "as they occur" in ECS, which may help them with e.g. EQL queries.
|
||||
*
|
||||
* Given an object like: {a:{b: 1}, c: 'd'} it will yield title/description entries like so:
|
||||
* {title: "a.b", description: "1"}, {title: "c", description: "d"}
|
||||
*
|
||||
* @param {object} obj The object to turn into `<dt><dd>` entries
|
||||
* @deprecated
|
||||
* Get an event (from memory) by its `event.id`.
|
||||
* @deprecated Use the API to find events by ID
|
||||
*/
|
||||
const objectToDescriptionListEntries = function* (
|
||||
obj: object,
|
||||
prefix = ''
|
||||
): Generator<{ title: string; description: string }> {
|
||||
const nextPrefix = prefix.length ? `${prefix}.` : '';
|
||||
for (const [metaKey, metaValue] of Object.entries(obj)) {
|
||||
if (typeof metaValue === 'number' || typeof metaValue === 'string') {
|
||||
yield { title: nextPrefix + metaKey, description: `${metaValue}` };
|
||||
} else if (metaValue instanceof Array) {
|
||||
yield {
|
||||
title: nextPrefix + metaKey,
|
||||
description: metaValue
|
||||
.filter((arrayEntry) => {
|
||||
return typeof arrayEntry === 'number' || typeof arrayEntry === 'string';
|
||||
})
|
||||
.join(','),
|
||||
};
|
||||
} else if (typeof metaValue === 'object') {
|
||||
yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey);
|
||||
export const eventByID = createSelector(relatedEventsByEntityId, (relatedEvents) => {
|
||||
// A map of nodeID to a map of eventID to events. Lazily populated.
|
||||
const memo = new Map<string, Map<string | number, SafeResolverEvent>>();
|
||||
return ({ eventID, nodeID }: { eventID: string; nodeID: string }) => {
|
||||
// We keep related events in a map by their nodeID.
|
||||
const eventsWrapper = relatedEvents.get(nodeID);
|
||||
if (!eventsWrapper) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function that returns the information needed to display related event details based on
|
||||
* the related event's entityID and its own ID.
|
||||
* @deprecated
|
||||
*/
|
||||
export const relatedEventDisplayInfoByEntityAndSelfID: (
|
||||
state: DataState
|
||||
) => (
|
||||
entityId: string,
|
||||
relatedEventId: string | number
|
||||
) => [
|
||||
EndpointEvent | LegacyEndpointEvent | undefined,
|
||||
number,
|
||||
string | undefined,
|
||||
SectionData,
|
||||
string
|
||||
] = createSelector(relatedEventsByEntityId, function relatedEventDetails(
|
||||
/* eslint-disable no-shadow */
|
||||
relatedEventsByEntityId
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
return defaultMemoize((entityId: string, relatedEventId: string | number) => {
|
||||
const relatedEventsForThisProcess = relatedEventsByEntityId.get(entityId);
|
||||
if (!relatedEventsForThisProcess) {
|
||||
return [undefined, 0, undefined, [], ''];
|
||||
// When an event from a nodeID is requested, build a map for all events related to that node.
|
||||
if (!memo.has(nodeID)) {
|
||||
const map = new Map<string | number, SafeResolverEvent>();
|
||||
for (const event of eventsWrapper.events) {
|
||||
const id = eventModel.eventIDSafeVersion(event);
|
||||
if (id !== undefined) {
|
||||
map.set(id, event);
|
||||
}
|
||||
}
|
||||
memo.set(nodeID, map);
|
||||
}
|
||||
const specificEvent = relatedEventsForThisProcess.events.find(
|
||||
(evt) => eventModel.eventId(evt) === relatedEventId
|
||||
);
|
||||
// For breadcrumbs:
|
||||
const specificCategory = specificEvent && eventModel.primaryEventCategory(specificEvent);
|
||||
const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => {
|
||||
return eventModel.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal;
|
||||
}, 0);
|
||||
// Assuming these details (agent, ecs, process) aren't as helpful, can revisit
|
||||
const { agent, ecs, process, ...relevantData } = specificEvent as SafeResolverEvent & {
|
||||
// Type this with various unknown keys so that ts will let us delete those keys
|
||||
ecs: unknown;
|
||||
process: unknown;
|
||||
};
|
||||
|
||||
let displayDate = '';
|
||||
const sectionData: SectionData = Object.entries(relevantData)
|
||||
.map(([sectionTitle, val]) => {
|
||||
if (sectionTitle === '@timestamp') {
|
||||
displayDate = formatDate(val);
|
||||
return { sectionTitle: '', entries: [] };
|
||||
}
|
||||
if (typeof val !== 'object') {
|
||||
return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] };
|
||||
}
|
||||
return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] };
|
||||
})
|
||||
.filter((v) => v.sectionTitle !== '' && v.entries.length);
|
||||
|
||||
return [specificEvent, countOfCategory, specificCategory, sectionData, displayDate];
|
||||
});
|
||||
const eventMap = memo.get(nodeID);
|
||||
if (!eventMap) {
|
||||
// This shouldn't be possible.
|
||||
return undefined;
|
||||
}
|
||||
return eventMap.get(eventID);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -298,44 +222,65 @@ export const relatedEventDisplayInfoByEntityAndSelfID: (
|
|||
*/
|
||||
export const relatedEventsByCategory: (
|
||||
state: DataState
|
||||
) => (entityID: string) => (ecsCategory: string) => ResolverEvent[] = createSelector(
|
||||
) => (node: string, eventCategory: string) => SafeResolverEvent[] = createSelector(
|
||||
relatedEventsByEntityId,
|
||||
function (
|
||||
/* eslint-disable no-shadow */
|
||||
relatedEventsByEntityId
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
return defaultMemoize((entityId: string) => {
|
||||
return defaultMemoize((ecsCategory: string) => {
|
||||
const relatedById = relatedEventsByEntityId.get(entityId);
|
||||
// With no related events, we can't return related by category
|
||||
if (!relatedById) {
|
||||
return [];
|
||||
// A map of nodeID -> event category -> SafeResolverEvent[]
|
||||
const nodeMap: Map<string, Map<string, SafeResolverEvent[]>> = new Map();
|
||||
for (const [nodeID, events] of relatedEventsByEntityId) {
|
||||
// A map of eventCategory -> SafeResolverEvent[]
|
||||
let categoryMap = nodeMap.get(nodeID);
|
||||
if (!categoryMap) {
|
||||
categoryMap = new Map();
|
||||
nodeMap.set(nodeID, categoryMap);
|
||||
}
|
||||
|
||||
for (const event of events.events) {
|
||||
for (const category of eventModel.eventCategory(event)) {
|
||||
let eventsInCategory = categoryMap.get(category);
|
||||
if (!eventsInCategory) {
|
||||
eventsInCategory = [];
|
||||
categoryMap.set(category, eventsInCategory);
|
||||
}
|
||||
eventsInCategory.push(event);
|
||||
}
|
||||
return relatedById.events.reduce(
|
||||
(eventsByCategory: ResolverEvent[], candidate: ResolverEvent) => {
|
||||
if (
|
||||
[candidate && eventModel.allEventCategories(candidate)].flat().includes(ecsCategory)
|
||||
) {
|
||||
eventsByCategory.push(candidate);
|
||||
}
|
||||
return eventsByCategory;
|
||||
},
|
||||
[]
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Use the same empty array for all values that are missing
|
||||
const emptyArray: SafeResolverEvent[] = [];
|
||||
|
||||
return (entityID: string, category: string): SafeResolverEvent[] => {
|
||||
const categoryMap = nodeMap.get(entityID);
|
||||
if (!categoryMap) {
|
||||
return emptyArray;
|
||||
}
|
||||
const eventsInCategory = categoryMap.get(category);
|
||||
return eventsInCategory ?? emptyArray;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* returns a map of entity_ids to booleans indicating if it is waiting on related event
|
||||
* A value of `undefined` can be interpreted as `not yet requested`
|
||||
* @deprecated
|
||||
*/
|
||||
export function relatedEventsReady(data: DataState): Map<string, boolean> {
|
||||
return data.relatedEventsReady;
|
||||
}
|
||||
export const relatedEventCountByType: (
|
||||
state: DataState
|
||||
) => (nodeID: string, eventType: string) => number | undefined = createSelector(
|
||||
relatedEventsStats,
|
||||
(statsMap) => {
|
||||
return (nodeID: string, eventType: string): number | undefined => {
|
||||
const stats = statsMap(nodeID);
|
||||
if (stats) {
|
||||
const value = Object.prototype.hasOwnProperty.call(stats.events.byCategory, eventType);
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* `true` if there were more children than we got in the last request.
|
||||
|
@ -355,113 +300,6 @@ export function hasMoreAncestors(state: DataState): boolean {
|
|||
return resolverTree ? resolverTreeModel.hasMoreAncestors(resolverTree) : false;
|
||||
}
|
||||
|
||||
interface RelatedInfoFunctions {
|
||||
shouldShowLimitForCategory: (category: string) => boolean;
|
||||
numberNotDisplayedForCategory: (category: string) => number;
|
||||
numberActuallyDisplayedForCategory: (category: string) => number;
|
||||
}
|
||||
/**
|
||||
* A map of `entity_id`s to functions that provide information about
|
||||
* related events by ECS `.category` Primarily to avoid having business logic
|
||||
* in UI components.
|
||||
* @deprecated
|
||||
*/
|
||||
export const relatedEventInfoByEntityId: (
|
||||
state: DataState
|
||||
) => (entityID: string) => RelatedInfoFunctions | null = createSelector(
|
||||
relatedEventsByEntityId,
|
||||
relatedEventsStats,
|
||||
function selectLineageLimitInfo(
|
||||
/* eslint-disable no-shadow */
|
||||
relatedEventsByEntityId,
|
||||
relatedEventsStats
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
return (entityId) => {
|
||||
const stats = relatedEventsStats(entityId);
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
const eventsResponseForThisEntry = relatedEventsByEntityId.get(entityId);
|
||||
const hasMoreEvents =
|
||||
eventsResponseForThisEntry && eventsResponseForThisEntry.nextEvent !== null;
|
||||
/**
|
||||
* Get the "aggregate" total for the event category (i.e. _all_ events that would qualify as being "in category")
|
||||
* For a set like `[DNS,File][File,DNS][Registry]` The first and second events would contribute to the aggregate total for DNS being 2.
|
||||
* This is currently aligned with how the backed provides this information.
|
||||
*
|
||||
* @param eventCategory {string} The ECS category like 'file','dns',etc.
|
||||
*/
|
||||
const aggregateTotalForCategory = (eventCategory: string): number => {
|
||||
return stats.events.byCategory[eventCategory] || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all the related events in the category provided.
|
||||
*
|
||||
* @param eventCategory {string} The ECS category like 'file','dns',etc.
|
||||
*/
|
||||
const unmemoizedMatchingEventsForCategory = (eventCategory: string): ResolverEvent[] => {
|
||||
if (!eventsResponseForThisEntry) {
|
||||
return [];
|
||||
}
|
||||
return eventsResponseForThisEntry.events.filter((resolverEvent) => {
|
||||
for (const category of [eventModel.allEventCategories(resolverEvent)].flat()) {
|
||||
if (category === eventCategory) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const matchingEventsForCategory = unmemoizedMatchingEventsForCategory;
|
||||
|
||||
/**
|
||||
* The number of events that occurred before the API limit was reached.
|
||||
* The number of events that came back form the API that have `eventCategory` in their list of categories.
|
||||
*
|
||||
* @param eventCategory {string} The ECS category like 'file','dns',etc.
|
||||
*/
|
||||
const numberActuallyDisplayedForCategory = (eventCategory: string): number => {
|
||||
return matchingEventsForCategory(eventCategory)?.length || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* The total number counted by the backend - the number displayed
|
||||
*
|
||||
* @param eventCategory {string} The ECS category like 'file','dns',etc.
|
||||
*/
|
||||
const numberNotDisplayedForCategory = (eventCategory: string): number => {
|
||||
return (
|
||||
aggregateTotalForCategory(eventCategory) -
|
||||
numberActuallyDisplayedForCategory(eventCategory)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* `true` when the `nextEvent` cursor appeared in the results and we are short on the number needed to
|
||||
* fullfill the aggregate count.
|
||||
*
|
||||
* @param eventCategory {string} The ECS category like 'file','dns',etc.
|
||||
*/
|
||||
const shouldShowLimitForCategory = (eventCategory: string): boolean => {
|
||||
if (hasMoreEvents && numberNotDisplayedForCategory(eventCategory) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const entryValue = {
|
||||
shouldShowLimitForCategory,
|
||||
numberNotDisplayedForCategory,
|
||||
numberActuallyDisplayedForCategory,
|
||||
};
|
||||
return entryValue;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* If the tree resource needs to be fetched then these are the parameters that should be used.
|
||||
*/
|
||||
|
@ -470,14 +308,14 @@ export function treeParametersToFetch(state: DataState): TreeFetcherParameters |
|
|||
* If there are current tree parameters that don't match the parameters used in the pending request (if there is a pending request) and that don't match the parameters used in the last completed request (if there was a last completed request) then we need to fetch the tree resource using the current parameters.
|
||||
*/
|
||||
if (
|
||||
state.tree.currentParameters !== undefined &&
|
||||
state.tree?.currentParameters !== undefined &&
|
||||
!treeFetcherParametersModel.equal(
|
||||
state.tree.currentParameters,
|
||||
state.tree.lastResponse?.parameters
|
||||
state.tree?.currentParameters,
|
||||
state.tree?.lastResponse?.parameters
|
||||
) &&
|
||||
!treeFetcherParametersModel.equal(
|
||||
state.tree.currentParameters,
|
||||
state.tree.pendingRequestParameters
|
||||
state.tree?.currentParameters,
|
||||
state.tree?.pendingRequestParameters
|
||||
)
|
||||
) {
|
||||
return state.tree.currentParameters;
|
||||
|
@ -533,10 +371,11 @@ export const layout = createSelector(
|
|||
*/
|
||||
export const processEventForID: (
|
||||
state: DataState
|
||||
) => (nodeID: string) => ResolverEvent | null = createSelector(
|
||||
) => (nodeID: string) => SafeResolverEvent | null = createSelector(
|
||||
tree,
|
||||
(indexedProcessTree) => (nodeID: string) =>
|
||||
indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID) as ResolverEvent
|
||||
(indexedProcessTree) => (nodeID: string) => {
|
||||
return indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -547,7 +386,7 @@ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null
|
|||
processEventForID,
|
||||
({ ariaLevels }, processEventGetter) => (nodeID: string) => {
|
||||
const node = processEventGetter(nodeID);
|
||||
return node ? ariaLevels.get(node as SafeResolverEvent) ?? null : null;
|
||||
return node ? ariaLevels.get(node) ?? null : null;
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -582,7 +421,7 @@ export const ariaFlowtoCandidate: (
|
|||
* Getting the following sibling of a node has an `O(n)` time complexity where `n` is the number of children the parent of the node has.
|
||||
* For this reason, we calculate the following siblings of the node and all of its siblings at once and cache them.
|
||||
*/
|
||||
const nodeEvent: ResolverEvent | null = eventGetter(nodeID);
|
||||
const nodeEvent: SafeResolverEvent | null = eventGetter(nodeID);
|
||||
|
||||
if (!nodeEvent) {
|
||||
// this should never happen.
|
||||
|
@ -592,23 +431,30 @@ export const ariaFlowtoCandidate: (
|
|||
// nodes with the same parent ID
|
||||
const children = indexedProcessTreeModel.children(
|
||||
indexedProcessTree,
|
||||
uniqueParentPidForProcess(nodeEvent)
|
||||
eventModel.parentEntityIDSafeVersion(nodeEvent)
|
||||
);
|
||||
|
||||
let previousChild: ResolverEvent | null = null;
|
||||
let previousChild: SafeResolverEvent | null = null;
|
||||
// Loop over all nodes that have the same parent ID (even if the parent ID is undefined or points to a node that isn't in the tree.)
|
||||
for (const child of children) {
|
||||
if (previousChild !== null) {
|
||||
// Set the `child` as the following sibling of `previousChild`.
|
||||
memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child as ResolverEvent));
|
||||
const previousChildEntityID = eventModel.entityIDSafeVersion(previousChild);
|
||||
const followingSiblingEntityID = eventModel.entityIDSafeVersion(child);
|
||||
if (previousChildEntityID !== undefined && followingSiblingEntityID !== undefined) {
|
||||
memo.set(previousChildEntityID, followingSiblingEntityID);
|
||||
}
|
||||
}
|
||||
// Set the child as the previous child.
|
||||
previousChild = child as ResolverEvent;
|
||||
previousChild = child;
|
||||
}
|
||||
|
||||
if (previousChild) {
|
||||
// if there is a previous child, it has no following sibling.
|
||||
memo.set(uniquePidForProcess(previousChild), null);
|
||||
const entityID = eventModel.entityIDSafeVersion(previousChild);
|
||||
if (entityID !== undefined) {
|
||||
memo.set(entityID, null);
|
||||
}
|
||||
}
|
||||
|
||||
return memoizedGetter(nodeID);
|
||||
|
@ -708,10 +554,10 @@ export function treeRequestParametersToAbort(state: DataState): TreeFetcherParam
|
|||
* If there is a pending request, and its not for the current parameters (even, if the current parameters are undefined) then we should abort the request.
|
||||
*/
|
||||
if (
|
||||
state.tree.pendingRequestParameters !== undefined &&
|
||||
state.tree?.pendingRequestParameters !== undefined &&
|
||||
!treeFetcherParametersModel.equal(
|
||||
state.tree.pendingRequestParameters,
|
||||
state.tree.currentParameters
|
||||
state.tree?.pendingRequestParameters,
|
||||
state.tree?.currentParameters
|
||||
)
|
||||
) {
|
||||
return state.tree.pendingRequestParameters;
|
||||
|
@ -725,19 +571,19 @@ export function treeRequestParametersToAbort(state: DataState): TreeFetcherParam
|
|||
*/
|
||||
export const relatedEventTotalForProcess: (
|
||||
state: DataState
|
||||
) => (event: ResolverEvent) => number | null = createSelector(
|
||||
) => (event: SafeResolverEvent) => number | null = createSelector(
|
||||
relatedEventsStats,
|
||||
(statsForProcess) => {
|
||||
return (event: ResolverEvent) => {
|
||||
const stats = statsForProcess(uniquePidForProcess(event));
|
||||
return (event: SafeResolverEvent) => {
|
||||
const nodeID = eventModel.entityIDSafeVersion(event);
|
||||
if (nodeID === undefined) {
|
||||
return null;
|
||||
}
|
||||
const stats = statsForProcess(nodeID);
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
let total = 0;
|
||||
for (const value of Object.values(stats.events.byCategory)) {
|
||||
total += value;
|
||||
}
|
||||
return total;
|
||||
return stats.events.total;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Store, createStore } from 'redux';
|
|||
import { ResolverAction } from '../actions';
|
||||
import { resolverReducer } from '../reducer';
|
||||
import { ResolverState } from '../../types';
|
||||
import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { LegacyEndpointEvent, SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { visibleNodesAndEdgeLines } from '../selectors';
|
||||
import { mockProcessEvent } from '../../models/process_event_test_helpers';
|
||||
import { mock as mockResolverTree } from '../../models/resolver_tree';
|
||||
|
@ -102,7 +102,7 @@ describe('resolver visible entities', () => {
|
|||
});
|
||||
describe('when rendering a large tree with a small viewport', () => {
|
||||
beforeEach(() => {
|
||||
const events: ResolverEvent[] = [
|
||||
const events: SafeResolverEvent[] = [
|
||||
processA,
|
||||
processB,
|
||||
processC,
|
||||
|
@ -130,7 +130,7 @@ describe('resolver visible entities', () => {
|
|||
});
|
||||
describe('when rendering a large tree with a large viewport', () => {
|
||||
beforeEach(() => {
|
||||
const events: ResolverEvent[] = [
|
||||
const events: SafeResolverEvent[] = [
|
||||
processA,
|
||||
processB,
|
||||
processC,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { animatePanning } from './camera/methods';
|
||||
import { layout } from './selectors';
|
||||
import { ResolverState } from '../types';
|
||||
import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
|
||||
const animationDuration = 1000;
|
||||
|
||||
|
@ -17,10 +17,10 @@ const animationDuration = 1000;
|
|||
export function animateProcessIntoView(
|
||||
state: ResolverState,
|
||||
startTime: number,
|
||||
process: ResolverEvent
|
||||
process: SafeResolverEvent
|
||||
): ResolverState {
|
||||
const { processNodePositions } = layout(state);
|
||||
const position = processNodePositions.get(process as SafeResolverEvent);
|
||||
const position = processNodePositions.get(process);
|
||||
if (position) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
import { Dispatch, MiddlewareAPI } from 'redux';
|
||||
import { ResolverState, DataAccessLayer } from '../../types';
|
||||
import { ResolverRelatedEvents } from '../../../../common/endpoint/types';
|
||||
import { ResolverTreeFetcher } from './resolver_tree_fetcher';
|
||||
|
||||
import { ResolverAction } from '../actions';
|
||||
import { RelatedEventsFetcher } from './related_events_fetcher';
|
||||
|
||||
type MiddlewareFactory<S = ResolverState> = (
|
||||
dataAccessLayer: DataAccessLayer
|
||||
|
@ -25,33 +26,12 @@ type MiddlewareFactory<S = ResolverState> = (
|
|||
export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: DataAccessLayer) => {
|
||||
return (api) => (next) => {
|
||||
const resolverTreeFetcher = ResolverTreeFetcher(dataAccessLayer, api);
|
||||
const relatedEventsFetcher = RelatedEventsFetcher(dataAccessLayer, api);
|
||||
return async (action: ResolverAction) => {
|
||||
next(action);
|
||||
|
||||
resolverTreeFetcher();
|
||||
|
||||
if (
|
||||
action.type === 'userRequestedRelatedEventData' ||
|
||||
action.type === 'appDetectedMissingEventData'
|
||||
) {
|
||||
const entityIdToFetchFor = action.payload;
|
||||
let result: ResolverRelatedEvents | undefined;
|
||||
try {
|
||||
result = await dataAccessLayer.relatedEvents(entityIdToFetchFor);
|
||||
} catch {
|
||||
api.dispatch({
|
||||
type: 'serverFailedToReturnRelatedEventData',
|
||||
payload: action.payload,
|
||||
});
|
||||
}
|
||||
|
||||
if (result) {
|
||||
api.dispatch({
|
||||
type: 'serverReturnedRelatedEventData',
|
||||
payload: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
relatedEventsFetcher();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { Dispatch, MiddlewareAPI } from 'redux';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ResolverRelatedEvents } from '../../../../common/endpoint/types';
|
||||
|
||||
import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types';
|
||||
import * as selectors from '../selectors';
|
||||
import { ResolverAction } from '../actions';
|
||||
|
||||
export function RelatedEventsFetcher(
|
||||
dataAccessLayer: DataAccessLayer,
|
||||
api: MiddlewareAPI<Dispatch<ResolverAction>, ResolverState>
|
||||
): () => void {
|
||||
let last: PanelViewAndParameters | undefined;
|
||||
|
||||
// Call this after each state change.
|
||||
// This fetches the ResolverTree for the current entityID
|
||||
// if the entityID changes while
|
||||
return async () => {
|
||||
const state = api.getState();
|
||||
|
||||
const newParams = selectors.panelViewAndParameters(state);
|
||||
const oldParams = last;
|
||||
// Update this each time before fetching data (or even if we don't fetch data) so that subsequent actions that call this (concurrently) will have up to date info.
|
||||
last = newParams;
|
||||
|
||||
// If the panel view params have changed and the current panel view is either `nodeEventsOfType` or `eventDetail`, then fetch the related events for that nodeID.
|
||||
if (
|
||||
!isEqual(newParams, oldParams) &&
|
||||
(newParams.panelView === 'nodeEventsOfType' || newParams.panelView === 'eventDetail')
|
||||
) {
|
||||
const nodeID = newParams.panelParameters.nodeID;
|
||||
|
||||
const result: ResolverRelatedEvents | undefined = await dataAccessLayer.relatedEvents(nodeID);
|
||||
|
||||
if (result) {
|
||||
api.dispatch({
|
||||
type: 'serverReturnedRelatedEventData',
|
||||
payload: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -9,7 +9,7 @@ import { cameraReducer } from './camera/reducer';
|
|||
import { dataReducer } from './data/reducer';
|
||||
import { ResolverAction } from './actions';
|
||||
import { ResolverState, ResolverUIState } from '../types';
|
||||
import { uniquePidForProcess } from '../models/process_event';
|
||||
import * as eventModel from '../../../common/endpoint/models/event';
|
||||
|
||||
const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
|
||||
state = {
|
||||
|
@ -37,17 +37,18 @@ const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
|
|||
selectedNode: action.payload,
|
||||
};
|
||||
return next;
|
||||
} else if (
|
||||
action.type === 'userBroughtProcessIntoView' ||
|
||||
action.type === 'appDetectedNewIdFromQueryParams'
|
||||
) {
|
||||
const nodeID = uniquePidForProcess(action.payload.process);
|
||||
const next: ResolverUIState = {
|
||||
...state,
|
||||
ariaActiveDescendant: nodeID,
|
||||
selectedNode: nodeID,
|
||||
};
|
||||
return next;
|
||||
} else if (action.type === 'userBroughtProcessIntoView') {
|
||||
const nodeID = eventModel.entityIDSafeVersion(action.payload.process);
|
||||
if (nodeID !== undefined) {
|
||||
const next: ResolverUIState = {
|
||||
...state,
|
||||
ariaActiveDescendant: nodeID,
|
||||
selectedNode: nodeID,
|
||||
};
|
||||
return next;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
} else if (action.type === 'appReceivedNewExternalProperties') {
|
||||
const next: ResolverUIState = {
|
||||
...state,
|
||||
|
@ -68,10 +69,7 @@ const concernReducers = combineReducers({
|
|||
|
||||
export const resolverReducer: Reducer<ResolverState, ResolverAction> = (state, action) => {
|
||||
const nextState = concernReducers(state, action);
|
||||
if (
|
||||
action.type === 'userBroughtProcessIntoView' ||
|
||||
action.type === 'appDetectedNewIdFromQueryParams'
|
||||
) {
|
||||
if (action.type === 'userBroughtProcessIntoView') {
|
||||
return animateProcessIntoView(nextState, action.payload.time, action.payload.process);
|
||||
} else {
|
||||
return nextState;
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as cameraSelectors from './camera/selectors';
|
|||
import * as dataSelectors from './data/selectors';
|
||||
import * as uiSelectors from './ui/selectors';
|
||||
import { ResolverState, IsometricTaxiLayout } from '../types';
|
||||
import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types';
|
||||
import { ResolverNodeStats, SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { entityIDSafeVersion } from '../../../common/endpoint/models/event';
|
||||
|
||||
/**
|
||||
|
@ -61,6 +61,11 @@ export const isProcessTerminated = composeSelectors(
|
|||
dataSelectors.isProcessTerminated
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve an event from memory using the event's ID.
|
||||
*/
|
||||
export const eventByID = composeSelectors(dataStateSelector, dataSelectors.eventByID);
|
||||
|
||||
/**
|
||||
* Given a nodeID (aka entity_id) get the indexed process event.
|
||||
* Legacy functions take process events instead of nodeID, use this to get
|
||||
|
@ -68,7 +73,7 @@ export const isProcessTerminated = composeSelectors(
|
|||
*/
|
||||
export const processEventForID: (
|
||||
state: ResolverState
|
||||
) => (nodeID: string) => ResolverEvent | null = composeSelectors(
|
||||
) => (nodeID: string) => SafeResolverEvent | null = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.processEventForID
|
||||
);
|
||||
|
@ -119,11 +124,18 @@ export const relatedEventsStats: (
|
|||
* of their individual `event.category`s. E.g. a [DNS, Network] would count as two
|
||||
* towards the aggregate total.
|
||||
*/
|
||||
export const relatedEventAggregateTotalByEntityId: (
|
||||
export const relatedEventTotalCount: (
|
||||
state: ResolverState
|
||||
) => (nodeID: string) => number = composeSelectors(
|
||||
) => (nodeID: string) => number | undefined = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.relatedEventAggregateTotalByEntityId
|
||||
dataSelectors.relatedEventTotalCount
|
||||
);
|
||||
|
||||
export const relatedEventCountByType: (
|
||||
state: ResolverState
|
||||
) => (nodeID: string, eventType: string) => number | undefined = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.relatedEventCountByType
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -135,16 +147,6 @@ export const relatedEventsByEntityId = composeSelectors(
|
|||
dataSelectors.relatedEventsByEntityId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a function that returns the information needed to display related event details based on
|
||||
* the related event's entityID and its own ID.
|
||||
* @deprecated
|
||||
*/
|
||||
export const relatedEventDisplayInfoByEntityAndSelfId = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.relatedEventDisplayInfoByEntityAndSelfID
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a function that returns a function (when supplied with an entity id for a node)
|
||||
* that returns related events for a node that match an event.category (when supplied with the category)
|
||||
|
@ -155,26 +157,6 @@ export const relatedEventsByCategory = composeSelectors(
|
|||
dataSelectors.relatedEventsByCategory
|
||||
);
|
||||
|
||||
/**
|
||||
* Entity ids to booleans for waiting status
|
||||
* @deprecated
|
||||
*/
|
||||
export const relatedEventsReady = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.relatedEventsReady
|
||||
);
|
||||
|
||||
/**
|
||||
* Business logic lookup functions by ECS category by entity id.
|
||||
* Example usage:
|
||||
* const numberOfFileEvents = infoByEntityId.get(`someEntityId`)?.getAggregateTotalForCategory(`file`);
|
||||
* @deprecated
|
||||
*/
|
||||
export const relatedEventInfoByEntityId = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.relatedEventInfoByEntityId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the id of the "current" tree node (fake-focused)
|
||||
*/
|
||||
|
|
|
@ -8,9 +8,9 @@ import { decode, encode } from 'rison-node';
|
|||
|
||||
import { createSelector } from 'reselect';
|
||||
import { PanelViewAndParameters, ResolverUIState } from '../../types';
|
||||
import { ResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { isPanelViewAndParameters } from '../../models/location_search';
|
||||
import { eventId } from '../../../../common/endpoint/models/event';
|
||||
import { eventID } from '../../../../common/endpoint/models/event';
|
||||
|
||||
/**
|
||||
* id of the "current" tree node (fake-focused)
|
||||
|
@ -124,12 +124,12 @@ export const relatedEventDetailHrefs: (
|
|||
) => (
|
||||
category: string,
|
||||
nodeID: string,
|
||||
events: ResolverEvent[]
|
||||
events: SafeResolverEvent[]
|
||||
) => Map<string, string | undefined> = createSelector(relativeHref, (relativeHref) => {
|
||||
return (category: string, nodeID: string, events: ResolverEvent[]) => {
|
||||
return (category: string, nodeID: string, events: SafeResolverEvent[]) => {
|
||||
const hrefsByEntityID = new Map<string, string | undefined>();
|
||||
events.map((event) => {
|
||||
const entityID = String(eventId(event));
|
||||
const entityID = String(eventID(event));
|
||||
const eventDetailPanelParams: PanelViewAndParameters = {
|
||||
panelView: 'eventDetail',
|
||||
panelParameters: {
|
||||
|
|
|
@ -211,9 +211,8 @@ export interface TreeFetcherParameters {
|
|||
*/
|
||||
export interface DataState {
|
||||
readonly relatedEvents: Map<string, ResolverRelatedEvents>;
|
||||
readonly relatedEventsReady: Map<string, boolean>;
|
||||
|
||||
readonly tree: {
|
||||
readonly tree?: {
|
||||
/**
|
||||
* The parameters passed from the resolver properties
|
||||
*/
|
||||
|
@ -614,8 +613,9 @@ export interface ResolverPluginSetup {
|
|||
dataAccessLayer: {
|
||||
/**
|
||||
* A mock `DataAccessLayer` that returns a tree that has no ancestor nodes but which has 2 children nodes.
|
||||
* The origin has 2 related registry events
|
||||
*/
|
||||
noAncestorsTwoChildren: () => { dataAccessLayer: DataAccessLayer };
|
||||
noAncestorsTwoChildrenWithRelatedEventsOnOrigin: () => { dataAccessLayer: DataAccessLayer };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -53,7 +55,7 @@ const StyledElapsedTime = styled.div<StyledElapsedTime>`
|
|||
/**
|
||||
* A placeholder line segment view that connects process nodes.
|
||||
*/
|
||||
const EdgeLineComponent = React.memo(
|
||||
export const EdgeLine = React.memo(
|
||||
({
|
||||
className,
|
||||
edgeLineMetadata,
|
||||
|
@ -155,7 +157,3 @@ const EdgeLineComponent = React.memo(
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
EdgeLineComponent.displayName = 'EdgeLine';
|
||||
|
||||
export const EdgeLine = EdgeLineComponent;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
/* eslint-disable react/button-has-type */
|
||||
|
||||
import React, { useCallback, useMemo, useContext } from 'react';
|
||||
|
@ -54,7 +56,7 @@ const StyledGraphControls = styled.div<StyledGraphControls>`
|
|||
/**
|
||||
* Controls for zooming, panning, and centering in Resolver
|
||||
*/
|
||||
const GraphControlsComponent = React.memo(
|
||||
export const GraphControls = React.memo(
|
||||
({
|
||||
className,
|
||||
}: {
|
||||
|
@ -204,7 +206,3 @@ const GraphControlsComponent = React.memo(
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
GraphControlsComponent.displayName = 'GraphControlsComponent';
|
||||
|
||||
export const GraphControls = GraphControlsComponent;
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { LimitWarningsEuiCallOut } from './styles';
|
||||
|
||||
const lineageLimitMessage = (
|
||||
<FormattedMessage
|
||||
|
@ -15,11 +17,7 @@ const lineageLimitMessage = (
|
|||
/>
|
||||
);
|
||||
|
||||
const LineageTitleMessage = React.memo(function LineageTitleMessage({
|
||||
numberOfEntries,
|
||||
}: {
|
||||
numberOfEntries: number;
|
||||
}) {
|
||||
const LineageTitleMessage = React.memo(function ({ numberOfEntries }: { numberOfEntries: number }) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle"
|
||||
|
@ -29,7 +27,7 @@ const LineageTitleMessage = React.memo(function LineageTitleMessage({
|
|||
);
|
||||
});
|
||||
|
||||
const RelatedEventsLimitMessage = React.memo(function RelatedEventsLimitMessage({
|
||||
const RelatedEventsLimitMessage = React.memo(function ({
|
||||
category,
|
||||
numberOfEventsMissing,
|
||||
}: {
|
||||
|
@ -45,7 +43,7 @@ const RelatedEventsLimitMessage = React.memo(function RelatedEventsLimitMessage(
|
|||
);
|
||||
});
|
||||
|
||||
const RelatedLimitTitleMessage = React.memo(function RelatedLimitTitleMessage({
|
||||
const RelatedLimitTitleMessage = React.memo(function ({
|
||||
category,
|
||||
numberOfEventsDisplayed,
|
||||
}: {
|
||||
|
@ -64,13 +62,11 @@ const RelatedLimitTitleMessage = React.memo(function RelatedLimitTitleMessage({
|
|||
/**
|
||||
* Limit warning for hitting the /events API limit
|
||||
*/
|
||||
export const RelatedEventLimitWarning = React.memo(function RelatedEventLimitWarning({
|
||||
className,
|
||||
export const RelatedEventLimitWarning = React.memo(function ({
|
||||
eventType,
|
||||
numberActuallyDisplayed,
|
||||
numberMissing,
|
||||
}: {
|
||||
className?: string;
|
||||
eventType: string;
|
||||
numberActuallyDisplayed: number;
|
||||
numberMissing: number;
|
||||
|
@ -79,9 +75,8 @@ export const RelatedEventLimitWarning = React.memo(function RelatedEventLimitWar
|
|||
* Based on API limits, all related events may not be displayed.
|
||||
*/
|
||||
return (
|
||||
<EuiCallOut
|
||||
<LimitWarningsEuiCallOut
|
||||
size="s"
|
||||
className={className}
|
||||
title={
|
||||
<RelatedLimitTitleMessage
|
||||
category={eventType}
|
||||
|
@ -92,27 +87,20 @@ export const RelatedEventLimitWarning = React.memo(function RelatedEventLimitWar
|
|||
<p>
|
||||
<RelatedEventsLimitMessage category={eventType} numberOfEventsMissing={numberMissing} />
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</LimitWarningsEuiCallOut>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Limit warning for hitting a limit of nodes in the tree
|
||||
*/
|
||||
export const LimitWarning = React.memo(function LimitWarning({
|
||||
className,
|
||||
numberDisplayed,
|
||||
}: {
|
||||
className?: string;
|
||||
numberDisplayed: number;
|
||||
}) {
|
||||
export const LimitWarning = React.memo(function ({ numberDisplayed }: { numberDisplayed: number }) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
<LimitWarningsEuiCallOut
|
||||
size="s"
|
||||
className={className}
|
||||
title={<LineageTitleMessage numberOfEntries={numberDisplayed} />}
|
||||
>
|
||||
<p>{lineageLimitMessage}</p>
|
||||
</EuiCallOut>
|
||||
</LimitWarningsEuiCallOut>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children';
|
||||
import { Simulator } from '../test_utilities/simulator';
|
||||
// Extend jest with a custom matcher
|
||||
import '../test_utilities/extend_jest';
|
||||
|
||||
let simulator: Simulator;
|
||||
let databaseDocumentID: string;
|
||||
|
||||
// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
|
||||
const resolverComponentInstanceID = 'resolverComponentInstanceID';
|
||||
|
||||
describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => {
|
||||
beforeEach(async () => {
|
||||
// create a mock data access layer
|
||||
const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren();
|
||||
|
||||
// save a reference to the `_id` supported by the mock data layer
|
||||
databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
|
||||
|
||||
// create a resolver simulator, using the data access layer and an arbitrary component instance ID
|
||||
simulator = new Simulator({
|
||||
databaseDocumentID,
|
||||
dataAccessLayer,
|
||||
resolverComponentInstanceID,
|
||||
indices: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('shows 1 node with the words "Analyzed Event" in the label', async () => {
|
||||
await expect(
|
||||
simulator.map(() => {
|
||||
return simulator.testSubject('resolver:node:description').map((element) => element.text());
|
||||
})
|
||||
).toYieldEqualTo(['Analyzed Event · Running Process', 'Running Process', 'Running Process']);
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history';
|
||||
|
||||
import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children';
|
||||
import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin';
|
||||
import { Simulator } from '../test_utilities/simulator';
|
||||
// Extend jest with a custom matcher
|
||||
import '../test_utilities/extend_jest';
|
||||
|
@ -14,7 +14,7 @@ import { urlSearch } from '../test_utilities/url_search';
|
|||
// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
|
||||
const resolverComponentInstanceID = 'resolverComponentInstanceID';
|
||||
|
||||
describe(`Resolver: when analyzing a tree with no ancestors and two children, and when the component instance ID is ${resolverComponentInstanceID}`, () => {
|
||||
describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => {
|
||||
/**
|
||||
* Get (or lazily create and get) the simulator.
|
||||
*/
|
||||
|
@ -32,7 +32,10 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an
|
|||
|
||||
beforeEach(() => {
|
||||
// create a mock data access layer
|
||||
const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren();
|
||||
const {
|
||||
metadata: dataAccessLayerMetadata,
|
||||
dataAccessLayer,
|
||||
} = noAncestorsTwoChildrenWithRelatedEventsOnOrigin();
|
||||
|
||||
entityIDs = dataAccessLayerMetadata.entityIDs;
|
||||
|
||||
|
@ -184,6 +187,38 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an
|
|||
})
|
||||
);
|
||||
});
|
||||
describe("and when the user clicks the link to the node's events", () => {
|
||||
beforeEach(async () => {
|
||||
const nodeEventsListLink = await simulator().resolve(
|
||||
'resolver:node-detail:node-events-link'
|
||||
);
|
||||
|
||||
if (nodeEventsListLink) {
|
||||
nodeEventsListLink.simulate('click', { button: 0 });
|
||||
}
|
||||
});
|
||||
it('should show a link to view 2 registry events', async () => {
|
||||
await expect(
|
||||
simulator().map(() => {
|
||||
// The link text is split across two columns. The first column is the count and the second column has the type.
|
||||
const type = simulator().testSubject('resolver:panel:node-events:event-type-count');
|
||||
const link = simulator().testSubject('resolver:panel:node-events:event-type-link');
|
||||
return {
|
||||
typeLength: type.length,
|
||||
linkLength: link.length,
|
||||
typeText: type.text(),
|
||||
linkText: link.text(),
|
||||
};
|
||||
})
|
||||
).toYieldEqualTo({
|
||||
typeLength: 1,
|
||||
linkLength: 1,
|
||||
linkText: 'registry',
|
||||
// EUI's Table adds the column name to the value.
|
||||
typeText: 'Count2',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('and when the node list link has been clicked', () => {
|
||||
beforeEach(async () => {
|
||||
const nodeListLink = await simulator().resolve(
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBreadcrumb, EuiBetaBadge } from '@elastic/eui';
|
||||
import React, { memo } from 'react';
|
||||
import { BetaHeader, ThemedBreadcrumbs } from './styles';
|
||||
import { useColors } from '../use_colors';
|
||||
|
||||
/**
|
||||
* Breadcrumb menu
|
||||
*/
|
||||
export const Breadcrumbs = memo(function ({ breadcrumbs }: { breadcrumbs: EuiBreadcrumb[] }) {
|
||||
const { resolverBreadcrumbBackground, resolverEdgeText } = useColors();
|
||||
return (
|
||||
<>
|
||||
<BetaHeader>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate(
|
||||
'xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'BETA',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</BetaHeader>
|
||||
<ThemedBreadcrumbs
|
||||
background={resolverBreadcrumbBackground}
|
||||
text={resolverEdgeText}
|
||||
breadcrumbs={breadcrumbs}
|
||||
truncate={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -43,7 +43,7 @@ export const CubeForProcess = memo(function ({
|
|||
className={className}
|
||||
width="2.15em"
|
||||
height="2.15em"
|
||||
viewBox="0 0 100% 100%"
|
||||
viewBox="0 0 34 34"
|
||||
data-test-subj={dataTestSubj}
|
||||
isOrigin={isOrigin}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { deepObjectEntries } from './deep_object_entries';
|
||||
|
||||
describe('deepObjectEntries', () => {
|
||||
const valuesAndExpected: Array<[
|
||||
objectValue: object,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expected: Array<[path: Array<keyof any>, fieldValue: unknown]>
|
||||
]> = [
|
||||
[{}, []], // No 'field' values found
|
||||
[{ a: {} }, []], // No 'field' values found
|
||||
[{ a: { b: undefined } }, []], // No 'field' values found
|
||||
[{ a: { b: undefined, c: [] } }, []], // No 'field' values found
|
||||
[{ a: { b: undefined, c: [null] } }, []], // No 'field' values found
|
||||
[{ a: { b: undefined, c: [null, undefined, 1] } }, [[['a', 'c'], 1]]], // Only `1` is a non-null value. It is under `a.c` because we ignore array indices
|
||||
[
|
||||
{ a: { b: undefined, c: [null, undefined, 1, { d: ['e'] }] } },
|
||||
[
|
||||
// 1 and 'e' are valid fields.
|
||||
[['a', 'c'], 1],
|
||||
[['a', 'c', 'd'], 'e'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
describe.each(valuesAndExpected)('when passed %j', (value, expected) => {
|
||||
it(`should return ${JSON.stringify(expected)}`, () => {
|
||||
expect(deepObjectEntries(value)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Sort of like object entries, but does a DFS of an object.
|
||||
* Instead of getting a key, an array of keys is returned.
|
||||
* The array of keys represents the path to the value.
|
||||
* `undefined` and `null` values are omitted.
|
||||
*/
|
||||
export function deepObjectEntries(root: object): Array<[path: string[], value: unknown]> {
|
||||
const queue: Array<{ path: string[]; value: unknown }> = [{ path: [], value: root }];
|
||||
const result: Array<[path: string[], value: unknown]> = [];
|
||||
while (queue.length) {
|
||||
const next = queue.shift();
|
||||
if (next === undefined) {
|
||||
// this should be impossible
|
||||
throw new Error();
|
||||
}
|
||||
const { path, value } = next;
|
||||
if (Array.isArray(value)) {
|
||||
// branch on arrays
|
||||
queue.push(
|
||||
...value.map((element) => ({
|
||||
path: [...path], // unlike with object paths, don't add the number indices to `path`
|
||||
value: element,
|
||||
}))
|
||||
);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// branch on non-null objects
|
||||
queue.push(
|
||||
...Object.keys(value).map((key) => ({
|
||||
path: [...path, key],
|
||||
value: (value as Record<string, unknown>)[key],
|
||||
}))
|
||||
);
|
||||
} else if (value !== undefined && value !== null) {
|
||||
// emit other non-null, defined, values
|
||||
result.push([path, value]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import { DescriptiveName } from './descriptive_name';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
|
||||
describe('DescriptiveName', () => {
|
||||
let generator: EndpointDocGenerator;
|
||||
let wrapper: (event: SafeResolverEvent) => ReactWrapper;
|
||||
beforeEach(() => {
|
||||
generator = new EndpointDocGenerator('seed');
|
||||
wrapper = (event: SafeResolverEvent) =>
|
||||
mount(
|
||||
<I18nProvider>
|
||||
<DescriptiveName event={event} />
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
it('returns the right name for a registry event', () => {
|
||||
const extensions = { registry: { key: `HKLM/Windows/Software/abc` } };
|
||||
const event = generator.generateEvent({ eventCategory: 'registry', extensions });
|
||||
expect(wrapper(event).text()).toEqual(`HKLM/Windows/Software/abc`);
|
||||
});
|
||||
|
||||
it('returns the right name for a network event', () => {
|
||||
const randomIP = `${generator.randomIP()}`;
|
||||
const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } };
|
||||
const event = generator.generateEvent({ eventCategory: 'network', extensions });
|
||||
expect(wrapper(event).text()).toEqual(`outbound ${randomIP}`);
|
||||
});
|
||||
|
||||
it('returns the right name for a file event', () => {
|
||||
const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } };
|
||||
const event = generator.generateEvent({ eventCategory: 'file', extensions });
|
||||
expect(wrapper(event).text()).toEqual('C:\\My Documents\\business\\January\\processName');
|
||||
});
|
||||
|
||||
it('returns the right name for a dns event', () => {
|
||||
const extensions = { dns: { question: { name: `${generator.randomIP()}` } } };
|
||||
const event = generator.generateEvent({ eventCategory: 'dns', extensions });
|
||||
expect(wrapper(event).text()).toEqual(extensions.dns.question.name);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
isLegacyEventSafeVersion,
|
||||
processNameSafeVersion,
|
||||
entityIDSafeVersion,
|
||||
} from '../../../../common/endpoint/models/event';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
|
||||
/**
|
||||
* Based on the ECS category of the event, attempt to provide a more descriptive name
|
||||
* (e.g. the `event.registry.key` for `registry` or the `dns.question.name` for `dns`, etc.).
|
||||
* This function returns the data in the form of `{subject, descriptor}` where `subject` will
|
||||
* tend to be the more distinctive term (e.g. 137.213.212.7 for a network event) and the
|
||||
* `descriptor` can be used to present more useful/meaningful view (e.g. `inbound 137.213.212.7`
|
||||
* in the example above).
|
||||
* see: https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html
|
||||
* @param event The ResolverEvent to get the descriptive name for
|
||||
*/
|
||||
export function DescriptiveName({ event }: { event: SafeResolverEvent }) {
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.legacyEventLabel"
|
||||
defaultMessage="{ processName }"
|
||||
values={{ processName: processNameSafeVersion(event) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This list of attempts can be expanded/adjusted as the underlying model changes over time:
|
||||
*/
|
||||
|
||||
// Stable, per ECS 1.5: https://www.elastic.co/guide/en/ecs/current/ecs-allowed-values-event-category.html
|
||||
|
||||
if (event.network?.forwarded_ip) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.networkEventLabel"
|
||||
defaultMessage="{ networkDirection } { forwardedIP }"
|
||||
values={{
|
||||
forwardedIP: String(event.network?.forwarded_ip),
|
||||
networkDirection: String(event.network?.direction),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.file?.path) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.fileEventLabel"
|
||||
defaultMessage="{ filePath }"
|
||||
values={{
|
||||
filePath: String(event.file?.path),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.registry?.path) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.registryPathLabel"
|
||||
defaultMessage="{ registryPath }"
|
||||
values={{
|
||||
registryPath: String(event.registry?.path),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.registry?.key) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.registryKeyLabel"
|
||||
defaultMessage="{ registryKey }"
|
||||
values={{
|
||||
registryKey: String(event.registry?.key),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.dns?.question?.name) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.dnsQuestionNameLabel"
|
||||
defaultMessage="{ dnsQuestionName }"
|
||||
values={{
|
||||
dnsQuestionName: String(event.dns?.question?.name),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.entityIDLabel"
|
||||
defaultMessage="{ entityID }"
|
||||
values={{
|
||||
entityID: entityIDSafeVersion(event),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -4,23 +4,269 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo, useEffect, Fragment } from 'react';
|
||||
/* eslint-disable no-continue */
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React, { memo, useMemo, Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { StyledPanel } from '../styles';
|
||||
import { StyledBreadcrumbs, BoldCode, StyledTime, GeneratedText } from './panel_content_utilities';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { BoldCode, StyledTime, GeneratedText, formatDate } from './panel_content_utilities';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { useResolverDispatch } from '../use_resolver_dispatch';
|
||||
import { PanelContentError } from './panel_content_error';
|
||||
import { PanelLoading } from './panel_loading';
|
||||
import { ResolverState } from '../../types';
|
||||
import { useNavigateOrReplace } from '../use_navigate_or_replace';
|
||||
import { DescriptiveName } from './descriptive_name';
|
||||
import { useLinkProps } from '../use_link_props';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { deepObjectEntries } from './deep_object_entries';
|
||||
|
||||
export const EventDetail = memo(function EventDetail({
|
||||
nodeID,
|
||||
eventID,
|
||||
eventType,
|
||||
}: {
|
||||
nodeID: string;
|
||||
eventID: string;
|
||||
/** The event type to show in the breadcrumbs */
|
||||
eventType: string;
|
||||
}) {
|
||||
const event = useSelector((state: ResolverState) =>
|
||||
selectors.eventByID(state)({ nodeID, eventID })
|
||||
);
|
||||
const processEvent = useSelector((state: ResolverState) =>
|
||||
selectors.processEventForID(state)(nodeID)
|
||||
);
|
||||
if (event && processEvent) {
|
||||
return (
|
||||
<EventDetailContents
|
||||
nodeID={nodeID}
|
||||
event={event}
|
||||
processEvent={processEvent}
|
||||
eventType={eventType}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<StyledPanel>
|
||||
<PanelLoading />
|
||||
</StyledPanel>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This view presents a detailed view of all the available data for a related event, split and titled by the "section"
|
||||
* it appears in the underlying ResolverEvent
|
||||
*/
|
||||
const EventDetailContents = memo(function ({
|
||||
nodeID,
|
||||
event,
|
||||
eventType,
|
||||
processEvent,
|
||||
}: {
|
||||
nodeID: string;
|
||||
event: SafeResolverEvent;
|
||||
/**
|
||||
* Event type to use in the breadcrumbs
|
||||
*/
|
||||
eventType: string;
|
||||
processEvent: SafeResolverEvent;
|
||||
}) {
|
||||
const formattedDate = useMemo(() => {
|
||||
const timestamp = eventModel.timestampSafeVersion(event);
|
||||
if (timestamp !== undefined) {
|
||||
return formatDate(new Date(timestamp));
|
||||
}
|
||||
}, [event]);
|
||||
|
||||
return (
|
||||
<StyledPanel>
|
||||
<EventDetailBreadcrumbs
|
||||
nodeID={nodeID}
|
||||
nodeName={eventModel.processNameSafeVersion(processEvent)}
|
||||
event={event}
|
||||
breadcrumbEventCategory={eventType}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText size="s">
|
||||
<BoldCode>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType"
|
||||
values={{
|
||||
category: eventType,
|
||||
eventType: String(eventModel.eventType(event)),
|
||||
}}
|
||||
defaultMessage="{category} {eventType}"
|
||||
/>
|
||||
</BoldCode>
|
||||
<StyledTime dateTime={formattedDate}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime"
|
||||
values={{ date: formattedDate }}
|
||||
defaultMessage="@ {date}"
|
||||
/>
|
||||
</StyledTime>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<StyledDescriptiveName>
|
||||
<GeneratedText>
|
||||
<DescriptiveName event={event} />
|
||||
</GeneratedText>
|
||||
</StyledDescriptiveName>
|
||||
<EuiSpacer size="l" />
|
||||
<EventDetailFields event={event} />
|
||||
</StyledPanel>
|
||||
);
|
||||
});
|
||||
|
||||
function EventDetailFields({ event }: { event: SafeResolverEvent }) {
|
||||
const sections = useMemo(() => {
|
||||
const returnValue: Array<{
|
||||
namespace: React.ReactNode;
|
||||
descriptions: Array<{ title: React.ReactNode; description: React.ReactNode }>;
|
||||
}> = [];
|
||||
for (const [key, value] of Object.entries(event)) {
|
||||
// ignore these keys
|
||||
if (key === 'agent' || key === 'ecs' || key === 'process' || key === '@timestamp') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const section = {
|
||||
// Group the fields by their top-level namespace
|
||||
namespace: <GeneratedText>{key}</GeneratedText>,
|
||||
descriptions: deepObjectEntries(value).map(([path, fieldValue]) => ({
|
||||
title: <GeneratedText>{path.join('.')}</GeneratedText>,
|
||||
description: <GeneratedText>{String(fieldValue)}</GeneratedText>,
|
||||
})),
|
||||
};
|
||||
returnValue.push(section);
|
||||
}
|
||||
return returnValue;
|
||||
}, [event]);
|
||||
return (
|
||||
<>
|
||||
{sections.map(({ namespace, descriptions }, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{index === 0 ? null : <EuiSpacer size="m" />}
|
||||
<EuiTitle size="xxxs">
|
||||
<EuiTextColor color="subdued">
|
||||
<StyledFlexTitle>
|
||||
{namespace}
|
||||
<TitleHr />
|
||||
</StyledFlexTitle>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<StyledDescriptionList
|
||||
type="column"
|
||||
align="left"
|
||||
titleProps={{ className: 'desc-title' }}
|
||||
compressed
|
||||
listItems={descriptions}
|
||||
/>
|
||||
{index === sections.length - 1 ? null : <EuiSpacer size="m" />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EventDetailBreadcrumbs({
|
||||
nodeID,
|
||||
nodeName,
|
||||
event,
|
||||
breadcrumbEventCategory,
|
||||
}: {
|
||||
nodeID: string;
|
||||
nodeName?: string;
|
||||
event: SafeResolverEvent;
|
||||
breadcrumbEventCategory: string;
|
||||
}) {
|
||||
const countByCategory = useSelector((state: ResolverState) =>
|
||||
selectors.relatedEventCountByType(state)(nodeID, breadcrumbEventCategory)
|
||||
);
|
||||
const relatedEventCount: number | undefined = useSelector((state: ResolverState) =>
|
||||
selectors.relatedEventTotalCount(state)(nodeID)
|
||||
);
|
||||
const nodesLinkNavProps = useLinkProps({
|
||||
panelView: 'nodes',
|
||||
});
|
||||
|
||||
const nodeDetailLinkNavProps = useLinkProps({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID },
|
||||
});
|
||||
|
||||
const nodeEventsLinkNavProps = useLinkProps({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID },
|
||||
});
|
||||
|
||||
const nodeEventsOfTypeLinkNavProps = useLinkProps({
|
||||
panelView: 'nodeEventsOfType',
|
||||
panelParameters: { nodeID, eventType: breadcrumbEventCategory },
|
||||
});
|
||||
const breadcrumbs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events',
|
||||
{
|
||||
defaultMessage: 'Events',
|
||||
}
|
||||
),
|
||||
...nodesLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: nodeName,
|
||||
...nodeDetailLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents"
|
||||
values={{ totalCount: relatedEventCount }}
|
||||
defaultMessage="{totalCount} Events"
|
||||
/>
|
||||
),
|
||||
...nodeEventsLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory"
|
||||
values={{ count: countByCategory, category: breadcrumbEventCategory }}
|
||||
defaultMessage="{count} {category}"
|
||||
/>
|
||||
),
|
||||
...nodeEventsOfTypeLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: <DescriptiveName event={event} />,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
breadcrumbEventCategory,
|
||||
countByCategory,
|
||||
event,
|
||||
nodeDetailLinkNavProps,
|
||||
nodeEventsLinkNavProps,
|
||||
nodeName,
|
||||
relatedEventCount,
|
||||
nodesLinkNavProps,
|
||||
nodeEventsOfTypeLinkNavProps,
|
||||
]);
|
||||
return <Breadcrumbs breadcrumbs={breadcrumbs} />;
|
||||
}
|
||||
|
||||
// Adding some styles to prevent horizontal scrollbars, per request from UX review
|
||||
const StyledDescriptionList = memo(styled(EuiDescriptionList)`
|
||||
&.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title {
|
||||
max-width: 8em;
|
||||
|
@ -38,7 +284,6 @@ const StyledDescriptiveName = memo(styled(EuiText)`
|
|||
overflow-wrap: break-word;
|
||||
`);
|
||||
|
||||
// Styling subtitles, per UX review:
|
||||
const StyledFlexTitle = memo(styled('h3')`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
|
@ -57,270 +302,3 @@ const TitleHr = memo(() => {
|
|||
<StyledTitleRule className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall override" />
|
||||
);
|
||||
});
|
||||
TitleHr.displayName = 'TitleHR';
|
||||
|
||||
/**
|
||||
* Take description list entries and prepare them for display by
|
||||
* seeding with `<wbr />` tags.
|
||||
*
|
||||
* @param entries {title: string, description: string}[]
|
||||
*/
|
||||
function entriesForDisplay(entries: Array<{ title: string; description: string }>) {
|
||||
return entries.map((entry) => {
|
||||
return {
|
||||
description: <GeneratedText>{entry.description}</GeneratedText>,
|
||||
title: <GeneratedText>{entry.title}</GeneratedText>,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This view presents a detailed view of all the available data for a related event, split and titled by the "section"
|
||||
* it appears in the underlying ResolverEvent
|
||||
*/
|
||||
export const EventDetail = memo(function ({
|
||||
nodeID,
|
||||
eventID,
|
||||
}: {
|
||||
nodeID: string;
|
||||
eventID: string;
|
||||
}) {
|
||||
const parentEvent = useSelector((state: ResolverState) =>
|
||||
selectors.processEventForID(state)(nodeID)
|
||||
);
|
||||
|
||||
const relatedEventsStats = useSelector((state: ResolverState) =>
|
||||
selectors.relatedEventsStats(state)(nodeID)
|
||||
);
|
||||
const countForParent: number = Object.values(relatedEventsStats?.events.byCategory || {}).reduce(
|
||||
(sum, val) => sum + val,
|
||||
0
|
||||
);
|
||||
const processName = (parentEvent && event.eventName(parentEvent)) || '*';
|
||||
const processEntityId = (parentEvent && event.entityId(parentEvent)) || '';
|
||||
const totalCount = countForParent || 0;
|
||||
const eventsString = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events',
|
||||
{
|
||||
defaultMessage: 'Events',
|
||||
}
|
||||
);
|
||||
const naString = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA',
|
||||
{
|
||||
defaultMessage: 'N/A',
|
||||
}
|
||||
);
|
||||
|
||||
const relatedsReadyMap = useSelector(selectors.relatedEventsReady);
|
||||
const relatedsReady = relatedsReadyMap.get(processEntityId!);
|
||||
const dispatch = useResolverDispatch();
|
||||
const nodesHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({ panelView: 'nodes' })
|
||||
);
|
||||
const nodesLinkNavProps = useNavigateOrReplace({
|
||||
search: nodesHref,
|
||||
});
|
||||
|
||||
/**
|
||||
* If we don't have the related events for the parent yet, use this effect
|
||||
* to request them.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof relatedsReady === 'undefined' &&
|
||||
processEntityId !== null &&
|
||||
processEntityId !== undefined
|
||||
) {
|
||||
dispatch({
|
||||
type: 'appDetectedMissingEventData',
|
||||
payload: processEntityId,
|
||||
});
|
||||
}
|
||||
}, [relatedsReady, dispatch, processEntityId]);
|
||||
|
||||
const [
|
||||
relatedEventToShowDetailsFor,
|
||||
countBySameCategory,
|
||||
relatedEventCategory = naString,
|
||||
sections,
|
||||
formattedDate,
|
||||
] = useSelector((state: ResolverState) =>
|
||||
selectors.relatedEventDisplayInfoByEntityAndSelfId(state)(nodeID, eventID)
|
||||
);
|
||||
|
||||
const { subject = '', descriptor = '' } = relatedEventToShowDetailsFor
|
||||
? event.descriptiveName(relatedEventToShowDetailsFor)
|
||||
: {};
|
||||
|
||||
const nodeDetailHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID: processEntityId },
|
||||
})
|
||||
);
|
||||
const nodeDetailLinkNavProps = useNavigateOrReplace({
|
||||
search: nodeDetailHref,
|
||||
});
|
||||
|
||||
const nodeEventsHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID: processEntityId },
|
||||
})
|
||||
);
|
||||
const nodeEventsLinkNavProps = useNavigateOrReplace({
|
||||
search: nodeEventsHref,
|
||||
});
|
||||
|
||||
const nodeEventsOfTypeHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeEventsOfType',
|
||||
panelParameters: { nodeID: processEntityId, eventType: relatedEventCategory },
|
||||
})
|
||||
);
|
||||
const nodeEventsOfTypeLinkNavProps = useNavigateOrReplace({
|
||||
search: nodeEventsOfTypeHref,
|
||||
});
|
||||
const crumbs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
text: eventsString,
|
||||
...nodesLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: processName,
|
||||
...nodeDetailLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents"
|
||||
values={{ totalCount }}
|
||||
defaultMessage="{totalCount} Events"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
...nodeEventsLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory"
|
||||
values={{ count: countBySameCategory, category: relatedEventCategory }}
|
||||
defaultMessage="{count} {category}"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
...nodeEventsOfTypeLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: relatedEventToShowDetailsFor ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveName"
|
||||
values={{ subject, descriptor }}
|
||||
defaultMessage="{descriptor} {subject}"
|
||||
/>
|
||||
) : (
|
||||
naString
|
||||
),
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
}, [
|
||||
processName,
|
||||
eventsString,
|
||||
totalCount,
|
||||
countBySameCategory,
|
||||
naString,
|
||||
relatedEventCategory,
|
||||
relatedEventToShowDetailsFor,
|
||||
subject,
|
||||
descriptor,
|
||||
nodeEventsOfTypeLinkNavProps,
|
||||
nodeEventsLinkNavProps,
|
||||
nodeDetailLinkNavProps,
|
||||
nodesLinkNavProps,
|
||||
]);
|
||||
|
||||
if (!relatedsReady) {
|
||||
return <PanelLoading />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Could happen if user e.g. loads a URL with a bad crumbEvent
|
||||
*/
|
||||
if (!relatedEventToShowDetailsFor) {
|
||||
const errString = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing',
|
||||
{
|
||||
defaultMessage: 'Related event not found.',
|
||||
}
|
||||
);
|
||||
return <PanelContentError translatedErrorMessage={errString} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPanel>
|
||||
<StyledBreadcrumbs breadcrumbs={crumbs} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText size="s">
|
||||
<BoldCode>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType"
|
||||
values={{
|
||||
category: relatedEventCategory,
|
||||
eventType: String(event.ecsEventType(relatedEventToShowDetailsFor)),
|
||||
}}
|
||||
defaultMessage="{category} {eventType}"
|
||||
/>
|
||||
</BoldCode>
|
||||
<StyledTime dateTime={formattedDate}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime"
|
||||
values={{ date: formattedDate }}
|
||||
defaultMessage="@ {date}"
|
||||
/>
|
||||
</StyledTime>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<StyledDescriptiveName>
|
||||
<GeneratedText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle"
|
||||
values={{ subject, descriptor }}
|
||||
defaultMessage="{descriptor} {subject}"
|
||||
/>
|
||||
</GeneratedText>
|
||||
</StyledDescriptiveName>
|
||||
<EuiSpacer size="l" />
|
||||
{sections.map(({ sectionTitle, entries }, index) => {
|
||||
const displayEntries = entriesForDisplay(entries);
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{index === 0 ? null : <EuiSpacer size="m" />}
|
||||
<EuiTitle size="xxxs">
|
||||
<EuiTextColor color="subdued">
|
||||
<StyledFlexTitle>
|
||||
{sectionTitle}
|
||||
<TitleHr />
|
||||
</StyledFlexTitle>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<StyledDescriptionList
|
||||
type="column"
|
||||
align="left"
|
||||
titleProps={{ className: 'desc-title' }}
|
||||
compressed
|
||||
listItems={displayEntries}
|
||||
/>
|
||||
{index === sections.length - 1 ? null : <EuiSpacer size="m" />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</StyledPanel>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -11,16 +11,13 @@ import { useSelector } from 'react-redux';
|
|||
import * as selectors from '../../store/selectors';
|
||||
import { NodeEventsOfType } from './node_events_of_type';
|
||||
import { NodeEvents } from './node_events';
|
||||
import { NodeDetail } from './node_details';
|
||||
import { NodeDetail } from './node_detail';
|
||||
import { NodeList } from './node_list';
|
||||
import { EventDetail } from './event_detail';
|
||||
import { PanelViewAndParameters } from '../../types';
|
||||
|
||||
/**
|
||||
*
|
||||
* This component implements the strategy laid out above by determining the "right" view and doing some other housekeeping e.g. effects to keep the UI-selected node in line with what's indicated by the URL parameters.
|
||||
*
|
||||
* @returns {JSX.Element} The "right" table content to show based on the query params as described above
|
||||
* Show the panel that matches the `panelViewAndParameters` (derived from the browser's location.search)
|
||||
*/
|
||||
export const PanelRouter = memo(function () {
|
||||
const params: PanelViewAndParameters = useSelector(selectors.panelViewAndParameters);
|
||||
|
@ -40,6 +37,7 @@ export const PanelRouter = memo(function () {
|
|||
<EventDetail
|
||||
nodeID={params.panelParameters.nodeID}
|
||||
eventID={params.panelParameters.eventID}
|
||||
eventType={params.panelParameters.eventType}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -15,23 +15,17 @@ import styled from 'styled-components';
|
|||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import { StyledDescriptionList, StyledTitle } from './styles';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { formatDate, StyledBreadcrumbs, GeneratedText } from './panel_content_utilities';
|
||||
import {
|
||||
processPath,
|
||||
processPid,
|
||||
userInfoForProcess,
|
||||
processParentPid,
|
||||
md5HashForProcess,
|
||||
argsForProcess,
|
||||
} from '../../models/process_event';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import { formatDate, GeneratedText } from './panel_content_utilities';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
import { processPath, processPID } from '../../models/process_event';
|
||||
import { CubeForProcess } from './cube_for_process';
|
||||
import { ResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { useCubeAssets } from '../use_cube_assets';
|
||||
import { ResolverState } from '../../types';
|
||||
import { PanelLoading } from './panel_loading';
|
||||
import { StyledPanel } from '../styles';
|
||||
import { useNavigateOrReplace } from '../use_navigate_or_replace';
|
||||
import { useLinkProps } from '../use_link_props';
|
||||
|
||||
const StyledCubeForProcess = styled(CubeForProcess)`
|
||||
position: relative;
|
||||
|
@ -44,7 +38,11 @@ export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) {
|
|||
);
|
||||
return (
|
||||
<StyledPanel>
|
||||
{processEvent === null ? <PanelLoading /> : <NodeDetailView processEvent={processEvent} />}
|
||||
{processEvent === null ? (
|
||||
<PanelLoading />
|
||||
) : (
|
||||
<NodeDetailView nodeID={nodeID} processEvent={processEvent} />
|
||||
)}
|
||||
</StyledPanel>
|
||||
);
|
||||
});
|
||||
|
@ -53,21 +51,22 @@ export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) {
|
|||
* A description list view of all the Metadata that goes with a particular process event, like:
|
||||
* Created, PID, User/Domain, etc.
|
||||
*/
|
||||
const NodeDetailView = memo(function NodeDetailView({
|
||||
const NodeDetailView = memo(function ({
|
||||
processEvent,
|
||||
nodeID,
|
||||
}: {
|
||||
processEvent: ResolverEvent;
|
||||
processEvent: SafeResolverEvent;
|
||||
nodeID: string;
|
||||
}) {
|
||||
const processName = event.eventName(processEvent);
|
||||
const entityId = event.entityId(processEvent);
|
||||
const processName = eventModel.processNameSafeVersion(processEvent);
|
||||
const isProcessTerminated = useSelector((state: ResolverState) =>
|
||||
selectors.isProcessTerminated(state)(entityId)
|
||||
selectors.isProcessTerminated(state)(nodeID)
|
||||
);
|
||||
const relatedEventTotal = useSelector((state: ResolverState) => {
|
||||
return selectors.relatedEventAggregateTotalByEntityId(state)(entityId);
|
||||
return selectors.relatedEventTotalCount(state)(nodeID);
|
||||
});
|
||||
const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => {
|
||||
const eventTime = event.eventTimestamp(processEvent);
|
||||
const eventTime = eventModel.eventTimestamp(processEvent);
|
||||
const dateTime = eventTime === undefined ? null : formatDate(eventTime);
|
||||
|
||||
const createdEntry = {
|
||||
|
@ -82,32 +81,32 @@ const NodeDetailView = memo(function NodeDetailView({
|
|||
|
||||
const pidEntry = {
|
||||
title: 'process.pid',
|
||||
description: processPid(processEvent),
|
||||
description: processPID(processEvent),
|
||||
};
|
||||
|
||||
const userEntry = {
|
||||
title: 'user.name',
|
||||
description: userInfoForProcess(processEvent)?.name,
|
||||
description: eventModel.userName(processEvent),
|
||||
};
|
||||
|
||||
const domainEntry = {
|
||||
title: 'user.domain',
|
||||
description: userInfoForProcess(processEvent)?.domain,
|
||||
description: eventModel.userDomain(processEvent),
|
||||
};
|
||||
|
||||
const parentPidEntry = {
|
||||
title: 'process.parent.pid',
|
||||
description: processParentPid(processEvent),
|
||||
description: eventModel.parentPID(processEvent),
|
||||
};
|
||||
|
||||
const md5Entry = {
|
||||
title: 'process.hash.md5',
|
||||
description: md5HashForProcess(processEvent),
|
||||
description: eventModel.md5HashForProcess(processEvent),
|
||||
};
|
||||
|
||||
const commandLineEntry = {
|
||||
title: 'process.args',
|
||||
description: argsForProcess(processEvent),
|
||||
description: eventModel.argsForProcess(processEvent),
|
||||
};
|
||||
|
||||
// This is the data in {title, description} form for the EuiDescriptionList to display
|
||||
|
@ -134,12 +133,8 @@ const NodeDetailView = memo(function NodeDetailView({
|
|||
return processDescriptionListData;
|
||||
}, [processEvent]);
|
||||
|
||||
const nodesHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({ panelView: 'nodes' })
|
||||
);
|
||||
|
||||
const nodesLinkNavProps = useNavigateOrReplace({
|
||||
search: nodesHref,
|
||||
const nodesLinkNavProps = useLinkProps({
|
||||
panelView: 'nodes',
|
||||
});
|
||||
|
||||
const crumbs = useMemo(() => {
|
||||
|
@ -162,27 +157,20 @@ const NodeDetailView = memo(function NodeDetailView({
|
|||
defaultMessage="Details for: {processName}"
|
||||
/>
|
||||
),
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
}, [processName, nodesLinkNavProps]);
|
||||
const { descriptionText } = useCubeAssets(isProcessTerminated, false);
|
||||
|
||||
const nodeDetailHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID: entityId },
|
||||
})
|
||||
);
|
||||
|
||||
const nodeDetailNavProps = useNavigateOrReplace({
|
||||
search: nodeDetailHref!,
|
||||
const nodeDetailNavProps = useLinkProps({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID },
|
||||
});
|
||||
|
||||
const titleID = useMemo(() => htmlIdGenerator('resolverTable')(), []);
|
||||
return (
|
||||
<>
|
||||
<StyledBreadcrumbs breadcrumbs={crumbs} />
|
||||
<Breadcrumbs breadcrumbs={crumbs} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xs">
|
||||
<StyledTitle aria-describedby={titleID}>
|
||||
|
@ -201,7 +189,7 @@ const NodeDetailView = memo(function NodeDetailView({
|
|||
</EuiTextColor>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink {...nodeDetailNavProps}>
|
||||
<EuiLink {...nodeDetailNavProps} data-test-subj="resolver:node-detail:node-events-link">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.processDescList.numberOfEvents"
|
||||
values={{ relatedEventTotal }}
|
|
@ -4,19 +4,21 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTableColumn, EuiButtonEmpty, EuiSpacer, EuiInMemoryTable } from '@elastic/eui';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
|
||||
import { ResolverNodeStats } from '../../../../common/endpoint/types';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { ResolverState } from '../../types';
|
||||
import { StyledPanel } from '../styles';
|
||||
import { useNavigateOrReplace } from '../use_navigate_or_replace';
|
||||
import { PanelLoading } from './panel_loading';
|
||||
import { useLinkProps } from '../use_link_props';
|
||||
|
||||
export function NodeEvents({ nodeID }: { nodeID: string }) {
|
||||
const processEvent = useSelector((state: ResolverState) =>
|
||||
|
@ -26,11 +28,21 @@ export function NodeEvents({ nodeID }: { nodeID: string }) {
|
|||
selectors.relatedEventsStats(state)(nodeID)
|
||||
);
|
||||
if (processEvent === null || relatedEventsStats === undefined) {
|
||||
return <PanelLoading />;
|
||||
return (
|
||||
<StyledPanel>
|
||||
<PanelLoading />
|
||||
</StyledPanel>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<StyledPanel>
|
||||
<EventCountsForProcess processEvent={processEvent} relatedStats={relatedEventsStats} />
|
||||
<NodeEventsBreadcrumbs
|
||||
nodeName={event.processNameSafeVersion(processEvent)}
|
||||
nodeID={nodeID}
|
||||
totalEventCount={relatedEventsStats.events.total}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EventCategoryLinks nodeID={nodeID} relatedStats={relatedEventsStats} />
|
||||
</StyledPanel>
|
||||
);
|
||||
}
|
||||
|
@ -47,120 +59,29 @@ export function NodeEvents({ nodeID }: { nodeID: string }) {
|
|||
* | 2 | Network |
|
||||
*
|
||||
*/
|
||||
const EventCountsForProcess = memo(function EventCountsForProcess({
|
||||
processEvent,
|
||||
const EventCategoryLinks = memo(function ({
|
||||
nodeID,
|
||||
relatedStats,
|
||||
}: {
|
||||
processEvent: ResolverEvent;
|
||||
nodeID: string;
|
||||
relatedStats: ResolverNodeStats;
|
||||
}) {
|
||||
interface EventCountsTableView {
|
||||
name: string;
|
||||
eventType: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const relatedEventsState = { stats: relatedStats.events.byCategory };
|
||||
const processName = processEvent && event.eventName(processEvent);
|
||||
const processEntityId = event.entityId(processEvent);
|
||||
/**
|
||||
* totalCount: This will reflect the aggregated total by category for all related events
|
||||
* e.g. [dns,file],[dns,file],[registry] will have an aggregate total of 5. This is to keep the
|
||||
* total number consistent with the "broken out" totals we see elsewhere in the app.
|
||||
* E.g. on the rleated list by type, the above would show as:
|
||||
* 2 dns
|
||||
* 2 file
|
||||
* 1 registry
|
||||
* So it would be extremely disorienting to show the user a "3" above that as a total.
|
||||
*/
|
||||
const totalCount = Object.values(relatedStats.events.byCategory).reduce(
|
||||
(sum, val) => sum + val,
|
||||
0
|
||||
);
|
||||
const eventsString = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events',
|
||||
{
|
||||
defaultMessage: 'Events',
|
||||
}
|
||||
);
|
||||
const eventsHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({ panelView: 'nodes' })
|
||||
);
|
||||
|
||||
const eventLinkNavProps = useNavigateOrReplace({
|
||||
search: eventsHref,
|
||||
});
|
||||
|
||||
const processDetailHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID: processEntityId },
|
||||
})
|
||||
);
|
||||
|
||||
const processDetailNavProps = useNavigateOrReplace({
|
||||
search: processDetailHref,
|
||||
});
|
||||
|
||||
const nodeDetailHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID: processEntityId },
|
||||
})
|
||||
);
|
||||
|
||||
const nodeDetailNavProps = useNavigateOrReplace({
|
||||
search: nodeDetailHref!,
|
||||
});
|
||||
const crumbs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
text: eventsString,
|
||||
...eventLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: processName,
|
||||
...processDetailNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb"
|
||||
values={{ totalCount }}
|
||||
defaultMessage="{totalCount} Events"
|
||||
/>
|
||||
),
|
||||
...nodeDetailNavProps,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
processName,
|
||||
totalCount,
|
||||
eventsString,
|
||||
eventLinkNavProps,
|
||||
nodeDetailNavProps,
|
||||
processDetailNavProps,
|
||||
]);
|
||||
const rows = useMemo(() => {
|
||||
return Object.entries(relatedEventsState.stats).map(
|
||||
return Object.entries(relatedStats.events.byCategory).map(
|
||||
([eventType, count]): EventCountsTableView => {
|
||||
return {
|
||||
name: eventType,
|
||||
eventType,
|
||||
count,
|
||||
};
|
||||
}
|
||||
);
|
||||
}, [relatedEventsState]);
|
||||
}, [relatedStats.events.byCategory]);
|
||||
|
||||
const eventDetailHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'eventDetail',
|
||||
panelParameters: { nodeID: processEntityId, eventType: name, eventID: processEntityId },
|
||||
})
|
||||
);
|
||||
|
||||
const eventDetailNavProps = useNavigateOrReplace({
|
||||
search: eventDetailHref,
|
||||
});
|
||||
const columns = useMemo<Array<EuiBasicTableColumn<EventCountsTableView>>>(
|
||||
() => [
|
||||
{
|
||||
|
@ -168,29 +89,100 @@ const EventCountsForProcess = memo(function EventCountsForProcess({
|
|||
name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.count', {
|
||||
defaultMessage: 'Count',
|
||||
}),
|
||||
'data-test-subj': 'resolver:panel:node-events:event-type-count',
|
||||
width: '20%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
field: 'eventType',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.eventType', {
|
||||
defaultMessage: 'Event Type',
|
||||
}),
|
||||
width: '80%',
|
||||
sortable: true,
|
||||
render(name: string) {
|
||||
return <EuiButtonEmpty {...eventDetailNavProps}>{name}</EuiButtonEmpty>;
|
||||
render(eventType: string) {
|
||||
return (
|
||||
<NodeEventsLink nodeID={nodeID} eventType={eventType}>
|
||||
{eventType}
|
||||
</NodeEventsLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[eventDetailNavProps]
|
||||
[nodeID]
|
||||
);
|
||||
return <EuiInMemoryTable<EventCountsTableView> items={rows} columns={columns} sorting />;
|
||||
});
|
||||
|
||||
const NodeEventsBreadcrumbs = memo(function ({
|
||||
nodeID,
|
||||
nodeName,
|
||||
totalEventCount,
|
||||
}: {
|
||||
nodeID: string;
|
||||
nodeName: React.ReactNode;
|
||||
totalEventCount: number;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<StyledBreadcrumbs breadcrumbs={crumbs} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiInMemoryTable<EventCountsTableView> items={rows} columns={columns} sorting />
|
||||
</>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events',
|
||||
{
|
||||
defaultMessage: 'Events',
|
||||
}
|
||||
),
|
||||
...useLinkProps({
|
||||
panelView: 'nodes',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: nodeName,
|
||||
...useLinkProps({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID },
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb"
|
||||
values={{ totalCount: totalEventCount }}
|
||||
defaultMessage="{totalCount} Events"
|
||||
/>
|
||||
),
|
||||
...useLinkProps({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID },
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
EventCountsForProcess.displayName = 'EventCountsForProcess';
|
||||
|
||||
const NodeEventsLink = memo(
|
||||
({
|
||||
nodeID,
|
||||
eventType,
|
||||
children,
|
||||
}: {
|
||||
nodeID: string;
|
||||
eventType: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const props = useLinkProps({
|
||||
panelView: 'nodeEventsOfType',
|
||||
panelParameters: {
|
||||
nodeID,
|
||||
eventType,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<EuiButtonEmpty data-test-subj="resolver:panel:node-events:event-type-link" {...props}>
|
||||
{children}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,297 +4,225 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React, { memo, useMemo, useEffect, Fragment } from 'react';
|
||||
import React, { memo, useCallback, Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import { StyledPanel } from '../styles';
|
||||
import { formatDate, StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
|
||||
import { formatDate, BoldCode, StyledTime } from './panel_content_utilities';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { useResolverDispatch } from '../use_resolver_dispatch';
|
||||
import { RelatedEventLimitWarning } from '../limit_warnings';
|
||||
import { ResolverState } from '../../types';
|
||||
import { useNavigateOrReplace } from '../use_navigate_or_replace';
|
||||
import { useRelatedEventDetailNavigation } from '../use_related_event_detail_navigation';
|
||||
import { PanelLoading } from './panel_loading';
|
||||
import { DescriptiveName } from './descriptive_name';
|
||||
import { useLinkProps } from '../use_link_props';
|
||||
|
||||
/**
|
||||
* This view presents a list of related events of a given type for a given process.
|
||||
* It will appear like:
|
||||
*
|
||||
* | |
|
||||
* | :----------------------------------------------------- |
|
||||
* | **registry deletion** @ *3:32PM..* *HKLM/software...* |
|
||||
* | **file creation** @ *3:34PM..* *C:/directory/file.exe* |
|
||||
* Render a list of events that are related to `nodeID` and that have a category of `eventType`.
|
||||
*/
|
||||
|
||||
interface MatchingEventEntry {
|
||||
formattedDate: string;
|
||||
eventType: string;
|
||||
eventCategory: string;
|
||||
name: { subject: string; descriptor?: string };
|
||||
setQueryParams: () => void;
|
||||
}
|
||||
|
||||
const StyledRelatedLimitWarning = styled(RelatedEventLimitWarning)`
|
||||
flex-flow: row wrap;
|
||||
display: block;
|
||||
align-items: baseline;
|
||||
margin-top: 1em;
|
||||
|
||||
& .euiCallOutHeader {
|
||||
display: inline;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
& .euiText {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
& .euiText p {
|
||||
display: inline;
|
||||
}
|
||||
`;
|
||||
|
||||
const NodeCategoryEntries = memo(function ({
|
||||
crumbs,
|
||||
matchingEventEntries,
|
||||
export const NodeEventsOfType = memo(function NodeEventsOfType({
|
||||
nodeID,
|
||||
eventType,
|
||||
processEntityId,
|
||||
}: {
|
||||
crumbs: Array<{
|
||||
text: string | JSX.Element | null;
|
||||
onClick: (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement, MouseEvent>) => void;
|
||||
href?: string;
|
||||
}>;
|
||||
matchingEventEntries: MatchingEventEntry[];
|
||||
nodeID: string;
|
||||
eventType: string;
|
||||
processEntityId: string;
|
||||
}) {
|
||||
const relatedLookupsByCategory = useSelector(selectors.relatedEventInfoByEntityId);
|
||||
const lookupsForThisNode = relatedLookupsByCategory(processEntityId);
|
||||
const shouldShowLimitWarning = lookupsForThisNode?.shouldShowLimitForCategory(eventType);
|
||||
const numberDisplayed = lookupsForThisNode?.numberActuallyDisplayedForCategory(eventType);
|
||||
const numberMissing = lookupsForThisNode?.numberNotDisplayedForCategory(eventType);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledBreadcrumbs breadcrumbs={crumbs} />
|
||||
{shouldShowLimitWarning && typeof numberDisplayed !== 'undefined' && numberMissing ? (
|
||||
<StyledRelatedLimitWarning
|
||||
eventType={eventType}
|
||||
numberActuallyDisplayed={numberDisplayed}
|
||||
numberMissing={numberMissing}
|
||||
/>
|
||||
) : null}
|
||||
<EuiSpacer size="l" />
|
||||
<>
|
||||
{matchingEventEntries.map((eventView, index) => {
|
||||
const { subject, descriptor = '' } = eventView.name;
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<EuiText>
|
||||
<BoldCode>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType"
|
||||
values={{
|
||||
category: eventView.eventCategory,
|
||||
eventType: eventView.eventType,
|
||||
}}
|
||||
defaultMessage="{category} {eventType}"
|
||||
/>
|
||||
</BoldCode>
|
||||
<StyledTime dateTime={eventView.formattedDate}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime"
|
||||
values={{ date: eventView.formattedDate }}
|
||||
defaultMessage="@ {date}"
|
||||
/>
|
||||
</StyledTime>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiButtonEmpty onClick={eventView.setQueryParams}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName"
|
||||
values={{ subject, descriptor }}
|
||||
defaultMessage="{descriptor} {subject}"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
{index === matchingEventEntries.length - 1 ? null : <EuiHorizontalRule margin="m" />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export function NodeEventsOfType({ nodeID, eventType }: { nodeID: string; eventType: string }) {
|
||||
const processEvent = useSelector((state: ResolverState) =>
|
||||
selectors.processEventForID(state)(nodeID)
|
||||
);
|
||||
const relatedEventsStats = useSelector((state: ResolverState) =>
|
||||
selectors.relatedEventsStats(state)(nodeID)
|
||||
const eventCount = useSelector(
|
||||
(state: ResolverState) => selectors.relatedEventsStats(state)(nodeID)?.events.total
|
||||
);
|
||||
const eventsInCategoryCount = useSelector(
|
||||
(state: ResolverState) =>
|
||||
selectors.relatedEventsStats(state)(nodeID)?.events.byCategory[eventType]
|
||||
);
|
||||
const events = useSelector(
|
||||
useCallback(
|
||||
(state: ResolverState) => {
|
||||
return selectors.relatedEventsByCategory(state)(nodeID, eventType);
|
||||
},
|
||||
[eventType, nodeID]
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledPanel>
|
||||
<NodeEventList
|
||||
processEvent={processEvent}
|
||||
eventType={eventType}
|
||||
relatedStats={relatedEventsStats}
|
||||
/>
|
||||
{eventCount === undefined || processEvent === null ? (
|
||||
<PanelLoading />
|
||||
) : (
|
||||
<>
|
||||
<NodeEventsOfTypeBreadcrumbs
|
||||
nodeName={eventModel.processNameSafeVersion(processEvent)}
|
||||
eventType={eventType}
|
||||
eventCount={eventCount}
|
||||
nodeID={nodeID}
|
||||
eventsInCategoryCount={eventsInCategoryCount}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<NodeEventList eventType={eventType} nodeID={nodeID} events={events} />
|
||||
</>
|
||||
)}
|
||||
</StyledPanel>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const NodeEventList = memo(function ({
|
||||
processEvent,
|
||||
/**
|
||||
* Rendered for each event in the list.
|
||||
*/
|
||||
const NodeEventsListItem = memo(function ({
|
||||
event,
|
||||
nodeID,
|
||||
eventType,
|
||||
relatedStats,
|
||||
}: {
|
||||
processEvent: ResolverEvent | null;
|
||||
event: SafeResolverEvent;
|
||||
nodeID: string;
|
||||
eventType: string;
|
||||
relatedStats: ResolverNodeStats | undefined;
|
||||
}) {
|
||||
const processName = processEvent && event.eventName(processEvent);
|
||||
const processEntityId = processEvent ? event.entityId(processEvent) : '';
|
||||
const nodesHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({ panelView: 'nodes' })
|
||||
);
|
||||
|
||||
const nodesLinkNavProps = useNavigateOrReplace({
|
||||
search: nodesHref,
|
||||
const timestamp = eventModel.eventTimestamp(event);
|
||||
const date = timestamp !== undefined ? formatDate(timestamp) : timestamp;
|
||||
const linkProps = useLinkProps({
|
||||
panelView: 'eventDetail',
|
||||
panelParameters: {
|
||||
nodeID,
|
||||
eventType,
|
||||
eventID: String(eventModel.eventID(event)),
|
||||
},
|
||||
});
|
||||
const totalCount = relatedStats
|
||||
? Object.values(relatedStats.events.byCategory).reduce((sum, val) => sum + val, 0)
|
||||
: 0;
|
||||
const eventsString = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events',
|
||||
{
|
||||
defaultMessage: 'Events',
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiText>
|
||||
<BoldCode>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType"
|
||||
values={{
|
||||
category: eventModel.eventCategory(event).join(', '),
|
||||
eventType: eventModel.eventType(event).join(', '),
|
||||
}}
|
||||
defaultMessage="{category} {eventType}"
|
||||
/>
|
||||
</BoldCode>
|
||||
<StyledTime dateTime={date}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime"
|
||||
values={{ date }}
|
||||
defaultMessage="@ {date}"
|
||||
/>
|
||||
</StyledTime>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiButtonEmpty {...linkProps}>
|
||||
<DescriptiveName event={event} />
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const relatedsReadyMap = useSelector(selectors.relatedEventsReady);
|
||||
const relatedsReady = processEntityId && relatedsReadyMap.get(processEntityId);
|
||||
|
||||
const dispatch = useResolverDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof relatedsReady === 'undefined') {
|
||||
dispatch({
|
||||
type: 'appDetectedMissingEventData',
|
||||
payload: processEntityId,
|
||||
});
|
||||
}
|
||||
}, [relatedsReady, dispatch, processEntityId]);
|
||||
|
||||
const relatedByCategory = useSelector(selectors.relatedEventsByCategory);
|
||||
const eventsForCurrentCategory = relatedByCategory(processEntityId)(eventType);
|
||||
const relatedEventDetailNavigation = useRelatedEventDetailNavigation({
|
||||
nodeID: processEntityId,
|
||||
category: eventType,
|
||||
events: eventsForCurrentCategory,
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders a list of events with a separator in between.
|
||||
*/
|
||||
const NodeEventList = memo(function NodeEventList({
|
||||
eventType,
|
||||
events,
|
||||
nodeID,
|
||||
}: {
|
||||
eventType: string;
|
||||
/**
|
||||
* A list entry will be displayed for each of these
|
||||
* The events to list.
|
||||
*/
|
||||
const matchingEventEntries: MatchingEventEntry[] = useMemo(() => {
|
||||
return eventsForCurrentCategory.map((resolverEvent) => {
|
||||
const eventTime = event.eventTimestamp(resolverEvent);
|
||||
const formattedDate = typeof eventTime === 'undefined' ? '' : formatDate(eventTime);
|
||||
const entityId = event.eventId(resolverEvent);
|
||||
return {
|
||||
formattedDate,
|
||||
eventCategory: `${eventType}`,
|
||||
eventType: `${event.ecsEventType(resolverEvent)}`,
|
||||
name: event.descriptiveName(resolverEvent),
|
||||
setQueryParams: () => relatedEventDetailNavigation(entityId),
|
||||
};
|
||||
});
|
||||
}, [eventType, eventsForCurrentCategory, relatedEventDetailNavigation]);
|
||||
|
||||
const nodeDetailHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID: processEntityId },
|
||||
})
|
||||
events: SafeResolverEvent[];
|
||||
nodeID: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{events.map((event, index) => (
|
||||
<Fragment key={index}>
|
||||
<NodeEventsListItem nodeID={nodeID} eventType={eventType} event={event} />
|
||||
{index === events.length - 1 ? null : <EuiHorizontalRule margin="m" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const nodeDetailNavProps = useNavigateOrReplace({
|
||||
search: nodeDetailHref,
|
||||
/**
|
||||
* Renders `Breadcrumbs`.
|
||||
*/
|
||||
const NodeEventsOfTypeBreadcrumbs = memo(function ({
|
||||
nodeName,
|
||||
eventType,
|
||||
eventCount,
|
||||
nodeID,
|
||||
/**
|
||||
* The count of events in the category that this list is showing.
|
||||
*/
|
||||
eventsInCategoryCount,
|
||||
}: {
|
||||
nodeName: React.ReactNode;
|
||||
eventType: string;
|
||||
/**
|
||||
* The events to list.
|
||||
*/
|
||||
eventCount: number;
|
||||
nodeID: string;
|
||||
/**
|
||||
* The count of events in the category that this list is showing.
|
||||
*/
|
||||
eventsInCategoryCount: number | undefined;
|
||||
}) {
|
||||
const nodesLinkNavProps = useLinkProps({
|
||||
panelView: 'nodes',
|
||||
});
|
||||
|
||||
const nodeEventsHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID: processEntityId },
|
||||
})
|
||||
);
|
||||
|
||||
const nodeEventsNavProps = useNavigateOrReplace({
|
||||
search: nodeEventsHref,
|
||||
const nodeDetailNavProps = useLinkProps({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID },
|
||||
});
|
||||
const crumbs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
text: eventsString,
|
||||
...nodesLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: processName,
|
||||
...nodeDetailNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents"
|
||||
values={{ totalCount }}
|
||||
defaultMessage="{totalCount} Events"
|
||||
/>
|
||||
),
|
||||
...nodeEventsNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory"
|
||||
values={{ count: matchingEventEntries.length, category: eventType }}
|
||||
defaultMessage="{count} {category}"
|
||||
/>
|
||||
),
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
}, [
|
||||
eventType,
|
||||
eventsString,
|
||||
matchingEventEntries.length,
|
||||
processName,
|
||||
totalCount,
|
||||
nodeDetailNavProps,
|
||||
nodesLinkNavProps,
|
||||
nodeEventsNavProps,
|
||||
]);
|
||||
|
||||
if (!relatedsReady) {
|
||||
return <PanelLoading />;
|
||||
}
|
||||
const nodeEventsNavProps = useLinkProps({
|
||||
panelView: 'nodeEvents',
|
||||
panelParameters: { nodeID },
|
||||
});
|
||||
|
||||
return (
|
||||
<NodeCategoryEntries
|
||||
crumbs={crumbs}
|
||||
processEntityId={processEntityId}
|
||||
matchingEventEntries={matchingEventEntries}
|
||||
eventType={eventType}
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events',
|
||||
{
|
||||
defaultMessage: 'Events',
|
||||
}
|
||||
),
|
||||
...nodesLinkNavProps,
|
||||
},
|
||||
{
|
||||
text: nodeName,
|
||||
...nodeDetailNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents"
|
||||
values={{ totalCount: eventCount }}
|
||||
defaultMessage="{totalCount} Events"
|
||||
/>
|
||||
),
|
||||
...nodeEventsNavProps,
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory"
|
||||
values={{ count: eventsInCategoryCount, category: eventType }}
|
||||
defaultMessage="{count} {category}"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -4,9 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @elastic/eui/href-or-on-click */
|
||||
|
||||
/* eslint-disable no-duplicate-imports */
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo, useMemo, useCallback, useContext } from 'react';
|
||||
import {
|
||||
EuiBasicTableColumn,
|
||||
EuiBadge,
|
||||
|
@ -16,71 +22,31 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { SideEffectContext } from '../side_effect_context';
|
||||
import { StyledPanel } from '../styles';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import {
|
||||
StyledLabelTitle,
|
||||
StyledAnalyzedEvent,
|
||||
StyledLabelContainer,
|
||||
StyledButtonTextContainer,
|
||||
} from './styles';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { formatter, StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { formatter } from './panel_content_utilities';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
import { CubeForProcess } from './cube_for_process';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { LimitWarning } from '../limit_warnings';
|
||||
import { ResolverState } from '../../types';
|
||||
import { useNavigateOrReplace } from '../use_navigate_or_replace';
|
||||
import { useLinkProps } from '../use_link_props';
|
||||
import { useColors } from '../use_colors';
|
||||
|
||||
const StyledLimitWarning = styled(LimitWarning)`
|
||||
flex-flow: row wrap;
|
||||
display: block;
|
||||
align-items: baseline;
|
||||
margin-top: 1em;
|
||||
|
||||
& .euiCallOutHeader {
|
||||
display: inline;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
& .euiText {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
& .euiText p {
|
||||
display: inline;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledAnalyzedEvent = styled.div`
|
||||
color: ${(props) => props.color};
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const StyledLabelTitle = styled.div``;
|
||||
|
||||
const StyledLabelContainer = styled.div`
|
||||
display: inline-block;
|
||||
flex: 3;
|
||||
min-width: 0;
|
||||
|
||||
${StyledAnalyzedEvent},
|
||||
${StyledLabelTitle} {
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { ResolverAction } from '../../store/actions';
|
||||
|
||||
interface ProcessTableView {
|
||||
name?: string;
|
||||
timestamp?: Date;
|
||||
nodeID: string;
|
||||
event: SafeResolverEvent;
|
||||
href: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,8 +65,8 @@ export const NodeList = memo(() => {
|
|||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render(name: string, item: ProcessTableView) {
|
||||
return <NodeDetailLink name={name} item={item} />;
|
||||
render(name: string | undefined, item: ProcessTableView) {
|
||||
return <NodeDetailLink name={name} event={item.event} nodeID={item.nodeID} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -132,42 +98,26 @@ export const NodeList = memo(() => {
|
|||
[]
|
||||
);
|
||||
|
||||
const { processNodePositions } = useSelector(selectors.layout);
|
||||
const nodeHrefs: Map<SafeResolverEvent, string | null | undefined> = useSelector(
|
||||
(state: ResolverState) => {
|
||||
const relativeHref = selectors.relativeHref(state);
|
||||
return new Map(
|
||||
[...processNodePositions.keys()].map((processEvent) => {
|
||||
const nodeID = event.entityIDSafeVersion(processEvent);
|
||||
if (nodeID === undefined) {
|
||||
return [processEvent, null];
|
||||
}
|
||||
return [
|
||||
processEvent,
|
||||
relativeHref({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: {
|
||||
nodeID,
|
||||
},
|
||||
}),
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
const processTableView: ProcessTableView[] = useMemo(
|
||||
() =>
|
||||
[...processNodePositions.keys()].map((processEvent) => {
|
||||
const name = event.processNameSafeVersion(processEvent);
|
||||
return {
|
||||
name,
|
||||
timestamp: event.timestampAsDateSafeVersion(processEvent),
|
||||
event: processEvent,
|
||||
href: nodeHrefs.get(processEvent) ?? undefined,
|
||||
};
|
||||
}),
|
||||
[processNodePositions, nodeHrefs]
|
||||
const processTableView: ProcessTableView[] = useSelector(
|
||||
useCallback((state: ResolverState) => {
|
||||
const { processNodePositions } = selectors.layout(state);
|
||||
const view: ProcessTableView[] = [];
|
||||
for (const processEvent of processNodePositions.keys()) {
|
||||
const name = eventModel.processNameSafeVersion(processEvent);
|
||||
const nodeID = eventModel.entityIDSafeVersion(processEvent);
|
||||
if (nodeID !== undefined) {
|
||||
view.push({
|
||||
name,
|
||||
timestamp: eventModel.timestampAsDateSafeVersion(processEvent),
|
||||
nodeID,
|
||||
event: processEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
return view;
|
||||
}, [])
|
||||
);
|
||||
|
||||
const numberOfProcesses = processTableView.length;
|
||||
|
||||
const crumbs = useMemo(() => {
|
||||
|
@ -176,7 +126,6 @@ export const NodeList = memo(() => {
|
|||
text: i18n.translate('xpack.securitySolution.resolver.panel.nodeList.title', {
|
||||
defaultMessage: 'All Process Events',
|
||||
}),
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
@ -187,8 +136,8 @@ export const NodeList = memo(() => {
|
|||
const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []);
|
||||
return (
|
||||
<StyledPanel>
|
||||
<StyledBreadcrumbs breadcrumbs={crumbs} />
|
||||
{showWarning && <StyledLimitWarning numberDisplayed={numberOfProcesses} />}
|
||||
<Breadcrumbs breadcrumbs={crumbs} />
|
||||
{showWarning && <LimitWarning numberDisplayed={numberOfProcesses} />}
|
||||
<EuiSpacer size="l" />
|
||||
<EuiInMemoryTable<ProcessTableView>
|
||||
rowProps={rowProps}
|
||||
|
@ -201,16 +150,40 @@ export const NodeList = memo(() => {
|
|||
);
|
||||
});
|
||||
|
||||
function NodeDetailLink({ name, item }: { name: string; item: ProcessTableView }) {
|
||||
const entityID = event.entityIDSafeVersion(item.event);
|
||||
const originID = useSelector(selectors.originID);
|
||||
const isOrigin = originID === entityID;
|
||||
function NodeDetailLink({
|
||||
name,
|
||||
nodeID,
|
||||
event,
|
||||
}: {
|
||||
name?: string;
|
||||
nodeID: string;
|
||||
event: SafeResolverEvent;
|
||||
}) {
|
||||
const isOrigin = useSelector((state: ResolverState) => {
|
||||
return selectors.originID(state) === nodeID;
|
||||
});
|
||||
const isTerminated = useSelector((state: ResolverState) =>
|
||||
entityID === undefined ? false : selectors.isProcessTerminated(state)(entityID)
|
||||
nodeID === undefined ? false : selectors.isProcessTerminated(state)(nodeID)
|
||||
);
|
||||
const { descriptionText } = useColors();
|
||||
const linkProps = useLinkProps({ panelView: 'nodeDetail', panelParameters: { nodeID } });
|
||||
const dispatch: (action: ResolverAction) => void = useDispatch();
|
||||
const { timestamp } = useContext(SideEffectContext);
|
||||
const handleOnClick = useCallback(
|
||||
(mouseEvent: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
linkProps.onClick(mouseEvent);
|
||||
dispatch({
|
||||
type: 'userBroughtProcessIntoView',
|
||||
payload: {
|
||||
process: event,
|
||||
time: timestamp(),
|
||||
},
|
||||
});
|
||||
},
|
||||
[timestamp, linkProps, dispatch, event]
|
||||
);
|
||||
return (
|
||||
<EuiButtonEmpty {...useNavigateOrReplace({ search: item.href })}>
|
||||
<EuiButtonEmpty onClick={handleOnClick} href={linkProps.href}>
|
||||
{name === '' ? (
|
||||
<EuiBadge color="warning">
|
||||
{i18n.translate(
|
||||
|
|
|
@ -7,11 +7,8 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigateOrReplace } from '../use_navigate_or_replace';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { ResolverState } from '../../types';
|
||||
import { StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
import { useLinkProps } from '../use_link_props';
|
||||
|
||||
/**
|
||||
* Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state.
|
||||
|
@ -24,12 +21,10 @@ export const PanelContentError = memo(function ({
|
|||
}: {
|
||||
translatedErrorMessage: string;
|
||||
}) {
|
||||
const nodesHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({ panelView: 'nodes' })
|
||||
);
|
||||
const nodesLinkNavProps = useNavigateOrReplace({
|
||||
search: nodesHref,
|
||||
const nodesLinkNavProps = useLinkProps({
|
||||
panelView: 'nodes',
|
||||
});
|
||||
|
||||
const crumbs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
|
@ -42,13 +37,12 @@ export const PanelContentError = memo(function ({
|
|||
text: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.error', {
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
}, [nodesLinkNavProps]);
|
||||
return (
|
||||
<>
|
||||
<StyledBreadcrumbs breadcrumbs={crumbs} />
|
||||
<Breadcrumbs breadcrumbs={crumbs} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText textAlign="center">{translatedErrorMessage}</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
|
@ -60,4 +54,3 @@ export const PanelContentError = memo(function ({
|
|||
</>
|
||||
);
|
||||
});
|
||||
PanelContentError.displayName = 'TableServiceError';
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
/* eslint-disable react/display-name */
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBreadcrumbs, EuiCode, EuiBetaBadge } from '@elastic/eui';
|
||||
import { EuiCode } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import React, { memo } from 'react';
|
||||
import { useColors } from '../use_colors';
|
||||
|
||||
/**
|
||||
* A bold version of EuiCode to display certain titles with
|
||||
|
@ -21,30 +20,6 @@ export const BoldCode = styled(EuiCode)`
|
|||
}
|
||||
`;
|
||||
|
||||
const BetaHeader = styled(`header`)`
|
||||
margin-bottom: 1em;
|
||||
`;
|
||||
|
||||
const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>`
|
||||
&.euiBreadcrumbs {
|
||||
background-color: ${(props) => props.background};
|
||||
color: ${(props) => props.text};
|
||||
padding: 1em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
& .euiBreadcrumbSeparator {
|
||||
background: ${(props) => props.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const betaBadgeLabel = i18n.translate(
|
||||
'xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'BETA',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* A component that renders an element with breaking opportunities (`<wbr>`s)
|
||||
* spliced into text children at word boundaries.
|
||||
|
@ -85,31 +60,6 @@ export const StyledTime = memo(styled('time')`
|
|||
text-align: start;
|
||||
`);
|
||||
|
||||
type Breadcrumbs = Parameters<typeof EuiBreadcrumbs>[0]['breadcrumbs'];
|
||||
/**
|
||||
* Breadcrumb menu with adjustments per direction from UX team
|
||||
*/
|
||||
export const StyledBreadcrumbs = memo(function StyledBreadcrumbs({
|
||||
breadcrumbs,
|
||||
}: {
|
||||
breadcrumbs: Breadcrumbs;
|
||||
}) {
|
||||
const { resolverBreadcrumbBackground, resolverEdgeText } = useColors();
|
||||
return (
|
||||
<>
|
||||
<BetaHeader>
|
||||
<EuiBetaBadge label={betaBadgeLabel} />
|
||||
</BetaHeader>
|
||||
<ThemedBreadcrumbs
|
||||
background={resolverBreadcrumbBackground}
|
||||
text={resolverEdgeText}
|
||||
breadcrumbs={breadcrumbs}
|
||||
truncate={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Long formatter (to second) for DateTime
|
||||
*/
|
||||
|
@ -122,12 +72,6 @@ export const formatter = new Intl.DateTimeFormat(i18n.getLocale(), {
|
|||
second: '2-digit',
|
||||
});
|
||||
|
||||
const invalidDateText = i18n.translate(
|
||||
'xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate',
|
||||
{
|
||||
defaultMessage: 'Invalid Date',
|
||||
}
|
||||
);
|
||||
/**
|
||||
* @returns {string} A nicely formatted string for a date
|
||||
*/
|
||||
|
@ -140,6 +84,8 @@ export function formatDate(
|
|||
if (isFinite(date.getTime())) {
|
||||
return formatter.format(date);
|
||||
} else {
|
||||
return invalidDateText;
|
||||
return i18n.translate('xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate', {
|
||||
defaultMessage: 'Invalid Date',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,10 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { useNavigateOrReplace } from '../use_navigate_or_replace';
|
||||
import { ResolverState } from '../../types';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
import { useLinkProps } from '../use_link_props';
|
||||
|
||||
export function PanelLoading() {
|
||||
const waitingString = i18n.translate(
|
||||
|
@ -26,11 +23,8 @@ export function PanelLoading() {
|
|||
defaultMessage: 'Events',
|
||||
}
|
||||
);
|
||||
const nodesHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({ panelView: 'nodes' })
|
||||
);
|
||||
const nodesLinkNavProps = useNavigateOrReplace({
|
||||
search: nodesHref,
|
||||
const nodesLinkNavProps = useLinkProps({
|
||||
panelView: 'nodes',
|
||||
});
|
||||
const waitCrumbs = useMemo(() => {
|
||||
return [
|
||||
|
@ -42,7 +36,7 @@ export function PanelLoading() {
|
|||
}, [nodesLinkNavProps, eventsString]);
|
||||
return (
|
||||
<>
|
||||
<StyledBreadcrumbs breadcrumbs={waitCrumbs} />
|
||||
<Breadcrumbs breadcrumbs={waitCrumbs} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle>
|
||||
<h4>{waitingString}</h4>
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-duplicate-imports */
|
||||
|
||||
import { EuiBreadcrumbs } from '@elastic/eui';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
|
||||
|
@ -15,3 +20,48 @@ export const StyledDescriptionList = styled(EuiDescriptionList)`
|
|||
export const StyledTitle = styled('h4')`
|
||||
overflow-wrap: break-word;
|
||||
`;
|
||||
|
||||
export const BetaHeader = styled(`header`)`
|
||||
margin-bottom: 1em;
|
||||
`;
|
||||
|
||||
export const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>`
|
||||
&.euiBreadcrumbs {
|
||||
background-color: ${(props) => props.background};
|
||||
color: ${(props) => props.text};
|
||||
padding: 1em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
& .euiBreadcrumbSeparator {
|
||||
background: ${(props) => props.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledButtonTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
export const StyledAnalyzedEvent = styled.div`
|
||||
color: ${(props) => props.color};
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
export const StyledLabelTitle = styled.div``;
|
||||
|
||||
export const StyledLabelContainer = styled.div`
|
||||
display: inline-block;
|
||||
flex: 3;
|
||||
min-width: 0;
|
||||
|
||||
${StyledAnalyzedEvent},
|
||||
${StyledLabelTitle} {
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -12,15 +12,15 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { NodeSubMenu } from './submenu';
|
||||
import { applyMatrix3 } from '../models/vector2';
|
||||
import { Vector2, Matrix3, ResolverState } from '../types';
|
||||
import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { useResolverDispatch } from './use_resolver_dispatch';
|
||||
import * as eventModel from '../../../common/endpoint/models/event';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { useNavigateOrReplace } from './use_navigate_or_replace';
|
||||
import { fontSize } from './font_size';
|
||||
import { useCubeAssets } from './use_cube_assets';
|
||||
import { useSymbolIDs } from './use_symbol_ids';
|
||||
import { useColors } from './use_colors';
|
||||
import { useLinkProps } from './use_link_props';
|
||||
|
||||
interface StyledActionsContainer {
|
||||
readonly color: string;
|
||||
|
@ -192,7 +192,6 @@ const UnstyledProcessEventDot = React.memo(
|
|||
|
||||
/**
|
||||
* Type in non-SVG components scales as follows:
|
||||
* (These values were adjusted to match the proportions in the comps provided by UX/Design)
|
||||
* 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this.
|
||||
* 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise
|
||||
*/
|
||||
|
@ -239,15 +238,10 @@ const UnstyledProcessEventDot = React.memo(
|
|||
const isOrigin = nodeID === originID;
|
||||
|
||||
const dispatch = useResolverDispatch();
|
||||
const processDetailHref = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID },
|
||||
})
|
||||
);
|
||||
|
||||
const processDetailNavProps = useNavigateOrReplace({
|
||||
search: processDetailHref,
|
||||
const processDetailNavProps = useLinkProps({
|
||||
panelView: 'nodeDetail',
|
||||
panelParameters: { nodeID },
|
||||
});
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
|
@ -272,7 +266,7 @@ const UnstyledProcessEventDot = React.memo(
|
|||
);
|
||||
|
||||
const grandTotal: number | null = useSelector((state: ResolverState) =>
|
||||
selectors.relatedEventTotalForProcess(state)(event as ResolverEvent)
|
||||
selectors.relatedEventTotalForProcess(state)(event)
|
||||
);
|
||||
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
|
@ -376,12 +370,13 @@ const UnstyledProcessEventDot = React.memo(
|
|||
backgroundColor={colorMap.resolverBackground}
|
||||
color={colorMap.descriptionText}
|
||||
isDisplaying={isShowingDescriptionText}
|
||||
data-test-subj="resolver:node:description"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.processDescription"
|
||||
defaultMessage="{originText}{descriptionText}"
|
||||
defaultMessage="{isEventBeingAnalyzed, select, true {Analyzed Event · {descriptionText}} false {{descriptionText}}}"
|
||||
values={{
|
||||
originText: isOrigin ? 'Analyzed Event · ' : '',
|
||||
isEventBeingAnalyzed: isOrigin,
|
||||
descriptionText,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { EuiPanel, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
@ -62,3 +62,26 @@ export const GraphContainer = styled.div`
|
|||
flex-grow: 1;
|
||||
contain: layout;
|
||||
`;
|
||||
|
||||
/**
|
||||
* See `RelatedEventLimitWarning`
|
||||
*/
|
||||
export const LimitWarningsEuiCallOut = styled(EuiCallOut)`
|
||||
flex-flow: row wrap;
|
||||
display: block;
|
||||
align-items: baseline;
|
||||
margin-top: 1em;
|
||||
|
||||
& .euiCallOutHeader {
|
||||
display: inline;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
& .euiText {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
& .euiText p {
|
||||
display: inline;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useCamera, useAutoUpdatingClientRect } from './use_camera';
|
|||
import { Provider } from 'react-redux';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { Matrix3, ResolverStore, SideEffectSimulator } from '../types';
|
||||
import { ResolverEvent } from '../../../common/endpoint/types';
|
||||
import { SafeResolverEvent } from '../../../common/endpoint/types';
|
||||
import { SideEffectContext } from './side_effect_context';
|
||||
import { applyMatrix3 } from '../models/vector2';
|
||||
import { sideEffectSimulatorFactory } from './side_effect_simulator_factory';
|
||||
|
@ -33,7 +33,7 @@ describe('useCamera on an unpainted element', () => {
|
|||
beforeEach(async () => {
|
||||
store = createStore(resolverReducer);
|
||||
|
||||
const Test = function Test() {
|
||||
const Test = function () {
|
||||
const camera = useCamera();
|
||||
const { ref, onMouseDown } = camera;
|
||||
projectionMatrix = camera.projectionMatrix;
|
||||
|
@ -160,9 +160,9 @@ describe('useCamera on an unpainted element', () => {
|
|||
expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled();
|
||||
});
|
||||
describe('when the camera begins animation', () => {
|
||||
let process: ResolverEvent;
|
||||
let process: SafeResolverEvent;
|
||||
beforeEach(() => {
|
||||
const events: ResolverEvent[] = [];
|
||||
const events: SafeResolverEvent[] = [];
|
||||
const numberOfEvents: number = 10;
|
||||
|
||||
for (let index = 0; index < numberOfEvents; index++) {
|
||||
|
@ -190,9 +190,9 @@ describe('useCamera on an unpainted element', () => {
|
|||
} else {
|
||||
throw new Error('failed to create tree');
|
||||
}
|
||||
const processes: ResolverEvent[] = [
|
||||
const processes: SafeResolverEvent[] = [
|
||||
...selectors.layout(store.getState()).processNodePositions.keys(),
|
||||
] as ResolverEvent[];
|
||||
];
|
||||
process = processes[processes.length - 1];
|
||||
if (!process) {
|
||||
throw new Error('missing the process to bring into view');
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { useSelector } from 'react-redux';
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { useNavigateOrReplace } from './use_navigate_or_replace';
|
||||
|
||||
import * as selectors from '../store/selectors';
|
||||
import { PanelViewAndParameters, ResolverState } from '../types';
|
||||
|
||||
type EventHandlerCallback = MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||
|
||||
/**
|
||||
* Get an `onClick` function and an `href` string. Use these as props for `<a />` elements.
|
||||
* `onClick` will use navigate to the `panelViewAndParameters` using `history.push`.
|
||||
* the `href` points to `panelViewAndParameters`.
|
||||
* Existing `search` parameters are maintained.
|
||||
*/
|
||||
export function useLinkProps(
|
||||
panelViewAndParameters: PanelViewAndParameters
|
||||
): { href: string; onClick: EventHandlerCallback } {
|
||||
const search = useSelector((state: ResolverState) =>
|
||||
selectors.relativeHref(state)(panelViewAndParameters)
|
||||
);
|
||||
|
||||
return useNavigateOrReplace({
|
||||
search,
|
||||
});
|
||||
}
|
|
@ -12,7 +12,7 @@ import * as selectors from '../store/selectors';
|
|||
/**
|
||||
* A hook that takes a nodeID and a record of categories, and returns a function that
|
||||
* navigates to the proper url when called with a category.
|
||||
* @deprecated
|
||||
* @deprecated See `useLinkProps`
|
||||
*/
|
||||
export function useRelatedEventByCategoryNavigation({
|
||||
nodeID,
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ResolverState } from '../types';
|
||||
import { ResolverEvent } from '../../../common/endpoint/types';
|
||||
import * as selectors from '../store/selectors';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function useRelatedEventDetailNavigation({
|
||||
nodeID,
|
||||
category,
|
||||
events,
|
||||
}: {
|
||||
nodeID: string;
|
||||
category: string;
|
||||
events: ResolverEvent[];
|
||||
}) {
|
||||
const relatedEventDetailUrls = useSelector((state: ResolverState) =>
|
||||
selectors.relatedEventDetailHrefs(state)(category, nodeID, events)
|
||||
);
|
||||
const history = useHistory();
|
||||
return useCallback(
|
||||
(entityID: string | number | undefined) => {
|
||||
if (entityID !== undefined) {
|
||||
const urlForEntityID = relatedEventDetailUrls.get(String(entityID));
|
||||
if (urlForEntityID !== null && urlForEntityID !== undefined) {
|
||||
return history.replace({ search: urlForEntityID });
|
||||
}
|
||||
}
|
||||
},
|
||||
[history, relatedEventDetailUrls]
|
||||
);
|
||||
}
|
|
@ -9,7 +9,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server';
|
|||
import {
|
||||
parentEntityIDSafeVersion,
|
||||
entityIDSafeVersion,
|
||||
getAncestryAsArray,
|
||||
ancestry,
|
||||
} from '../../../../../common/endpoint/models/event';
|
||||
import {
|
||||
SafeResolverAncestry,
|
||||
|
@ -35,7 +35,8 @@ export class AncestryQueryHandler implements QueryHandler<SafeResolverAncestry>
|
|||
legacyEndpointID: string | undefined,
|
||||
originNode: SafeResolverLifecycleNode | undefined
|
||||
) {
|
||||
this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels);
|
||||
const event = originNode?.lifecycle[0];
|
||||
this.ancestorsToFind = (event ? ancestry(event) : []).slice(0, levels);
|
||||
this.query = new LifecycleQuery(indexPattern, legacyEndpointID);
|
||||
|
||||
// add the origin node to the response if it exists
|
||||
|
@ -108,7 +109,7 @@ export class AncestryQueryHandler implements QueryHandler<SafeResolverAncestry>
|
|||
this.levels = this.levels - ancestryNodes.size;
|
||||
// the results come back in ascending order on timestamp so the first entry in the
|
||||
// results should be the further ancestor (most distant grandparent)
|
||||
this.ancestorsToFind = getAncestryAsArray(results[0]).slice(0, this.levels);
|
||||
this.ancestorsToFind = ancestry(results[0]).slice(0, this.levels);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,12 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
parentEntityIDSafeVersion,
|
||||
isProcessRunning,
|
||||
getAncestryAsArray,
|
||||
entityIDSafeVersion,
|
||||
} from '../../../../../common/endpoint/models/event';
|
||||
import * as eventModel from '../../../../../common/endpoint/models/event';
|
||||
import {
|
||||
SafeResolverChildren,
|
||||
SafeResolverChildNode,
|
||||
|
@ -72,7 +67,7 @@ export class ChildrenNodesHelper {
|
|||
*/
|
||||
addLifecycleEvents(lifecycle: SafeResolverEvent[]) {
|
||||
for (const event of lifecycle) {
|
||||
const entityID = entityIDSafeVersion(event);
|
||||
const entityID = eventModel.entityIDSafeVersion(event);
|
||||
if (entityID) {
|
||||
const cachedChild = this.getOrCreateChildNode(entityID);
|
||||
cachedChild.lifecycle.push(event);
|
||||
|
@ -93,19 +88,19 @@ export class ChildrenNodesHelper {
|
|||
const nonLeafNodes: Set<SafeResolverChildNode> = new Set();
|
||||
|
||||
const isDistantGrandchild = (event: ChildEvent) => {
|
||||
const ancestry = getAncestryAsArray(event);
|
||||
const ancestry = eventModel.ancestry(event);
|
||||
return ancestry.length > 0 && queriedNodes.has(ancestry[ancestry.length - 1]);
|
||||
};
|
||||
|
||||
for (const event of startEvents) {
|
||||
const parentID = parentEntityIDSafeVersion(event);
|
||||
const entityID = entityIDSafeVersion(event);
|
||||
if (parentID && entityID && isProcessRunning(event)) {
|
||||
const parentID = eventModel.parentEntityIDSafeVersion(event);
|
||||
const entityID = eventModel.entityIDSafeVersion(event);
|
||||
if (parentID && entityID && eventModel.isProcessRunning(event)) {
|
||||
// don't actually add the start event to the node, because that'll be done in
|
||||
// a different call
|
||||
const childNode = this.getOrCreateChildNode(entityID);
|
||||
|
||||
const ancestry = getAncestryAsArray(event);
|
||||
const ancestry = eventModel.ancestry(event);
|
||||
// This is to handle the following unlikely but possible scenario:
|
||||
// if an alert was generated by the kernel process (parent process of all other processes) then
|
||||
// the direct children of that process would only have an ancestry array of [parent_kernel], a single value in the array.
|
||||
|
|
|
@ -15878,19 +15878,14 @@
|
|||
"xpack.securitySolution.endpoint.resolver.panel.error.goBack": "このリンクをクリックすると、すべてのプロセスのリストに戻ります。",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "イベント",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "イベント",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "イベント",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount}件のイベント",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "関連イベントが見つかりません。",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "イベントを待機しています...",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} {category}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.detailsForProcessName": "詳細:{processName}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveName": "{descriptor} {subject}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle": "{descriptor} {subject}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events": "イベント",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA": "N/A",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents": "{totalCount}件のイベント",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} {category}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount}件のイベント",
|
||||
|
|
|
@ -15888,19 +15888,14 @@
|
|||
"xpack.securitySolution.endpoint.resolver.panel.error.goBack": "单击此链接以返回到所有进程的列表。",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "事件",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "事件",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "事件",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount} 个事件",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "找不到相关事件。",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "等候事件......",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} 个{category}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.detailsForProcessName": "{processName} 的详情",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveName": "{descriptor} {subject}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle": "{descriptor} {subject}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events": "事件",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA": "不可用",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents": "{totalCount} 个事件",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} 个{category}",
|
||||
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount} 个事件",
|
||||
|
|
|
@ -61,12 +61,12 @@ const AppRoot = React.memo(
|
|||
storeFactory,
|
||||
ResolverWithoutProviders,
|
||||
mocks: {
|
||||
dataAccessLayer: { noAncestorsTwoChildren },
|
||||
dataAccessLayer: { noAncestorsTwoChildrenWithRelatedEventsOnOrigin },
|
||||
},
|
||||
} = resolverPluginSetup;
|
||||
const dataAccessLayer: DataAccessLayer = useMemo(
|
||||
() => noAncestorsTwoChildren().dataAccessLayer,
|
||||
[noAncestorsTwoChildren]
|
||||
() => noAncestorsTwoChildrenWithRelatedEventsOnOrigin().dataAccessLayer,
|
||||
[noAncestorsTwoChildrenWithRelatedEventsOnOrigin]
|
||||
);
|
||||
|
||||
const store = useMemo(() => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue