Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Show proper icon for termination status of all processes #73235

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Does this have to run the .has every time it renders? Could it avoid that if it were memoized?

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({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Not related to this PR, but I'm having trouble understanding why this file isn't part of assets

isProcessTerminated,
isProcessOrigin,
}: {
isProcessTerminated: boolean;
isProcessOrigin: boolean;
}) {
const { cubeAssetsForNode } = useResolverTheme();
const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, false);

return (
<>
Expand Down