Skip to content

Commit

Permalink
[Security Solution] Show proper icon for termination status of all pr…
Browse files Browse the repository at this point in the history
…ocesses (#73235)

* Show proper icon for termination status of all processes

* Add basic test for isProcessTerminated selector
  • Loading branch information
kqualters-elastic authored Jul 28, 2020
1 parent ef83e77 commit 8c52d39
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
mockTreeWithNoAncestorsAnd2Children,
mockTreeWith2AncestorsAndNoChildren,
mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents,
mockTreeWithAllProcessesTerminated,
} from '../mocks/resolver_tree';
import { uniquePidForProcess } from '../../models/process_event';
import { EndpointEvent } from '../../../../common/endpoint/types';
Expand Down Expand Up @@ -299,6 +300,34 @@ describe('data state', () => {
expect(selectors.ariaFlowtoCandidate(state())(secondAncestorID)).toBe(null);
});
});
describe('with a tree with all processes terminated', () => {
const originID = 'c';
const firstAncestorID = 'b';
const secondAncestorID = 'a';
beforeEach(() => {
actions.push({
type: 'serverReturnedResolverData',
payload: {
result: mockTreeWithAllProcessesTerminated({
originID,
firstAncestorID,
secondAncestorID,
}),
// this value doesn't matter
databaseDocumentID: '',
},
});
});
it('should have origin as terminated', () => {
expect(selectors.isProcessTerminated(state())(originID)).toBe(true);
});
it('should have first ancestor as termianted', () => {
expect(selectors.isProcessTerminated(state())(firstAncestorID)).toBe(true);
});
it('should have second ancestor as terminated', () => {
expect(selectors.isProcessTerminated(state())(secondAncestorID)).toBe(true);
});
});
describe('with a tree with 2 children and no ancestors', () => {
const originID = 'c';
const firstChildID = 'd';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,19 @@ export const terminatedProcesses = createSelector(resolverTreeResponse, function
);
});

/**
* A function that given an entity id returns a boolean indicating if the id is in the set of terminated processes.
*/
export const isProcessTerminated = createSelector(terminatedProcesses, function (
/* eslint-disable no-shadow */
terminatedProcesses
/* eslint-enable no-shadow */
) {
return (entityId: string) => {
return terminatedProcesses.has(entityId);
};
});

/**
* Process events that will be graphed.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ export function mockEndpointEvent({
name,
parentEntityId,
timestamp,
lifecycleType,
}: {
entityID: string;
name: string;
parentEntityId: string | undefined;
timestamp: number;
lifecycleType?: string;
}): EndpointEvent {
return {
'@timestamp': timestamp,
event: {
type: 'start',
type: lifecycleType ? lifecycleType : 'start',
category: 'process',
},
process: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,69 @@ export function mockTreeWith2AncestorsAndNoChildren({
} as unknown) as ResolverTree;
}

export function mockTreeWithAllProcessesTerminated({
originID,
firstAncestorID,
secondAncestorID,
}: {
secondAncestorID: string;
firstAncestorID: string;
originID: string;
}): ResolverTree {
const secondAncestor: ResolverEvent = mockEndpointEvent({
entityID: secondAncestorID,
name: 'a',
parentEntityId: 'none',
timestamp: 0,
});
const firstAncestor: ResolverEvent = mockEndpointEvent({
entityID: firstAncestorID,
name: 'b',
parentEntityId: secondAncestorID,
timestamp: 1,
});
const originEvent: ResolverEvent = mockEndpointEvent({
entityID: originID,
name: 'c',
parentEntityId: firstAncestorID,
timestamp: 2,
});
const secondAncestorTermination: ResolverEvent = mockEndpointEvent({
entityID: secondAncestorID,
name: 'a',
parentEntityId: 'none',
timestamp: 0,
lifecycleType: 'end',
});
const firstAncestorTermination: ResolverEvent = mockEndpointEvent({
entityID: firstAncestorID,
name: 'b',
parentEntityId: secondAncestorID,
timestamp: 1,
lifecycleType: 'end',
});
const originEventTermination: ResolverEvent = mockEndpointEvent({
entityID: originID,
name: 'c',
parentEntityId: firstAncestorID,
timestamp: 2,
lifecycleType: 'end',
});
return ({
entityID: originID,
children: {
childNodes: [],
},
ancestry: {
ancestors: [
{ lifecycle: [secondAncestor, secondAncestorTermination] },
{ lifecycle: [firstAncestor, firstAncestorTermination] },
],
},
lifecycle: [originEvent, originEventTermination],
} as unknown) as ResolverTree;
}

export function mockTreeWithNoAncestorsAnd2Children({
originID,
firstChildID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelecto
*/
export const isAnimating = composeSelectors(cameraStateSelector, cameraSelectors.isAnimating);

/**
* Whether or not a given entity id is in the set of termination events.
*/
export const isProcessTerminated = composeSelectors(
dataStateSelector,
dataSelectors.isProcessTerminated
);

/**
* Given a nodeID (aka entity_id) get the indexed process event.
* Legacy functions take process events instead of nodeID, use this to get
Expand Down
20 changes: 2 additions & 18 deletions x-pack/plugins/security_solution/public/resolver/view/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,19 +162,10 @@ const PanelContent = memo(function PanelContent() {
return 'processListWithCounts';
}, [uiSelectedEvent, crumbEvent, crumbId, graphableProcessEntityIds]);

const terminatedProcesses = useSelector(selectors.terminatedProcesses);
const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined;
const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false;

const panelInstance = useMemo(() => {
if (panelToShow === 'processDetails') {
return (
<ProcessDetails
processEvent={uiSelectedEvent!}
pushToQueryParams={pushToQueryParams}
isProcessTerminated={isProcessTerminated}
isProcessOrigin={false}
/>
<ProcessDetails processEvent={uiSelectedEvent!} pushToQueryParams={pushToQueryParams} />
);
}

Expand Down Expand Up @@ -213,21 +204,14 @@ const PanelContent = memo(function PanelContent() {
);
}
// The default 'Event List' / 'List of all processes' view
return (
<ProcessListWithCounts
pushToQueryParams={pushToQueryParams}
isProcessTerminated={isProcessTerminated}
isProcessOrigin={false}
/>
);
return <ProcessListWithCounts pushToQueryParams={pushToQueryParams} />;
}, [
uiSelectedEvent,
crumbEvent,
crumbId,
pushToQueryParams,
relatedStatsForIdFromParams,
panelToShow,
isProcessTerminated,
]);

return <>{panelInstance}</>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import {
htmlIdGenerator,
Expand All @@ -15,6 +16,7 @@ import {
} from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from 'react-intl';
import * as selectors from '../../store/selectors';
import * as event from '../../../../common/endpoint/models/event';
import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities';
import {
Expand All @@ -41,16 +43,14 @@ const StyledDescriptionList = styled(EuiDescriptionList)`
*/
export const ProcessDetails = memo(function ProcessDetails({
processEvent,
isProcessTerminated,
isProcessOrigin,
pushToQueryParams,
}: {
processEvent: ResolverEvent;
isProcessTerminated: boolean;
isProcessOrigin: boolean;
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
}) {
const processName = event.eventName(processEvent);
const entityId = event.entityId(processEvent);
const isProcessTerminated = useSelector(selectors.isProcessTerminated)(entityId);
const processInfoEntry = useMemo(() => {
const eventTime = event.eventTimestamp(processEvent);
const dateTime = eventTime ? formatDate(eventTime) : '';
Expand Down Expand Up @@ -151,8 +151,8 @@ export const ProcessDetails = memo(function ProcessDetails({
if (!processEvent) {
return { descriptionText: '' };
}
return cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
}, [processEvent, cubeAssetsForNode, isProcessTerminated, isProcessOrigin]);
return cubeAssetsForNode(isProcessTerminated, false);
}, [processEvent, cubeAssetsForNode, isProcessTerminated]);

const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []);
return (
Expand All @@ -161,10 +161,7 @@ export const ProcessDetails = memo(function ProcessDetails({
<EuiSpacer size="l" />
<EuiTitle size="xs">
<h4 aria-describedby={titleId}>
<CubeForProcess
isProcessTerminated={isProcessTerminated}
isProcessOrigin={isProcessOrigin}
/>
<CubeForProcess isProcessTerminated={isProcessTerminated} />
{processName}
</h4>
</EuiTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,8 @@ const StyledLimitWarning = styled(LimitWarning)`
*/
export const ProcessListWithCounts = memo(function ProcessListWithCounts({
pushToQueryParams,
isProcessTerminated,
isProcessOrigin,
}: {
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
isProcessTerminated: boolean;
isProcessOrigin: boolean;
}) {
interface ProcessTableView {
name: string;
Expand All @@ -65,6 +61,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({

const dispatch = useResolverDispatch();
const { timestamp } = useContext(SideEffectContext);
const isProcessTerminated = useSelector(selectors.isProcessTerminated);
const handleBringIntoViewClick = useCallback(
(processTableViewItem) => {
dispatch({
Expand Down Expand Up @@ -92,6 +89,8 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
sortable: true,
truncateText: true,
render(name: string, item: ProcessTableView) {
const entityId = event.entityId(item.event);
const isTerminated = isProcessTerminated(entityId);
return name === '' ? (
<EuiBadge color="warning">
{i18n.translate(
Expand All @@ -108,10 +107,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' });
}}
>
<CubeForProcess
isProcessTerminated={isProcessTerminated}
isProcessOrigin={isProcessOrigin}
/>
<CubeForProcess isProcessTerminated={isTerminated} />
{name}
</EuiButtonEmpty>
);
Expand Down Expand Up @@ -143,7 +139,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
},
},
],
[pushToQueryParams, handleBringIntoViewClick, isProcessOrigin, isProcessTerminated]
[pushToQueryParams, handleBringIntoViewClick, isProcessTerminated]
);

const { processNodePositions } = useSelector(selectors.layout);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import { useResolverTheme } from '../assets';
*/
export const CubeForProcess = memo(function CubeForProcess({
isProcessTerminated,
isProcessOrigin,
}: {
isProcessTerminated: boolean;
isProcessOrigin: boolean;
}) {
const { cubeAssetsForNode } = useResolverTheme();
const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, false);

return (
<>
Expand Down

0 comments on commit 8c52d39

Please sign in to comment.