Skip to content

Commit 8c05633

Browse files
[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)
1 parent 7dd4058 commit 8c05633

File tree

3 files changed

+118
-5
lines changed

3 files changed

+118
-5
lines changed

x-pack/solutions/security/plugins/security_solution/public/resolver/view/panels/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ export const PanelRouter = memo(function ({
3232
selectors.panelViewAndParameters(state.analyzer[id])
3333
);
3434
if (params.panelView === 'nodeDetail') {
35-
return <NodeDetail id={id} nodeID={params.panelParameters.nodeID} />;
35+
return (
36+
<NodeDetail
37+
id={id}
38+
nodeID={params.panelParameters.nodeID}
39+
nodeEventOnClick={nodeEventOnClick}
40+
/>
41+
);
3642
} else if (params.panelView === 'nodeEvents') {
3743
return <NodeEvents id={id} nodeID={params.panelParameters.nodeID} />;
3844
} else if (params.panelView === 'nodeEventsInCategory') {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import * as redux from 'react-redux';
10+
import { render } from '@testing-library/react';
11+
import { TestProviders } from '../../../common/mock';
12+
import { NodeDetailView } from './node_detail';
13+
import { useCubeAssets } from '../use_cube_assets';
14+
import { useLinkProps } from '../use_link_props';
15+
16+
const mockUseCubeAssets = useCubeAssets as jest.Mock;
17+
jest.mock('../use_cube_assets');
18+
19+
const mockUseLinkProps = useLinkProps as jest.Mock;
20+
jest.mock('../use_link_props');
21+
22+
const processEvent = {
23+
_id: 'test_id',
24+
_index: '_index',
25+
'@timestamp': 1726589803115,
26+
event: {
27+
id: 'event id',
28+
kind: 'event',
29+
category: 'process',
30+
},
31+
};
32+
33+
describe('<NodeDetailView />', () => {
34+
beforeEach(() => {
35+
jest.clearAllMocks();
36+
mockUseLinkProps.mockReturnValue({ href: '#', onClick: jest.fn() });
37+
mockUseCubeAssets.mockReturnValue({
38+
descriptionText: 'test process',
39+
});
40+
jest.spyOn(redux, 'useSelector').mockReturnValueOnce('success');
41+
jest.spyOn(redux, 'useSelector').mockReturnValueOnce(1);
42+
});
43+
44+
it('should render', () => {
45+
const { getByTestId, queryByTestId } = render(
46+
<TestProviders>
47+
<NodeDetailView id="test" nodeID="test" processEvent={processEvent} />
48+
</TestProviders>
49+
);
50+
expect(getByTestId('resolver:panel:node-detail')).toBeInTheDocument();
51+
expect(getByTestId('resolver:node-detail:title')).toBeInTheDocument();
52+
expect(getByTestId('resolver:node-detail:node-events-link')).toBeInTheDocument();
53+
expect(getByTestId('resolver:node-detail')).toBeInTheDocument();
54+
expect(queryByTestId('resolver:node-detail:title-link')).not.toBeInTheDocument();
55+
});
56+
57+
it('should render process name as link when nodeEventOnClick is available', () => {
58+
const nodeEventOnClick = jest.fn();
59+
const { getByTestId } = render(
60+
<TestProviders>
61+
<NodeDetailView
62+
id="test"
63+
nodeID="test"
64+
nodeEventOnClick={nodeEventOnClick}
65+
processEvent={processEvent}
66+
/>
67+
</TestProviders>
68+
);
69+
expect(getByTestId('resolver:node-detail:title-link')).toBeInTheDocument();
70+
getByTestId('resolver:node-detail:title-link').click();
71+
expect(nodeEventOnClick).toBeCalledWith({
72+
documentId: 'test_id',
73+
indexName: '_index',
74+
scopeId: 'test',
75+
isAlert: false,
76+
});
77+
});
78+
});

x-pack/solutions/security/plugins/security_solution/public/resolver/view/panels/node_detail.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@elastic/eui';
2121
import { FormattedMessage } from '@kbn/i18n-react';
2222
import styled from 'styled-components';
23+
import { EventKind } from '../../../flyout/document_details/shared/constants/event_kinds';
2324
import { StyledTitle } from './styles';
2425
import * as selectors from '../../store/selectors';
2526
import * as eventModel from '../../../../common/endpoint/models/event';
@@ -41,6 +42,7 @@ import { useLinkProps } from '../use_link_props';
4142
import { useFormattedDate } from './use_formatted_date';
4243
import { PanelContentError } from './panel_content_error';
4344
import type { State } from '../../../common/store/types';
45+
import type { NodeEventOnClick } from './node_events_of_type';
4446

4547
const StyledCubeForProcess = styled(CubeForProcess)`
4648
position: relative;
@@ -51,7 +53,15 @@ const nodeDetailError = i18n.translate('xpack.securitySolution.resolver.panel.no
5153
});
5254

5355
// eslint-disable-next-line react/display-name
54-
export const NodeDetail = memo(function ({ id, nodeID }: { id: string; nodeID: string }) {
56+
export const NodeDetail = memo(function ({
57+
id,
58+
nodeID,
59+
nodeEventOnClick,
60+
}: {
61+
id: string;
62+
nodeID: string;
63+
nodeEventOnClick?: NodeEventOnClick;
64+
}) {
5565
const processEvent = useSelector((state: State) =>
5666
nodeDataModel.firstEvent(selectors.nodeDataForID(state.analyzer[id])(nodeID))
5767
);
@@ -62,7 +72,12 @@ export const NodeDetail = memo(function ({ id, nodeID }: { id: string; nodeID: s
6272
return nodeStatus === 'loading' ? (
6373
<PanelLoading id={id} />
6474
) : processEvent ? (
65-
<NodeDetailView id={id} nodeID={nodeID} processEvent={processEvent} />
75+
<NodeDetailView
76+
id={id}
77+
nodeID={nodeID}
78+
processEvent={processEvent}
79+
nodeEventOnClick={nodeEventOnClick}
80+
/>
6681
) : (
6782
<PanelContentError id={id} translatedErrorMessage={nodeDetailError} />
6883
);
@@ -78,14 +93,16 @@ export interface NodeDetailsTableView {
7893
* Created, PID, User/Domain, etc.
7994
*/
8095
// eslint-disable-next-line react/display-name
81-
const NodeDetailView = memo(function ({
96+
export const NodeDetailView = memo(function ({
8297
id,
8398
processEvent,
8499
nodeID,
100+
nodeEventOnClick,
85101
}: {
86102
id: string;
87103
processEvent: SafeResolverEvent;
88104
nodeID: string;
105+
nodeEventOnClick?: NodeEventOnClick;
89106
}) {
90107
const processName = eventModel.processNameSafeVersion(processEvent);
91108
const nodeState = useSelector((state: State) =>
@@ -96,6 +113,9 @@ const NodeDetailView = memo(function ({
96113
});
97114
const eventTime = eventModel.eventTimestamp(processEvent);
98115
const dateTime = useFormattedDate(eventTime);
116+
const isAlert = eventModel.eventKind(processEvent)[0] === EventKind.signal;
117+
const documentId = eventModel.documentID(processEvent);
118+
const indexName = eventModel.indexName(processEvent);
99119

100120
const processInfoEntry: NodeDetailsTableView[] = useMemo(() => {
101121
const createdEntry = {
@@ -278,7 +298,16 @@ const NodeDetailView = memo(function ({
278298
state={nodeState}
279299
/>
280300
<span data-test-subj="resolver:node-detail:title">
281-
<GeneratedText>{processName}</GeneratedText>
301+
{nodeEventOnClick ? (
302+
<EuiLink
303+
data-test-subj="resolver:node-detail:title-link"
304+
onClick={nodeEventOnClick({ documentId, indexName, scopeId: id, isAlert })}
305+
>
306+
<GeneratedText>{processName}</GeneratedText>
307+
</EuiLink>
308+
) : (
309+
<GeneratedText>{processName}</GeneratedText>
310+
)}
282311
</span>
283312
</StyledTitle>
284313
</EuiTitle>

0 commit comments

Comments
 (0)