[Security Solution][Endpoint][SentinelOne] Consolidated agent status component (#181632)

## Summary

Adds an agent status component that shows agent isolation status, os and
last checkin info for endpoint/non-endpoint agents.

Updates agent status based on `agentStatusClientEnabled` ff for

1. Endpoint list
2. Endpoint details
3. Endpoint/SentinelOne alerts
4. Endpoint/SentinelOne alerts -> timeline view
5. Endpoint/SentinelOne responder headers
6. Hosts page -> Endpoint Host overview

<details><summary>Screenshots</summary>
<h4>1. Endpoint list</h4>
<img
src="259f3053-7834-4c06-8b91-6519486386ad"/>

<h4>2. Endpoint details</h4>
<img
src="4a04817a-fa6d-47a6-a938-a4b640f3d039"/>

<h4>3. Endpoint alert</h4>
<img
src="4d9a6975-bed9-49a4-b6d8-2d7685692180"/>

<h4>3. SentinelOne Alert</h4>
<img
src="3616fb49-afc9-4584-880f-fab1e50ea4ee"/>

<h4>4. Endpoint alert -> Timeline view -> details flyout </h4>
<img
src="7d2ead35-5401-4f01-9d5d-89b2e4ff9d9b"
/>

<h4>4. SentinelOne alert -> Timeline view -> details flyout </h4>
<img
src="edd1839a-18ef-44d8-8688-35c181527a29"
/>

<h4>5. Endpoint responder</h4>
<img
src="eaeb7293-84ad-4195-9126-eb1e330f822f"/>

<h4>5. SentinelOne responder</h4>
<img
src="8ea1a50a-53d2-40ed-aeb4-3620d4e9ebfc"/>

<h4>6.  Host Page -> Endpoint overview</h4>
<img
src="c443331a-0cd8-4fa1-a59c-b254dbaae6d9"/>
</details> 

### Checklist

- [ ] [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
This commit is contained in:
Ash 2024-05-15 20:37:57 +02:00 committed by GitHub
parent b00862240c
commit 3227e4c4eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 997 additions and 469 deletions

1
.github/CODEOWNERS vendored
View file

@ -1565,6 +1565,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
## Security Solution sub teams - security-defend-workflows
/x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/public/common/lib/endpoint*/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/public/common/components/agents/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/common/endpoint/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/server/endpoint/ @elastic/security-defend-workflows

View file

@ -0,0 +1,169 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui';
import type { EndpointPendingActions } from '../../../../../common/endpoint/types';
import type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants';
import { RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP } from '../../../../../common/endpoint/service/response_actions/constants';
import { ISOLATED_LABEL, ISOLATING_LABEL, RELEASING_LABEL } from './endpoint/endpoint_agent_status';
import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator';
const TOOLTIP_CONTENT_STYLES: React.CSSProperties = Object.freeze({ width: 150 });
interface AgentResponseActionsStatusProps {
/** The host's individual pending action list as return by the pending action summary api */
pendingActions: EndpointPendingActions['pending_actions'];
/** Is host currently isolated */
isIsolated: boolean;
'data-test-subj'?: string;
}
export const AgentResponseActionsStatus = memo<AgentResponseActionsStatusProps>(
({ pendingActions, isIsolated, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
interface PendingActionsState {
actionList: Array<{ label: string; count: number }>;
totalPending: number;
wasReleasing: boolean;
wasIsolating: boolean;
hasMultipleActionTypesPending: boolean;
hasPendingIsolate: boolean;
hasPendingUnIsolate: boolean;
}
const {
totalPending,
actionList,
wasReleasing,
wasIsolating,
hasMultipleActionTypesPending,
hasPendingIsolate,
hasPendingUnIsolate,
} = useMemo<PendingActionsState>(() => {
const list: Array<{ label: string; count: number }> = [];
let actionTotal = 0;
const pendingActionEntries = Object.entries(pendingActions);
const actionTypesCount = pendingActionEntries.length;
pendingActionEntries.sort().forEach(([actionName, actionCount]) => {
actionTotal += actionCount;
list.push({
count: actionCount,
label:
RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP[
actionName as ResponseActionsApiCommandNames
] ?? actionName,
});
});
const pendingIsolate = pendingActions.isolate ?? 0;
const pendingUnIsolate = pendingActions.unisolate ?? 0;
return {
actionList: list,
totalPending: actionTotal,
wasReleasing: pendingIsolate === 0 && pendingUnIsolate > 0,
wasIsolating: pendingIsolate > 0 && pendingUnIsolate === 0,
hasMultipleActionTypesPending: actionTypesCount > 1,
hasPendingIsolate: pendingIsolate > 0,
hasPendingUnIsolate: pendingUnIsolate > 0,
};
}, [pendingActions]);
const badgeDisplayValue = useMemo(() => {
return hasPendingIsolate ? (
ISOLATING_LABEL
) : hasPendingUnIsolate ? (
RELEASING_LABEL
) : isIsolated ? (
ISOLATED_LABEL
) : (
<FormattedMessage
id="xpack.securitySolution.agentStatus.actionStatus.multiplePendingActions"
defaultMessage="{count} {count, plural, one {action} other {actions}} pending"
values={{
count: totalPending,
}}
/>
);
}, [hasPendingIsolate, hasPendingUnIsolate, isIsolated, totalPending]);
const isolatedBadge = useMemo(() => {
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
{ISOLATED_LABEL}
</EuiBadge>
);
}, [dataTestSubj]);
// If nothing is pending
if (totalPending === 0) {
// and host is either releasing and or currently released, then render nothing
if ((!wasIsolating && wasReleasing) || !isIsolated) {
return null;
}
// else host was isolating or is isolated, then show isolation badge
else if ((!isIsolated && wasIsolating && !wasReleasing) || isIsolated) {
return isolatedBadge;
}
}
// If there are different types of action pending
// --OR--
// the only type of actions pending is NOT isolate/release,
// then show a summary with tooltip
if (hasMultipleActionTypesPending || (!hasPendingIsolate && !hasPendingUnIsolate)) {
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj} iconType="plus" iconSide="right">
<EuiToolTip
display="block"
anchorClassName="eui-textTruncate"
anchorProps={{ 'data-test-subj': getTestId('tooltipTrigger') }}
content={
<div style={TOOLTIP_CONTENT_STYLES} data-test-subj={`${dataTestSubj}-tooltipContent`}>
<div>
<FormattedMessage
id="xpack.securitySolution.agentStatus.actionStatus.tooltipPendingActions"
defaultMessage="Pending actions:"
/>
</div>
{actionList.map(({ count, label }) => {
return (
<EuiFlexGroup gutterSize="none" key={label}>
<EuiFlexItem>{label}</EuiFlexItem>
<EuiFlexItem grow={false}>{count}</EuiFlexItem>
</EuiFlexGroup>
);
})}
</div>
}
>
<EuiTextColor color="subdued" data-test-subj={`${dataTestSubj}-pending`}>
{badgeDisplayValue}
</EuiTextColor>
</EuiToolTip>
</EuiBadge>
);
}
// show pending isolation badge if a single type of isolation action has pending numbers.
// We don't care about the count here because if there were more than 1 of the same type
// (ex. 3 isolate... 0 release), then the action status displayed is still the same - "isolating".
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<EuiTextColor color="subdued" data-test-subj={getTestId('pending')}>
{badgeDisplayValue}
</EuiTextColor>
</EuiBadge>
);
}
);
AgentResponseActionsStatus.displayName = 'AgentResponseActionsStatus';

View file

@ -0,0 +1,244 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { AgentStatus } from './agent_status';
import {
useAgentStatusHook,
useGetAgentStatus,
} from '../../../../management/hooks/agents/use_get_agent_status';
import {
RESPONSE_ACTION_AGENT_TYPE,
type ResponseActionAgentType,
} from '../../../../../common/endpoint/service/response_actions/constants';
import type { AppContextTestRender } from '../../../mock/endpoint';
import { createAppRootMockRenderer } from '../../../mock/endpoint';
import { HostStatus } from '../../../../../common/endpoint/types';
jest.mock('../../../hooks/use_experimental_features');
jest.mock('../../../../management/hooks/agents/use_get_agent_status');
const getAgentStatusMock = useGetAgentStatus as jest.Mock;
const useAgentStatusHookMock = useAgentStatusHook as jest.Mock;
describe('AgentStatus component', () => {
let render: (agentType?: ResponseActionAgentType) => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
const agentId = 'agent-id-1234';
const baseData = {
agentId,
found: true,
isolated: false,
lastSeen: new Date().toISOString(),
pendingActions: {},
status: HostStatus.HEALTHY,
};
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
render = (agentType?: ResponseActionAgentType) =>
(renderResult = mockedContext.render(
<AgentStatus agentId={agentId} agentType={agentType || 'endpoint'} data-test-subj="test" />
));
getAgentStatusMock.mockReturnValue({ data: {} });
useAgentStatusHookMock.mockImplementation(() => useGetAgentStatus);
});
afterEach(() => {
jest.clearAllMocks();
});
describe.each(RESPONSE_ACTION_AGENT_TYPE)('`%s` agentType', (agentType) => {
it('should show agent health status info', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: { ...baseData, agentType, status: HostStatus.OFFLINE },
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.queryByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Offline');
expect(actionStatusBadge).toBeFalsy();
});
it('should show agent health status info and Isolated status', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
isolated: true,
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.getByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Healthy');
expect(actionStatusBadge.textContent).toEqual('Isolated');
});
it('should show agent health status info and Releasing status', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
isolated: true,
pendingActions: {
unisolate: 1,
},
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.getByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Healthy');
expect(actionStatusBadge.textContent).toEqual('Releasing');
});
it('should show agent health status info and Isolating status', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
pendingActions: {
isolate: 1,
},
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.getByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Healthy');
expect(actionStatusBadge.textContent).toEqual('Isolating');
});
it('should show agent health status info and Releasing status also when multiple actions are pending', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
isolated: true,
pendingActions: {
unisolate: 1,
execute: 1,
'kill-process': 1,
},
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.getByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Healthy');
expect(actionStatusBadge.textContent).toEqual('Releasing');
});
it('should show agent health status info and Isolating status also when multiple actions are pending', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
pendingActions: {
isolate: 1,
execute: 1,
'kill-process': 1,
},
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.getByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Healthy');
expect(actionStatusBadge.textContent).toEqual('Isolating');
});
it('should show agent health status info and pending action status when not isolating/releasing', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
pendingActions: {
'kill-process': 1,
'running-processes': 1,
},
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.getByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Healthy');
expect(actionStatusBadge.textContent).toEqual('2 actions pending');
});
it('should show agent health status info and Isolated when pending actions', () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
isolated: true,
pendingActions: {
'kill-process': 1,
'running-processes': 1,
},
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const statusBadge = renderResult.getByTestId('test-agentStatus');
const actionStatusBadge = renderResult.getByTestId('test-actionStatuses');
expect(statusBadge.textContent).toEqual('Healthy');
expect(actionStatusBadge.textContent).toEqual('Isolated');
});
});
});

View file

@ -8,15 +8,14 @@
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { getAgentStatusText } from '../../../common/components/endpoint/agent_status_text';
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../management/pages/endpoint_hosts/view/host_constants';
import { useAgentStatusHook } from './use_sentinelone_host_isolation';
import {
ISOLATED_LABEL,
ISOLATING_LABEL,
RELEASING_LABEL,
} from '../../../common/components/endpoint/endpoint_agent_status';
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
import type { EndpointPendingActions } from '../../../../../common/endpoint/types';
import { useAgentStatusHook } from '../../../../management/hooks/agents/use_get_agent_status';
import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator';
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../management/pages/endpoint_hosts/view/host_constants';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { getAgentStatusText } from '../agent_status_text';
import { AgentResponseActionsStatus } from './agent_response_action_status';
export enum SENTINEL_ONE_NETWORK_STATUS {
CONNECTING = 'connecting',
@ -31,37 +30,39 @@ const EuiFlexGroupStyled = styled(EuiFlexGroup)`
}
`;
export const SentinelOneAgentStatus = React.memo(
({ agentId, 'data-test-subj': dataTestSubj }: { agentId: string; 'data-test-subj'?: string }) => {
export const AgentStatus = React.memo(
({
agentId,
agentType,
'data-test-subj': dataTestSubj,
}: {
agentId: string;
agentType: ResponseActionAgentType;
'data-test-subj'?: string;
}) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const useAgentStatus = useAgentStatusHook();
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
'sentinelOneManualHostActionsEnabled'
);
const { data, isLoading, isFetched } = useAgentStatus([agentId], 'sentinel_one', {
const { data, isLoading, isFetched } = useAgentStatus([agentId], agentType, {
enabled: sentinelOneManualHostActionsEnabled,
});
const agentStatus = data?.[`${agentId}`];
const isCurrentlyIsolated = Boolean(agentStatus?.isolated);
const pendingActions = agentStatus?.pendingActions;
const label = useMemo(() => {
const currentNetworkStatus = agentStatus?.isolated;
const pendingActions = agentStatus?.pendingActions;
if (pendingActions) {
if (pendingActions.isolate > 0) {
return ISOLATING_LABEL;
}
if (pendingActions.unisolate > 0) {
return RELEASING_LABEL;
}
const [hasPendingActions, hostPendingActions] = useMemo<
[boolean, EndpointPendingActions['pending_actions']]
>(() => {
if (!pendingActions) {
return [false, {}];
}
if (currentNetworkStatus) {
return ISOLATED_LABEL;
}
}, [agentStatus?.isolated, agentStatus?.pendingActions]);
return [Object.keys(pendingActions).length > 0, pendingActions];
}, [pendingActions]);
return (
<EuiFlexGroupStyled
@ -75,6 +76,7 @@ export const SentinelOneAgentStatus = React.memo(
<EuiBadge
color={HOST_STATUS_TO_BADGE_COLOR[agentStatus.status]}
className="eui-textTruncate"
data-test-subj={getTestId('agentStatus')}
>
{getAgentStatusText(agentStatus.status)}
</EuiBadge>
@ -82,11 +84,13 @@ export const SentinelOneAgentStatus = React.memo(
'-'
)}
</EuiFlexItem>
{isFetched && !isLoading && label && (
{(isCurrentlyIsolated || hasPendingActions) && (
<EuiFlexItem grow={false} className="eui-textTruncate isolation-status">
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<>{label}</>
</EuiBadge>
<AgentResponseActionsStatus
data-test-subj={getTestId('actionStatuses')}
isIsolated={isCurrentlyIsolated}
pendingActions={hostPendingActions}
/>
</EuiFlexItem>
)}
</EuiFlexGroupStyled>
@ -94,4 +98,4 @@ export const SentinelOneAgentStatus = React.memo(
}
);
SentinelOneAgentStatus.displayName = 'SentinelOneAgentStatus';
AgentStatus.displayName = 'AgentStatus';

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { AppContextTestRender } from '../../../mock/endpoint';
import { createAppRootMockRenderer } from '../../../mock/endpoint';
import type { AppContextTestRender } from '../../../../mock/endpoint';
import { createAppRootMockRenderer } from '../../../../mock/endpoint';
import type {
EndpointAgentStatusByIdProps,
EndpointAgentStatusProps,
@ -15,18 +15,18 @@ import { EndpointAgentStatus, EndpointAgentStatusById } from './endpoint_agent_s
import type {
EndpointPendingActions,
HostInfoInterface,
} from '../../../../../common/endpoint/types';
import { HostStatus } from '../../../../../common/endpoint/types';
} from '../../../../../../common/endpoint/types';
import { HostStatus } from '../../../../../../common/endpoint/types';
import React from 'react';
import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
import { composeHttpHandlerMocks } from '../../../mock/endpoint/http_handler_mock_factory';
import type { EndpointMetadataHttpMocksInterface } from '../../../../management/pages/endpoint_hosts/mocks';
import { endpointMetadataHttpMocks } from '../../../../management/pages/endpoint_hosts/mocks';
import type { ResponseActionsHttpMocksInterface } from '../../../../management/mocks/response_actions_http_mocks';
import { responseActionsHttpMocks } from '../../../../management/mocks/response_actions_http_mocks';
import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator';
import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data';
import { composeHttpHandlerMocks } from '../../../../mock/endpoint/http_handler_mock_factory';
import type { EndpointMetadataHttpMocksInterface } from '../../../../../management/pages/endpoint_hosts/mocks';
import { endpointMetadataHttpMocks } from '../../../../../management/pages/endpoint_hosts/mocks';
import type { ResponseActionsHttpMocksInterface } from '../../../../../management/mocks/response_actions_http_mocks';
import { responseActionsHttpMocks } from '../../../../../management/mocks/response_actions_http_mocks';
import { waitFor, within, fireEvent } from '@testing-library/react';
import { getEmptyValue } from '../../empty_value';
import { getEmptyValue } from '../../../empty_value';
import { clone, set } from 'lodash';
type AgentStatusApiMocksInterface = EndpointMetadataHttpMocksInterface &

View file

@ -0,0 +1,168 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { DEFAULT_POLL_INTERVAL } from '../../../../../management/common/constants';
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../../management/pages/endpoint_hosts/view/host_constants';
import { getEmptyValue } from '../../../empty_value';
import { useGetEndpointPendingActionsSummary } from '../../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary';
import { useTestIdGenerator } from '../../../../../management/hooks/use_test_id_generator';
import type { EndpointPendingActions, HostInfo } from '../../../../../../common/endpoint/types';
import { useGetEndpointDetails } from '../../../../../management/hooks';
import { getAgentStatusText } from '../../agent_status_text';
import { AgentResponseActionsStatus } from '../agent_response_action_status';
export const ISOLATING_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating',
{ defaultMessage: 'Isolating' }
);
export const RELEASING_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating',
{ defaultMessage: 'Releasing' }
);
export const ISOLATED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isolated',
{ defaultMessage: 'Isolated' }
);
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
.isolation-status {
margin-left: ${({ theme }) => theme.eui.euiSizeS};
}
`;
export interface EndpointAgentStatusProps {
endpointHostInfo: HostInfo;
/**
* If set to `true` (Default), then the endpoint isolation state and response actions count
* will be kept up to date by querying the API periodically.
* Only used if `pendingActions` is not defined.
*/
autoRefresh?: boolean;
/**
* The pending actions for the host (as return by the pending actions summary api).
* If undefined, then this component will call the API to retrieve that list of pending actions.
* NOTE: if this prop is defined, it will invalidate `autoRefresh` prop.
*/
pendingActions?: EndpointPendingActions['pending_actions'];
'data-test-subj'?: string;
}
/**
* Displays the status of an Endpoint agent along with its Isolation state or the number of pending
* response actions against it.
*
* TIP: if you only have the Endpoint's `agent.id`, then consider using `EndpointAgentStatusById`,
* which will call the needed APIs to get the information necessary to display the status.
*/
// TODO: used by `EndpointAgentStatusById`
// remove usage/code when `agentStatusClientEnabled` FF is enabled and removed
export const EndpointAgentStatus = memo<EndpointAgentStatusProps>(
({ endpointHostInfo, autoRefresh = true, pendingActions, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const { data: endpointPendingActions } = useGetEndpointPendingActionsSummary(
[endpointHostInfo.metadata.agent.id],
{
refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false,
enabled: !pendingActions,
}
);
const [hasPendingActions, hostPendingActions] = useMemo<
[boolean, EndpointPendingActions['pending_actions']]
>(() => {
if (!endpointPendingActions && !pendingActions) {
return [false, {}];
}
const pending = pendingActions
? pendingActions
: endpointPendingActions?.data[0].pending_actions ?? {};
return [Object.keys(pending).length > 0, pending];
}, [endpointPendingActions, pendingActions]);
const status = endpointHostInfo.host_status;
const isIsolated = Boolean(endpointHostInfo.metadata.Endpoint.state?.isolation);
return (
<EuiFlexGroupStyled
gutterSize="none"
responsive={false}
className="eui-textTruncate"
data-test-subj={dataTestSubj}
>
<EuiFlexItem grow={false}>
<EuiBadge
color={status != null ? HOST_STATUS_TO_BADGE_COLOR[status] : 'warning'}
data-test-subj={getTestId('agentStatus')}
className="eui-textTruncate"
>
{getAgentStatusText(status)}
</EuiBadge>
</EuiFlexItem>
{(isIsolated || hasPendingActions) && (
<EuiFlexItem grow={false} className="eui-textTruncate isolation-status">
<AgentResponseActionsStatus
data-test-subj={getTestId('actionStatuses')}
isIsolated={isIsolated}
pendingActions={hostPendingActions}
/>
</EuiFlexItem>
)}
</EuiFlexGroupStyled>
);
}
);
EndpointAgentStatus.displayName = 'EndpointAgentStatus';
export interface EndpointAgentStatusByIdProps {
endpointAgentId: string;
/**
* If set to `true` (Default), then the endpoint status and isolation/action counts will
* be kept up to date by querying the API periodically
*/
autoRefresh?: boolean;
'data-test-subj'?: string;
}
/**
* Given an Endpoint Agent Id, it will make the necessary API calls and then display the agent
* status using the `<EndpointAgentStatus />` component.
*
* NOTE: if the `HostInfo` is already available, consider using `<EndpointAgentStatus/>` component
* instead in order to avoid duplicate API calls.
*/
export const EndpointAgentStatusById = memo<EndpointAgentStatusByIdProps>(
({ endpointAgentId, autoRefresh, 'data-test-subj': dataTestSubj }) => {
const { data } = useGetEndpointDetails(endpointAgentId, {
refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false,
});
if (!data) {
return (
<EuiText size="xs" data-test-subj={dataTestSubj}>
<p>{getEmptyValue()}</p>
</EuiText>
);
}
return (
<EndpointAgentStatus
endpointHostInfo={data}
data-test-subj={dataTestSubj}
autoRefresh={autoRefresh}
/>
);
}
);
EndpointAgentStatusById.displayName = 'EndpointAgentStatusById';

View file

@ -5,5 +5,6 @@
* 2.0.
*/
export * from './endpoint_agent_status';
export type { EndpointAgentStatusProps } from './endpoint_agent_status';
export * from './endpoint/endpoint_agent_status';
export type { EndpointAgentStatusProps } from './endpoint/endpoint_agent_status';
export * from './agent_status';

View file

@ -1,332 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { DEFAULT_POLL_INTERVAL } from '../../../../management/common/constants';
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../management/pages/endpoint_hosts/view/host_constants';
import { getEmptyValue } from '../../empty_value';
import {
RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP,
type ResponseActionsApiCommandNames,
} from '../../../../../common/endpoint/service/response_actions/constants';
import { useGetEndpointPendingActionsSummary } from '../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary';
import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator';
import type { EndpointPendingActions, HostInfo } from '../../../../../common/endpoint/types';
import { useGetEndpointDetails } from '../../../../management/hooks';
import { getAgentStatusText } from '../agent_status_text';
const TOOLTIP_CONTENT_STYLES: React.CSSProperties = Object.freeze({ width: 150 });
export const ISOLATING_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating',
{ defaultMessage: 'Isolating' }
);
export const RELEASING_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating',
{ defaultMessage: 'Releasing' }
);
export const ISOLATED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isolated',
{ defaultMessage: 'Isolated' }
);
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
.isolation-status {
margin-left: ${({ theme }) => theme.eui.euiSizeS};
}
`;
export interface EndpointAgentStatusProps {
endpointHostInfo: HostInfo;
/**
* If set to `true` (Default), then the endpoint isolation state and response actions count
* will be kept up to date by querying the API periodically.
* Only used if `pendingActions` is not defined.
*/
autoRefresh?: boolean;
/**
* The pending actions for the host (as return by the pending actions summary api).
* If undefined, then this component will call the API to retrieve that list of pending actions.
* NOTE: if this prop is defined, it will invalidate `autoRefresh` prop.
*/
pendingActions?: EndpointPendingActions['pending_actions'];
'data-test-subj'?: string;
}
/**
* Displays the status of an Endpoint agent along with its Isolation state or the number of pending
* response actions against it.
*
* TIP: if you only have the Endpoint's `agent.id`, then consider using `EndpointAgentStatusById`,
* which will call the needed APIs to get the information necessary to display the status.
*/
export const EndpointAgentStatus = memo<EndpointAgentStatusProps>(
({ endpointHostInfo, autoRefresh = true, pendingActions, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const { data: endpointPendingActions } = useGetEndpointPendingActionsSummary(
[endpointHostInfo.metadata.agent.id],
{
refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false,
enabled: !pendingActions,
}
);
const [hasPendingActions, hostPendingActions] = useMemo<
[boolean, EndpointPendingActions['pending_actions']]
>(() => {
if (!endpointPendingActions && !pendingActions) {
return [false, {}];
}
const pending = pendingActions
? pendingActions
: endpointPendingActions?.data[0].pending_actions ?? {};
return [Object.keys(pending).length > 0, pending];
}, [endpointPendingActions, pendingActions]);
const status = endpointHostInfo.host_status;
const isIsolated = Boolean(endpointHostInfo.metadata.Endpoint.state?.isolation);
return (
<EuiFlexGroupStyled
gutterSize="none"
responsive={false}
className="eui-textTruncate"
data-test-subj={dataTestSubj}
>
<EuiFlexItem grow={false}>
<EuiBadge
color={status != null ? HOST_STATUS_TO_BADGE_COLOR[status] : 'warning'}
data-test-subj={getTestId('agentStatus')}
className="eui-textTruncate"
>
{getAgentStatusText(status)}
</EuiBadge>
</EuiFlexItem>
{(isIsolated || hasPendingActions) && (
<EuiFlexItem grow={false} className="eui-textTruncate isolation-status">
<EndpointHostResponseActionsStatus
data-test-subj={getTestId('actionStatuses')}
isIsolated={isIsolated}
pendingActions={hostPendingActions}
/>
</EuiFlexItem>
)}
</EuiFlexGroupStyled>
);
}
);
EndpointAgentStatus.displayName = 'EndpointAgentStatus';
export interface EndpointAgentStatusByIdProps {
endpointAgentId: string;
/**
* If set to `true` (Default), then the endpoint status and isolation/action counts will
* be kept up to date by querying the API periodically
*/
autoRefresh?: boolean;
'data-test-subj'?: string;
}
/**
* Given an Endpoint Agent Id, it will make the necessary API calls and then display the agent
* status using the `<EndpointAgentStatus />` component.
*
* NOTE: if the `HostInfo` is already available, consider using `<EndpointAgentStatus/>` component
* instead in order to avoid duplicate API calls.
*/
export const EndpointAgentStatusById = memo<EndpointAgentStatusByIdProps>(
({ endpointAgentId, autoRefresh, 'data-test-subj': dataTestSubj }) => {
const { data } = useGetEndpointDetails(endpointAgentId, {
refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false,
});
const emptyValue = (
<EuiText size="xs" data-test-subj={dataTestSubj}>
<p>{getEmptyValue()}</p>
</EuiText>
);
if (!data) {
return emptyValue;
}
return (
<EndpointAgentStatus
endpointHostInfo={data}
data-test-subj={dataTestSubj}
autoRefresh={autoRefresh}
/>
);
}
);
EndpointAgentStatusById.displayName = 'EndpointAgentStatusById';
interface EndpointHostResponseActionsStatusProps {
/** The host's individual pending action list as return by the pending action summary api */
pendingActions: EndpointPendingActions['pending_actions'];
/** Is host currently isolated */
isIsolated: boolean;
'data-test-subj'?: string;
}
const EndpointHostResponseActionsStatus = memo<EndpointHostResponseActionsStatusProps>(
({ pendingActions, isIsolated, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
interface PendingActionsState {
actionList: Array<{ label: string; count: number }>;
totalPending: number;
wasReleasing: boolean;
wasIsolating: boolean;
hasMultipleActionTypesPending: boolean;
hasPendingIsolate: boolean;
hasPendingUnIsolate: boolean;
}
const {
totalPending,
actionList,
wasReleasing,
wasIsolating,
hasMultipleActionTypesPending,
hasPendingIsolate,
hasPendingUnIsolate,
} = useMemo<PendingActionsState>(() => {
const list: Array<{ label: string; count: number }> = [];
let actionTotal = 0;
let actionTypesCount = 0;
Object.entries(pendingActions)
.sort()
.forEach(([actionName, actionCount]) => {
actionTotal += actionCount;
actionTypesCount += 1;
list.push({
count: actionCount,
label:
RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP[
actionName as ResponseActionsApiCommandNames
] ?? actionName,
});
});
const pendingIsolate = pendingActions.isolate ?? 0;
const pendingUnIsolate = pendingActions.unisolate ?? 0;
return {
actionList: list,
totalPending: actionTotal,
wasReleasing: pendingIsolate === 0 && pendingUnIsolate > 0,
wasIsolating: pendingIsolate > 0 && pendingUnIsolate === 0,
hasMultipleActionTypesPending: actionTypesCount > 1,
hasPendingIsolate: pendingIsolate > 0,
hasPendingUnIsolate: pendingUnIsolate > 0,
};
}, [pendingActions]);
const badgeDisplayValue = useMemo(() => {
return hasPendingIsolate ? (
ISOLATING_LABEL
) : hasPendingUnIsolate ? (
RELEASING_LABEL
) : isIsolated ? (
ISOLATED_LABEL
) : (
<FormattedMessage
id="xpack.securitySolution.endpoint.agentAndActionsStatus.multiplePendingActions"
defaultMessage="{count} {count, plural, one {action} other {actions}} pending"
values={{
count: totalPending,
}}
/>
);
}, [hasPendingIsolate, hasPendingUnIsolate, isIsolated, totalPending]);
const isolatedBadge = useMemo(() => {
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
{ISOLATED_LABEL}
</EuiBadge>
);
}, [dataTestSubj]);
// If nothing is pending
if (totalPending === 0) {
// and host is either releasing and or currently released, then render nothing
if ((!wasIsolating && wasReleasing) || !isIsolated) {
return null;
}
// else host was isolating or is isolated, then show isolation badge
else if ((!isIsolated && wasIsolating && !wasReleasing) || isIsolated) {
return isolatedBadge;
}
}
// If there are different types of action pending
// --OR--
// the only type of actions pending is NOT isolate/release,
// then show a summary with tooltip
if (hasMultipleActionTypesPending || (!hasPendingIsolate && !hasPendingUnIsolate)) {
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj} iconType="plus" iconSide="right">
<EuiToolTip
display="block"
anchorClassName="eui-textTruncate"
anchorProps={{ 'data-test-subj': getTestId('tooltipTrigger') }}
content={
<div style={TOOLTIP_CONTENT_STYLES} data-test-subj={`${dataTestSubj}-tooltipContent`}>
<div>
<FormattedMessage
id="xpack.securitySolution.endpoint.agentAndActionsStatus.tooltipPendingActions"
defaultMessage="Pending actions:"
/>
</div>
{actionList.map(({ count, label }) => {
return (
<EuiFlexGroup gutterSize="none" key={label}>
<EuiFlexItem>{label}</EuiFlexItem>
<EuiFlexItem grow={false}>{count}</EuiFlexItem>
</EuiFlexGroup>
);
})}
</div>
}
>
<EuiTextColor color="subdued" data-test-subj={`${dataTestSubj}-pending`}>
{badgeDisplayValue}
</EuiTextColor>
</EuiToolTip>
</EuiBadge>
);
}
// show pending isolation badge if a single type of isolation action has pending numbers.
// We don't care about the count here because if there were more than 1 of the same type
// (ex. 3 isolate... 0 release), then the action status displayed is still the same - "isolating".
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<EuiTextColor color="subdued" data-test-subj={getTestId('pending')}>
{badgeDisplayValue}
</EuiTextColor>
</EuiBadge>
);
}
);
EndpointHostResponseActionsStatus.displayName = 'EndpointHostResponseActionsStatus';

View file

@ -7,6 +7,7 @@
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import type { Platform } from '../../../management/components/endpoint_responder/components/header_info/platforms';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check';
import type { ThirdPartyAgentInfo } from '../../../../common/types';
@ -165,6 +166,7 @@ export const useResponderActionData = ({
agentType: 'endpoint',
capabilities: (hostInfo.metadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [],
hostName: hostInfo.metadata.host.name,
platform: hostInfo.metadata.host.os.name.toLowerCase() as Platform,
});
}
if (onClick) onClick();

View file

@ -14,10 +14,10 @@ import {
useAgentStatusHook,
useGetAgentStatus,
useGetSentinelOneAgentStatus,
} from './use_sentinelone_host_isolation';
} from '../../../management/hooks/agents/use_get_agent_status';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
jest.mock('./use_sentinelone_host_isolation');
jest.mock('../../../management/hooks/agents/use_get_agent_status');
jest.mock('../../../common/hooks/use_experimental_features');
type AgentType = 'endpoint' | 'sentinel_one' | 'crowdstrike';

View file

@ -25,7 +25,7 @@ import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
import { getFieldValue } from './helpers';
import { useUserPrivileges } from '../../../common/components/user_privileges';
import type { AlertTableContextMenuItem } from '../alerts_table/types';
import { useAgentStatusHook } from './use_sentinelone_host_isolation';
import { useAgentStatusHook } from '../../../management/hooks/agents/use_get_agent_status';
interface UseHostIsolationActionProps {
closePopover: () => void;

View file

@ -23,11 +23,11 @@ import {
useAgentStatusHook,
useGetAgentStatus,
useGetSentinelOneAgentStatus,
} from '../../../../detections/components/host_isolation/use_sentinelone_host_isolation';
} from '../../../../management/hooks/agents/use_get_agent_status';
import { type ExpandableFlyoutApi, useExpandableFlyoutApi } from '@kbn/expandable-flyout';
jest.mock('../../../../management/hooks');
jest.mock('../../../../detections/components/host_isolation/use_sentinelone_host_isolation');
jest.mock('../../../../management/hooks/agents/use_get_agent_status');
jest.mock('@kbn/expandable-flyout', () => ({
useExpandableFlyoutApi: jest.fn(),
@ -54,9 +54,11 @@ const panelContextValue = {
const renderHighlightedFieldsCell = (values: string[], field: string) =>
render(
<RightPanelContext.Provider value={panelContextValue}>
<HighlightedFieldsCell values={values} field={field} />
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<HighlightedFieldsCell values={values} field={field} />
</RightPanelContext.Provider>
</TestProviders>
);
describe('<HighlightedFieldsCell />', () => {
@ -65,7 +67,11 @@ describe('<HighlightedFieldsCell />', () => {
});
it('should render a basic cell', () => {
const { getByTestId } = render(<HighlightedFieldsCell values={['value']} field={'field'} />);
const { getByTestId } = render(
<TestProviders>
<HighlightedFieldsCell values={['value']} field={'field'} />
</TestProviders>
);
expect(getByTestId(HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID)).toBeInTheDocument();
});
@ -108,7 +114,7 @@ describe('<HighlightedFieldsCell />', () => {
expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument();
});
// TODO: 8.15 simplify when `agentStatusClientEnabled` FF is enabled/removed
// TODO: 8.15 simplify when `agentStatusClientEnabled` FF is enabled and removed
it.each(Object.keys(hooksToMock))(
'should render SentinelOne agent status cell if field is agent.status and `origialField` is `observer.serial_number` with %s hook',
(hookName) => {
@ -135,7 +141,11 @@ describe('<HighlightedFieldsCell />', () => {
);
it('should not render if values is null', () => {
const { container } = render(<HighlightedFieldsCell values={null} field={'field'} />);
const { container } = render(
<TestProviders>
<HighlightedFieldsCell values={null} field={'field'} />
</TestProviders>
);
expect(container).toBeEmptyDOMElement();
});

View file

@ -6,12 +6,15 @@
*/
import type { VFC } from 'react';
import React, { useCallback } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { EuiFlexItem, EuiLink } from '@elastic/eui';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { SentinelOneAgentStatus } from '../../../../detections/components/host_isolation/sentinel_one_agent_status';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../common/utils/sentinelone_alert_check';
import { EndpointAgentStatusById } from '../../../../common/components/endpoint/endpoint_agent_status';
import {
AgentStatus,
EndpointAgentStatusById,
} from '../../../../common/components/agents/agent_status';
import { useRightPanelContext } from '../context';
import {
AGENT_STATUS_FIELD_NAME,
@ -77,41 +80,74 @@ export interface HighlightedFieldsCellProps {
values: string[] | null | undefined;
}
const FieldsAgentStatus = memo(
({
value,
isSentinelOneAgentIdField,
}: {
value: string | undefined;
isSentinelOneAgentIdField: boolean;
}) => {
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
if (isSentinelOneAgentIdField || agentStatusClientEnabled) {
return (
<AgentStatus
agentId={String(value ?? '')}
agentType={isSentinelOneAgentIdField ? 'sentinel_one' : 'endpoint'}
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
/>
);
} else {
// TODO: remove usage of `EndpointAgentStatusById` when `agentStatusClientEnabled` FF is enabled and removed
return (
<EndpointAgentStatusById
endpointAgentId={String(value ?? '')}
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
/>
);
}
}
);
FieldsAgentStatus.displayName = 'FieldsAgentStatus';
/**
* console.log('c::*, values != null
* Renders a component in the highlighted fields table cell based on the field name
*/
export const HighlightedFieldsCell: VFC<HighlightedFieldsCellProps> = ({
values,
field,
originalField,
}) => (
<>
{values != null &&
values.map((value, i) => {
return (
<EuiFlexItem
grow={false}
key={`${i}-${value}`}
data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`}
>
{field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? (
<LinkFieldCell value={value} />
) : field === AGENT_STATUS_FIELD_NAME &&
originalField === SENTINEL_ONE_AGENT_ID_FIELD ? (
<SentinelOneAgentStatus
agentId={String(value ?? '')}
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
/>
) : field === AGENT_STATUS_FIELD_NAME ? (
<EndpointAgentStatusById
endpointAgentId={String(value ?? '')}
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
/>
) : (
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
)}
</EuiFlexItem>
);
})}
</>
);
}) => {
const isSentinelOneAgentIdField = useMemo(
() => originalField === SENTINEL_ONE_AGENT_ID_FIELD,
[originalField]
);
return (
<>
{values != null &&
values.map((value, i) => {
return (
<EuiFlexItem
grow={false}
key={`${i}-${value}`}
data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`}
>
{field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? (
<LinkFieldCell value={value} />
) : field === AGENT_STATUS_FIELD_NAME ? (
<FieldsAgentStatus
value={value}
isSentinelOneAgentIdField={isSentinelOneAgentIdField}
/>
) : (
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
)}
</EuiFlexItem>
);
})}
</>
);
};

View file

@ -10,7 +10,7 @@ import { EuiHealth } from '@elastic/eui';
import type { EntityTableRows } from '../../shared/components/entity_table/types';
import type { ObservedEntityData } from '../../shared/components/observed_entity/types';
import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status';
import { EndpointAgentStatus } from '../../../../common/components/agents/agent_status';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import type { HostItem } from '../../../../../common/search_strategy';
import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy';

View file

@ -19,7 +19,7 @@ import type { CommandExecutionComponentProps } from '../../console/types';
import { FormattedError } from '../../formatted_error';
import { ConsoleCodeBlock } from '../../console/components/console_code_block';
import { POLICY_STATUS_TO_TEXT } from '../../../pages/endpoint_hosts/view/host_constants';
import { getAgentStatusText } from '../../../../common/components/endpoint/agent_status_text';
import { getAgentStatusText } from '../../../../common/components/agents/agent_status_text';
export const EndpointStatusActionResult = memo<
CommandExecutionComponentProps<

View file

@ -0,0 +1,128 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import { AgentInfo } from './agent_info';
import {
useAgentStatusHook,
useGetAgentStatus,
} from '../../../../../hooks/agents/use_get_agent_status';
import type { ResponseActionAgentType } from '../../../../../../../common/endpoint/service/response_actions/constants';
import { RESPONSE_ACTION_AGENT_TYPE } from '../../../../../../../common/endpoint/service/response_actions/constants';
import type { Platform } from '../platforms';
import { HostStatus } from '../../../../../../../common/endpoint/types';
jest.mock('../../../../../hooks/agents/use_get_agent_status');
const getAgentStatusMock = useGetAgentStatus as jest.Mock;
const useAgentStatusHookMock = useAgentStatusHook as jest.Mock;
describe('Responder header Agent Info', () => {
let render: (
agentType?: ResponseActionAgentType,
platform?: Platform
) => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
const agentId = 'agent-id-1234';
const baseData = {
agentId,
found: true,
isolated: false,
lastSeen: new Date().toISOString(),
pendingActions: {},
status: HostStatus.HEALTHY,
};
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
render = (agentType?: ResponseActionAgentType, platform?: Platform) =>
(renderResult = mockedContext.render(
<AgentInfo
agentId={agentId}
agentType={agentType || 'endpoint'}
hostName={'test-agent'}
platform={platform || 'linux'}
/>
));
getAgentStatusMock.mockReturnValue({ data: {} });
useAgentStatusHookMock.mockImplementation(() => useGetAgentStatus);
});
afterEach(() => {
jest.clearAllMocks();
});
describe.each(RESPONSE_ACTION_AGENT_TYPE)('`%s` agentType', (agentType) => {
it('should show endpoint name', async () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: { ...baseData, agentType, status: HostStatus.OFFLINE },
},
isLoading: false,
isFetched: true,
});
render(agentType);
const name = await renderResult.findByTestId('responderHeaderHostName');
expect(name.textContent).toBe('test-agent');
});
it('should show agent and isolation status', async () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: {
...baseData,
agentType,
status: HostStatus.HEALTHY,
pendingActions: { isolate: 1 },
},
},
isLoading: false,
isFetched: true,
});
render(agentType);
const agentStatus = await renderResult.findByTestId(
`responderHeader-${agentType}-agentIsolationStatus`
);
expect(agentStatus.textContent).toBe(`HealthyIsolating`);
});
it('should show last checkin time', async () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: { ...baseData, agentType, status: HostStatus.HEALTHY },
},
isLoading: false,
isFetched: true,
});
render(agentType);
const lastUpdated = await renderResult.findByTestId('responderHeaderLastSeen');
expect(lastUpdated).toBeTruthy();
});
it('should show platform icon', async () => {
getAgentStatusMock.mockReturnValue({
data: {
[agentId]: { ...baseData, agentType, status: HostStatus.OFFLINE },
},
isLoading: false,
isFetched: true,
});
render(agentType);
const platformIcon = await renderResult.findByTestId('responderHeaderHostPlatformIcon');
expect(platformIcon).toBeTruthy();
});
});
});

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo } from 'react';
import { AgentStatus } from '../../../../../../common/components/agents/agent_status';
import { useAgentStatusHook } from '../../../../../hooks/agents/use_get_agent_status';
import type { ThirdPartyAgentInfo } from '../../../../../../../common/types';
import { HeaderAgentInfo } from '../header_agent_info';
import type { Platform } from '../platforms';
interface AgentInfoProps {
agentId: ThirdPartyAgentInfo['agent']['id'];
agentType: ThirdPartyAgentInfo['agent']['type'];
platform: ThirdPartyAgentInfo['host']['os']['family'];
hostName: ThirdPartyAgentInfo['host']['name'];
}
export const AgentInfo = memo<AgentInfoProps>(({ agentId, platform, hostName, agentType }) => {
const getAgentStatus = useAgentStatusHook();
const { data } = getAgentStatus([agentId], agentType);
const agentStatus = data?.[agentId];
const lastCheckin = agentStatus ? agentStatus.lastSeen : '';
return (
<HeaderAgentInfo
platform={platform.toLowerCase() as Platform}
hostName={hostName}
lastCheckin={lastCheckin}
>
<AgentStatus
agentId={agentId}
agentType={agentType}
data-test-subj={`responderHeader-${agentType}-agentIsolationStatus`}
/>
</HeaderAgentInfo>
);
});
AgentInfo.displayName = 'AgentInfo';

View file

@ -7,7 +7,7 @@
import React, { memo } from 'react';
import { EuiSkeletonText } from '@elastic/eui';
import { EndpointAgentStatus } from '../../../../../../common/components/endpoint/endpoint_agent_status';
import { EndpointAgentStatus } from '../../../../../../common/components/agents/agent_status';
import { HeaderAgentInfo } from '../header_agent_info';
import { useGetEndpointDetails } from '../../../../../hooks';
import type { Platform } from '../platforms';

View file

@ -6,21 +6,22 @@
*/
import React, { memo } from 'react';
import { AgentStatus } from '../../../../../../common/components/agents/agent_status';
import { useAgentStatusHook } from '../../../../../hooks/agents/use_get_agent_status';
import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features';
import { useAgentStatusHook } from '../../../../../../detections/components/host_isolation/use_sentinelone_host_isolation';
import { SentinelOneAgentStatus } from '../../../../../../detections/components/host_isolation/sentinel_one_agent_status';
import type { ThirdPartyAgentInfo } from '../../../../../../../common/types';
import { HeaderAgentInfo } from '../header_agent_info';
import type { Platform } from '../platforms';
interface HeaderSentinelOneInfoProps {
agentId: ThirdPartyAgentInfo['agent']['id'];
agentType: ThirdPartyAgentInfo['agent']['type'];
platform: ThirdPartyAgentInfo['host']['os']['family'];
hostName: ThirdPartyAgentInfo['host']['name'];
}
export const HeaderSentinelOneInfo = memo<HeaderSentinelOneInfoProps>(
({ agentId, platform, hostName }) => {
({ agentId, agentType, platform, hostName }) => {
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
'sentinelOneManualHostActionsEnabled'
);
@ -35,8 +36,9 @@ export const HeaderSentinelOneInfo = memo<HeaderSentinelOneInfoProps>(
hostName={hostName}
lastCheckin={lastCheckin}
>
<SentinelOneAgentStatus
<AgentStatus
agentId={agentId}
agentType={agentType}
data-test-subj="responderHeaderSentinelOneAgentIsolationStatus"
/>
</HeaderAgentInfo>

View file

@ -15,13 +15,13 @@ import {
useAgentStatusHook,
useGetAgentStatus,
useGetSentinelOneAgentStatus,
} from '../../../../detections/components/host_isolation/use_sentinelone_host_isolation';
} from '../../../hooks/agents/use_get_agent_status';
import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details';
import { mockEndpointDetailsApiResult } from '../../../pages/endpoint_hosts/store/mock_endpoint_result_list';
import { OfflineCallout } from './offline_callout';
jest.mock('../../../hooks/endpoint/use_get_endpoint_details');
jest.mock('../../../../detections/components/host_isolation/use_sentinelone_host_isolation');
jest.mock('../../../hooks/agents/use_get_agent_status');
const getEndpointDetails = useGetEndpointDetails as jest.Mock;
const getSentinelOneAgentStatus = useGetSentinelOneAgentStatus as jest.Mock;

View file

@ -10,7 +10,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useAgentStatusHook } from '../../../../detections/components/host_isolation/use_sentinelone_host_isolation';
import { useAgentStatusHook } from '../../../hooks/agents/use_get_agent_status';
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
import { useGetEndpointDetails } from '../../../hooks';
import { HostStatus } from '../../../../../common/endpoint/types';

View file

@ -8,4 +8,5 @@
export { getEndpointConsoleCommands } from './lib/console_commands_definition';
export { ActionLogButton } from './components/action_log_button';
export { HeaderEndpointInfo } from './components/header_info/endpoint/header_endpoint_info';
export { AgentInfo } from './components/header_info/agent_info/agent_info';
export { OfflineCallout } from './components/offline_callout';

View file

@ -47,6 +47,7 @@ export const useGetSentinelOneAgentStatus = (
});
};
// 8.14, 8.15 used for fetching agent status
export const useGetAgentStatus = (
agentIds: string[],
agentType: string,

View file

@ -12,7 +12,7 @@ import { useLicense } from '../../common/hooks/use_license';
import type { MaybeImmutable } from '../../../common/endpoint/types';
import type { EndpointCapabilities } from '../../../common/endpoint/service/response_actions/constants';
import { type ResponseActionAgentType } from '../../../common/endpoint/service/response_actions/constants';
import { HeaderSentinelOneInfo } from '../components/endpoint_responder/components/header_info/sentinel_one/header_sentinel_one_info';
import { AgentInfo } from '../components/endpoint_responder/components/header_info/agent_info/agent_info';
import { useUserPrivileges } from '../../common/components/user_privileges';
import {
@ -33,6 +33,7 @@ export interface BasicConsoleProps {
hostName: string;
/** Required for Endpoint agents. */
capabilities: MaybeImmutable<EndpointCapabilities[]>;
platform: string;
}
type ResponderInfoProps =
@ -41,7 +42,6 @@ type ResponderInfoProps =
})
| (BasicConsoleProps & {
agentType: Exclude<ResponseActionAgentType, 'endpoint'>;
platform: string;
});
export const useWithShowResponder = (): ShowResponseActionsConsole => {
@ -51,10 +51,11 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
'responseActionsSentinelOneV1Enabled'
);
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
return useCallback(
(props: ResponderInfoProps) => {
const { agentId, agentType, capabilities, hostName } = props;
const { agentId, agentType, capabilities, hostName, platform } = props;
// If no authz, just exit and log something to the console
if (agentType === 'endpoint' && !endpointPrivileges.canAccessResponseConsole) {
window.console.error(new Error(`Access denied to ${agentType} response actions console`));
@ -81,18 +82,21 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
'data-test-subj': `${agentType}ResponseActionsConsole`,
storagePrefix: 'xpack.securitySolution.Responder',
TitleComponent: () => {
if (agentType === 'endpoint') {
return <HeaderEndpointInfo endpointId={agentId} />;
}
if (agentType === 'sentinel_one') {
if (agentStatusClientEnabled || agentType !== 'endpoint') {
return (
<HeaderSentinelOneInfo
<AgentInfo
agentId={agentId}
agentType={agentType}
hostName={hostName}
platform={props.platform}
platform={platform}
/>
);
}
// TODO: 8.15 remove this if block when agentStatusClientEnabled is enabled/removed
if (agentType === 'endpoint') {
return <HeaderEndpointInfo endpointId={agentId} />;
}
return null;
},
};
@ -104,6 +108,7 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
agentId,
hostName,
capabilities,
platform,
},
consoleProps,
PageTitleComponent: () => {
@ -139,6 +144,12 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
.show();
}
},
[endpointPrivileges, isEnterpriseLicense, isSentinelOneV1Enabled, consoleManager]
[
endpointPrivileges,
isEnterpriseLicense,
consoleManager,
agentStatusClientEnabled,
isSentinelOneV1Enabled,
]
);
};

View file

@ -17,7 +17,11 @@ import {
} from '@elastic/eui';
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EndpointAgentStatus } from '../../../../../common/components/endpoint/endpoint_agent_status';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import {
AgentStatus,
EndpointAgentStatus,
} from '../../../../../common/components/agents/agent_status';
import { isPolicyOutOfDate } from '../../utils';
import type { HostInfo } from '../../../../../../common/endpoint/types';
import { useEndpointSelector } from '../hooks';
@ -54,6 +58,7 @@ interface EndpointDetailsContentProps {
export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
({ hostInfo, policyInfo }) => {
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
const queryParams = useEndpointSelector(uiQueryParams);
const policyStatus = useMemo(
() => hostInfo.metadata.Endpoint.policy.applied.status,
@ -95,7 +100,10 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
/>
</ColumnTitle>
),
description: (
// TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed
description: agentStatusClientEnabled ? (
<AgentStatus agentId={hostInfo.metadata.agent.id} agentType="endpoint" />
) : (
<EndpointAgentStatus
pendingActions={getHostPendingActions(hostInfo.metadata.agent.id)}
endpointHostInfo={hostInfo}
@ -219,6 +227,7 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
},
];
}, [
agentStatusClientEnabled,
hostInfo,
getHostPendingActions,
missingPolicies,

View file

@ -8,6 +8,7 @@
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import type { Platform } from '../../../../components/endpoint_responder/components/header_info/platforms';
import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
import { useWithShowResponder } from '../../../../hooks';
@ -136,6 +137,7 @@ export const useEndpointActionItems = (
capabilities:
(endpointMetadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [],
hostName: endpointMetadata.host.name,
platform: endpointMetadata.host.os.name.toLowerCase() as Platform,
});
},
children: (

View file

@ -10,13 +10,13 @@ import styled from 'styled-components';
import type { CriteriaWithPagination } from '@elastic/eui';
import {
EuiBasicTable,
EuiEmptyPrompt,
EuiLoadingLogo,
type EuiBasicTableColumn,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiHorizontalRule,
EuiLoadingLogo,
type EuiSelectableProps,
EuiSpacer,
EuiSuperDatePicker,
@ -32,10 +32,14 @@ import type {
AgentPolicyDetailsDeployAgentAction,
CreatePackagePolicyRouteState,
} from '@kbn/fleet-plugin/public';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { TransformFailedCallout } from './components/transform_failed_callout';
import type { EndpointIndexUIQueryParams } from '../types';
import { EndpointListNavLink } from './components/endpoint_list_nav_link';
import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status';
import {
AgentStatus,
EndpointAgentStatus,
} from '../../../../common/components/agents/agent_status';
import { EndpointDetailsFlyout } from './details';
import * as selectors from '../store/selectors';
import { getEndpointPendingActionsCallback } from '../store/selectors';
@ -78,6 +82,7 @@ const StyledDatePicker = styled.div`
`;
interface GetEndpointListColumnsProps {
agentStatusClientEnabled: boolean;
canReadPolicyManagement: boolean;
backToEndpointList: PolicyDetailsRouteState['backLink'];
getHostPendingActions: ReturnType<typeof getEndpointPendingActionsCallback>;
@ -102,6 +107,7 @@ const columnWidths: Record<
};
const getEndpointListColumns = ({
agentStatusClientEnabled,
canReadPolicyManagement,
backToEndpointList,
getHostPendingActions,
@ -152,7 +158,10 @@ const getEndpointListColumns = ({
}),
sortable: true,
render: (hostStatus: HostInfo['host_status'], endpointInfo) => {
return (
// TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed
return agentStatusClientEnabled ? (
<AgentStatus agentId={endpointInfo.metadata.agent.id} agentType="endpoint" />
) : (
<EndpointAgentStatus
endpointHostInfo={endpointInfo}
pendingActions={getHostPendingActions(endpointInfo.metadata.agent.id)}
@ -341,6 +350,8 @@ const stateHandleDeployEndpointsClick: AgentPolicyDetailsDeployAgentAction = {
};
export const EndpointList = () => {
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
const history = useHistory();
const {
listData,
@ -528,6 +539,7 @@ export const EndpointList = () => {
const columns = useMemo(
() =>
getEndpointListColumns({
agentStatusClientEnabled,
canReadPolicyManagement,
backToEndpointList,
getAppUrl,
@ -536,6 +548,7 @@ export const EndpointList = () => {
search,
}),
[
agentStatusClientEnabled,
backToEndpointList,
canReadPolicyManagement,
getAppUrl,

View file

@ -9,7 +9,11 @@ import { EuiHealth } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import {
AgentStatus,
EndpointAgentStatus,
} from '../../../../common/components/agents/agent_status';
import { OverviewDescriptionList } from '../../../../common/components/overview_description_list';
import type { DescriptionList } from '../../../../../common/utility_types';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
@ -25,6 +29,7 @@ interface Props {
}
export const EndpointOverview = React.memo<Props>(({ contextID, data, scopeId }) => {
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
const getDefaultRenderer = useCallback(
(fieldName: string, fieldData: EndpointFields, attrName: string) => (
<DefaultFieldRenderer
@ -77,18 +82,23 @@ export const EndpointOverview = React.memo<Props>(({ contextID, data, scopeId })
{
title: i18n.FLEET_AGENT_STATUS,
description:
// TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed
data != null && data.hostInfo ? (
<EndpointAgentStatus
endpointHostInfo={data.hostInfo}
data-test-subj="endpointHostAgentStatus"
/>
agentStatusClientEnabled ? (
<AgentStatus agentId={data.hostInfo.metadata.agent.id} agentType="endpoint" />
) : (
<EndpointAgentStatus
endpointHostInfo={data.hostInfo}
data-test-subj="endpointHostAgentStatus"
/>
)
) : (
getEmptyTagValue()
),
},
],
];
}, [data, getDefaultRenderer]);
}, [agentStatusClientEnabled, data, getDefaultRenderer]);
return (
<>

View file

@ -13,14 +13,17 @@ import { isEmpty, isNumber } from 'lodash/fp';
import React from 'react';
import { css } from '@emotion/css';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import type { BrowserField } from '../../../../../common/containers/source';
import {
ALERT_HOST_CRITICALITY,
ALERT_USER_CRITICALITY,
} from '../../../../../../common/field_maps/field_names';
import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../../common/utils/sentinelone_alert_check';
import { SentinelOneAgentStatus } from '../../../../../detections/components/host_isolation/sentinel_one_agent_status';
import { EndpointAgentStatusById } from '../../../../../common/components/endpoint/endpoint_agent_status';
import {
AgentStatus,
EndpointAgentStatusById,
} from '../../../../../common/components/agents/agent_status';
import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants';
import { DefaultDraggable } from '../../../../../common/components/draggables';
import { Bytes, BYTES_FORMAT } from './bytes';
@ -104,6 +107,8 @@ const FormattedFieldValueComponent: React.FC<{
value,
linkValue,
}) => {
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
if (isObjectArray || asPlainText) {
return <span data-test-subj={`formatted-field-${fieldName}`}>{value}</span>;
} else if (fieldType === IP_FIELD_TYPE) {
@ -273,7 +278,7 @@ const FormattedFieldValueComponent: React.FC<{
fieldName === AGENT_STATUS_FIELD_NAME &&
fieldFromBrowserField?.name === SENTINEL_ONE_AGENT_ID_FIELD
) {
return <SentinelOneAgentStatus agentId={String(value ?? '')} />;
return <AgentStatus agentId={String(value ?? '')} agentType="sentinel_one" />;
} else if (fieldName === ALERT_HOST_CRITICALITY || fieldName === ALERT_USER_CRITICALITY) {
return (
<AssetCriticalityLevel
@ -287,7 +292,13 @@ const FormattedFieldValueComponent: React.FC<{
/>
);
} else if (fieldName === AGENT_STATUS_FIELD_NAME) {
return (
return agentStatusClientEnabled ? (
<AgentStatus
agentId={String(value ?? '')}
agentType="endpoint"
data-test-subj="endpointHostAgentStatus"
/>
) : (
<EndpointAgentStatusById
endpointAgentId={String(value ?? '')}
data-test-subj="endpointHostAgentStatus"

View file

@ -33018,7 +33018,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "Une fois que vous avez activé cette fonctionnalité, vous pouvez obtenir un accès rapide aux scores de risque de {riskEntity} dans cette section. Les données pourront prendre jusqu'à une heure pour être générées après l'activation du module.",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "Mettre à niveau le score de risque de {riskEntity}",
"xpack.securitySolution.endpoint.actions.unsupported.message": "La version actuelle de l'agent {agentType} ne prend pas en charge {command}. Mettez à niveau votre Elastic Agent via Fleet vers la dernière version pour activer cette action de réponse.",
"xpack.securitySolution.endpoint.agentAndActionsStatus.multiplePendingActions": "{count} {count, plural, one {action} other {actions}} en attente",
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "rév. {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {Succès} warning {Avertissement} failure {Échec} other {Inconnu}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "Une erreur s'est produite lors de la tentative de récupération des statistiques d'artefacts : \"{error}\"",
@ -35523,7 +35522,6 @@
"xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating": "Isolation",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isolated": "Isolé",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating": "Libération",
"xpack.securitySolution.endpoint.agentAndActionsStatus.tooltipPendingActions": "Actions en attente :",
"xpack.securitySolution.endpoint.blocklist.fleetIntegration.title": "Liste noire",
"xpack.securitySolution.endpoint.blocklists.fleetIntegration.title": "Liste noire",
"xpack.securitySolution.endpoint.details.agentStatus": "Statut de l'agent",
@ -45103,4 +45101,4 @@
"xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet",
"xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
}
}

View file

@ -32986,7 +32986,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "この機能を有効化すると、このセクションで{riskEntity}リスクスコアにすばやくアクセスできます。モジュールを有効化した後、データの生成までに1時間かかる場合があります。",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "{riskEntity}リスクスコアをアップグレード",
"xpack.securitySolution.endpoint.actions.unsupported.message": "現在のバージョンの{agentType}エージェントは、{command}をサポートしていません。この応答アクションを有効化するには、Fleet経由でElasticエージェントを最新バージョンにアップグレードしてください。",
"xpack.securitySolution.endpoint.agentAndActionsStatus.multiplePendingActions": "{count} {count, plural, other {個のアクション}}が保留中です",
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "rev. {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失敗} other {不明}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "アーティファクト統計情報の取得中にエラーが発生しました:\"{error}\"",
@ -35492,7 +35491,6 @@
"xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating": "分離中",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isolated": "分離済み",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating": "リリース中",
"xpack.securitySolution.endpoint.agentAndActionsStatus.tooltipPendingActions": "保留中のアクション:",
"xpack.securitySolution.endpoint.blocklist.fleetIntegration.title": "ブロックリスト",
"xpack.securitySolution.endpoint.blocklists.fleetIntegration.title": "ブロックリスト",
"xpack.securitySolution.endpoint.details.agentStatus": "エージェントステータス",
@ -45073,4 +45071,4 @@
"xpack.serverlessObservability.nav.projectSettings": "プロジェクト設定",
"xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
}
}

View file

@ -33029,7 +33029,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "一旦启用此功能,您将可以在此部分快速访问{riskEntity}风险分数。启用此模板后,可能需要一小时才能生成数据。",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "升级{riskEntity}风险分数",
"xpack.securitySolution.endpoint.actions.unsupported.message": "当前版本的 {agentType} 代理不支持 {command}。通过 Fleet 将您的 Elastic 代理升级到最新版本以启用此响应操作。",
"xpack.securitySolution.endpoint.agentAndActionsStatus.multiplePendingActions": "{count} 个{count, plural, other {操作}}待处理",
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "修订版 {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失败} other {未知}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "尝试提取项目统计时出错:“{error}”",
@ -35535,7 +35534,6 @@
"xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating": "正在隔离",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isolated": "已隔离",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating": "正在释放",
"xpack.securitySolution.endpoint.agentAndActionsStatus.tooltipPendingActions": "未决操作:",
"xpack.securitySolution.endpoint.blocklist.fleetIntegration.title": "阻止列表",
"xpack.securitySolution.endpoint.blocklists.fleetIntegration.title": "阻止列表",
"xpack.securitySolution.endpoint.details.agentStatus": "代理状态",
@ -45121,4 +45119,4 @@
"xpack.serverlessObservability.nav.projectSettings": "项目设置",
"xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
}
}