[SecuritySolution] Hover actions not working on the overview section of the host details page (#210819)

## Summary

Fixes https://github.com/elastic/kibana/issues/210815


Steps to verify:
1. Ingest some data
2. Visit host details page
3. Hover onto host ID and IP address, verify filter in / filter out /
add to timeline / show top N works correctly.


https://github.com/user-attachments/assets/75148ebb-154d-42a4-ae75-127925564d8a
This commit is contained in:
Angela Chuang 2025-02-13 18:47:41 +00:00 committed by GitHub
parent 789986ce48
commit b2db698f03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 60 additions and 13 deletions

View file

@ -81,6 +81,7 @@ import { AlertCountByRuleByStatus } from '../../../../common/components/alert_co
import { useLicense } from '../../../../common/hooks/use_license';
import { ResponderActionButton } from '../../../../common/components/endpoint/responder';
import { useRefetchOverviewPageRiskScore } from '../../../../entity_analytics/api/hooks/use_refetch_overview_page_risk_score';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
const ES_HOST_FIELD = 'host.name';
const HostOverviewManage = manageQuery(HostOverview);
@ -265,6 +266,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
hostName={detailName}
indexNames={selectedPatterns}
jobNameById={jobNameById}
scopeId={SourcererScopeName.default}
/>
)}
</AnomalyTableProvider>

View file

@ -147,6 +147,7 @@ exports[`IP Overview Component rendering it renders the default IP Overview 1`]
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
scopeId="default"
startDate="2019-06-15T06:00:00.000Z"
type="details"
updateFlowTargetAction={[MockFunction]}
@ -300,6 +301,7 @@ exports[`IP Overview Component rendering it renders the side panel IP overview 1
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
scopeId="default"
startDate="2019-06-15T06:00:00.000Z"
type="details"
updateFlowTargetAction={[MockFunction]}

View file

@ -17,6 +17,7 @@ import { mockData } from './mock';
import { mockAnomalies } from '../../../../common/components/ml/mock';
import type { NarrowDateRange } from '../../../../common/components/ml/types';
import { FlowTargetSourceDest } from '../../../../../common/search_strategy';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
describe('IP Overview Component', () => {
describe('rendering', () => {
@ -38,6 +39,7 @@ describe('IP Overview Component', () => {
}>,
indexPatterns: [],
jobNameById: {},
scopeId: SourcererScopeName.default,
};
test('it renders the default IP Overview', () => {

View file

@ -38,6 +38,7 @@ import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect';
import { OverviewDescriptionList } from '../../../../common/components/overview_description_list';
import type { SourcererScopeName } from '../../../../sourcerer/store/model';
export interface IpOverviewProps {
anomaliesData: Anomalies | null;
@ -51,6 +52,7 @@ export interface IpOverviewProps {
isLoadingAnomaliesData: boolean;
loading: boolean;
narrowDateRange: NarrowDateRange;
scopeId: SourcererScopeName;
startDate: string;
type: networkModel.NetworkType;
indexPatterns: string[];
@ -71,6 +73,7 @@ export const IpOverview = React.memo<IpOverviewProps>(
isLoadingAnomaliesData,
anomaliesData,
narrowDateRange,
scopeId,
indexPatterns,
jobNameById,
}) => {
@ -145,13 +148,20 @@ export const IpOverview = React.memo<IpOverviewProps>(
title: i18n.HOST_ID,
description:
typeData && data.host
? hostIdRenderer({ host: data.host, ipFilter: ip, contextID })
? hostIdRenderer({
host: data.host,
ipFilter: ip,
contextID,
scopeId,
})
: getEmptyTagValue(),
},
{
title: i18n.HOST_NAME,
description:
typeData && data.host ? hostNameRenderer(data.host, ip, contextID) : getEmptyTagValue(),
typeData && data.host
? hostNameRenderer(scopeId, data.host, ip, contextID)
: getEmptyTagValue(),
},
],
[

View file

@ -59,6 +59,7 @@ import {
CellActionsMode,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
const NetworkDetailsManage = manageQuery(IpOverview);
@ -224,6 +225,7 @@ const NetworkDetailsComponent: React.FC = () => {
narrowDateRange={narrowDateRange}
indexPatterns={selectedPatterns}
jobNameById={jobNameById}
scopeId={SourcererScopeName.default}
/>
<EuiHorizontalRule />

View file

@ -27,6 +27,7 @@ import { useAnomaliesTableData } from '../../../common/components/ml/anomaly/use
import { useInstalledSecurityJobNameById } from '../../../common/components/ml/hooks/use_installed_security_jobs';
import { EmptyPrompt } from '../../../common/components/empty_prompt';
import type { NarrowDateRange } from '../../../common/components/ml/types';
import { SourcererScopeName } from '../../../sourcerer/store/model';
export interface NetworkDetailsProps {
/**
@ -116,6 +117,7 @@ export const NetworkDetails = ({ ip, flowTarget }: NetworkDetailsProps) => {
narrowDateRange={narrowDateRange}
indexPatterns={selectedPatterns}
jobNameById={jobNameById}
scopeId={SourcererScopeName.default}
/>
) : (
<EmptyPrompt />

View file

@ -176,7 +176,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
title: i18n.HOST_ID,
description:
data && data.host
? hostIdRenderer({ host: data.host, noLink: true })
? hostIdRenderer({ host: data.host, noLink: true, scopeId })
: getEmptyTagValue(),
},
{
@ -202,7 +202,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
),
},
],
[data, indexNames, hostName]
[data, scopeId, indexNames, hostName]
);
const firstColumn = useMemo(
() =>

View file

@ -21,6 +21,7 @@ import type { AutonomousSystem } from '../../../../common/search_strategy';
import { FlowTarget } from '../../../../common/search_strategy';
import type { HostEcs } from '@kbn/securitysolution-ecs';
import { mockGetUrlForApp } from '@kbn/security-solution-navigation/mocks/context';
import { SourcererScopeName } from '../../../sourcerer/store/model';
jest.mock('../../../common/lib/kibana');
jest.mock('@kbn/security-solution-navigation/src/context');
@ -32,6 +33,8 @@ mockGetUrlForApp.mockImplementation(
jest.mock('../../../common/hooks/use_get_field_spec');
describe('Field Renderers', () => {
const scopeId = SourcererScopeName.default;
describe('#locationRenderer', () => {
test('it renders correctly against snapshot', () => {
const { asFragment } = render(
@ -104,24 +107,32 @@ describe('Field Renderers', () => {
describe('#hostIdRenderer', () => {
test('it renders correctly against snapshot', () => {
const { asFragment } = render(
<TestProviders>{hostNameRenderer(mockData.complete.host, '10.10.10.10')}</TestProviders>
<TestProviders>
{hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.10')}
</TestProviders>
);
expect(asFragment()).toMatchSnapshot();
});
test('it renders emptyTagValue when non-matching IP is provided', () => {
render(
<TestProviders>{hostNameRenderer(mockData.complete.host, '10.10.10.11')}</TestProviders>
<TestProviders>
{hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.11')}
</TestProviders>
);
expect(screen.getByText(getEmptyValue())).toBeInTheDocument();
});
test('it renders emptyTagValue when no host.id is provided', () => {
render(<TestProviders>{hostNameRenderer(emptyIdHost, FlowTarget.source)}</TestProviders>);
render(
<TestProviders>{hostNameRenderer(scopeId, emptyIdHost, FlowTarget.source)}</TestProviders>
);
expect(screen.getByText(getEmptyValue())).toBeInTheDocument();
});
test('it renders emptyTagValue when no host.ip is provided', () => {
render(<TestProviders>{hostNameRenderer(emptyIpHost, FlowTarget.source)}</TestProviders>);
render(
<TestProviders>{hostNameRenderer(scopeId, emptyIpHost, FlowTarget.source)}</TestProviders>
);
expect(screen.getByText(getEmptyValue())).toBeInTheDocument();
});
});
@ -129,7 +140,9 @@ describe('Field Renderers', () => {
describe('#hostNameRenderer', () => {
test('it renders correctly against snapshot', () => {
const { asFragment } = render(
<TestProviders>{hostNameRenderer(mockData.complete.host, '10.10.10.10')}</TestProviders>
<TestProviders>
{hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.10')}
</TestProviders>
);
expect(asFragment()).toMatchSnapshot();
@ -137,21 +150,29 @@ describe('Field Renderers', () => {
test('it renders emptyTagValue when non-matching IP is provided', () => {
render(
<TestProviders>{hostNameRenderer(mockData.complete.host, '10.10.10.11')}</TestProviders>
<TestProviders>
{hostNameRenderer(scopeId, mockData.complete.host, '10.10.10.11')}
</TestProviders>
);
expect(screen.getByText(getEmptyValue())).toBeInTheDocument();
});
test('it renders emptyTagValue when no host.id is provided', () => {
render(<TestProviders>{hostNameRenderer(emptyIdHost, FlowTarget.source)}</TestProviders>);
render(
<TestProviders>{hostNameRenderer(scopeId, emptyIdHost, FlowTarget.source)}</TestProviders>
);
expect(screen.getByText(getEmptyValue())).toBeInTheDocument();
});
test('it renders emptyTagValue when no host.ip is provided', () => {
render(<TestProviders>{hostNameRenderer(emptyIpHost, FlowTarget.source)}</TestProviders>);
render(
<TestProviders>{hostNameRenderer(scopeId, emptyIpHost, FlowTarget.source)}</TestProviders>
);
expect(screen.getByText(getEmptyValue())).toBeInTheDocument();
});
test('it renders emptyTagValue when no host.name is provided', () => {
render(<TestProviders>{hostNameRenderer(emptyNameHost, FlowTarget.source)}</TestProviders>);
render(
<TestProviders>{hostNameRenderer(scopeId, emptyNameHost, FlowTarget.source)}</TestProviders>
);
expect(screen.getByText(getEmptyValue())).toBeInTheDocument();
});
});

View file

@ -19,6 +19,7 @@ import { DefaultDraggable } from '../../../common/components/draggables';
import { getEmptyTagValue } from '../../../common/components/empty_value';
import { HostDetailsLink, ReputationLink, WhoIsLink } from '../../../common/components/links';
import * as i18n from '../../../explore/network/components/details/translations';
import type { SourcererScopeName } from '../../../sourcerer/store/model';
export const IpOverviewId = 'ip-overview';
@ -91,6 +92,7 @@ interface HostIdRendererTypes {
host: HostEcs;
ipFilter?: string;
noLink?: boolean;
scopeId: string | undefined;
}
export const hostIdRenderer = ({
@ -98,6 +100,7 @@ export const hostIdRenderer = ({
host,
ipFilter,
noLink,
scopeId,
}: HostIdRendererTypes): React.ReactElement =>
host.id && host.ip && (ipFilter == null || host.ip.includes(ipFilter)) ? (
<>
@ -110,6 +113,7 @@ export const hostIdRenderer = ({
value={host.id[0]}
isAggregatable={true}
fieldType={'keyword'}
scopeId={scopeId}
>
{noLink ? (
<>{host.id}</>
@ -126,6 +130,7 @@ export const hostIdRenderer = ({
);
export const hostNameRenderer = (
scopeId: SourcererScopeName,
host?: HostEcs,
ipFilter?: string,
contextID?: string
@ -143,6 +148,7 @@ export const hostNameRenderer = (
value={host.name[0]}
isAggregatable={true}
fieldType={'keyword'}
scopeId={scopeId}
>
<HostDetailsLink hostName={host.name[0]}>
{host.name ? host.name : getEmptyTagValue()}