mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Resolver] Add events link to Process Detail Panel (#76195)
* [Security_Solution][Resolver]Add events link to Process Detail Panel
This commit is contained in:
parent
093f588720
commit
5345af9281
5 changed files with 77 additions and 4 deletions
|
@ -59,6 +59,7 @@ describe('Resolver Data Middleware', () => {
|
|||
let firstChildNodeInTree: TreeNode;
|
||||
let eventStatsForFirstChildNode: { total: number; byCategory: Record<string, number> };
|
||||
let categoryToOverCount: string;
|
||||
let aggregateCategoryTotalForFirstChildNode: number;
|
||||
let tree: ResolverTree;
|
||||
|
||||
/**
|
||||
|
@ -73,6 +74,7 @@ describe('Resolver Data Middleware', () => {
|
|||
firstChildNodeInTree,
|
||||
eventStatsForFirstChildNode,
|
||||
categoryToOverCount,
|
||||
aggregateCategoryTotalForFirstChildNode,
|
||||
} = mockedTree());
|
||||
if (tree) {
|
||||
dispatchTree(tree);
|
||||
|
@ -138,6 +140,13 @@ describe('Resolver Data Middleware', () => {
|
|||
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(() => {
|
||||
|
@ -262,6 +271,7 @@ function mockedTree() {
|
|||
tree: tree!,
|
||||
firstChildNodeInTree,
|
||||
eventStatsForFirstChildNode: statsResults.eventStats,
|
||||
aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal,
|
||||
categoryToOverCount: statsResults.firstCategory,
|
||||
};
|
||||
}
|
||||
|
@ -288,6 +298,7 @@ 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
|
||||
|
@ -295,6 +306,12 @@ function compileStatsForChild(
|
|||
|
||||
let firstCategory: string | undefined;
|
||||
|
||||
// This is the "aggregate total" which is displayed to users as the total count
|
||||
// of related events for the node. It is tallied by incrementing for every discrete
|
||||
// event.category in an event.category array (or just 1 for a plain string). E.g. two events
|
||||
// categories 'file' and ['dns','network'] would have an `aggregate total` of 3.
|
||||
let aggregateCategoryTotal: number = 0;
|
||||
|
||||
const compiledStats = node.relatedEvents.reduce(
|
||||
(counts: Record<string, number>, relatedEvent) => {
|
||||
// `relatedEvent.event.category` is `string | string[]`.
|
||||
|
@ -310,6 +327,7 @@ function compileStatsForChild(
|
|||
|
||||
// Increment the count of events with this category
|
||||
counts[category] = counts[category] ? counts[category] + 1 : 1;
|
||||
aggregateCategoryTotal++;
|
||||
}
|
||||
return counts;
|
||||
},
|
||||
|
@ -327,5 +345,6 @@ function compileStatsForChild(
|
|||
byCategory: compiledStats,
|
||||
},
|
||||
firstCategory,
|
||||
aggregateCategoryTotal,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -170,6 +170,26 @@ 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.
|
||||
*/
|
||||
export const relatedEventAggregateTotalByEntityId: (
|
||||
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
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* returns a map of entity_ids to related event data.
|
||||
*/
|
||||
|
|
|
@ -114,6 +114,18 @@ export const relatedEventsStats: (
|
|||
dataSelectors.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.
|
||||
*/
|
||||
export const relatedEventAggregateTotalByEntityId: (
|
||||
state: ResolverState
|
||||
) => (nodeID: string) => number = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.relatedEventAggregateTotalByEntityId
|
||||
);
|
||||
|
||||
/**
|
||||
* Map of related events... by entity id
|
||||
*/
|
||||
|
|
|
@ -17,6 +17,7 @@ import { EventCountsForProcess } from './event_counts_for_process';
|
|||
import { ProcessDetails } from './process_details';
|
||||
import { ProcessListWithCounts } from './process_list_with_counts';
|
||||
import { RelatedEventDetail } from './related_event_detail';
|
||||
import { ResolverState } from '../../types';
|
||||
|
||||
/**
|
||||
* The team decided to use this table to determine which breadcrumbs/view to display:
|
||||
|
@ -102,6 +103,12 @@ const PanelContent = memo(function PanelContent() {
|
|||
? relatedEventStats(idFromParams)
|
||||
: undefined;
|
||||
|
||||
const parentCount = useSelector((state: ResolverState) => {
|
||||
if (idFromParams === '') {
|
||||
return 0;
|
||||
}
|
||||
return selectors.relatedEventAggregateTotalByEntityId(state)(idFromParams);
|
||||
});
|
||||
/**
|
||||
* Determine which set of breadcrumbs to display based on the query parameters
|
||||
* for the table & breadcrumb nav.
|
||||
|
@ -186,9 +193,6 @@ const PanelContent = memo(function PanelContent() {
|
|||
}
|
||||
|
||||
if (panelToShow === 'relatedEventDetail') {
|
||||
const parentCount: number = Object.values(
|
||||
relatedStatsForIdFromParams?.events.byCategory || {}
|
||||
).reduce((sum, val) => sum + val, 0);
|
||||
return (
|
||||
<RelatedEventDetail
|
||||
relatedEventId={crumbId}
|
||||
|
@ -199,7 +203,7 @@ const PanelContent = memo(function PanelContent() {
|
|||
}
|
||||
// The default 'Event List' / 'List of all processes' view
|
||||
return <ProcessListWithCounts />;
|
||||
}, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow]);
|
||||
}, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow, parentCount]);
|
||||
|
||||
return <>{panelInstance}</>;
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiDescriptionList,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
@ -58,6 +59,9 @@ export const ProcessDetails = memo(function ProcessDetails({
|
|||
const isProcessTerminated = useSelector((state: ResolverState) =>
|
||||
selectors.isProcessTerminated(state)(entityId)
|
||||
);
|
||||
const relatedEventTotal = useSelector((state: ResolverState) => {
|
||||
return selectors.relatedEventAggregateTotalByEntityId(state)(entityId);
|
||||
});
|
||||
const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => {
|
||||
const eventTime = event.eventTimestamp(processEvent);
|
||||
const dateTime = eventTime === undefined ? null : formatDate(eventTime);
|
||||
|
@ -164,6 +168,12 @@ export const ProcessDetails = memo(function ProcessDetails({
|
|||
return cubeAssetsForNode(isProcessTerminated, false);
|
||||
}, [processEvent, cubeAssetsForNode, isProcessTerminated]);
|
||||
|
||||
const handleEventsLinkClick = useMemo(() => {
|
||||
return () => {
|
||||
pushToQueryParams({ crumbId: entityId, crumbEvent: 'all' });
|
||||
};
|
||||
}, [entityId, pushToQueryParams]);
|
||||
|
||||
const titleID = useMemo(() => htmlIdGenerator('resolverTable')(), []);
|
||||
return (
|
||||
<>
|
||||
|
@ -185,6 +195,14 @@ export const ProcessDetails = memo(function ProcessDetails({
|
|||
<span id={titleID}>{descriptionText}</span>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink onClick={handleEventsLinkClick}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.processDescList.numberOfEvents"
|
||||
values={{ relatedEventTotal }}
|
||||
defaultMessage="{relatedEventTotal} Events"
|
||||
/>
|
||||
</EuiLink>
|
||||
<EuiSpacer size="l" />
|
||||
<StyledDescriptionList
|
||||
data-test-subj="resolver:node-detail"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue