[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:
Brent Kimmel 2020-09-02 09:48:33 -04:00 committed by GitHub
parent 093f588720
commit 5345af9281
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 4 deletions

View file

@ -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,
};
}

View file

@ -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.
*/

View file

@ -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
*/

View file

@ -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}</>;
});

View file

@ -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"