Skip to content

Commit 63a08b4

Browse files
authored
[Security Solution][Resolver] Add events link to Process Detail Panel (#76195) (#76779)
* [Security_Solution][Resolver]Add events link to Process Detail Panel
1 parent fc27aee commit 63a08b4

5 files changed

Lines changed: 77 additions & 4 deletions

File tree

x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('Resolver Data Middleware', () => {
6060
let firstChildNodeInTree: TreeNode;
6161
let eventStatsForFirstChildNode: { total: number; byCategory: Record<string, number> };
6262
let categoryToOverCount: string;
63+
let aggregateCategoryTotalForFirstChildNode: number;
6364
let tree: ResolverTree;
6465

6566
/**
@@ -74,6 +75,7 @@ describe('Resolver Data Middleware', () => {
7475
firstChildNodeInTree,
7576
eventStatsForFirstChildNode,
7677
categoryToOverCount,
78+
aggregateCategoryTotalForFirstChildNode,
7779
} = mockedTree());
7880
if (tree) {
7981
dispatchTree(tree);
@@ -139,6 +141,13 @@ describe('Resolver Data Middleware', () => {
139141
expect(notDisplayed(typeCounted)).toBe(0);
140142
}
141143
});
144+
it('should return an overall correct count for the number of related events', () => {
145+
const aggregateTotalByEntityId = selectors.relatedEventAggregateTotalByEntityId(
146+
store.getState()
147+
);
148+
const countForId = aggregateTotalByEntityId(firstChildNodeInTree.id);
149+
expect(countForId).toBe(aggregateCategoryTotalForFirstChildNode);
150+
});
142151
});
143152
describe('when data was received and stats show more related events than the API can provide', () => {
144153
beforeEach(() => {
@@ -263,6 +272,7 @@ function mockedTree() {
263272
tree: tree!,
264273
firstChildNodeInTree,
265274
eventStatsForFirstChildNode: statsResults.eventStats,
275+
aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal,
266276
categoryToOverCount: statsResults.firstCategory,
267277
};
268278
}
@@ -289,13 +299,20 @@ function compileStatsForChild(
289299
};
290300
/** The category of the first event. */
291301
firstCategory: string;
302+
aggregateCategoryTotal: number;
292303
} {
293304
const totalRelatedEvents = node.relatedEvents.length;
294305
// For the purposes of testing, we pick one category to fake an extra event for
295306
// so we can test if the event limit selectors do the right thing.
296307

297308
let firstCategory: string | undefined;
298309

310+
// This is the "aggregate total" which is displayed to users as the total count
311+
// of related events for the node. It is tallied by incrementing for every discrete
312+
// event.category in an event.category array (or just 1 for a plain string). E.g. two events
313+
// categories 'file' and ['dns','network'] would have an `aggregate total` of 3.
314+
let aggregateCategoryTotal: number = 0;
315+
299316
const compiledStats = node.relatedEvents.reduce(
300317
(counts: Record<string, number>, relatedEvent) => {
301318
// `relatedEvent.event.category` is `string | string[]`.
@@ -311,6 +328,7 @@ function compileStatsForChild(
311328

312329
// Increment the count of events with this category
313330
counts[category] = counts[category] ? counts[category] + 1 : 1;
331+
aggregateCategoryTotal++;
314332
}
315333
return counts;
316334
},
@@ -328,5 +346,6 @@ function compileStatsForChild(
328346
byCategory: compiledStats,
329347
},
330348
firstCategory,
349+
aggregateCategoryTotal,
331350
};
332351
}

x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,26 @@ export const relatedEventsStats: (
167167
}
168168
);
169169

170+
/**
171+
* This returns the "aggregate total" for related events, tallied as the sum
172+
* of their individual `event.category`s. E.g. a [DNS, Network] would count as two
173+
* towards the aggregate total.
174+
*/
175+
export const relatedEventAggregateTotalByEntityId: (
176+
state: DataState
177+
) => (entityId: string) => number = createSelector(relatedEventsStats, (relatedStats) => {
178+
return (entityId) => {
179+
const statsForEntity = relatedStats(entityId);
180+
if (statsForEntity === undefined) {
181+
return 0;
182+
}
183+
return Object.values(statsForEntity?.events?.byCategory || {}).reduce(
184+
(sum, val) => sum + val,
185+
0
186+
);
187+
};
188+
});
189+
170190
/**
171191
* returns a map of entity_ids to related event data.
172192
*/

x-pack/plugins/security_solution/public/resolver/store/selectors.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ export const relatedEventsStats: (
114114
dataSelectors.relatedEventsStats
115115
);
116116

117+
/**
118+
* This returns the "aggregate total" for related events, tallied as the sum
119+
* of their individual `event.category`s. E.g. a [DNS, Network] would count as two
120+
* towards the aggregate total.
121+
*/
122+
export const relatedEventAggregateTotalByEntityId: (
123+
state: ResolverState
124+
) => (nodeID: string) => number = composeSelectors(
125+
dataStateSelector,
126+
dataSelectors.relatedEventAggregateTotalByEntityId
127+
);
128+
117129
/**
118130
* Map of related events... by entity id
119131
*/

x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { EventCountsForProcess } from './event_counts_for_process';
1717
import { ProcessDetails } from './process_details';
1818
import { ProcessListWithCounts } from './process_list_with_counts';
1919
import { RelatedEventDetail } from './related_event_detail';
20+
import { ResolverState } from '../../types';
2021

2122
/**
2223
* The team decided to use this table to determine which breadcrumbs/view to display:
@@ -102,6 +103,12 @@ const PanelContent = memo(function PanelContent() {
102103
? relatedEventStats(idFromParams)
103104
: undefined;
104105

106+
const parentCount = useSelector((state: ResolverState) => {
107+
if (idFromParams === '') {
108+
return 0;
109+
}
110+
return selectors.relatedEventAggregateTotalByEntityId(state)(idFromParams);
111+
});
105112
/**
106113
* Determine which set of breadcrumbs to display based on the query parameters
107114
* for the table & breadcrumb nav.
@@ -186,9 +193,6 @@ const PanelContent = memo(function PanelContent() {
186193
}
187194

188195
if (panelToShow === 'relatedEventDetail') {
189-
const parentCount: number = Object.values(
190-
relatedStatsForIdFromParams?.events.byCategory || {}
191-
).reduce((sum, val) => sum + val, 0);
192196
return (
193197
<RelatedEventDetail
194198
relatedEventId={crumbId}
@@ -199,7 +203,7 @@ const PanelContent = memo(function PanelContent() {
199203
}
200204
// The default 'Event List' / 'List of all processes' view
201205
return <ProcessListWithCounts />;
202-
}, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow]);
206+
}, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow, parentCount]);
203207

204208
return <>{panelInstance}</>;
205209
});

x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
EuiText,
1414
EuiTextColor,
1515
EuiDescriptionList,
16+
EuiLink,
1617
} from '@elastic/eui';
1718
import styled from 'styled-components';
1819
import { FormattedMessage } from 'react-intl';
@@ -58,6 +59,9 @@ export const ProcessDetails = memo(function ProcessDetails({
5859
const isProcessTerminated = useSelector((state: ResolverState) =>
5960
selectors.isProcessTerminated(state)(entityId)
6061
);
62+
const relatedEventTotal = useSelector((state: ResolverState) => {
63+
return selectors.relatedEventAggregateTotalByEntityId(state)(entityId);
64+
});
6165
const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => {
6266
const eventTime = event.eventTimestamp(processEvent);
6367
const dateTime = eventTime === undefined ? null : formatDate(eventTime);
@@ -164,6 +168,12 @@ export const ProcessDetails = memo(function ProcessDetails({
164168
return cubeAssetsForNode(isProcessTerminated, false);
165169
}, [processEvent, cubeAssetsForNode, isProcessTerminated]);
166170

171+
const handleEventsLinkClick = useMemo(() => {
172+
return () => {
173+
pushToQueryParams({ crumbId: entityId, crumbEvent: 'all' });
174+
};
175+
}, [entityId, pushToQueryParams]);
176+
167177
const titleID = useMemo(() => htmlIdGenerator('resolverTable')(), []);
168178
return (
169179
<>
@@ -185,6 +195,14 @@ export const ProcessDetails = memo(function ProcessDetails({
185195
<span id={titleID}>{descriptionText}</span>
186196
</EuiTextColor>
187197
</EuiText>
198+
<EuiSpacer size="s" />
199+
<EuiLink onClick={handleEventsLinkClick}>
200+
<FormattedMessage
201+
id="xpack.securitySolution.endpoint.resolver.panel.processDescList.numberOfEvents"
202+
values={{ relatedEventTotal }}
203+
defaultMessage="{relatedEventTotal} Events"
204+
/>
205+
</EuiLink>
188206
<EuiSpacer size="l" />
189207
<StyledDescriptionList
190208
data-test-subj="resolver:node-detail"

0 commit comments

Comments
 (0)