From a72580c7bfa409eb130df4dd84f0940976d6bdf4 Mon Sep 17 00:00:00 2001
From: Robert Austin
Date: Wed, 23 Sep 2020 11:58:17 -0400
Subject: [PATCH] [Resolver] Refactoring panel view (#77928) (#78295)
* 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.
---
.../common/endpoint/models/event.test.ts | 48 +-
.../common/endpoint/models/event.ts | 180 +++----
.../common/endpoint/types/index.ts | 6 +-
.../mocks/no_ancestors_two_children.ts | 9 +-
..._children_in_index_called_awesome_index.ts | 2 +-
..._children_with_related_events_on_origin.ts | 2 +-
.../public/resolver/index.ts | 4 +-
.../public/resolver/mocks/endpoint_event.ts | 35 +-
.../public/resolver/mocks/resolver_tree.ts | 222 ++++----
.../resolver/models/process_event.test.ts | 26 +-
.../public/resolver/models/process_event.ts | 94 ++--
.../public/resolver/models/resolver_tree.ts | 14 +-
.../public/resolver/store/actions.ts | 35 +-
.../public/resolver/store/data/action.ts | 9 -
.../resolver/store/data/reducer.test.ts | 148 +-----
.../public/resolver/store/data/reducer.ts | 16 +-
.../resolver/store/data/selectors.test.ts | 4 +-
.../public/resolver/store/data/selectors.ts | 424 +++++-----------
.../store/data/visible_entities.test.ts | 6 +-
.../public/resolver/store/methods.ts | 6 +-
.../public/resolver/store/middleware/index.ts | 28 +-
.../middleware/related_events_fetcher.ts | 49 ++
.../public/resolver/store/reducer.ts | 30 +-
.../public/resolver/store/selectors.ts | 52 +-
.../public/resolver/store/ui/selectors.ts | 10 +-
.../public/resolver/types.ts | 6 +-
.../public/resolver/view/edge_line.tsx | 8 +-
.../public/resolver/view/graph_controls.tsx | 8 +-
.../public/resolver/view/limit_warnings.tsx | 36 +-
.../public/resolver/view/node.test.tsx | 42 ++
.../public/resolver/view/panel.test.tsx | 41 +-
.../resolver/view/panels/breadcrumbs.tsx | 40 ++
.../resolver/view/panels/cube_for_process.tsx | 2 +-
.../view/panels/deep_object_entries.test.ts | 36 ++
.../view/panels/deep_object_entries.ts | 44 ++
.../view/panels/descriptive_name.test.tsx | 50 ++
.../resolver/view/panels/descriptive_name.tsx | 114 +++++
.../resolver/view/panels/event_detail.tsx | 472 +++++++++---------
.../public/resolver/view/panels/index.tsx | 8 +-
.../{node_details.tsx => node_detail.tsx} | 76 ++-
.../resolver/view/panels/node_events.tsx | 218 ++++----
.../view/panels/node_events_of_type.tsx | 426 +++++++---------
.../public/resolver/view/panels/node_list.tsx | 175 +++----
.../view/panels/panel_content_error.tsx | 19 +-
.../view/panels/panel_content_utilities.tsx | 62 +--
.../resolver/view/panels/panel_loading.tsx | 16 +-
.../public/resolver/view/panels/styles.tsx | 50 ++
.../resolver/view/process_event_dot.tsx | 23 +-
.../public/resolver/view/styles.tsx | 25 +-
.../public/resolver/view/use_camera.test.tsx | 12 +-
.../public/resolver/view/use_link_props.ts | 32 ++
...se_related_event_by_category_navigation.ts | 2 +-
.../use_related_event_detail_navigation.ts | 40 --
.../resolver/utils/ancestry_query_handler.ts | 7 +-
.../routes/resolver/utils/children_helper.ts | 19 +-
.../translations/translations/ja-JP.json | 5 -
.../translations/translations/zh-CN.json | 5 -
.../applications/resolver_test/index.tsx | 6 +-
58 files changed, 1668 insertions(+), 1916 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/node.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx
rename x-pack/plugins/security_solution/public/resolver/view/panels/{node_details.tsx => node_detail.tsx} (74%)
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts
delete mode 100644 x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts
index 2b0aa1601ab3..fed32293e00a 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts
@@ -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({
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
index 9634659b1a5d..00eb48bb62a5 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
@@ -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 {
- 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 extends object ? { [K in keyof T]?: DeepPartial } : 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) };
- }
-
- // To be somewhat defensive, we'll check for the presence of these.
- const partialEvent: DeepPartial = 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) };
+export function eventKind(event: SafeResolverEvent): string[] {
+ if (isLegacyEventSafeVersion(event)) {
+ return [];
+ } else {
+ return values(event.event?.kind);
}
-
- // Fall back on entityId if we can't fish a more descriptive name out.
- return { subject: entityId(event) };
}
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index 6afec7590347..d97fdfbf7d18 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -183,7 +183,7 @@ export interface ResolverTree {
relatedEvents: Omit;
relatedAlerts: Omit;
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;
}
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
index 0883a3787fcc..fd086bd9b984 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
@@ -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 {
return Promise.resolve({
entityID,
- events: [
- mockEndpointEvent({
- entityID,
- name: 'event',
- timestamp: 0,
- }),
- ],
+ events: [],
nextEvent: null,
});
},
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
index ec0fa9348578..86450b25eb1d 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
@@ -61,7 +61,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
events: [
mockEndpointEvent({
entityID,
- name: 'event',
+ processName: 'event',
timestamp: 0,
}),
],
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
index 95ec0cd1a5f7..ec773a09ae8e 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
@@ -66,7 +66,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
entityID,
events,
nextEvent: null,
- } as ResolverRelatedEvents);
+ });
},
/**
diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts
index 409f82c9d156..08a3722f4049 100644
--- a/x-pack/plugins/security_solution/public/resolver/index.ts
+++ b/x-pack/plugins/security_solution/public/resolver/index.ts
@@ -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,
},
},
};
diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts
index 083f6b8baa59..d19ca285ff3f 100644
--- a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts
+++ b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts
@@ -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;
+ };
}
diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts
index 8bd5953e9cb4..8691ecac4d1c 100644
--- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts
+++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts
@@ -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 = {};
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);
}
diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts
index 4d48b34fb284..380b15cf9da4 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts
@@ -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', () => {
diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts
index ea588731a55c..1510fc7f9f36 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts
@@ -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
diff --git a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts
index 446e371832d3..775b88246b61 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts
@@ -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,
diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
index 6a02d5b76bc4..3348c962efde 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
@@ -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;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts
index 59d1494ae8c2..0cb1cd1cec77 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts
@@ -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;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts
index 1e2de06ea4af..5714345de043 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts
@@ -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 };
- 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 = {};
// 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, 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,
};
}
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts
index c8df95aaee6f..1819407a1951 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts
@@ -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 = (state = initialState, action) => {
@@ -44,7 +42,7 @@ export const dataReducer: Reducer = (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 = (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 = (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;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts
index 539325faffdf..d9717b52d9ce 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts
@@ -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();
}
});
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
index d714ddb18147..fe7e8b5f22f1 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
@@ -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 = new Map();
+ const events: Map = 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` 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>();
+ 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, [], ''];
- }
- 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: [] };
+ // 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();
+ for (const event of eventsWrapper.events) {
+ const id = eventModel.eventIDSafeVersion(event);
+ if (id !== undefined) {
+ map.set(id, event);
}
- 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];
- });
+ }
+ memo.set(nodeID, map);
+ }
+ 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> = 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 {
- 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));
- if (!stats) {
+ return (event: SafeResolverEvent) => {
+ const nodeID = eventModel.entityIDSafeVersion(event);
+ if (nodeID === undefined) {
return null;
}
- let total = 0;
- for (const value of Object.values(stats.events.byCategory)) {
- total += value;
+ const stats = statsForProcess(nodeID);
+ if (!stats) {
+ return null;
}
- return total;
+ return stats.events.total;
};
}
);
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts
index 28948debae89..506acefe5167 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts
@@ -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,
diff --git a/x-pack/plugins/security_solution/public/resolver/store/methods.ts b/x-pack/plugins/security_solution/public/resolver/store/methods.ts
index 8dd15b1a44d0..f121b2aa8688 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/methods.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/methods.ts
@@ -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,
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts
index ef6b1f5eb3c6..5dca858b4fab 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts
@@ -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 = (
dataAccessLayer: DataAccessLayer
@@ -25,33 +26,12 @@ type MiddlewareFactory = (
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();
};
};
};
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
new file mode 100644
index 000000000000..b83e3cff9073
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
@@ -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, 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,
+ });
+ }
+ }
+ };
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts
index bf62fd0e60df..ae1e9a58a209 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts
@@ -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 = (
state = {
@@ -37,17 +37,18 @@ const uiReducer: Reducer = (
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 = (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;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
index 96b080206b61..3c99a186ac0c 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
@@ -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,30 +124,27 @@ 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
);
-/**
- * Map of related events... by entity id
- * @deprecated
- */
-export const relatedEventsByEntityId = composeSelectors(
+export const relatedEventCountByType: (
+ state: ResolverState
+) => (nodeID: string, eventType: string) => number | undefined = composeSelectors(
dataStateSelector,
- dataSelectors.relatedEventsByEntityId
+ dataSelectors.relatedEventCountByType
);
/**
- * Returns a function that returns the information needed to display related event details based on
- * the related event's entityID and its own ID.
+ * Map of related events... by entity id
* @deprecated
*/
-export const relatedEventDisplayInfoByEntityAndSelfId = composeSelectors(
+export const relatedEventsByEntityId = composeSelectors(
dataStateSelector,
- dataSelectors.relatedEventDisplayInfoByEntityAndSelfID
+ dataSelectors.relatedEventsByEntityId
);
/**
@@ -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)
*/
diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
index 6bc41832b92f..a8882d835fce 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
@@ -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 = createSelector(relativeHref, (relativeHref) => {
- return (category: string, nodeID: string, events: ResolverEvent[]) => {
+ return (category: string, nodeID: string, events: SafeResolverEvent[]) => {
const hrefsByEntityID = new Map();
events.map((event) => {
- const entityID = String(eventId(event));
+ const entityID = String(eventID(event));
const eventDetailPanelParams: PanelViewAndParameters = {
panelView: 'eventDetail',
panelParameters: {
diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts
index 952a1c5764d8..4dc614abe334 100644
--- a/x-pack/plugins/security_solution/public/resolver/types.ts
+++ b/x-pack/plugins/security_solution/public/resolver/types.ts
@@ -211,9 +211,8 @@ export interface TreeFetcherParameters {
*/
export interface DataState {
readonly relatedEvents: Map;
- readonly relatedEventsReady: Map;
- 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 };
};
};
}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx
index 53b889004798..777a7292e9c2 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx
@@ -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`
/**
* 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;
diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx
index 75aecf6747cc..dbeca840a4b6 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx
@@ -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`
/**
* 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;
diff --git a/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx
index 3f2b7c769cad..bc57c4e28b9c 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx
@@ -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 = (
);
-const LineageTitleMessage = React.memo(function LineageTitleMessage({
- numberOfEntries,
-}: {
- numberOfEntries: number;
-}) {
+const LineageTitleMessage = React.memo(function ({ numberOfEntries }: { numberOfEntries: number }) {
return (
-
+
);
});
/**
* 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 (
- }
>
{lineageLimitMessage}
-
+
);
});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx
new file mode 100644
index 000000000000..0b381f6771f0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx
@@ -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']);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx
index 7cfbd9a79466..2f23469606ac 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx
@@ -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(
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx
new file mode 100644
index 000000000000..ed3919800936
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx
@@ -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 (
+ <>
+
+
+
+
+ >
+ );
+});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx
index 4e9d64f5a76a..cc5f39e985d9 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx
@@ -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}
>
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts
new file mode 100644
index 000000000000..1c4e1f4199bc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts
@@ -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, 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);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts
new file mode 100644
index 000000000000..a508b00be573
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts
@@ -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)[key],
+ }))
+ );
+ } else if (value !== undefined && value !== null) {
+ // emit other non-null, defined, values
+ result.push([path, value]);
+ }
+ }
+ return result;
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx
new file mode 100644
index 000000000000..e869ab1ecd45
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx
@@ -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(
+
+
+
+ );
+ });
+ 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);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx
new file mode 100644
index 000000000000..195ebceee061
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx
@@ -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 (
+
+ );
+ }
+
+ /**
+ * 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 (
+
+ );
+ }
+
+ if (event.file?.path) {
+ return (
+
+ );
+ }
+
+ if (event.registry?.path) {
+ return (
+
+ );
+ }
+
+ if (event.registry?.key) {
+ return (
+
+ );
+ }
+
+ if (event.dns?.question?.name) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx
index 24d2a4a8f43f..72f0d54d51fa 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx
@@ -4,275 +4,103 @@
* 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';
-
-// 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;
- overflow-wrap: break-word;
- }
- &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description {
- max-width: calc(100% - 8.5em);
- overflow-wrap: break-word;
- }
-`);
-
-// Also prevents horizontal scrollbars on long descriptive names
-const StyledDescriptiveName = memo(styled(EuiText)`
- padding-right: 1em;
- overflow-wrap: break-word;
-`);
-
-// Styling subtitles, per UX review:
-const StyledFlexTitle = memo(styled('h3')`
- display: flex;
- flex-flow: row;
- font-size: 1.2em;
-`);
-const StyledTitleRule = memo(styled('hr')`
- &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override {
- display: block;
- flex: 1;
- margin-left: 0.5em;
- }
-`);
+import { DescriptiveName } from './descriptive_name';
+import { useLinkProps } from '../use_link_props';
+import { SafeResolverEvent } from '../../../../common/endpoint/types';
+import { deepObjectEntries } from './deep_object_entries';
-const TitleHr = memo(() => {
- return (
-
+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 (
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
});
-TitleHr.displayName = 'TitleHR';
-
-/**
- * Take description list entries and prepare them for display by
- * seeding with `` tags.
- *
- * @param entries {title: string, description: string}[]
- */
-function entriesForDisplay(entries: Array<{ title: string; description: string }>) {
- return entries.map((entry) => {
- return {
- description: {entry.description},
- title: {entry.title},
- };
- });
-}
/**
* 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 ({
+const EventDetailContents = memo(function ({
nodeID,
- eventID,
+ event,
+ eventType,
+ processEvent,
}: {
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,
- });
-
+ event: SafeResolverEvent;
/**
- * If we don't have the related events for the parent yet, use this effect
- * to request them.
+ * Event type to use in the breadcrumbs
*/
- useEffect(() => {
- if (
- typeof relatedsReady === 'undefined' &&
- processEntityId !== null &&
- processEntityId !== undefined
- ) {
- dispatch({
- type: 'appDetectedMissingEventData',
- payload: processEntityId,
- });
+ eventType: string;
+ processEvent: SafeResolverEvent;
+}) {
+ const formattedDate = useMemo(() => {
+ const timestamp = eventModel.timestampSafeVersion(event);
+ if (timestamp !== undefined) {
+ return formatDate(new Date(timestamp));
}
- }, [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: (
- <>
-
- >
- ),
- ...nodeEventsLinkNavProps,
- },
- {
- text: (
- <>
-
- >
- ),
- ...nodeEventsOfTypeLinkNavProps,
- },
- {
- text: relatedEventToShowDetailsFor ? (
-
- ) : (
- naString
- ),
- onClick: () => {},
- },
- ];
- }, [
- processName,
- eventsString,
- totalCount,
- countBySameCategory,
- naString,
- relatedEventCategory,
- relatedEventToShowDetailsFor,
- subject,
- descriptor,
- nodeEventsOfTypeLinkNavProps,
- nodeEventsLinkNavProps,
- nodeDetailLinkNavProps,
- nodesLinkNavProps,
- ]);
-
- if (!relatedsReady) {
- return ;
- }
-
- /**
- * 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 ;
- }
+ }, [event]);
return (
-
+
@@ -288,23 +116,49 @@ export const EventDetail = memo(function ({
-
+
- {sections.map(({ sectionTitle, entries }, index) => {
- const displayEntries = entriesForDisplay(entries);
+
+
+ );
+});
+
+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: {key},
+ descriptions: deepObjectEntries(value).map(([path, fieldValue]) => ({
+ title: {path.join('.')},
+ description: {String(fieldValue)},
+ })),
+ };
+ returnValue.push(section);
+ }
+ return returnValue;
+ }, [event]);
+ return (
+ <>
+ {sections.map(({ namespace, descriptions }, index) => {
return (
{index === 0 ? null : }
- {sectionTitle}
+ {namespace}
@@ -315,12 +169,136 @@ export const EventDetail = memo(function ({
align="left"
titleProps={{ className: 'desc-title' }}
compressed
- listItems={displayEntries}
+ listItems={descriptions}
/>
{index === sections.length - 1 ? null : }
);
})}
-
+ >
+ );
+}
+
+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: (
+
+ ),
+ ...nodeEventsLinkNavProps,
+ },
+ {
+ text: (
+
+ ),
+ ...nodeEventsOfTypeLinkNavProps,
+ },
+ {
+ text: ,
+ },
+ ];
+ }, [
+ breadcrumbEventCategory,
+ countByCategory,
+ event,
+ nodeDetailLinkNavProps,
+ nodeEventsLinkNavProps,
+ nodeName,
+ relatedEventCount,
+ nodesLinkNavProps,
+ nodeEventsOfTypeLinkNavProps,
+ ]);
+ return ;
+}
+
+const StyledDescriptionList = memo(styled(EuiDescriptionList)`
+ &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title {
+ max-width: 8em;
+ overflow-wrap: break-word;
+ }
+ &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description {
+ max-width: calc(100% - 8.5em);
+ overflow-wrap: break-word;
+ }
+`);
+
+// Also prevents horizontal scrollbars on long descriptive names
+const StyledDescriptiveName = memo(styled(EuiText)`
+ padding-right: 1em;
+ overflow-wrap: break-word;
+`);
+
+const StyledFlexTitle = memo(styled('h3')`
+ display: flex;
+ flex-flow: row;
+ font-size: 1.2em;
+`);
+const StyledTitleRule = memo(styled('hr')`
+ &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override {
+ display: block;
+ flex: 1;
+ margin-left: 0.5em;
+ }
+`);
+
+const TitleHr = memo(() => {
+ return (
+
);
});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx
index da5cb1acfed6..df9cbe9ced54 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx
@@ -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 () {
);
} else {
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx
similarity index 74%
rename from x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx
rename to x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx
index 48d5089eb564..04e9de61f625 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx
@@ -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 (
- {processEvent === null ? : }
+ {processEvent === null ? (
+
+ ) : (
+
+ )}
);
});
@@ -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 (
<>
-
+
@@ -201,7 +189,7 @@ const NodeDetailView = memo(function NodeDetailView({
-
+
@@ -26,11 +28,21 @@ export function NodeEvents({ nodeID }: { nodeID: string }) {
selectors.relatedEventsStats(state)(nodeID)
);
if (processEvent === null || relatedEventsStats === undefined) {
- return ;
+ return (
+
+
+
+ );
} else {
return (
-
+
+
+
);
}
@@ -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: (
-
- ),
- ...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]);
-
- const eventDetailHref = useSelector((state: ResolverState) =>
- selectors.relativeHref(state)({
- panelView: 'eventDetail',
- panelParameters: { nodeID: processEntityId, eventType: name, eventID: processEntityId },
- })
- );
+ }, [relatedStats.events.byCategory]);
- const eventDetailNavProps = useNavigateOrReplace({
- search: eventDetailHref,
- });
const columns = useMemo>>(
() => [
{
@@ -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 {name};
+ render(eventType: string) {
+ return (
+
+ {eventType}
+
+ );
},
},
],
- [eventDetailNavProps]
+ [nodeID]
);
+ return items={rows} columns={columns} sorting />;
+});
+
+const NodeEventsBreadcrumbs = memo(function ({
+ nodeID,
+ nodeName,
+ totalEventCount,
+}: {
+ nodeID: string;
+ nodeName: React.ReactNode;
+ totalEventCount: number;
+}) {
return (
- <>
-
-
- items={rows} columns={columns} sorting />
- >
+
+ ),
+ ...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 (
+
+ {children}
+
+ );
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx
index afff8d4b75c1..281794ac24d2 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx
@@ -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) => 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 (
- <>
-
- {shouldShowLimitWarning && typeof numberDisplayed !== 'undefined' && numberMissing ? (
-
- ) : null}
-
- <>
- {matchingEventEntries.map((eventView, index) => {
- const { subject, descriptor = '' } = eventView.name;
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {index === matchingEventEntries.length - 1 ? null : }
-
- );
- })}
- >
- >
- );
-});
-
-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 (
-
+ {eventCount === undefined || processEvent === null ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
);
-}
+});
-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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
+});
- 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) => (
+
+
+ {index === events.length - 1 ? null : }
+
+ ))}
+ >
);
+});
- 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: (
-
- ),
- ...nodeEventsNavProps,
- },
- {
- text: (
-
- ),
- onClick: () => {},
- },
- ];
- }, [
- eventType,
- eventsString,
- matchingEventEntries.length,
- processName,
- totalCount,
- nodeDetailNavProps,
- nodesLinkNavProps,
- nodeEventsNavProps,
- ]);
- if (!relatedsReady) {
- return ;
- }
+ const nodeEventsNavProps = useLinkProps({
+ panelView: 'nodeEvents',
+ panelParameters: { nodeID },
+ });
return (
-
+ ),
+ ...nodeEventsNavProps,
+ },
+ {
+ text: (
+
+ ),
+ },
+ ]}
/>
);
});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx
index 6113cea4c4ed..8fc6e7cc66c7 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx
@@ -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 ;
+ render(name: string | undefined, item: ProcessTableView) {
+ return ;
},
},
{
@@ -132,42 +98,26 @@ export const NodeList = memo(() => {
[]
);
- const { processNodePositions } = useSelector(selectors.layout);
- const nodeHrefs: Map = 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 (
-
- {showWarning && }
+
+ {showWarning && }
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) => {
+ linkProps.onClick(mouseEvent);
+ dispatch({
+ type: 'userBroughtProcessIntoView',
+ payload: {
+ process: event,
+ time: timestamp(),
+ },
+ });
+ },
+ [timestamp, linkProps, dispatch, event]
+ );
return (
-
+
{name === '' ? (
{i18n.translate(
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx
index 3b10a8db2bf1..199758145f11 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx
@@ -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 (
<>
-
+
{translatedErrorMessage}
@@ -60,4 +54,3 @@ export const PanelContentError = memo(function ({
>
);
});
-PanelContentError.displayName = 'TableServiceError';
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
index a7d76277c6ab..5ca34b33b239 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
@@ -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 (``s)
* spliced into text children at word boundaries.
@@ -85,31 +60,6 @@ export const StyledTime = memo(styled('time')`
text-align: start;
`);
-type Breadcrumbs = Parameters[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 (
- <>
-
-
-
-
- >
- );
-});
-
/**
* 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',
+ });
}
}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx
index 864990e4d96a..2de0bf5d320e 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx
@@ -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 (
<>
-
+
{waitingString}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx
index c5d5ae53a558..09a25ac125a2 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx
@@ -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;
+ }
+`;
diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
index 65ec395080f8..4d647760edb9 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
@@ -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"
>
diff --git a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx
index fb4d4d289d25..7def5d3362d4 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx
@@ -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;
+ }
+`;
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
index 495cd238d22f..5406b444cee5 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
@@ -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');
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts b/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts
new file mode 100644
index 000000000000..5645edec7e1c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts
@@ -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;
+
+/**
+ * Get an `onClick` function and an `href` string. Use these as props for `` 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,
+ });
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts
index f994350132c3..6810837ae031 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts
@@ -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,
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts
deleted file mode 100644
index 9fc74a7567c4..000000000000
--- a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts
+++ /dev/null
@@ -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]
- );
-}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
index b796913118c9..5bc911fb075b 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
@@ -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
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
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);
};
/**
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts
index f54472141c1d..1a871891b1ed 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts
@@ -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 = 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.
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 0ff132282034..b7809fa7659b 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -15898,19 +15898,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}件のイベント",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 683cbdbb6bc8..9d1d3cc28323 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -15908,19 +15908,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} 个事件",
diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
index f3d1eb60bf1c..d70d46fcbc01 100644
--- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
+++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
@@ -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(() => {