Skip to content

Commit dda5381

Browse files
[SecuritySolution][Alerts table] Fix issue with multiple ip addresses in strings (#209475)
## Summary Fixes #191767 Multiple IPs are now displayed as individual links, even in the case where multiple IPs are passed as a single string (e.g. `127.0.0.1,127.0.0.2`). Clicking on an individual link will open the flyout correctly as well. https://github.com/user-attachments/assets/74b05cff-3843-4149-bf27-cd0af07aa558 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine <[email protected]>
1 parent 2c28139 commit dda5381

File tree

5 files changed

+113
-108
lines changed

5 files changed

+113
-108
lines changed

x-pack/solutions/security/plugins/security_solution/public/common/components/links/index.test.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,17 @@ describe('Custom Links', () => {
8080
expect(wrapper.find('EuiLink').first().prop('href')).toEqual(
8181
`/ip/${encodeURIComponent(ipv4)}/source/events`
8282
);
83-
expect(wrapper.text()).toEqual(`${ipv4}${ipv4a}`);
83+
expect(wrapper.text()).toEqual(`${ipv4}, ${ipv4a}`);
84+
expect(wrapper.find('EuiLink').last().prop('href')).toEqual(
85+
`/ip/${encodeURIComponent(ipv4a)}/source/events`
86+
);
87+
});
88+
test('can handle a string array of ips', () => {
89+
const wrapper = mount(<NetworkDetailsLink ip={`${ipv4}, ${ipv4a}`} />);
90+
expect(wrapper.find('EuiLink').first().prop('href')).toEqual(
91+
`/ip/${encodeURIComponent(ipv4)}/source/events`
92+
);
93+
expect(wrapper.text()).toEqual(`${ipv4}, ${ipv4a}`);
8494
expect(wrapper.find('EuiLink').last().prop('href')).toEqual(
8595
`/ip/${encodeURIComponent(ipv4a)}/source/events`
8696
);

x-pack/solutions/security/plugins/security_solution/public/common/components/links/index.tsx

+63-40
Original file line numberDiff line numberDiff line change
@@ -298,58 +298,81 @@ export interface NetworkDetailsLinkProps {
298298
ip: string | string[];
299299
flowTarget?: FlowTarget | FlowTargetSourceDest;
300300
isButton?: boolean;
301-
onClick?: (e: SyntheticEvent) => void | undefined;
301+
onClick?: (ip: string) => void;
302302
title?: string;
303303
}
304304

305-
const NetworkDetailsLinkComponent: React.FC<NetworkDetailsLinkProps> = ({
306-
Component,
307-
children,
308-
ip,
309-
flowTarget = FlowTarget.source,
305+
const NetworkDetailsLinkComponent: React.FC<NetworkDetailsLinkProps> = ({ ip, ...restProps }) => {
306+
// We see that sometimes the `ip` is passed as a string value of "IP1,IP2".
307+
// Therefore we're breaking up this string into individual IPs first.
308+
const actualIp = useMemo(() => {
309+
if (typeof ip === 'string' && ip.includes(',')) {
310+
return ip.split(',').map((str) => str.trim());
311+
} else {
312+
return ip;
313+
}
314+
}, [ip]);
315+
316+
return isArray(actualIp) ? (
317+
actualIp.map((currentIp, index) => (
318+
<span key={`${currentIp}-${index}`}>
319+
<IpLinkComponent ip={currentIp} {...restProps} />
320+
{index === actualIp.length - 1 ? '' : ', '}
321+
</span>
322+
))
323+
) : (
324+
<IpLinkComponent ip={actualIp} {...restProps} />
325+
);
326+
};
327+
328+
export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent);
329+
330+
type IpLinkComponentProps = Omit<NetworkDetailsLinkProps, 'ip'> & { ip: string };
331+
332+
const IpLinkComponent: React.FC<IpLinkComponentProps> = ({
310333
isButton,
311334
onClick,
335+
ip: ipAddress,
336+
flowTarget = FlowTarget.source,
337+
Component,
312338
title,
339+
children,
313340
}) => {
314341
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
342+
const { onClick: onClickNavigation, href } = getSecuritySolutionLinkProps({
343+
deepLinkId: SecurityPageName.network,
344+
path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ipAddress)), flowTarget),
345+
});
315346

316-
const getLink = useCallback(
317-
(cIp: string, i: number) => {
318-
const { onClick: onClickNavigation, href } = getSecuritySolutionLinkProps({
319-
deepLinkId: SecurityPageName.network,
320-
path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(cIp)), flowTarget),
321-
});
322-
323-
const onLinkClick = onClick ?? ((e: SyntheticEvent) => onClickNavigation(e as MouseEvent));
324-
325-
return isButton ? (
326-
<GenericLinkButton
327-
Component={Component}
328-
key={`${cIp}-${i}`}
329-
dataTestSubj="data-grid-network-details"
330-
onClick={onLinkClick}
331-
href={href}
332-
title={title ?? cIp}
333-
>
334-
{children}
335-
</GenericLinkButton>
336-
) : (
337-
<LinkAnchor
338-
key={`${cIp}-${i}`}
339-
onClick={onLinkClick}
340-
href={href}
341-
data-test-subj="network-details"
342-
>
343-
{children ? children : cIp}
344-
</LinkAnchor>
345-
);
347+
const onLinkClick = useCallback(
348+
(e: SyntheticEvent) => {
349+
if (onClick) {
350+
e.preventDefault();
351+
onClick(ipAddress);
352+
} else {
353+
onClickNavigation(e as MouseEvent);
354+
}
346355
},
347-
[children, Component, flowTarget, getSecuritySolutionLinkProps, onClick, isButton, title]
356+
[onClick, onClickNavigation, ipAddress]
348357
);
349-
return isArray(ip) ? <>{ip.map(getLink)}</> : getLink(ip, 0);
350-
};
351358

352-
export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent);
359+
return isButton ? (
360+
<GenericLinkButton
361+
Component={Component}
362+
key={ipAddress}
363+
dataTestSubj="data-grid-network-details"
364+
onClick={onLinkClick}
365+
href={href}
366+
title={title ?? ipAddress}
367+
>
368+
{children}
369+
</GenericLinkButton>
370+
) : (
371+
<LinkAnchor key={ipAddress} onClick={onLinkClick} href={href} data-test-subj="network-details">
372+
{children ? children : ipAddress}
373+
</LinkAnchor>
374+
);
375+
};
353376

354377
export interface CaseDetailsLinkComponentProps {
355378
children?: React.ReactNode;

x-pack/solutions/security/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx

+11-23
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
DraggableWrapper,
2020
} from '../../../common/components/drag_and_drop/draggable_wrapper';
2121
import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers';
22-
import { Content } from '../../../common/components/draggables';
2322
import { getOrEmptyTagFromValue } from '../../../common/components/empty_value';
2423
import { parseQueryValue } from '../timeline/body/renderers/parse_query_value';
2524
import type { DataProvider } from '../timeline/data_providers/data_provider';
@@ -183,8 +182,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
183182
address && eventContext?.enableIpDetailsFlyout && eventContext?.timelineID;
184183

185184
const openNetworkDetailsSidePanel = useCallback(
186-
(e: React.SyntheticEvent) => {
187-
e.preventDefault();
185+
(ip: string) => {
188186
if (onClick) {
189187
onClick();
190188
}
@@ -194,7 +192,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
194192
right: {
195193
id: NetworkPanelKey,
196194
params: {
197-
ip: address,
195+
ip,
198196
scopeId: eventContext.timelineID,
199197
flowTarget: fieldName.includes(FlowTargetSourceDest.destination)
200198
? FlowTargetSourceDest.destination
@@ -204,7 +202,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
204202
});
205203
}
206204
},
207-
[onClick, eventContext, isInTimelineContext, address, fieldName, openFlyout]
205+
[onClick, eventContext, isInTimelineContext, fieldName, openFlyout]
208206
);
209207

210208
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
@@ -220,25 +218,15 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
220218
title={title}
221219
/>
222220
) : (
223-
<Content field={fieldName} tooltipContent={fieldName}>
224-
<NetworkDetailsLink
225-
Component={Component}
226-
ip={address}
227-
isButton={isButton}
228-
onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined}
229-
title={title}
230-
/>
231-
</Content>
221+
<NetworkDetailsLink
222+
Component={Component}
223+
ip={address}
224+
isButton={isButton}
225+
onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined}
226+
title={title}
227+
/>
232228
),
233-
[
234-
Component,
235-
address,
236-
fieldName,
237-
isButton,
238-
isInTimelineContext,
239-
openNetworkDetailsSidePanel,
240-
title,
241-
]
229+
[Component, address, isButton, isInTimelineContext, openNetworkDetailsSidePanel, title]
242230
);
243231

244232
const render: ComponentProps<typeof DraggableWrapper>['render'] = useCallback(

x-pack/solutions/security/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap

+14-22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap

+14-22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)