[SecuritySolution][Alerts table] Fix issue with multiple ip addresses in strings (#209475)

## Summary

Fixes https://github.com/elastic/kibana/issues/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 <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jan Monschke 2025-02-05 21:40:17 +01:00 committed by GitHub
parent 2c28139f45
commit dda538111e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 117 additions and 112 deletions

View file

@ -80,7 +80,17 @@ describe('Custom Links', () => {
expect(wrapper.find('EuiLink').first().prop('href')).toEqual(
`/ip/${encodeURIComponent(ipv4)}/source/events`
);
expect(wrapper.text()).toEqual(`${ipv4}${ipv4a}`);
expect(wrapper.text()).toEqual(`${ipv4}, ${ipv4a}`);
expect(wrapper.find('EuiLink').last().prop('href')).toEqual(
`/ip/${encodeURIComponent(ipv4a)}/source/events`
);
});
test('can handle a string array of ips', () => {
const wrapper = mount(<NetworkDetailsLink ip={`${ipv4}, ${ipv4a}`} />);
expect(wrapper.find('EuiLink').first().prop('href')).toEqual(
`/ip/${encodeURIComponent(ipv4)}/source/events`
);
expect(wrapper.text()).toEqual(`${ipv4}, ${ipv4a}`);
expect(wrapper.find('EuiLink').last().prop('href')).toEqual(
`/ip/${encodeURIComponent(ipv4a)}/source/events`
);

View file

@ -298,59 +298,82 @@ export interface NetworkDetailsLinkProps {
ip: string | string[];
flowTarget?: FlowTarget | FlowTargetSourceDest;
isButton?: boolean;
onClick?: (e: SyntheticEvent) => void | undefined;
onClick?: (ip: string) => void;
title?: string;
}
const NetworkDetailsLinkComponent: React.FC<NetworkDetailsLinkProps> = ({
Component,
children,
ip,
flowTarget = FlowTarget.source,
isButton,
onClick,
title,
}) => {
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
const NetworkDetailsLinkComponent: React.FC<NetworkDetailsLinkProps> = ({ ip, ...restProps }) => {
// We see that sometimes the `ip` is passed as a string value of "IP1,IP2".
// Therefore we're breaking up this string into individual IPs first.
const actualIp = useMemo(() => {
if (typeof ip === 'string' && ip.includes(',')) {
return ip.split(',').map((str) => str.trim());
} else {
return ip;
}
}, [ip]);
const getLink = useCallback(
(cIp: string, i: number) => {
const { onClick: onClickNavigation, href } = getSecuritySolutionLinkProps({
deepLinkId: SecurityPageName.network,
path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(cIp)), flowTarget),
});
const onLinkClick = onClick ?? ((e: SyntheticEvent) => onClickNavigation(e as MouseEvent));
return isButton ? (
<GenericLinkButton
Component={Component}
key={`${cIp}-${i}`}
dataTestSubj="data-grid-network-details"
onClick={onLinkClick}
href={href}
title={title ?? cIp}
>
{children}
</GenericLinkButton>
) : (
<LinkAnchor
key={`${cIp}-${i}`}
onClick={onLinkClick}
href={href}
data-test-subj="network-details"
>
{children ? children : cIp}
</LinkAnchor>
);
},
[children, Component, flowTarget, getSecuritySolutionLinkProps, onClick, isButton, title]
return isArray(actualIp) ? (
actualIp.map((currentIp, index) => (
<span key={`${currentIp}-${index}`}>
<IpLinkComponent ip={currentIp} {...restProps} />
{index === actualIp.length - 1 ? '' : ', '}
</span>
))
) : (
<IpLinkComponent ip={actualIp} {...restProps} />
);
return isArray(ip) ? <>{ip.map(getLink)}</> : getLink(ip, 0);
};
export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent);
type IpLinkComponentProps = Omit<NetworkDetailsLinkProps, 'ip'> & { ip: string };
const IpLinkComponent: React.FC<IpLinkComponentProps> = ({
isButton,
onClick,
ip: ipAddress,
flowTarget = FlowTarget.source,
Component,
title,
children,
}) => {
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
const { onClick: onClickNavigation, href } = getSecuritySolutionLinkProps({
deepLinkId: SecurityPageName.network,
path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ipAddress)), flowTarget),
});
const onLinkClick = useCallback(
(e: SyntheticEvent) => {
if (onClick) {
e.preventDefault();
onClick(ipAddress);
} else {
onClickNavigation(e as MouseEvent);
}
},
[onClick, onClickNavigation, ipAddress]
);
return isButton ? (
<GenericLinkButton
Component={Component}
key={ipAddress}
dataTestSubj="data-grid-network-details"
onClick={onLinkClick}
href={href}
title={title ?? ipAddress}
>
{children}
</GenericLinkButton>
) : (
<LinkAnchor key={ipAddress} onClick={onLinkClick} href={href} data-test-subj="network-details">
{children ? children : ipAddress}
</LinkAnchor>
);
};
export interface CaseDetailsLinkComponentProps {
children?: React.ReactNode;
/**

View file

@ -19,7 +19,6 @@ import {
DraggableWrapper,
} from '../../../common/components/drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers';
import { Content } from '../../../common/components/draggables';
import { getOrEmptyTagFromValue } from '../../../common/components/empty_value';
import { parseQueryValue } from '../timeline/body/renderers/parse_query_value';
import type { DataProvider } from '../timeline/data_providers/data_provider';
@ -183,8 +182,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
address && eventContext?.enableIpDetailsFlyout && eventContext?.timelineID;
const openNetworkDetailsSidePanel = useCallback(
(e: React.SyntheticEvent) => {
e.preventDefault();
(ip: string) => {
if (onClick) {
onClick();
}
@ -194,7 +192,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
right: {
id: NetworkPanelKey,
params: {
ip: address,
ip,
scopeId: eventContext.timelineID,
flowTarget: fieldName.includes(FlowTargetSourceDest.destination)
? FlowTargetSourceDest.destination
@ -204,7 +202,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
});
}
},
[onClick, eventContext, isInTimelineContext, address, fieldName, openFlyout]
[onClick, eventContext, isInTimelineContext, fieldName, openFlyout]
);
// 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> = ({
title={title}
/>
) : (
<Content field={fieldName} tooltipContent={fieldName}>
<NetworkDetailsLink
Component={Component}
ip={address}
isButton={isButton}
onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined}
title={title}
/>
</Content>
<NetworkDetailsLink
Component={Component}
ip={address}
isButton={isButton}
onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined}
title={title}
/>
),
[
Component,
address,
fieldName,
isButton,
isInTimelineContext,
openNetworkDetailsSidePanel,
title,
]
[Component, address, isButton, isInTimelineContext, openNetworkDetailsSidePanel, title]
);
const render: ComponentProps<typeof DraggableWrapper>['render'] = useCallback(

View file

@ -899,18 +899,14 @@ tr:hover .c3:focus::before {
class="c7"
data-test-subj="draggable-truncatable-content"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/192.168.1.2/source/events"
rel="noreferrer"
>
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/192.168.1.2/source/events"
rel="noreferrer"
>
192.168.1.2
</a>
</span>
192.168.1.2
</a>
</span>
</div>
</div>
@ -1641,18 +1637,14 @@ tr:hover .c3:focus::before {
class="c7"
data-test-subj="draggable-truncatable-content"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/10.1.2.3/source/events"
rel="noreferrer"
>
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/10.1.2.3/source/events"
rel="noreferrer"
>
10.1.2.3
</a>
</span>
10.1.2.3
</a>
</span>
</div>
</div>

View file

@ -1038,18 +1038,14 @@ tr:hover .c5:focus::before {
class="c9"
data-test-subj="draggable-truncatable-content"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/192.168.1.2/source/events"
rel="noreferrer"
>
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/192.168.1.2/source/events"
rel="noreferrer"
>
192.168.1.2
</a>
</span>
192.168.1.2
</a>
</span>
<p
class="emotion-euiScreenReaderOnly"
@ -1941,18 +1937,14 @@ tr:hover .c5:focus::before {
class="c9"
data-test-subj="draggable-truncatable-content"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/10.1.2.3/source/events"
rel="noreferrer"
>
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="network-details"
href="/ip/10.1.2.3/source/events"
rel="noreferrer"
>
10.1.2.3
</a>
</span>
10.1.2.3
</a>
</span>
<p
class="emotion-euiScreenReaderOnly"