in x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx [67:417]
export function ProcessTreeNode({
process,
isSessionLeader = false,
depth = 0,
onProcessSelected,
jumpToEntityId,
investigatedAlertId,
selectedProcess,
showTimestamp,
verboseMode,
searchResults,
scrollerRef,
onChangeJumpToEventVisibility,
onShowAlertDetails,
onJumpToOutput,
loadPreviousButton,
loadNextButton,
handleCollapseProcessTree,
trackEvent,
}: ProcessDeps) {
const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader || process.autoExpand);
const [alertsExpanded, setAlertsExpanded] = useState(false);
const { searchMatched } = process;
const dateFormat = useDateFormat();
useEffect(() => {
setChildrenExpanded(process.autoExpand);
}, [process.autoExpand]);
// forces nodes to expand if the selected process is a descendant
useEffect(() => {
if (!childrenExpanded && selectedProcess) {
if (selectedProcess.isDescendantOf(process)) {
setChildrenExpanded(true);
}
}
}, [selectedProcess, process, childrenExpanded]);
const alerts = process.getAlerts();
const hasAlerts = !!alerts.length;
const hasOutputs = process.hasOutput();
const hasInvestigatedAlert = useMemo(
() =>
!!(
hasAlerts &&
alerts.find(
(alert) => investigatedAlertId && investigatedAlertId === alert.kibana?.alert?.uuid
)
),
[hasAlerts, alerts, investigatedAlertId]
);
const isSelected = selectedProcess?.id === process.id;
const styles = useStyles({ depth, hasAlerts, hasInvestigatedAlert, isSelected, isSessionLeader });
const buttonStyles = useButtonStyles();
const nodeRef = useVisible({
viewPortEl: scrollerRef.current,
visibleCallback: useCallback(
(isVisible: any, isAbove: any) => {
onChangeJumpToEventVisibility(isVisible, isAbove);
},
[onChangeJumpToEventVisibility]
),
shouldAddListener: hasInvestigatedAlert,
});
const alertTypeCounts = useMemo(() => {
const alertCounts: AlertTypeCount[] = chain(alerts)
.groupBy((alert) => {
const category = alert.event?.category;
if (Array.isArray(category)) {
return category?.[0];
}
return category;
})
.map((processAlerts, alertCategory) => ({
category: alertCategory as ProcessEventAlertCategory,
count: processAlerts.length,
}))
.value();
return alertCounts;
}, [alerts]);
useEffect(() => {
if (process.id === selectedProcess?.id && nodeRef.current?.scrollIntoView) {
nodeRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, [selectedProcess, process, nodeRef]);
// Automatically expand alerts list when investigating an alert
useEffect(() => {
if (hasInvestigatedAlert) {
setAlertsExpanded(true);
}
}, [hasInvestigatedAlert]);
const onChildrenToggle = useCallback(() => {
const newValue = !childrenExpanded;
setChildrenExpanded(newValue);
trackEvent(newValue ? 'children_opened' : 'children_closed');
}, [childrenExpanded, trackEvent]);
const onAlertsToggle = useCallback(() => {
const newValue = !alertsExpanded;
setAlertsExpanded(newValue);
trackEvent(newValue ? 'alerts_opened' : 'alerts_closed');
}, [alertsExpanded, trackEvent]);
const onProcessClicked = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
const selection = window.getSelection();
// do not select the command if the user was just selecting text for copy.
if (selection && selection.type === 'Range') {
return;
}
// we pass true here to let the parent SessionView component that the process was selected
// by a user clicking on a row in the tree
onProcessSelected?.(process, true);
if (isSessionLeader && scrollerRef.current) {
scrollerRef.current.scrollTop = 0;
}
trackEvent('process_selected');
},
[isSessionLeader, onProcessSelected, process, scrollerRef, trackEvent]
);
const processDetails = process.getDetails();
const hasExec = process.hasExec();
const onOutputClicked = useCallback(() => {
const entityId = processDetails.process?.entity_id;
if (entityId) {
onJumpToOutput(entityId);
}
trackEvent('output_clicked');
}, [onJumpToOutput, processDetails.process?.entity_id, trackEvent]);
const processIcon = useMemo(() => {
if (!process.parent) {
return 'unlink';
} else if (hasExec) {
return 'console';
} else {
return 'branch';
}
}, [hasExec, process.parent]);
const iconTooltip = useMemo(() => {
if (!process.parent) {
return i18n.translate('xpack.sessionView.processNode.tooltipOrphan', {
defaultMessage: 'Process missing parent (orphan)',
});
} else if (hasExec) {
return i18n.translate('xpack.sessionView.processNode.tooltipExec', {
defaultMessage: "Process exec'd",
});
} else {
return i18n.translate('xpack.sessionView.processNode.tooltipFork', {
defaultMessage: 'Process forked (no exec)',
});
}
}, [hasExec, process.parent]);
const children = process.getChildren(verboseMode);
const user = processDetails?.process?.user;
const userName = useMemo(() => {
if (user?.name) {
return user.name;
} else if (user?.id === '0') {
return 'root';
} else if (user?.id) {
return `uid: ${user?.id}`;
}
return '-';
}, [user?.id, user?.name]);
if (!processDetails?.process) {
return null;
}
const id = process.id;
const {
args,
name,
tty,
parent,
working_directory: workingDirectory,
start,
} = processDetails.process;
const shouldRenderChildren = isSessionLeader || (childrenExpanded && children?.length > 0);
const childrenTreeDepth = depth + 1;
const showUserEscalation = !isSessionLeader && !!user?.id && user.id !== parent?.user?.id;
const interactiveSession = !!tty;
const sessionIcon = interactiveSession ? 'desktop' : 'gear';
const iconTestSubj = hasExec
? 'sessionView:processTreeNodeExecIcon'
: 'sessionView:processTreeNodeForkIcon';
const timeStampsNormal = formatDate(start, dateFormat);
const promptText = `${workingDirectory ?? ''} ${args?.join(' ')}`;
return (
<div>
<div
data-id={id}
key={id + searchMatched}
css={styles.processNode}
data-test-subj="sessionView:processTreeNode"
ref={nodeRef}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<div
data-test-subj="sessionView:processTreeNodeRow"
css={styles.wrapper}
onClick={onProcessClicked}
>
{isSessionLeader ? (
<span css={styles.sessionLeader}>
<EuiIcon type={sessionIcon} css={styles.icon} />
<Nbsp />
<b css={styles.darkText}>{dataOrDash(name || args?.[0])}</b>
<Nbsp />
<span>
<FormattedMessage id="xpack.sessionView.startedBy" defaultMessage="started by" />
</span>
<Nbsp />
<EuiIcon type="user" />
<Nbsp />
<b css={styles.darkText}>{userName}</b>
<Nbsp />
<span css={styles.jumpToTop}>
<EuiToolTip title={COLLAPSE_ALL}>
<EuiButtonIcon
size="xs"
iconType="fold"
onClick={handleCollapseProcessTree}
aria-label={COLLAPSE_ALL}
/>
</EuiToolTip>
</span>
</span>
) : (
<>
{showTimestamp && (
<span data-test-subj="sessionView:processTreeNodeTimestamp" css={styles.timeStamp}>
{timeStampsNormal}
</span>
)}
<EuiToolTip position="top" content={iconTooltip}>
<EuiIcon data-test-subj={iconTestSubj} type={processIcon} css={styles.icon} />
</EuiToolTip>
<span css={styles.textSection}>
<TextHighlight
text={promptText}
match={process.searchMatched}
highlightStyle={styles.searchHighlight}
>
<SplitText css={styles.workingDir}>
{dataOrDash(workingDirectory) + ' '}
</SplitText>
<SplitText css={styles.darkText}>{`${dataOrDash(args?.[0])}`}</SplitText>
<SplitText>
{args && args.length > 1 ? ' ' + args?.slice(1).join(' ') : ''}
</SplitText>
</TextHighlight>
</span>
</>
)}
{showUserEscalation && (
<EuiButton
data-test-subj="sessionView:processTreeNodeRootEscalationFlag"
css={buttonStyles.userChangedButton}
aria-label={EXEC_USER_CHANGE}
>
{EXEC_USER_CHANGE} ({userName})
</EuiButton>
)}
{!isSessionLeader && children.length > 0 && (
<ChildrenProcessesButton isExpanded={childrenExpanded} onToggle={onChildrenToggle} />
)}
{hasAlerts && (
<AlertButton
onToggle={onAlertsToggle}
alertTypeCounts={alertTypeCounts}
isExpanded={alertsExpanded}
alertsCount={alerts.length}
/>
)}
{hasOutputs && <OutputButton onClick={onOutputClicked} />}
</div>
</div>
{alertsExpanded && (
<ProcessTreeAlerts
alerts={alerts}
alertTypeCounts={alertTypeCounts}
investigatedAlertId={investigatedAlertId}
isProcessSelected={isSelected}
onAlertSelected={onProcessClicked}
onShowAlertDetails={onShowAlertDetails}
/>
)}
{shouldRenderChildren && (
<div css={styles.children}>
{loadPreviousButton}
{children.map((child) => {
return (
<ProcessTreeNode
key={child.id}
process={child}
depth={childrenTreeDepth}
onProcessSelected={onProcessSelected}
onJumpToOutput={onJumpToOutput}
jumpToEntityId={jumpToEntityId}
investigatedAlertId={investigatedAlertId}
selectedProcess={selectedProcess}
showTimestamp={showTimestamp}
verboseMode={verboseMode}
searchResults={searchResults}
scrollerRef={scrollerRef}
onChangeJumpToEventVisibility={onChangeJumpToEventVisibility}
onShowAlertDetails={onShowAlertDetails}
trackEvent={trackEvent}
/>
);
})}
{loadNextButton}
</div>
)}
</div>
);
}