[Security Solution][Analyzer] Enable process title to open event preview (#210118)

## Summary

Updated process event title to be a link, opens a event preview of that
process event

#### `enableVisualizationsInFlyout` advanced setting is on:
Link is enabled


https://github.com/user-attachments/assets/a7d1992a-0b7f-436c-9137-c6626077661b


#### `enableVisualizationsInFlyout` advanced setting is off:
Link is not enabled (no change)


![image](https://github.com/user-attachments/assets/ae8f30dd-f54c-47a6-90e3-37eba8dc2a51)


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
christineweng 2025-02-07 12:28:51 -06:00 committed by GitHub
parent 7dd40580bd
commit 8c05633cb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 118 additions and 5 deletions

View file

@ -32,7 +32,13 @@ export const PanelRouter = memo(function ({
selectors.panelViewAndParameters(state.analyzer[id])
);
if (params.panelView === 'nodeDetail') {
return <NodeDetail id={id} nodeID={params.panelParameters.nodeID} />;
return (
<NodeDetail
id={id}
nodeID={params.panelParameters.nodeID}
nodeEventOnClick={nodeEventOnClick}
/>
);
} else if (params.panelView === 'nodeEvents') {
return <NodeEvents id={id} nodeID={params.panelParameters.nodeID} />;
} else if (params.panelView === 'nodeEventsInCategory') {

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import * as redux from 'react-redux';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import { NodeDetailView } from './node_detail';
import { useCubeAssets } from '../use_cube_assets';
import { useLinkProps } from '../use_link_props';
const mockUseCubeAssets = useCubeAssets as jest.Mock;
jest.mock('../use_cube_assets');
const mockUseLinkProps = useLinkProps as jest.Mock;
jest.mock('../use_link_props');
const processEvent = {
_id: 'test_id',
_index: '_index',
'@timestamp': 1726589803115,
event: {
id: 'event id',
kind: 'event',
category: 'process',
},
};
describe('<NodeDetailView />', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseLinkProps.mockReturnValue({ href: '#', onClick: jest.fn() });
mockUseCubeAssets.mockReturnValue({
descriptionText: 'test process',
});
jest.spyOn(redux, 'useSelector').mockReturnValueOnce('success');
jest.spyOn(redux, 'useSelector').mockReturnValueOnce(1);
});
it('should render', () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<NodeDetailView id="test" nodeID="test" processEvent={processEvent} />
</TestProviders>
);
expect(getByTestId('resolver:panel:node-detail')).toBeInTheDocument();
expect(getByTestId('resolver:node-detail:title')).toBeInTheDocument();
expect(getByTestId('resolver:node-detail:node-events-link')).toBeInTheDocument();
expect(getByTestId('resolver:node-detail')).toBeInTheDocument();
expect(queryByTestId('resolver:node-detail:title-link')).not.toBeInTheDocument();
});
it('should render process name as link when nodeEventOnClick is available', () => {
const nodeEventOnClick = jest.fn();
const { getByTestId } = render(
<TestProviders>
<NodeDetailView
id="test"
nodeID="test"
nodeEventOnClick={nodeEventOnClick}
processEvent={processEvent}
/>
</TestProviders>
);
expect(getByTestId('resolver:node-detail:title-link')).toBeInTheDocument();
getByTestId('resolver:node-detail:title-link').click();
expect(nodeEventOnClick).toBeCalledWith({
documentId: 'test_id',
indexName: '_index',
scopeId: 'test',
isAlert: false,
});
});
});

View file

@ -20,6 +20,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import styled from 'styled-components';
import { EventKind } from '../../../flyout/document_details/shared/constants/event_kinds';
import { StyledTitle } from './styles';
import * as selectors from '../../store/selectors';
import * as eventModel from '../../../../common/endpoint/models/event';
@ -41,6 +42,7 @@ import { useLinkProps } from '../use_link_props';
import { useFormattedDate } from './use_formatted_date';
import { PanelContentError } from './panel_content_error';
import type { State } from '../../../common/store/types';
import type { NodeEventOnClick } from './node_events_of_type';
const StyledCubeForProcess = styled(CubeForProcess)`
position: relative;
@ -51,7 +53,15 @@ const nodeDetailError = i18n.translate('xpack.securitySolution.resolver.panel.no
});
// eslint-disable-next-line react/display-name
export const NodeDetail = memo(function ({ id, nodeID }: { id: string; nodeID: string }) {
export const NodeDetail = memo(function ({
id,
nodeID,
nodeEventOnClick,
}: {
id: string;
nodeID: string;
nodeEventOnClick?: NodeEventOnClick;
}) {
const processEvent = useSelector((state: State) =>
nodeDataModel.firstEvent(selectors.nodeDataForID(state.analyzer[id])(nodeID))
);
@ -62,7 +72,12 @@ export const NodeDetail = memo(function ({ id, nodeID }: { id: string; nodeID: s
return nodeStatus === 'loading' ? (
<PanelLoading id={id} />
) : processEvent ? (
<NodeDetailView id={id} nodeID={nodeID} processEvent={processEvent} />
<NodeDetailView
id={id}
nodeID={nodeID}
processEvent={processEvent}
nodeEventOnClick={nodeEventOnClick}
/>
) : (
<PanelContentError id={id} translatedErrorMessage={nodeDetailError} />
);
@ -78,14 +93,16 @@ export interface NodeDetailsTableView {
* Created, PID, User/Domain, etc.
*/
// eslint-disable-next-line react/display-name
const NodeDetailView = memo(function ({
export const NodeDetailView = memo(function ({
id,
processEvent,
nodeID,
nodeEventOnClick,
}: {
id: string;
processEvent: SafeResolverEvent;
nodeID: string;
nodeEventOnClick?: NodeEventOnClick;
}) {
const processName = eventModel.processNameSafeVersion(processEvent);
const nodeState = useSelector((state: State) =>
@ -96,6 +113,9 @@ const NodeDetailView = memo(function ({
});
const eventTime = eventModel.eventTimestamp(processEvent);
const dateTime = useFormattedDate(eventTime);
const isAlert = eventModel.eventKind(processEvent)[0] === EventKind.signal;
const documentId = eventModel.documentID(processEvent);
const indexName = eventModel.indexName(processEvent);
const processInfoEntry: NodeDetailsTableView[] = useMemo(() => {
const createdEntry = {
@ -278,7 +298,16 @@ const NodeDetailView = memo(function ({
state={nodeState}
/>
<span data-test-subj="resolver:node-detail:title">
<GeneratedText>{processName}</GeneratedText>
{nodeEventOnClick ? (
<EuiLink
data-test-subj="resolver:node-detail:title-link"
onClick={nodeEventOnClick({ documentId, indexName, scopeId: id, isAlert })}
>
<GeneratedText>{processName}</GeneratedText>
</EuiLink>
) : (
<GeneratedText>{processName}</GeneratedText>
)}
</span>
</StyledTitle>
</EuiTitle>