Skip to content

Commit

Permalink
[Security Solution][Resolver] Add events link to Process Detail Panel (
Browse files Browse the repository at this point in the history
…elastic#76195) (elastic#76779)

* [Security_Solution][Resolver]Add events link to Process Detail Panel
  • Loading branch information
bkimmel authored Sep 4, 2020
1 parent fc27aee commit 63a08b4
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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;

/**
Expand All @@ -74,6 +75,7 @@ describe('Resolver Data Middleware', () => {
firstChildNodeInTree,
eventStatsForFirstChildNode,
categoryToOverCount,
aggregateCategoryTotalForFirstChildNode,
} = mockedTree());
if (tree) {
dispatchTree(tree);
Expand Down Expand Up @@ -139,6 +141,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(() => {
Expand Down Expand Up @@ -263,6 +272,7 @@ function mockedTree() {
tree: tree!,
firstChildNodeInTree,
eventStatsForFirstChildNode: statsResults.eventStats,
aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal,
categoryToOverCount: statsResults.firstCategory,
};
}
Expand All @@ -289,13 +299,20 @@ 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
// so we can test if the event limit selectors do the right thing.

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[]`.
Expand All @@ -311,6 +328,7 @@ function compileStatsForChild(

// Increment the count of events with this category
counts[category] = counts[category] ? counts[category] + 1 : 1;
aggregateCategoryTotal++;
}
return counts;
},
Expand All @@ -328,5 +346,6 @@ function compileStatsForChild(
byCategory: compiledStats,
},
firstCategory,
aggregateCategoryTotal,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}
Expand All @@ -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}</>;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EuiText,
EuiTextColor,
EuiDescriptionList,
EuiLink,
} from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from 'react-intl';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 (
<>
Expand All @@ -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"
Expand Down

0 comments on commit 63a08b4

Please sign in to comment.