[Security Solution][Endpoint] Show consistent Endpoint Agent Status across security solution (#154961)

## Summary

- PR refactors the display of the Endpoint Host Agent status across the
multiple places in Security solution so that it uses 1 common component.
The Status of an Endpoint Host also displays the isolation state of the
endpoint along with any other Actions that might be pending against it.

- The refactor also address a prior issue where new added response
actions were not accounted for when the component displays (on hover in
a popover) the itemized count of pending response actions against the
host. The new implementation will display the summary of all pending
actions going forward as they are added without having to remember to
update the Component.

- The Endpoint host agent pending actions display was also adjusted to
ensure that isolation state is primary shown when there are multiple
pending actions, so that its always visible to the user the state of
isolation (see GIF below)

As a result of the refactor, several redundant components were deleted.

Pages that display Endpoint Host Agent status, and thus impacted by
these changes, are:

- Endpoint List
- Endpoint Details flyout
- Responder
- Alert Details
- Host Details
This commit is contained in:
Paul Tavares 2023-04-19 11:40:37 -04:00 committed by GitHub
parent b40b89e711
commit e35a1d46d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 890 additions and 907 deletions

View file

@ -12,8 +12,8 @@ import { merge, set } from 'lodash';
import { gte } from 'semver';
import type { EndpointCapabilities } from '../service/response_actions/constants';
import { BaseDataGenerator } from './base_data_generator';
import type { HostMetadataInterface, OSFields } from '../types';
import { EndpointStatus, HostPolicyResponseActionStatus } from '../types';
import type { HostMetadataInterface, OSFields, HostInfoInterface } from '../types';
import { EndpointStatus, HostPolicyResponseActionStatus, HostStatus } from '../types';
export interface GetCustomEndpointMetadataGeneratorOptions {
/** Version for agent/endpoint. Defaults to the stack version */
@ -184,6 +184,31 @@ export class EndpointMetadataGenerator extends BaseDataGenerator {
return merge(hostMetadataDoc, overrides);
}
/** Generates the complete `HostInfo` as returned by a call to the Endpoint host details api */
generateHostInfo(overrides: DeepPartial<HostInfoInterface> = {}): HostInfoInterface {
const hostInfo: HostInfoInterface = {
metadata: this.generate(),
host_status: HostStatus.HEALTHY,
policy_info: {
endpoint: {
id: 'policy-123',
revision: 4,
},
agent: {
applied: {
id: 'policy-123',
revision: 4,
},
configured: {
id: 'policy-123',
revision: 4,
},
},
},
};
return merge(hostInfo, overrides);
}
protected randomOsFields(): OSFields {
return this.randomChoice([
EndpointMetadataGenerator.windowsOSFields,

View file

@ -76,6 +76,18 @@ export const commandToRBACMap: Record<ConsoleResponseActionCommands, ResponseCon
execute: 'writeExecuteOperations',
});
export const RESPONSE_ACTION_API_COMMANDS_TO_CONSOLE_COMMAND_MAP = Object.freeze<
Record<ResponseActionsApiCommandNames, ConsoleResponseActionCommands>
>({
isolate: 'isolate',
unisolate: 'release',
execute: 'execute',
'get-file': 'get-file',
'running-processes': 'processes',
'kill-process': 'kill-process',
'suspend-process': 'suspend-process',
});
// 4 hrs in seconds
// 4 * 60 * 60
export const DEFAULT_EXECUTE_ACTION_TIMEOUT = 14400;

View file

@ -307,8 +307,9 @@ export interface ResponseActionApiResponse<TOutput extends object = object> {
export interface EndpointPendingActions {
agent_id: string;
pending_actions: {
/** Number of actions pending for each type. The `key` could be one of the `RESPONSE_ACTION_COMMANDS` values. */
/** Number of actions pending for each type */
pending_actions: Partial<Record<ResponseActionsApiCommandNames, number>> & {
// Defined any other key just in case we get back some other actions
[key: string]: number;
};
}

View file

@ -473,8 +473,10 @@ export type PolicyInfo = Immutable<{
id: string;
}>;
export type HostInfo = Immutable<{
metadata: HostMetadata;
// Host Information as returned by the Host Details API.
// NOTE: `HostInfo` type is the original and defined as Immutable.
export interface HostInfoInterface {
metadata: HostMetadataInterface;
host_status: HostStatus;
policy_info?: {
agent: {
@ -492,7 +494,9 @@ export type HostInfo = Immutable<{
*/
endpoint: PolicyInfo;
};
}>;
}
export type HostInfo = Immutable<HostInfoInterface>;
// Host metadata document streamed up to ES by the Endpoint running on host machines.
// NOTE: `HostMetadata` type is the original and defined as Immutable. If needing to

View file

@ -7,7 +7,7 @@
import type { CloudEcs, HostEcs, OsEcs } from '@kbn/securitysolution-ecs';
import type { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../../common';
import type { EndpointPendingActions, HostStatus } from '../../../../endpoint/types';
import type { EndpointPendingActions, HostInfo, HostStatus } from '../../../../endpoint/types';
import type { CommonFields } from '../..';
export enum HostPolicyResponseActionStatus {
@ -33,6 +33,8 @@ export interface EndpointFields {
elasticAgentStatus?: Maybe<HostStatus>;
fleetAgentId?: Maybe<string>;
id?: Maybe<string>;
/** The complete Endpoint Host Details information (which also includes some of the fields above */
hostInfo?: HostInfo;
}
interface AgentFields {

View file

@ -1,100 +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 } from 'react';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EndpointHostIsolationStatusProps } from './host_isolation';
export const AgentPendingActionStatusBadge = memo<
{ 'data-test-subj'?: string } & Pick<EndpointHostIsolationStatusProps, 'pendingActions'>
>(({ 'data-test-subj': dataTestSubj, pendingActions }) => {
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<EuiToolTip
display="block"
anchorClassName="eui-textTruncate"
content={
<div style={{ width: 150 }} data-test-subj={`${dataTestSubj}-tooltipContent`}>
<div>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingActions"
defaultMessage="Pending actions:"
/>
</div>
{!!pendingActions.pendingIsolate && (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingIsolate"
defaultMessage="Isolate"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{pendingActions.pendingIsolate}</EuiFlexItem>
</EuiFlexGroup>
)}
{!!pendingActions.pendingUnIsolate && (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingUnIsolate"
defaultMessage="Release"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{pendingActions.pendingUnIsolate}</EuiFlexItem>
</EuiFlexGroup>
)}
{!!pendingActions.pendingKillProcess && (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingKillProcess"
defaultMessage="Kill process"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{pendingActions.pendingKillProcess}</EuiFlexItem>
</EuiFlexGroup>
)}
{!!pendingActions.pendingSuspendProcess && (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingSuspendProcess"
defaultMessage="Suspend process"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{pendingActions.pendingSuspendProcess}</EuiFlexItem>
</EuiFlexGroup>
)}
{!!pendingActions.pendingRunningProcesses && (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingRunningProcesses"
defaultMessage="Processes"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{pendingActions.pendingRunningProcesses}</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
}
>
<EuiTextColor color="subdued" data-test-subj={`${dataTestSubj}-pending`}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.multiplePendingActions"
defaultMessage="{count} {count, plural, one {action} other {actions}} pending"
values={{
count: Object.values(pendingActions).reduce((prev, curr) => prev + curr, 0),
}}
/>
</EuiTextColor>
</EuiToolTip>
</EuiBadge>
);
});
AgentPendingActionStatusBadge.displayName = 'AgentPendingActionStatusBadge';

View file

@ -1,26 +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 from 'react';
import { EuiBadge } from '@elastic/eui';
import type { HostStatus } from '../../../../common/endpoint/types';
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../management/pages/endpoint_hosts/view/host_constants';
import { getAgentStatusText } from './agent_status_text';
export const AgentStatus = React.memo(({ hostStatus }: { hostStatus: HostStatus }) => {
return (
<EuiBadge
color={hostStatus != null ? HOST_STATUS_TO_BADGE_COLOR[hostStatus] : 'warning'}
data-test-subj="rowHostStatus"
className="eui-textTruncate"
>
{getAgentStatusText(hostStatus)}
</EuiBadge>
);
});
AgentStatus.displayName = 'AgentStatus';

View file

@ -0,0 +1,366 @@
/*
* 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 type { AppContextTestRender } from '../../../mock/endpoint';
import { createAppRootMockRenderer } from '../../../mock/endpoint';
import type {
EndpointAgentStatusByIdProps,
EndpointAgentStatusProps,
} from './endpoint_agent_status';
import { EndpointAgentStatus, EndpointAgentStatusById } from './endpoint_agent_status';
import type {
EndpointPendingActions,
HostInfoInterface,
} 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 { waitFor, within, fireEvent } from '@testing-library/react';
import { getEmptyValue } from '../../empty_value';
import { clone, set } from 'lodash';
type AgentStatusApiMocksInterface = EndpointMetadataHttpMocksInterface &
ResponseActionsHttpMocksInterface;
// API mocks composed from the endpoint metadata API mock and the response actions API mocks
const agentStatusApiMocks = composeHttpHandlerMocks<AgentStatusApiMocksInterface>([
endpointMetadataHttpMocks,
responseActionsHttpMocks,
]);
describe('When showing Endpoint Agent Status', () => {
const ENDPOINT_ISOLATION_OBJ_PATH = 'metadata.Endpoint.state.isolation';
let appTestContext: AppContextTestRender;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let endpointDetails: HostInfoInterface;
let actionsSummary: EndpointPendingActions;
let apiMocks: ReturnType<typeof agentStatusApiMocks>;
const triggerTooltip = () => {
fireEvent.mouseOver(renderResult.getByTestId('test-actionStatuses-tooltipTrigger'));
};
beforeEach(() => {
appTestContext = createAppRootMockRenderer();
apiMocks = agentStatusApiMocks(appTestContext.coreStart.http);
const actionGenerator = new EndpointActionGenerator('seed');
actionsSummary = actionGenerator.generateAgentPendingActionsSummary();
actionsSummary.pending_actions = {};
apiMocks.responseProvider.agentPendingActionsSummary.mockImplementation(() => {
return {
data: [actionsSummary],
};
});
const metadataGenerator = new EndpointDocGenerator('seed');
endpointDetails = {
metadata: metadataGenerator.generateHostMetadata(),
host_status: HostStatus.HEALTHY,
} as HostInfoInterface;
apiMocks.responseProvider.metadataDetails.mockImplementation(() => endpointDetails);
});
describe('and using `EndpointAgentStatus` component', () => {
let renderProps: EndpointAgentStatusProps;
beforeEach(() => {
renderProps = {
'data-test-subj': 'test',
endpointHostInfo: endpointDetails,
};
render = () => {
renderResult = appTestContext.render(<EndpointAgentStatus {...renderProps} />);
return renderResult;
};
});
it('should display status', () => {
const { getByTestId } = render();
expect(getByTestId('test').textContent).toEqual('Healthy');
});
it('should display status and isolated', () => {
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
const { getByTestId } = render();
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
});
it('should display status and isolated and display other pending actions in tooltip', async () => {
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
actionsSummary.pending_actions = {
'get-file': 2,
execute: 6,
};
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
triggerTooltip();
await waitFor(() => {
expect(
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
.textContent
).toEqual('Pending actions:execute6get-file2');
});
});
it('should display status and action count', async () => {
actionsSummary.pending_actions = {
'get-file': 2,
execute: 6,
};
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('Healthy8 actions pending');
});
it('should display status and isolating', async () => {
actionsSummary.pending_actions = {
isolate: 1,
};
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('HealthyIsolating');
});
it('should display status and isolating and have tooltip with other pending actions', async () => {
actionsSummary.pending_actions = {
isolate: 1,
'kill-process': 1,
};
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('HealthyIsolating');
triggerTooltip();
await waitFor(() => {
expect(
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
.textContent
).toEqual('Pending actions:isolate1kill-process1');
});
});
it('should display status and releasing', async () => {
actionsSummary.pending_actions = {
unisolate: 1,
};
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('HealthyReleasing');
});
it('should display status and releasing and show other pending actions in tooltip', async () => {
actionsSummary.pending_actions = {
unisolate: 1,
'kill-process': 1,
};
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('HealthyReleasing');
triggerTooltip();
await waitFor(() => {
expect(
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
.textContent
).toEqual('Pending actions:kill-process1release1');
});
});
it('should show individual action count in tooltip (including unknown actions) sorted asc', async () => {
actionsSummary.pending_actions = {
isolate: 1,
'get-file': 2,
execute: 6,
'kill-process': 1,
foo: 2,
};
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('HealthyIsolating');
triggerTooltip();
await waitFor(() => {
expect(
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
.textContent
).toEqual('Pending actions:execute6foo2get-file2isolate1kill-process1');
});
});
it('should still display status and isolation state if action summary api fails', async () => {
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
apiMocks.responseProvider.agentPendingActionsSummary.mockImplementation(() => {
throw new Error('test error');
});
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
});
describe('and `autoRefresh` prop is set to true', () => {
beforeEach(() => {
renderProps.autoRefresh = true;
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should keep actions up to date when autoRefresh is true', async () => {
apiMocks.responseProvider.agentPendingActionsSummary.mockReturnValueOnce({
data: [actionsSummary],
});
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual('Healthy');
apiMocks.responseProvider.agentPendingActionsSummary.mockReturnValueOnce({
data: [
{
...actionsSummary,
pending_actions: {
'kill-process': 2,
'running-processes': 2,
},
},
],
});
jest.runOnlyPendingTimers();
await waitFor(() => {
expect(getByTestId('test').textContent).toEqual('Healthy4 actions pending');
});
});
});
});
describe('And when using EndpointAgentStatusById', () => {
let renderProps: EndpointAgentStatusByIdProps;
beforeEach(() => {
jest.useFakeTimers();
renderProps = {
'data-test-subj': 'test',
endpointAgentId: '123',
};
render = () => {
renderResult = appTestContext.render(<EndpointAgentStatusById {...renderProps} />);
return renderResult;
};
});
afterEach(() => {
jest.useRealTimers();
});
it('should display status and isolated', async () => {
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
const { getByTestId } = render();
await waitFor(() => {
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
});
});
it('should display empty value if API call to host metadata fails', async () => {
apiMocks.responseProvider.metadataDetails.mockImplementation(() => {
throw new Error('test error');
});
const { getByTestId } = render();
await waitFor(() => {
expect(apiMocks.responseProvider.metadataDetails).toHaveBeenCalled();
});
expect(getByTestId('test').textContent).toEqual(getEmptyValue());
});
it('should keep agent status up to date when autoRefresh is true', async () => {
renderProps.autoFresh = true;
apiMocks.responseProvider.metadataDetails.mockReturnValueOnce(endpointDetails);
const { getByTestId } = render();
await waitFor(() => {
expect(getByTestId('test').textContent).toEqual('Healthy');
});
apiMocks.responseProvider.metadataDetails.mockReturnValueOnce(
set(clone(endpointDetails), 'metadata.Endpoint.state.isolation', true)
);
jest.runOnlyPendingTimers();
await waitFor(() => {
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
});
});
});
});

View file

@ -0,0 +1,343 @@
/*
* 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 type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants';
import { RESPONSE_ACTION_API_COMMANDS_TO_CONSOLE_COMMAND_MAP } from '../../../../../common/endpoint/service/response_actions/constants';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useGetEndpointPendingActionsSummary } from '../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary';
import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator';
import type { HostInfo, EndpointPendingActions } 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 });
const ISOLATING_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating',
{ defaultMessage: 'Isolating' }
);
const RELEASING_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating',
{ defaultMessage: 'Releasing' }
);
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
*/
autoFresh?: 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, autoFresh, 'data-test-subj': dataTestSubj }) => {
const { data } = useGetEndpointDetails(endpointAgentId, {
refetchInterval: autoFresh ? 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={autoFresh}
/>
);
}
);
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);
const isPendingStatusDisabled = useIsExperimentalFeatureEnabled(
'disableIsolationUIPendingStatuses'
);
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_COMMANDS_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 (isPendingStatusDisabled) {
// If nothing is pending and host is not currently isolated, then render nothing
if (!isIsolated) {
return null;
}
return isolatedBadge;
}
// 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

@ -5,5 +5,5 @@
* 2.0.
*/
export { EndpointAgentAndIsolationStatus } from './endpoint_agent_and_isolation_status';
export type { EndpointAgentAndIsolationStatusProps } from './endpoint_agent_and_isolation_status';
export * from './endpoint_agent_status';
export type { EndpointAgentStatusProps } from './endpoint_agent_status';

View file

@ -1,138 +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 from 'react';
import type { EndpointHostIsolationStatusProps } from './endpoint_host_isolation_status';
import { EndpointHostIsolationStatus } from './endpoint_host_isolation_status';
import type { AppContextTestRender } from '../../../mock/endpoint';
import { createAppRootMockRenderer } from '../../../mock/endpoint';
describe('when using the EndpointHostIsolationStatus component', () => {
let render: (
renderProps?: Partial<EndpointHostIsolationStatusProps>
) => ReturnType<AppContextTestRender['render']>;
let appContext: AppContextTestRender;
beforeEach(() => {
appContext = createAppRootMockRenderer();
render = (renderProps = {}) =>
appContext.render(
<EndpointHostIsolationStatus
{...{
'data-test-subj': 'test',
isIsolated: false,
pendingActions: {},
...renderProps,
}}
/>
);
});
it('should render `null` if not isolated and nothing is pending', () => {
const renderResult = render();
expect(renderResult.container.textContent).toBe('');
});
it('should show `Isolated` when no pending actions and isolated', () => {
const { getByTestId } = render({ isIsolated: true });
expect(getByTestId('test').textContent).toBe('Isolated');
});
it.each([
[
'Isolating',
{
pendingActions: {
pendingIsolate: 1,
},
},
],
[
'Releasing',
{
pendingActions: {
pendingUnIsolate: 1,
},
},
],
[
// Because they are both of the same type and there are no other types,
// the status should be `isolating`
'Isolating',
{
pendingActions: {
pendingIsolate: 2,
},
},
],
[
// Because they are both of the same type and there are no other types,
// the status should be `Releasing`
'Releasing',
{
pendingActions: {
pendingUnIsolate: 2,
},
},
],
[
'10 actions pending',
{
isIsolated: true,
pendingActions: {
pendingIsolate: 2,
pendingUnIsolate: 2,
pendingKillProcess: 2,
pendingSuspendProcess: 2,
pendingRunningProcesses: 2,
},
},
],
[
'1 action pending',
{
isIsolated: true,
pendingActions: {
pendingKillProcess: 1,
},
},
],
])('should show %s}', (expectedLabel, componentProps) => {
const { getByTestId } = render(componentProps);
expect(getByTestId('test').textContent).toBe(expectedLabel);
// Validate that the text color is set to `subdued`
expect(getByTestId('test-pending').classList.toString().includes('subdued')).toBe(true);
});
describe('and the disableIsolationUIPendingStatuses experimental feature flag is true', () => {
beforeEach(() => {
appContext.setExperimentalFlag({ disableIsolationUIPendingStatuses: true });
});
it('should render `null` if not isolated', () => {
const renderResult = render({
pendingActions: {
pendingIsolate: 10,
pendingUnIsolate: 20,
},
});
expect(renderResult.container.textContent).toBe('');
});
it('should show `Isolated` when no pending actions and isolated', () => {
const { getByTestId } = render({
isIsolated: true,
pendingActions: {
pendingIsolate: 10,
pendingUnIsolate: 20,
},
});
expect(getByTestId('test').textContent).toBe('Isolated');
});
});
});

View file

@ -1,167 +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, useRef, useEffect } from 'react';
import { EuiBadge, EuiTextColor } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { AgentPendingActionStatusBadge } from '../agent_pending_action_status_badge';
export interface EndpointHostIsolationStatusProps {
isIsolated: boolean;
pendingActions: {
/** the count of pending isolate actions */
pendingIsolate?: number;
/** the count of pending unisolate actions */
pendingUnIsolate?: number;
pendingKillProcess?: number;
pendingSuspendProcess?: number;
pendingRunningProcesses?: number;
};
'data-test-subj'?: string;
}
/**
* Component will display a host isolation status based on whether it is currently isolated or there are
* isolate/unisolate actions pending. If none of these are applicable, no UI component will be rendered
* (`null` is returned)
*/
export const EndpointHostIsolationStatus = memo<EndpointHostIsolationStatusProps>(
({ isIsolated, pendingActions, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const isPendingStatusDisabled = useIsExperimentalFeatureEnabled(
'disableIsolationUIPendingStatuses'
);
const {
pendingIsolate = 0,
pendingUnIsolate = 0,
pendingKillProcess = 0,
pendingSuspendProcess = 0,
pendingRunningProcesses = 0,
} = pendingActions;
const wasReleasing = useRef<boolean>(false);
const wasIsolating = useRef<boolean>(false);
const totalPending = useMemo(
() =>
pendingIsolate +
pendingUnIsolate +
pendingKillProcess +
pendingSuspendProcess +
pendingRunningProcesses,
[
pendingIsolate,
pendingKillProcess,
pendingRunningProcesses,
pendingSuspendProcess,
pendingUnIsolate,
]
);
const hasMultipleActionTypesPending = useMemo<boolean>(() => {
return (
Object.values(pendingActions).reduce((countOfTypes, pendingActionCount) => {
if (pendingActionCount > 0) {
return countOfTypes + 1;
}
return countOfTypes;
}, 0) > 1
);
}, [pendingActions]);
useEffect(() => {
wasReleasing.current = pendingIsolate === 0 && pendingUnIsolate > 0;
wasIsolating.current = pendingIsolate > 0 && pendingUnIsolate === 0;
}, [pendingIsolate, pendingUnIsolate]);
return useMemo(() => {
if (isPendingStatusDisabled) {
// If nothing is pending and host is not currently isolated, then render nothing
if (!isIsolated) {
return null;
}
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.isolated"
defaultMessage="Isolated"
/>
</EuiBadge>
);
}
// If nothing is pending
if (totalPending === 0) {
// and host is either releasing and or currently released, then render nothing
if ((!wasIsolating.current && wasReleasing.current) || !isIsolated) {
return null;
}
// else host was isolating or is isolated, then show isolation badge
else if ((!isIsolated && wasIsolating.current && !wasReleasing.current) || isIsolated) {
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.isolated"
defaultMessage="Isolated"
/>
</EuiBadge>
);
}
}
// 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 || (!pendingIsolate && !pendingUnIsolate)) {
return (
<AgentPendingActionStatusBadge
data-test-subj={dataTestSubj}
pendingActions={pendingActions}
/>
);
}
// 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')}>
{pendingIsolate ? (
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.isIsolating"
defaultMessage="Isolating"
/>
) : (
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.isUnIsolating"
defaultMessage="Releasing"
/>
)}
</EuiTextColor>
</EuiBadge>
);
}, [
isPendingStatusDisabled,
totalPending,
hasMultipleActionTypesPending,
pendingIsolate,
pendingUnIsolate,
dataTestSubj,
getTestId,
isIsolated,
pendingActions,
]);
}
);
EndpointHostIsolationStatus.displayName = 'EndpointHostIsolationStatus';

View file

@ -8,5 +8,4 @@
export * from './isolate_success';
export * from './isolate_form';
export * from './unisolate_form';
export * from './endpoint_host_isolation_status';
export * from './action_completion_return_button';

View file

@ -20,6 +20,28 @@ import { getTimelineEventData } from '../../../utils/get_timeline_event_data';
import { RiskSeverity } from '../../../../../../../common/search_strategy';
import { useRiskScore } from '../../../../../../explore/containers/risk_score';
jest.mock('../../../../../../management/hooks', () => {
const Generator = jest.requireActual(
'../../../../../../../common/endpoint/data_generators/endpoint_metadata_generator'
);
return {
useGetEndpointDetails: jest.fn(() => {
return {
data: new Generator.EndpointMetadataGenerator('seed').generateHostInfo({
metadata: {
Endpoint: {
state: {
isolation: true,
},
},
},
}),
};
}),
};
});
jest.mock('../../../../../../explore/containers/risk_score');
const mockUseRiskScore = useRiskScore as jest.Mock;
@ -76,7 +98,7 @@ describe('AlertDetailsPage - SummaryTab - HostPanel', () => {
describe('Agent status', () => {
it('should show healthy', () => {
const { getByTestId } = render(<HostPanelWithDefaultProps />);
expect(getByTestId('host-panel-agent-status')).toHaveTextContent('Healthy');
expect(getByTestId('endpointHostAgentStatus').textContent).toEqual('HealthyIsolated');
});
});

View file

@ -1,47 +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 type { AppContextTestRender } from '../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import type { EndpointAgentAndIsolationStatusProps } from './endpoint_agent_and_isolation_status';
import { EndpointAgentAndIsolationStatus } from './endpoint_agent_and_isolation_status';
import { HostStatus } from '../../../../common/endpoint/types';
import React from 'react';
describe('When using the EndpointAgentAndIsolationStatus component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let renderProps: EndpointAgentAndIsolationStatusProps;
beforeEach(() => {
const appTestContext = createAppRootMockRenderer();
renderProps = {
status: HostStatus.HEALTHY,
'data-test-subj': 'test',
pendingActions: {},
};
render = () => {
renderResult = appTestContext.render(<EndpointAgentAndIsolationStatus {...renderProps} />);
return renderResult;
};
});
it('should display host status only when `isIsolated` is undefined', () => {
render();
expect(renderResult.queryByTestId('test-isolationStatus')).toBeNull();
});
it('should display pending status and pending counts', () => {
renderProps.isIsolated = true;
render();
expect(renderResult.getByTestId('test-isolationStatus')).toBeTruthy();
});
});

View file

@ -1,61 +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 } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
import type { HostStatus } from '../../../../common/endpoint/types';
import { AgentStatus } from '../../../common/components/endpoint/agent_status';
import type { EndpointHostIsolationStatusProps } from '../../../common/components/endpoint/host_isolation';
import { EndpointHostIsolationStatus } from '../../../common/components/endpoint/host_isolation';
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
.isolation-status {
margin-left: ${({ theme }) => theme.eui.euiSizeS};
}
`;
export interface EndpointAgentAndIsolationStatusProps
extends Pick<EndpointHostIsolationStatusProps, 'pendingActions'> {
status: HostStatus;
/**
* If defined with a boolean, then the isolation status will be shown along with the agent status.
* The `pendingIsolate` and `pendingUnIsolate` props will only be used when this prop is set to a
* `boolean`
*/
isIsolated?: boolean;
'data-test-subj'?: string;
}
export const EndpointAgentAndIsolationStatus = memo<EndpointAgentAndIsolationStatusProps>(
({ status, isIsolated, pendingActions, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
return (
<EuiFlexGroupStyled
gutterSize="none"
responsive={false}
className="eui-textTruncate"
data-test-subj={dataTestSubj}
>
<EuiFlexItem grow={false}>
<AgentStatus hostStatus={status} />
</EuiFlexItem>
{isIsolated !== undefined && (
<EuiFlexItem grow={false} className="eui-textTruncate isolation-status">
<EndpointHostIsolationStatus
data-test-subj={getTestId('isolationStatus')}
isIsolated={isIsolated}
pendingActions={pendingActions}
/>
</EuiFlexItem>
)}
</EuiFlexGroupStyled>
);
}
);
EndpointAgentAndIsolationStatus.displayName = 'EndpointAgentAndIsolationStatus';

View file

@ -12,7 +12,6 @@ import { i18n } from '@kbn/i18n';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import type { HostInfo, PendingActionsResponse } from '../../../../../common/endpoint/types';
import type { EndpointCommandDefinitionMeta } from '../types';
import type { EndpointHostIsolationStatusProps } from '../../../../common/components/endpoint/host_isolation';
import { useGetEndpointPendingActionsSummary } from '../../../hooks/response_actions/use_get_endpoint_pending_actions_summary';
import { FormattedDate } from '../../../../common/components/formatted_date';
import { useGetEndpointDetails } from '../../../hooks';
@ -53,12 +52,10 @@ export const EndpointStatusActionResult = memo<
queryKey: [queryKey, endpointId],
});
const pendingIsolationActions = useMemo<
Pick<
Required<EndpointHostIsolationStatusProps['pendingActions']>,
'pendingIsolate' | 'pendingUnIsolate'
>
>(() => {
const pendingIsolationActions = useMemo<{
pendingIsolate: number;
pendingUnIsolate: number;
}>(() => {
if (endpointPendingActions?.data.length) {
const pendingActions = endpointPendingActions.data[0].pending_actions;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import React, { memo } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@ -17,8 +17,7 @@ import {
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details';
import type { EndpointHostIsolationStatusProps } from '../../../../common/components/endpoint/host_isolation';
import { EndpointAgentAndIsolationStatus } from '../../endpoint_agent_and_isolation_status';
import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status';
import { useGetEndpointPendingActionsSummary } from '../../../hooks/response_actions/use_get_endpoint_pending_actions_summary';
import type { Platform } from './platforms';
import { PlatformIcon } from './platforms';
@ -42,21 +41,6 @@ export const HeaderEndpointInfo = memo<HeaderEndpointInfoProps>(({ endpointId })
refetchInterval: 10000,
});
const pendingActionRequests = useMemo<
Pick<Required<EndpointHostIsolationStatusProps>, 'pendingActions'>
>(() => {
const pendingActions = endpointPendingActions?.data?.[0].pending_actions;
return {
pendingActions: {
pendingIsolate: pendingActions?.isolate ?? 0,
pendingUnIsolate: pendingActions?.unisolate ?? 0,
pendingKillProcess: pendingActions?.['kill-process'] ?? 0,
pendingSuspendProcess: pendingActions?.['suspend-process'] ?? 0,
pendingRunningProcesses: pendingActions?.['running-processes'] ?? 0,
},
};
}, [endpointPendingActions?.data]);
if (isFetching && endpointPendingActions === undefined) {
return <EuiSkeletonText lines={2} />;
}
@ -90,10 +74,8 @@ export const HeaderEndpointInfo = memo<HeaderEndpointInfoProps>(({ endpointId })
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EndpointAgentAndIsolationStatus
status={endpointDetails.host_status}
isIsolated={endpointDetails.metadata.Endpoint.state?.isolation}
{...pendingActionRequests}
<EndpointAgentStatus
endpointHostInfo={endpointDetails}
data-test-subj="responderHeaderEndpointAgentIsolationStatus"
/>
</EuiFlexItem>

View file

@ -45,7 +45,7 @@ import {
fleetGetPackagePoliciesListHttpMock,
} from '../../mocks';
type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
export type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
metadataList: () => MetadataListResponse;
metadataDetails: () => HostInfo;
}>;

View file

@ -113,6 +113,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
...state,
endpointDetails: {
...state.endpointDetails,
hostInfo: action.payload,
hostDetails: {
...state.endpointDetails.hostDetails,
details: action.payload.metadata,

View file

@ -11,7 +11,7 @@ import { createSelector } from 'reselect';
import { matchPath } from 'react-router-dom';
import { decode } from '@kbn/rison';
import type { Query } from '@kbn/es-query';
import type { Immutable, HostMetadata } from '../../../../../common/endpoint/types';
import type { Immutable, EndpointPendingActions } from '../../../../../common/endpoint/types';
import { HostStatus } from '../../../../../common/endpoint/types';
import type { EndpointState, EndpointIndexUIQueryParams } from '../types';
import { extractListPaginationParams } from '../../../common/routing';
@ -29,7 +29,6 @@ import {
import type { ServerApiError } from '../../../../common/types';
import { isEndpointHostIsolated } from '../../../../common/utils/validators';
import type { EndpointHostIsolationStatusProps } from '../../../../common/components/endpoint/host_isolation';
import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
export const listData = (state: Immutable<EndpointState>) => state.hosts;
@ -47,6 +46,9 @@ export const listError = (state: Immutable<EndpointState>) => state.error;
export const detailsData = (state: Immutable<EndpointState>) =>
state.endpointDetails.hostDetails.details;
export const fullDetailsHostInfo = (state: Immutable<EndpointState>) =>
state.endpointDetails.hostInfo;
export const detailsLoading = (state: Immutable<EndpointState>): boolean =>
state.endpointDetails.hostDetails.detailsLoading;
@ -266,48 +268,6 @@ export const getEndpointPendingActionsState = (
return state.endpointPendingActions;
};
/**
* Returns a function (callback) that can be used to retrieve the props for the `EndpointHostIsolationStatus`
* component for a given Endpoint
*/
export const getEndpointHostIsolationStatusPropsCallback: (
state: Immutable<EndpointState>
) => (endpoint: HostMetadata) => EndpointHostIsolationStatusProps = createSelector(
getEndpointPendingActionsState,
(pendingActionsState) => {
return (endpoint: HostMetadata) => {
let pendingIsolate = 0;
let pendingUnIsolate = 0;
let pendingKillProcess = 0;
let pendingSuspendProcess = 0;
let pendingRunningProcesses = 0;
if (isLoadedResourceState(pendingActionsState)) {
const endpointPendingActions = pendingActionsState.data.get(endpoint.elastic.agent.id);
if (endpointPendingActions) {
pendingIsolate = endpointPendingActions?.isolate ?? 0;
pendingUnIsolate = endpointPendingActions?.unisolate ?? 0;
pendingKillProcess = endpointPendingActions?.['kill-process'] ?? 0;
pendingSuspendProcess = endpointPendingActions?.['suspend-process'] ?? 0;
pendingRunningProcesses = endpointPendingActions?.['running-processes'] ?? 0;
}
}
return {
isIsolated: isEndpointHostIsolated(endpoint),
pendingActions: {
pendingIsolate,
pendingUnIsolate,
pendingKillProcess,
pendingSuspendProcess,
pendingRunningProcesses,
},
};
};
}
);
export const getMetadataTransformStats = (state: Immutable<EndpointState>) =>
state.metadataTransformStats;
@ -316,3 +276,24 @@ export const metadataTransformStats = (state: Immutable<EndpointState>) =>
export const isMetadataTransformStatsLoading = (state: Immutable<EndpointState>) =>
isLoadingResourceState(state.metadataTransformStats);
/**
* Returns a function (callback) that can be used to retrieve the list of pending actions against
* an endpoint currently displayed in the endpoint list
*/
export const getEndpointPendingActionsCallback: (
state: Immutable<EndpointState>
) => (endpointId: string) => EndpointPendingActions['pending_actions'] = createSelector(
getEndpointPendingActionsState,
(pendingActionsState) => {
return (endpointId: string) => {
let response: EndpointPendingActions['pending_actions'] = {};
if (isLoadedResourceState(pendingActionsState)) {
response = pendingActionsState.data.get(endpointId) ?? {};
}
return response;
};
}
);

View file

@ -36,6 +36,9 @@ export interface EndpointState {
/** api error from retrieving host list */
error?: ServerApiError;
endpointDetails: {
// Adding `hostInfo` to store full API response in order to support the
// refactoring effort with AgentStatus component
hostInfo?: HostInfo;
hostDetails: {
/** details data for a specific host */
details?: Immutable<HostMetadata>;

View file

@ -1,90 +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 from 'react';
import type { AppContextTestRender } from '../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
import { endpointPageHttpMock } from '../../mocks';
import { act } from '@testing-library/react';
import type { EndpointAgentStatusProps } from './endpoint_agent_status';
import { EndpointAgentStatus } from './endpoint_agent_status';
import type { HostMetadata } from '../../../../../../common/endpoint/types';
import { HostStatus } from '../../../../../../common/endpoint/types';
import { isLoadedResourceState } from '../../../../state';
import { KibanaServices } from '../../../../../common/lib/kibana';
jest.mock('../../../../../common/lib/kibana');
describe('When using the EndpointAgentStatus component', () => {
let render: (
props: EndpointAgentStatusProps
) => Promise<ReturnType<AppContextTestRender['render']>>;
let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
let renderResult: ReturnType<AppContextTestRender['render']>;
let httpMocks: ReturnType<typeof endpointPageHttpMock>;
let endpointMeta: HostMetadata;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
(KibanaServices.get as jest.Mock).mockReturnValue(mockedContext.startServices);
httpMocks = endpointPageHttpMock(mockedContext.coreStart.http);
waitForAction = mockedContext.middlewareSpy.waitForAction;
endpointMeta = httpMocks.responseProvider.metadataList().data[0].metadata;
render = async (props: EndpointAgentStatusProps) => {
renderResult = mockedContext.render(<EndpointAgentStatus {...props} />);
return renderResult;
};
act(() => {
mockedContext.history.push('/administration/endpoints');
});
});
it.each([
['Healthy', 'healthy'],
['Unhealthy', 'unhealthy'],
['Updating', 'updating'],
['Offline', 'offline'],
['Inactive', 'inactive'],
['Unhealthy', 'someUnknownValueHere'],
])('should show agent status of %s', async (expectedLabel, hostStatus) => {
await render({ hostStatus: hostStatus as HostStatus, endpointMetadata: endpointMeta });
expect(renderResult.getByTestId('rowHostStatus').textContent).toEqual(expectedLabel);
});
// FIXME: un-skip test once Islation pending statuses are supported
describe.skip('and host is isolated or pending isolation', () => {
beforeEach(async () => {
// Ensure pending action api sets pending action for the test endpoint metadata
const pendingActionsResponseProvider =
httpMocks.responseProvider.pendingActions.getMockImplementation();
httpMocks.responseProvider.pendingActions.mockImplementation((...args) => {
const response = pendingActionsResponseProvider!(...args);
response.data.some((pendingAction) => {
if (pendingAction.agent_id === endpointMeta.elastic.agent.id) {
pendingAction.pending_actions.isolate = 1;
return true;
}
return false;
});
return response;
});
const loadingPendingActions = waitForAction('endpointPendingActionsStateChanged', {
validate: (action) => isLoadedResourceState(action.payload),
});
await render({ hostStatus: HostStatus.HEALTHY, endpointMetadata: endpointMeta });
await loadingPendingActions;
});
it('should show host pending action', () => {
expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual('Isolating');
});
});
});

View file

@ -1,49 +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 } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import type { HostInfo, HostMetadata } from '../../../../../../common/endpoint/types';
import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation';
import { useEndpointSelector } from '../hooks';
import { getEndpointHostIsolationStatusPropsCallback } from '../../store/selectors';
import { AgentStatus } from '../../../../../common/components/endpoint/agent_status';
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
.isolation-status {
margin-left: ${({ theme }) => theme.eui.euiSizeS};
}
`;
export interface EndpointAgentStatusProps {
hostStatus: HostInfo['host_status'];
endpointMetadata: HostMetadata;
}
export const EndpointAgentStatus = memo<EndpointAgentStatusProps>(
({ endpointMetadata, hostStatus }) => {
const getEndpointIsolationStatusProps = useEndpointSelector(
getEndpointHostIsolationStatusPropsCallback
);
return (
<EuiFlexGroupStyled gutterSize="none" responsive={false} className="eui-textTruncate">
<EuiFlexItem grow={false}>
<AgentStatus hostStatus={hostStatus} />
</EuiFlexItem>
<EuiFlexItem grow={false} className="eui-textTruncate isolation-status">
<EndpointHostIsolationStatus
data-test-subj="rowIsolationStatus"
{...getEndpointIsolationStatusProps(endpointMetadata)}
/>
</EuiFlexItem>
</EuiFlexGroupStyled>
);
}
);
EndpointAgentStatus.displayName = 'EndpointAgentStatus';

View file

@ -17,17 +17,23 @@ 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 { isPolicyOutOfDate } from '../../utils';
import type { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types';
import { useEndpointSelector } from '../hooks';
import { nonExistingPolicies, policyResponseStatus, uiQueryParams } from '../../store/selectors';
import {
fullDetailsHostInfo,
getEndpointPendingActionsCallback,
nonExistingPolicies,
policyResponseStatus,
uiQueryParams,
} from '../../store/selectors';
import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants';
import { FormattedDate } from '../../../../../common/components/formatted_date';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { getEndpointDetailsPath } from '../../../../common/routing';
import { EndpointPolicyLink } from '../../../../components/endpoint_policy_link';
import { OutOfDate } from '../components/out_of_date';
import { EndpointAgentStatus } from '../components/endpoint_agent_status';
const EndpointDetailsContentStyled = styled.div`
dl dt {
@ -63,8 +69,9 @@ export const EndpointDetailsContent = memo(
const policyStatus = useEndpointSelector(
policyResponseStatus
) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR;
const getHostPendingActions = useEndpointSelector(getEndpointPendingActionsCallback);
const missingPolicies = useEndpointSelector(nonExistingPolicies);
const hostInfo = useEndpointSelector(fullDetailsHostInfo);
const policyResponseRoutePath = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -101,7 +108,14 @@ export const EndpointDetailsContent = memo(
/>
</ColumnTitle>
),
description: <EndpointAgentStatus hostStatus={hostStatus} endpointMetadata={details} />,
description: hostInfo ? (
<EndpointAgentStatus
pendingActions={getHostPendingActions(hostInfo.metadata.agent.id)}
endpointHostInfo={hostInfo}
/>
) : (
<></>
),
},
{
title: (
@ -214,7 +228,15 @@ export const EndpointDetailsContent = memo(
),
},
];
}, [details, hostStatus, policyStatus, policyStatusClickHandler, policyInfo, missingPolicies]);
}, [
details,
getHostPendingActions,
hostInfo,
missingPolicies,
policyInfo,
policyStatus,
policyStatusClickHandler,
]);
return (
<EndpointDetailsContentStyled>

View file

@ -29,6 +29,7 @@ import type {
CreatePackagePolicyRouteState,
AgentPolicyDetailsDeployAgentAction,
} from '@kbn/fleet-plugin/public';
import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status';
import { EndpointDetailsFlyout } from './details';
import * as selectors from '../store/selectors';
import { useEndpointSelector } from './hooks';
@ -60,7 +61,6 @@ import { AdminSearchBar } from './components/search_bar';
import { AdministrationListPage } from '../../../components/administration_list_page';
import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
import { TableRowActions } from './components/table_row_actions';
import { EndpointAgentStatus } from './components/endpoint_agent_status';
import { CallOut } from '../../../../common/components/callouts';
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
import { WARNING_TRANSFORM_STATES, APP_UI_ID } from '../../../../../common/constants';
@ -69,6 +69,7 @@ import { BackToExternalAppButton } from '../../../components/back_to_external_ap
import { ManagementEmptyStateWrapper } from '../../../components/management_empty_state_wrapper';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { useKibana } from '../../../../common/lib/kibana';
import { getEndpointPendingActionsCallback } from '../store/selectors';
const MAX_PAGINATED_ITEM = 9999;
const TRANSFORM_URL = '/data/transform';
@ -127,6 +128,7 @@ export const EndpointList = () => {
patternsError,
metadataTransformStats,
} = useEndpointSelector(selector);
const getHostPendingActions = useEndpointSelector(getEndpointPendingActionsCallback);
const {
canReadEndpointList,
canAccessFleet,
@ -370,7 +372,11 @@ export const EndpointList = () => {
}),
render: (hostStatus: HostInfo['host_status'], endpointInfo) => {
return (
<EndpointAgentStatus hostStatus={hostStatus} endpointMetadata={endpointInfo.metadata} />
<EndpointAgentStatus
endpointHostInfo={endpointInfo}
pendingActions={getHostPendingActions(endpointInfo.metadata.agent.id)}
data-test-subj="rowHostStatus"
/>
);
},
},
@ -536,7 +542,15 @@ export const EndpointList = () => {
],
},
];
}, [queryParams, search, getAppUrl, canReadPolicyManagement, backToEndpointList, PAD_LEFT]);
}, [
queryParams,
search,
getAppUrl,
getHostPendingActions,
canReadPolicyManagement,
backToEndpointList,
PAD_LEFT,
]);
const renderTableOrEmptyState = useMemo(() => {
if (endpointsExist) {

View file

@ -16,6 +16,7 @@ import { EndpointOverview } from '.';
import type { EndpointFields } from '../../../../../common/search_strategy/security_solution/hosts';
import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy/security_solution/hosts';
import { HostStatus } from '../../../../../common/endpoint/types';
import { EndpointMetadataGenerator } from '../../../../../common/endpoint/data_generators/endpoint_metadata_generator';
jest.mock('../../../../common/lib/kibana');
@ -44,6 +45,15 @@ describe('EndpointOverview Component', () => {
isolation: false,
elasticAgentStatus: HostStatus.HEALTHY,
pendingActions: {},
hostInfo: new EndpointMetadataGenerator('seed').generateHostInfo({
metadata: {
Endpoint: {
state: {
isolation: true,
},
},
},
}),
};
});
@ -52,7 +62,7 @@ describe('EndpointOverview Component', () => {
expect(findData.at(0).text()).toEqual(endpointData.endpointPolicy);
expect(findData.at(1).text()).toEqual(endpointData.policyStatus);
expect(findData.at(2).text()).toContain(endpointData.sensorVersion); // contain because drag adds a space
expect(findData.at(3).text()).toEqual('Healthy');
expect(findData.at(3).text()).toEqual('HealthyIsolated');
});
test('it renders with null data', () => {

View file

@ -9,6 +9,7 @@ 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 { OverviewDescriptionList } from '../../../../common/components/overview_description_list';
import type { DescriptionList } from '../../../../../common/utility_types';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
@ -16,8 +17,6 @@ import { DefaultFieldRenderer } from '../../../../timelines/components/field_ren
import * as i18n from './translations';
import type { EndpointFields } from '../../../../../common/search_strategy/security_solution/hosts';
import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy/security_solution/hosts';
import { AgentStatus } from '../../../../common/components/endpoint/agent_status';
import { EndpointHostIsolationStatus } from '../../../../common/components/endpoint/host_isolation';
interface Props {
contextID?: string;
@ -77,20 +76,11 @@ export const EndpointOverview = React.memo<Props>(({ contextID, data }) => {
{
title: i18n.FLEET_AGENT_STATUS,
description:
data != null && data.elasticAgentStatus ? (
<>
<AgentStatus hostStatus={data.elasticAgentStatus} />
<EndpointHostIsolationStatus
isIsolated={Boolean(data.isolation)}
pendingActions={{
pendingIsolate: data.pendingActions?.isolate ?? 0,
pendingUnIsolate: data.pendingActions?.unisolate ?? 0,
pendingKillProcess: data.pendingActions?.['kill-process'] ?? 0,
pendingSuspendProcess: data.pendingActions?.['suspend-process'] ?? 0,
pendingRunningProcesses: data.pendingActions?.['running-processes'] ?? 0,
}}
/>
</>
data != null && data.hostInfo ? (
<EndpointAgentStatus
endpointHostInfo={data.hostInfo}
data-test-subj="endpointHostAgentStatus"
/>
) : (
getEmptyTagValue()
),

View file

@ -1,60 +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 from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation';
import { useHostIsolationStatus } from '../../../../../detections/containers/detection_engine/alerts/use_host_isolation_status';
import { AgentStatus } from '../../../../../common/components/endpoint/agent_status';
import { EMPTY_STATUS } from './translations';
export const AgentStatuses = React.memo(
({
fieldName,
contextId,
eventId,
fieldType,
isAggregatable,
isDraggable,
value,
}: {
fieldName: string;
fieldType: string;
contextId: string;
eventId: string;
isAggregatable: boolean;
isDraggable: boolean;
value: string;
}) => {
const { isIsolated, agentStatus, pendingIsolation, pendingUnisolation } =
useHostIsolationStatus({ agentId: value });
return (
<EuiFlexGroup gutterSize="none">
{agentStatus !== undefined ? (
<EuiFlexItem grow={false}>
<AgentStatus hostStatus={agentStatus} />
</EuiFlexItem>
) : (
<EuiText>
<p>{EMPTY_STATUS}</p>
</EuiText>
)}
<EuiFlexItem grow={false}>
<EndpointHostIsolationStatus
isIsolated={isIsolated}
pendingActions={{
pendingIsolate: pendingIsolation,
pendingUnIsolate: pendingUnisolation,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
AgentStatuses.displayName = 'AgentStatuses';

View file

@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { isNumber, isEmpty } from 'lodash/fp';
import React from 'react';
import { EndpointAgentStatusById } from '../../../../../common/components/endpoint/endpoint_agent_status';
import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants';
import { DefaultDraggable } from '../../../../../common/components/draggables';
import { Bytes, BYTES_FORMAT } from './bytes';
@ -40,7 +41,6 @@ import {
import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers';
import { RuleStatus } from './rule_status';
import { HostName } from './host_name';
import { AgentStatuses } from './agent_statuses';
import { UserName } from './user_name';
// simple black-list to prevent dragging and dropping fields such as message name
@ -240,14 +240,9 @@ const FormattedFieldValueComponent: React.FC<{
);
} else if (fieldName === AGENT_STATUS_FIELD_NAME) {
return (
<AgentStatuses
contextId={contextId}
eventId={eventId}
fieldName={fieldName}
fieldType={fieldType}
isAggregatable={isAggregatable}
isDraggable={isDraggable}
value={typeof value === 'string' ? value : ''}
<EndpointAgentStatusById
endpointAgentId={String(value ?? '')}
data-test-subj="endpointHostAgentStatus"
/>
);
} else if (

View file

@ -40,16 +40,3 @@ export const LINK_ELASTIC_ENDPOINT_SECURITY = i18n.translate(
defaultMessage: 'Open in Endpoint Security',
}
);
export const EMPTY_STATUS = i18n.translate(
'xpack.securitySolution.hostIsolation.agentStatuses.empty',
{
defaultMessage: '-',
}
);
export const REASON_RENDERER_TITLE = (eventRendererName: string) =>
i18n.translate('xpack.securitySolution.event.reason.reasonRendererTitle', {
values: { eventRendererName },
defaultMessage: 'Event renderer: {eventRendererName} ',
});

View file

@ -201,6 +201,7 @@ export const getHostEndpoint = async (
: {};
return {
hostInfo: endpointData,
endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name,
policyStatus: endpointData.metadata.Endpoint.policy.applied.status,
sensorVersion: endpointData.metadata.agent.version,

View file

@ -27987,7 +27987,6 @@
"xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases": "Cette action a été attachée {caseCount, plural, one {au cas suivant} other {aux cas suivants}} :",
"xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage": "La libération de l'hôte {hostName} a été soumise avec succès",
"xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "{hostName} est actuellement {isolated}. Voulez-vous vraiment {unisolate} cet hôte ?",
"xpack.securitySolution.endpoint.hostIsolationStatus.multiplePendingActions": "{count} {count, plural, one {action} other {actions}} en attente",
"xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, healthy {Sain} unhealthy {Défectueux} updating {En cours de mise à jour} offline {Hors ligne} inactive {Inactif} unenrolled {Désinscrit} other {Défectueux}}",
"xpack.securitySolution.endpoint.list.policy.revisionNumber": "rév. {revNumber}",
"xpack.securitySolution.endpoint.list.totalCount": "Affichage de {totalItemCount, plural, one {# point de terminaison} other {# points de terminaison}}",
@ -28063,7 +28062,6 @@
"xpack.securitySolution.entityAnalytics.riskDashboard.nameTitle": "Nom de {riskEntity}",
"xpack.securitySolution.entityAnalytics.riskDashboard.riskClassificationTitle": "Classification de risque de {riskEntity}",
"xpack.securitySolution.entityAnalytics.riskDashboard.riskToolTip": "La classification de risque de {riskEntity} est déterminée par le score de risque de {riskEntityLowercase}. Les {riskEntity} classées comme Critique ou Élevée sont indiquées comme étant à risque.",
"xpack.securitySolution.event.reason.reasonRendererTitle": "Outils de rendu d'événement : {eventRendererName} ",
"xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "Le champ {field} est un objet, et il est composé de champs imbriqués qui peuvent être ajoutés en tant que colonne",
"xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "Afficher la colonne {field}",
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\" a été ajouté à la liste de filtres d'événements.",
@ -30387,15 +30385,6 @@
"xpack.securitySolution.endpoint.hostisolation.unisolate": "libération",
"xpack.securitySolution.endpoint.hostIsolation.unisolateHost": "Libérer l'hôte",
"xpack.securitySolution.endpoint.hostIsolationExceptions.fleetIntegration.title": "Exceptions d'isolation de l'hôte",
"xpack.securitySolution.endpoint.hostIsolationStatus.isIsolating": "Isolation",
"xpack.securitySolution.endpoint.hostIsolationStatus.isolated": "Isolé",
"xpack.securitySolution.endpoint.hostIsolationStatus.isUnIsolating": "Libération",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingActions": "Actions en attente :",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingIsolate": "Isoler",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingKillProcess": "Arrêter le processus",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingRunningProcesses": "Processus",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingSuspendProcess": "Suspendre le processus",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingUnIsolate": "Libération",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.environments": "protéger vos points de terminaison traditionnels ou vos environnements cloud dynamiques",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.seeDocumentationLink": "documentation",
"xpack.securitySolution.endpoint.list.actionmenu": "Ouvrir",
@ -31211,7 +31200,6 @@
"xpack.securitySolution.host.details.overview.platformTitle": "Plateforme",
"xpack.securitySolution.host.details.overview.regionTitle": "Région",
"xpack.securitySolution.host.details.versionLabel": "Version",
"xpack.securitySolution.hostIsolation.agentStatuses.empty": "-",
"xpack.securitySolution.hostIsolationExceptions.cardActionDeleteLabel": "Supprimer l'exception",
"xpack.securitySolution.hostIsolationExceptions.cardActionEditLabel": "Modifier l'exception",
"xpack.securitySolution.hostIsolationExceptions.deleteModtalTitle": "Supprimer l'exception d'isolation de l'hôte",

View file

@ -27966,7 +27966,6 @@
"xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases": "このアクションは次の{caseCount, plural, other {ケース}}に関連付けられました:",
"xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage": "ホスト{hostName}でのリリースは正常に送信されました",
"xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "{hostName}は現在{isolated}されています。このホストを{unisolate}しますか?",
"xpack.securitySolution.endpoint.hostIsolationStatus.multiplePendingActions": "{count}個の{count, plural, other {アクション}}が保留中",
"xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, healthy {正常} unhealthy {異常} updating {更新中} offline {オフライン} inactive {非アクティブ} unenrolled {登録解除済み} other {異常}}",
"xpack.securitySolution.endpoint.list.policy.revisionNumber": "rev. {revNumber}",
"xpack.securitySolution.endpoint.list.totalCount": "{totalItemCount, plural, other {#個のエンドポイント}}を表示中",
@ -28042,7 +28041,6 @@
"xpack.securitySolution.entityAnalytics.riskDashboard.nameTitle": "{riskEntity}名",
"xpack.securitySolution.entityAnalytics.riskDashboard.riskClassificationTitle": "{riskEntity}リスク分類",
"xpack.securitySolution.entityAnalytics.riskDashboard.riskToolTip": "{riskEntity}リスク分類は、{riskEntityLowercase}リスクスコアによって決定されます。「重大」または「高」に分類された{riskEntity}は、リスクが高いことが表示されます。",
"xpack.securitySolution.event.reason.reasonRendererTitle": "イベントレンダラー:{eventRendererName} ",
"xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field}フィールドはオブジェクトであり、列として追加できるネストされたフィールドに分解されます",
"xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "{field}列を表示",
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\"がイベントフィルターリストに追加されました。",
@ -30366,15 +30364,6 @@
"xpack.securitySolution.endpoint.hostisolation.unisolate": "リリース",
"xpack.securitySolution.endpoint.hostIsolation.unisolateHost": "ホストのリリース",
"xpack.securitySolution.endpoint.hostIsolationExceptions.fleetIntegration.title": "ホスト分離例外",
"xpack.securitySolution.endpoint.hostIsolationStatus.isIsolating": "分離中",
"xpack.securitySolution.endpoint.hostIsolationStatus.isolated": "分離済み",
"xpack.securitySolution.endpoint.hostIsolationStatus.isUnIsolating": "リリース中",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingActions": "保留中のアクション:",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingIsolate": "分離",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingKillProcess": "プロセスを終了",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingRunningProcesses": "プロセス",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingSuspendProcess": "プロセスを一時停止",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingUnIsolate": "リリース",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.environments": "従来のエンドポイントや動的クラウド環境を保護",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.seeDocumentationLink": "ドキュメンテーション",
"xpack.securitySolution.endpoint.list.actionmenu": "開く",
@ -31190,7 +31179,6 @@
"xpack.securitySolution.host.details.overview.platformTitle": "プラットフォーム",
"xpack.securitySolution.host.details.overview.regionTitle": "地域",
"xpack.securitySolution.host.details.versionLabel": "バージョン",
"xpack.securitySolution.hostIsolation.agentStatuses.empty": "-",
"xpack.securitySolution.hostIsolationExceptions.cardActionDeleteLabel": "例外の削除",
"xpack.securitySolution.hostIsolationExceptions.cardActionEditLabel": "例外の編集",
"xpack.securitySolution.hostIsolationExceptions.deleteModtalTitle": "ホスト分離例外を削除",

View file

@ -27982,7 +27982,6 @@
"xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases": "此操作已附加到以下{caseCount, plural, other {案例}}",
"xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage": "已成功提交主机 {hostName} 的释放",
"xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "{hostName} 当前 {isolated}。是否确定要{unisolate}此主机?",
"xpack.securitySolution.endpoint.hostIsolationStatus.multiplePendingActions": "{count} 个{count, plural, other {操作}}未决",
"xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, healthy {运行正常} unhealthy {运行不正常} updating {正在更新} offline {脱机} inactive {非活动} unenrolled {未注册} other {运行不正常}}",
"xpack.securitySolution.endpoint.list.policy.revisionNumber": "rev. {revNumber}",
"xpack.securitySolution.endpoint.list.totalCount": "正在显示 {totalItemCount, plural, other {# 个终端}}",
@ -28058,7 +28057,6 @@
"xpack.securitySolution.entityAnalytics.riskDashboard.nameTitle": "{riskEntity} 名称",
"xpack.securitySolution.entityAnalytics.riskDashboard.riskClassificationTitle": "{riskEntity} 风险分类",
"xpack.securitySolution.entityAnalytics.riskDashboard.riskToolTip": "{riskEntity} 风险分类由 {riskEntityLowercase} 风险分数决定。分类为紧急或高的{riskEntity}主机即表示存在风险。",
"xpack.securitySolution.event.reason.reasonRendererTitle": "事件呈现器:{eventRendererName}",
"xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field} 字段是对象,并分解为可以添加为列的嵌套字段",
"xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "查看 {field} 列",
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "“{name}”已添加到事件筛选列表。",
@ -30382,15 +30380,6 @@
"xpack.securitySolution.endpoint.hostisolation.unisolate": "释放",
"xpack.securitySolution.endpoint.hostIsolation.unisolateHost": "释放主机",
"xpack.securitySolution.endpoint.hostIsolationExceptions.fleetIntegration.title": "主机隔离例外",
"xpack.securitySolution.endpoint.hostIsolationStatus.isIsolating": "正在隔离",
"xpack.securitySolution.endpoint.hostIsolationStatus.isolated": "已隔离",
"xpack.securitySolution.endpoint.hostIsolationStatus.isUnIsolating": "正在释放",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingActions": "未决操作:",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingIsolate": "隔离",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingKillProcess": "结束进程",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingRunningProcesses": "进程",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingSuspendProcess": "挂起进程",
"xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingUnIsolate": "释放",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.environments": "保护您的传统终端或动态云环境",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.seeDocumentationLink": "文档",
"xpack.securitySolution.endpoint.list.actionmenu": "打开",
@ -31206,7 +31195,6 @@
"xpack.securitySolution.host.details.overview.platformTitle": "平台",
"xpack.securitySolution.host.details.overview.regionTitle": "地区",
"xpack.securitySolution.host.details.versionLabel": "版本",
"xpack.securitySolution.hostIsolation.agentStatuses.empty": "-",
"xpack.securitySolution.hostIsolationExceptions.cardActionDeleteLabel": "删除例外",
"xpack.securitySolution.hostIsolationExceptions.cardActionEditLabel": "编辑例外",
"xpack.securitySolution.hostIsolationExceptions.deleteModtalTitle": "删除主机隔离例外",