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

View file

@ -5,8 +5,8 @@
* 2.0. * 2.0.
*/ */
import type { AppContextTestRender } from '../../../mock/endpoint'; import type { AppContextTestRender } from '../../../../mock/endpoint';
import { createAppRootMockRenderer } from '../../../mock/endpoint'; import { createAppRootMockRenderer } from '../../../../mock/endpoint';
import type { import type {
EndpointAgentStatusByIdProps, EndpointAgentStatusByIdProps,
EndpointAgentStatusProps, EndpointAgentStatusProps,
@ -15,18 +15,18 @@ import { EndpointAgentStatus, EndpointAgentStatusById } from './endpoint_agent_s
import type { import type {
EndpointPendingActions, EndpointPendingActions,
HostInfoInterface, HostInfoInterface,
} from '../../../../../common/endpoint/types'; } from '../../../../../../common/endpoint/types';
import { HostStatus } from '../../../../../common/endpoint/types'; import { HostStatus } from '../../../../../../common/endpoint/types';
import React from 'react'; import React from 'react';
import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator'; import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data';
import { composeHttpHandlerMocks } from '../../../mock/endpoint/http_handler_mock_factory'; import { composeHttpHandlerMocks } from '../../../../mock/endpoint/http_handler_mock_factory';
import type { EndpointMetadataHttpMocksInterface } from '../../../../management/pages/endpoint_hosts/mocks'; import type { EndpointMetadataHttpMocksInterface } from '../../../../../management/pages/endpoint_hosts/mocks';
import { endpointMetadataHttpMocks } 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 type { ResponseActionsHttpMocksInterface } from '../../../../../management/mocks/response_actions_http_mocks';
import { responseActionsHttpMocks } 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 { waitFor, within, fireEvent } from '@testing-library/react';
import { getEmptyValue } from '../../empty_value'; import { getEmptyValue } from '../../../empty_value';
import { clone, set } from 'lodash'; import { clone, set } from 'lodash';
type AgentStatusApiMocksInterface = EndpointMetadataHttpMocksInterface & 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. * 2.0.
*/ */
export * from './endpoint_agent_status'; export * from './endpoint/endpoint_agent_status';
export type { EndpointAgentStatusProps } from './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 type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; 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 { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check'; import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check';
import type { ThirdPartyAgentInfo } from '../../../../common/types'; import type { ThirdPartyAgentInfo } from '../../../../common/types';
@ -165,6 +166,7 @@ export const useResponderActionData = ({
agentType: 'endpoint', agentType: 'endpoint',
capabilities: (hostInfo.metadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [], capabilities: (hostInfo.metadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [],
hostName: hostInfo.metadata.host.name, hostName: hostInfo.metadata.host.name,
platform: hostInfo.metadata.host.os.name.toLowerCase() as Platform,
}); });
} }
if (onClick) onClick(); if (onClick) onClick();

View file

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

View file

@ -25,7 +25,7 @@ import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
import { getFieldValue } from './helpers'; import { getFieldValue } from './helpers';
import { useUserPrivileges } from '../../../common/components/user_privileges'; import { useUserPrivileges } from '../../../common/components/user_privileges';
import type { AlertTableContextMenuItem } from '../alerts_table/types'; 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 { interface UseHostIsolationActionProps {
closePopover: () => void; closePopover: () => void;

View file

@ -23,11 +23,11 @@ import {
useAgentStatusHook, useAgentStatusHook,
useGetAgentStatus, useGetAgentStatus,
useGetSentinelOneAgentStatus, 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'; import { type ExpandableFlyoutApi, useExpandableFlyoutApi } from '@kbn/expandable-flyout';
jest.mock('../../../../management/hooks'); 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', () => ({ jest.mock('@kbn/expandable-flyout', () => ({
useExpandableFlyoutApi: jest.fn(), useExpandableFlyoutApi: jest.fn(),
@ -54,9 +54,11 @@ const panelContextValue = {
const renderHighlightedFieldsCell = (values: string[], field: string) => const renderHighlightedFieldsCell = (values: string[], field: string) =>
render( render(
<RightPanelContext.Provider value={panelContextValue}> <TestProviders>
<HighlightedFieldsCell values={values} field={field} /> <RightPanelContext.Provider value={panelContextValue}>
</RightPanelContext.Provider> <HighlightedFieldsCell values={values} field={field} />
</RightPanelContext.Provider>
</TestProviders>
); );
describe('<HighlightedFieldsCell />', () => { describe('<HighlightedFieldsCell />', () => {
@ -65,7 +67,11 @@ describe('<HighlightedFieldsCell />', () => {
}); });
it('should render a basic cell', () => { 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(); 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(); 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))( 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', 'should render SentinelOne agent status cell if field is agent.status and `origialField` is `observer.serial_number` with %s hook',
(hookName) => { (hookName) => {
@ -135,7 +141,11 @@ describe('<HighlightedFieldsCell />', () => {
); );
it('should not render if values is null', () => { 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(); expect(container).toBeEmptyDOMElement();
}); });

View file

@ -6,12 +6,15 @@
*/ */
import type { VFC } from 'react'; import type { VFC } from 'react';
import React, { useCallback } from 'react'; import React, { memo, useCallback, useMemo } from 'react';
import { EuiFlexItem, EuiLink } from '@elastic/eui'; import { EuiFlexItem, EuiLink } from '@elastic/eui';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; 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 { 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 { useRightPanelContext } from '../context';
import { import {
AGENT_STATUS_FIELD_NAME, AGENT_STATUS_FIELD_NAME,
@ -77,41 +80,74 @@ export interface HighlightedFieldsCellProps {
values: string[] | null | undefined; 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 * Renders a component in the highlighted fields table cell based on the field name
*/ */
export const HighlightedFieldsCell: VFC<HighlightedFieldsCellProps> = ({ export const HighlightedFieldsCell: VFC<HighlightedFieldsCellProps> = ({
values, values,
field, field,
originalField, originalField,
}) => ( }) => {
<> const isSentinelOneAgentIdField = useMemo(
{values != null && () => originalField === SENTINEL_ONE_AGENT_ID_FIELD,
values.map((value, i) => { [originalField]
return ( );
<EuiFlexItem
grow={false} return (
key={`${i}-${value}`} <>
data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`} {values != null &&
> values.map((value, i) => {
{field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? ( return (
<LinkFieldCell value={value} /> <EuiFlexItem
) : field === AGENT_STATUS_FIELD_NAME && grow={false}
originalField === SENTINEL_ONE_AGENT_ID_FIELD ? ( key={`${i}-${value}`}
<SentinelOneAgentStatus data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`}
agentId={String(value ?? '')} >
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID} {field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? (
/> <LinkFieldCell value={value} />
) : field === AGENT_STATUS_FIELD_NAME ? ( ) : field === AGENT_STATUS_FIELD_NAME ? (
<EndpointAgentStatusById <FieldsAgentStatus
endpointAgentId={String(value ?? '')} value={value}
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID} isSentinelOneAgentIdField={isSentinelOneAgentIdField}
/> />
) : ( ) : (
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span> <span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
)} )}
</EuiFlexItem> </EuiFlexItem>
); );
})} })}
</> </>
); );
};

View file

@ -10,7 +10,7 @@ import { EuiHealth } from '@elastic/eui';
import type { EntityTableRows } from '../../shared/components/entity_table/types'; import type { EntityTableRows } from '../../shared/components/entity_table/types';
import type { ObservedEntityData } from '../../shared/components/observed_entity/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 { getEmptyTagValue } from '../../../../common/components/empty_value';
import type { HostItem } from '../../../../../common/search_strategy'; import type { HostItem } from '../../../../../common/search_strategy';
import { HostPolicyResponseActionStatus } 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 { FormattedError } from '../../formatted_error';
import { ConsoleCodeBlock } from '../../console/components/console_code_block'; import { ConsoleCodeBlock } from '../../console/components/console_code_block';
import { POLICY_STATUS_TO_TEXT } from '../../../pages/endpoint_hosts/view/host_constants'; 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< export const EndpointStatusActionResult = memo<
CommandExecutionComponentProps< 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 React, { memo } from 'react';
import { EuiSkeletonText } from '@elastic/eui'; 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 { HeaderAgentInfo } from '../header_agent_info';
import { useGetEndpointDetails } from '../../../../../hooks'; import { useGetEndpointDetails } from '../../../../../hooks';
import type { Platform } from '../platforms'; import type { Platform } from '../platforms';

View file

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

View file

@ -15,13 +15,13 @@ import {
useAgentStatusHook, useAgentStatusHook,
useGetAgentStatus, useGetAgentStatus,
useGetSentinelOneAgentStatus, 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 { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details';
import { mockEndpointDetailsApiResult } from '../../../pages/endpoint_hosts/store/mock_endpoint_result_list'; import { mockEndpointDetailsApiResult } from '../../../pages/endpoint_hosts/store/mock_endpoint_result_list';
import { OfflineCallout } from './offline_callout'; import { OfflineCallout } from './offline_callout';
jest.mock('../../../hooks/endpoint/use_get_endpoint_details'); 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 getEndpointDetails = useGetEndpointDetails as jest.Mock;
const getSentinelOneAgentStatus = useGetSentinelOneAgentStatus 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 { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; 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 type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
import { useGetEndpointDetails } from '../../../hooks'; import { useGetEndpointDetails } from '../../../hooks';
import { HostStatus } from '../../../../../common/endpoint/types'; import { HostStatus } from '../../../../../common/endpoint/types';

View file

@ -8,4 +8,5 @@
export { getEndpointConsoleCommands } from './lib/console_commands_definition'; export { getEndpointConsoleCommands } from './lib/console_commands_definition';
export { ActionLogButton } from './components/action_log_button'; export { ActionLogButton } from './components/action_log_button';
export { HeaderEndpointInfo } from './components/header_info/endpoint/header_endpoint_info'; 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'; 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 = ( export const useGetAgentStatus = (
agentIds: string[], agentIds: string[],
agentType: 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 { MaybeImmutable } from '../../../common/endpoint/types';
import type { EndpointCapabilities } from '../../../common/endpoint/service/response_actions/constants'; import type { EndpointCapabilities } from '../../../common/endpoint/service/response_actions/constants';
import { type ResponseActionAgentType } 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 { useUserPrivileges } from '../../common/components/user_privileges';
import { import {
@ -33,6 +33,7 @@ export interface BasicConsoleProps {
hostName: string; hostName: string;
/** Required for Endpoint agents. */ /** Required for Endpoint agents. */
capabilities: MaybeImmutable<EndpointCapabilities[]>; capabilities: MaybeImmutable<EndpointCapabilities[]>;
platform: string;
} }
type ResponderInfoProps = type ResponderInfoProps =
@ -41,7 +42,6 @@ type ResponderInfoProps =
}) })
| (BasicConsoleProps & { | (BasicConsoleProps & {
agentType: Exclude<ResponseActionAgentType, 'endpoint'>; agentType: Exclude<ResponseActionAgentType, 'endpoint'>;
platform: string;
}); });
export const useWithShowResponder = (): ShowResponseActionsConsole => { export const useWithShowResponder = (): ShowResponseActionsConsole => {
@ -51,10 +51,11 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
'responseActionsSentinelOneV1Enabled' 'responseActionsSentinelOneV1Enabled'
); );
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
return useCallback( return useCallback(
(props: ResponderInfoProps) => { (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 no authz, just exit and log something to the console
if (agentType === 'endpoint' && !endpointPrivileges.canAccessResponseConsole) { if (agentType === 'endpoint' && !endpointPrivileges.canAccessResponseConsole) {
window.console.error(new Error(`Access denied to ${agentType} response actions console`)); 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`, 'data-test-subj': `${agentType}ResponseActionsConsole`,
storagePrefix: 'xpack.securitySolution.Responder', storagePrefix: 'xpack.securitySolution.Responder',
TitleComponent: () => { TitleComponent: () => {
if (agentType === 'endpoint') { if (agentStatusClientEnabled || agentType !== 'endpoint') {
return <HeaderEndpointInfo endpointId={agentId} />;
}
if (agentType === 'sentinel_one') {
return ( return (
<HeaderSentinelOneInfo <AgentInfo
agentId={agentId} agentId={agentId}
agentType={agentType}
hostName={hostName} 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; return null;
}, },
}; };
@ -104,6 +108,7 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
agentId, agentId,
hostName, hostName,
capabilities, capabilities,
platform,
}, },
consoleProps, consoleProps,
PageTitleComponent: () => { PageTitleComponent: () => {
@ -139,6 +144,12 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
.show(); .show();
} }
}, },
[endpointPrivileges, isEnterpriseLicense, isSentinelOneV1Enabled, consoleManager] [
endpointPrivileges,
isEnterpriseLicense,
consoleManager,
agentStatusClientEnabled,
isSentinelOneV1Enabled,
]
); );
}; };

View file

@ -17,7 +17,11 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-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 { isPolicyOutOfDate } from '../../utils';
import type { HostInfo } from '../../../../../../common/endpoint/types'; import type { HostInfo } from '../../../../../../common/endpoint/types';
import { useEndpointSelector } from '../hooks'; import { useEndpointSelector } from '../hooks';
@ -54,6 +58,7 @@ interface EndpointDetailsContentProps {
export const EndpointDetailsContent = memo<EndpointDetailsContentProps>( export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
({ hostInfo, policyInfo }) => { ({ hostInfo, policyInfo }) => {
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
const queryParams = useEndpointSelector(uiQueryParams); const queryParams = useEndpointSelector(uiQueryParams);
const policyStatus = useMemo( const policyStatus = useMemo(
() => hostInfo.metadata.Endpoint.policy.applied.status, () => hostInfo.metadata.Endpoint.policy.applied.status,
@ -95,7 +100,10 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
/> />
</ColumnTitle> </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 <EndpointAgentStatus
pendingActions={getHostPendingActions(hostInfo.metadata.agent.id)} pendingActions={getHostPendingActions(hostInfo.metadata.agent.id)}
endpointHostInfo={hostInfo} endpointHostInfo={hostInfo}
@ -219,6 +227,7 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
}, },
]; ];
}, [ }, [
agentStatusClientEnabled,
hostInfo, hostInfo,
getHostPendingActions, getHostPendingActions,
missingPolicies, missingPolicies,

View file

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

View file

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

View file

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

View file

@ -13,14 +13,17 @@ import { isEmpty, isNumber } from 'lodash/fp';
import React from 'react'; import React from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import type { BrowserField } from '../../../../../common/containers/source'; import type { BrowserField } from '../../../../../common/containers/source';
import { import {
ALERT_HOST_CRITICALITY, ALERT_HOST_CRITICALITY,
ALERT_USER_CRITICALITY, ALERT_USER_CRITICALITY,
} from '../../../../../../common/field_maps/field_names'; } from '../../../../../../common/field_maps/field_names';
import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../../common/utils/sentinelone_alert_check'; import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../../common/utils/sentinelone_alert_check';
import { SentinelOneAgentStatus } from '../../../../../detections/components/host_isolation/sentinel_one_agent_status'; import {
import { EndpointAgentStatusById } from '../../../../../common/components/endpoint/endpoint_agent_status'; AgentStatus,
EndpointAgentStatusById,
} from '../../../../../common/components/agents/agent_status';
import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants'; import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants';
import { DefaultDraggable } from '../../../../../common/components/draggables'; import { DefaultDraggable } from '../../../../../common/components/draggables';
import { Bytes, BYTES_FORMAT } from './bytes'; import { Bytes, BYTES_FORMAT } from './bytes';
@ -104,6 +107,8 @@ const FormattedFieldValueComponent: React.FC<{
value, value,
linkValue, linkValue,
}) => { }) => {
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
if (isObjectArray || asPlainText) { if (isObjectArray || asPlainText) {
return <span data-test-subj={`formatted-field-${fieldName}`}>{value}</span>; return <span data-test-subj={`formatted-field-${fieldName}`}>{value}</span>;
} else if (fieldType === IP_FIELD_TYPE) { } else if (fieldType === IP_FIELD_TYPE) {
@ -273,7 +278,7 @@ const FormattedFieldValueComponent: React.FC<{
fieldName === AGENT_STATUS_FIELD_NAME && fieldName === AGENT_STATUS_FIELD_NAME &&
fieldFromBrowserField?.name === SENTINEL_ONE_AGENT_ID_FIELD 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) { } else if (fieldName === ALERT_HOST_CRITICALITY || fieldName === ALERT_USER_CRITICALITY) {
return ( return (
<AssetCriticalityLevel <AssetCriticalityLevel
@ -287,7 +292,13 @@ const FormattedFieldValueComponent: React.FC<{
/> />
); );
} else if (fieldName === AGENT_STATUS_FIELD_NAME) { } else if (fieldName === AGENT_STATUS_FIELD_NAME) {
return ( return agentStatusClientEnabled ? (
<AgentStatus
agentId={String(value ?? '')}
agentType="endpoint"
data-test-subj="endpointHostAgentStatus"
/>
) : (
<EndpointAgentStatusById <EndpointAgentStatusById
endpointAgentId={String(value ?? '')} endpointAgentId={String(value ?? '')}
data-test-subj="endpointHostAgentStatus" 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.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.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.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.policy.revisionNumber": "rév. {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {Succès} warning {Avertissement} failure {Échec} other {Inconnu}}", "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}\"", "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.isIsolating": "Isolation",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isolated": "Isolé", "xpack.securitySolution.endpoint.agentAndActionsStatus.isolated": "Isolé",
"xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating": "Libération", "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.blocklist.fleetIntegration.title": "Liste noire",
"xpack.securitySolution.endpoint.blocklists.fleetIntegration.title": "Liste noire", "xpack.securitySolution.endpoint.blocklists.fleetIntegration.title": "Liste noire",
"xpack.securitySolution.endpoint.details.agentStatus": "Statut de l'agent", "xpack.securitySolution.endpoint.details.agentStatus": "Statut de l'agent",
@ -45103,4 +45101,4 @@
"xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet", "xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet",
"xpack.serverlessObservability.nav.synthetics": "Synthetics" "xpack.serverlessObservability.nav.synthetics": "Synthetics"
} }
} }

View file

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

View file

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