mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
2c28139f45
commit
dda538111e
5 changed files with 117 additions and 112 deletions
|
@ -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`
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue