diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts index e040fa4811677..e5a44c46f5afc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -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 { + 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, diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts index b304dcd819d8b..ea7ee057dc273 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts @@ -76,6 +76,18 @@ export const commandToRBACMap: Record +>({ + 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; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 58c07459de441..d9082f8aa1d8c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -307,8 +307,9 @@ export interface ResponseActionApiResponse { 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> & { + // Defined any other key just in case we get back some other actions [key: string]: number; }; } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index eb4d5c7421eef..ebbbd352dd83a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -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; // 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 diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 7f708ab3f6111..2e433b8cfd45f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -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; fleetAgentId?: Maybe; id?: Maybe; + /** The complete Endpoint Host Details information (which also includes some of the fields above */ + hostInfo?: HostInfo; } interface AgentFields { diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_pending_action_status_badge.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/agent_pending_action_status_badge.tsx deleted file mode 100644 index ee318996b97fe..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_pending_action_status_badge.tsx +++ /dev/null @@ -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 ->(({ 'data-test-subj': dataTestSubj, pendingActions }) => { - return ( - - -
- -
- {!!pendingActions.pendingIsolate && ( - - - - - {pendingActions.pendingIsolate} - - )} - {!!pendingActions.pendingUnIsolate && ( - - - - - {pendingActions.pendingUnIsolate} - - )} - {!!pendingActions.pendingKillProcess && ( - - - - - {pendingActions.pendingKillProcess} - - )} - {!!pendingActions.pendingSuspendProcess && ( - - - - - {pendingActions.pendingSuspendProcess} - - )} - {!!pendingActions.pendingRunningProcesses && ( - - - - - {pendingActions.pendingRunningProcesses} - - )} - - } - > - - prev + curr, 0), - }} - /> - -
-
- ); -}); - -AgentPendingActionStatusBadge.displayName = 'AgentPendingActionStatusBadge'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx deleted file mode 100644 index 150189e273cb3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx +++ /dev/null @@ -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 ( - - {getAgentStatusText(hostStatus)} - - ); -}); - -AgentStatus.displayName = 'AgentStatus'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx new file mode 100644 index 0000000000000..bf6f85712b6c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx @@ -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([ + endpointMetadataHttpMocks, + responseActionsHttpMocks, +]); + +describe('When showing Endpoint Agent Status', () => { + const ENDPOINT_ISOLATION_OBJ_PATH = 'metadata.Endpoint.state.isolation'; + + let appTestContext: AppContextTestRender; + let render: () => ReturnType; + let renderResult: ReturnType; + let endpointDetails: HostInfoInterface; + let actionsSummary: EndpointPendingActions; + let apiMocks: ReturnType; + + 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(); + 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(); + 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'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx new file mode 100644 index 0000000000000..e74cdcf41fd57 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx @@ -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( + ({ 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 ( + + + + {getAgentStatusText(status)} + + + {(isIsolated || hasPendingActions) && ( + + + + )} + + ); + } +); +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 `` component. + * + * NOTE: if the `HostInfo` is already available, consider using `` component + * instead in order to avoid duplicate API calls. + */ +export const EndpointAgentStatusById = memo( + ({ endpointAgentId, autoFresh, 'data-test-subj': dataTestSubj }) => { + const { data } = useGetEndpointDetails(endpointAgentId, { + refetchInterval: autoFresh ? DEFAULT_POLL_INTERVAL : false, + }); + + const emptyValue = ( + +

{getEmptyValue()}

+
+ ); + + if (!data) { + return emptyValue; + } + + return ( + + ); + } +); +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( + ({ 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(() => { + 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 + ) : ( + + ); + }, [hasPendingIsolate, hasPendingUnIsolate, isIsolated, totalPending]); + + const isolatedBadge = useMemo(() => { + return ( + + {ISOLATED_LABEL} + + ); + }, [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 ( + + +
+ +
+ {actionList.map(({ count, label }) => { + return ( + + {label} + {count} + + ); + })} + + } + > + + {badgeDisplayValue} + +
+
+ ); + } + + // 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 ( + + + {badgeDisplayValue} + + + ); + } +); +EndpointHostResponseActionsStatus.displayName = 'EndpointHostResponseActionsStatus'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/index.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/index.ts similarity index 57% rename from x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/index.ts rename to x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/index.ts index 8379f425733cb..1d94de32e333c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/index.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx deleted file mode 100644 index 7f4cee7fa8973..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx +++ /dev/null @@ -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 - ) => ReturnType; - let appContext: AppContextTestRender; - - beforeEach(() => { - appContext = createAppRootMockRenderer(); - - render = (renderProps = {}) => - appContext.render( - - ); - }); - - 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'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx deleted file mode 100644 index 650999029c545..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx +++ /dev/null @@ -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( - ({ 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(false); - const wasIsolating = useRef(false); - - const totalPending = useMemo( - () => - pendingIsolate + - pendingUnIsolate + - pendingKillProcess + - pendingSuspendProcess + - pendingRunningProcesses, - [ - pendingIsolate, - pendingKillProcess, - pendingRunningProcesses, - pendingSuspendProcess, - pendingUnIsolate, - ] - ); - - const hasMultipleActionTypesPending = useMemo(() => { - 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 ( - - - - ); - } - - // 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 ( - - - - ); - } - } - - // 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 ( - - ); - } - - // 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 ( - - - {pendingIsolate ? ( - - ) : ( - - )} - - - ); - }, [ - isPendingStatusDisabled, - totalPending, - hasMultipleActionTypesPending, - pendingIsolate, - pendingUnIsolate, - dataTestSubj, - getTestId, - isIsolated, - pendingActions, - ]); - } -); - -EndpointHostIsolationStatus.displayName = 'EndpointHostIsolationStatus'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts index 24b94cd6212b7..41763a6e88d37 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx index edca07bd3426f..210c700c2eb37 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx @@ -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(); - expect(getByTestId('host-panel-agent-status')).toHaveTextContent('Healthy'); + expect(getByTestId('endpointHostAgentStatus').textContent).toEqual('HealthyIsolated'); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/endpoint_agent_and_isolation_status.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/endpoint_agent_and_isolation_status.test.tsx deleted file mode 100644 index 742f5515e7ade..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/endpoint_agent_and_isolation_status.test.tsx +++ /dev/null @@ -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; - let renderResult: ReturnType; - let renderProps: EndpointAgentAndIsolationStatusProps; - - beforeEach(() => { - const appTestContext = createAppRootMockRenderer(); - - renderProps = { - status: HostStatus.HEALTHY, - 'data-test-subj': 'test', - pendingActions: {}, - }; - - render = () => { - renderResult = appTestContext.render(); - 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(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/endpoint_agent_and_isolation_status.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/endpoint_agent_and_isolation_status.tsx deleted file mode 100644 index 3e80d57d2d70b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_agent_and_isolation_status/endpoint_agent_and_isolation_status.tsx +++ /dev/null @@ -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 { - 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( - ({ status, isIsolated, pendingActions, 'data-test-subj': dataTestSubj }) => { - const getTestId = useTestIdGenerator(dataTestSubj); - return ( - - - - - {isIsolated !== undefined && ( - - - - )} - - ); - } -); -EndpointAgentAndIsolationStatus.displayName = 'EndpointAgentAndIsolationStatus'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx index 94b7aa9dd5160..e901e9b1a116d 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx @@ -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, - 'pendingIsolate' | 'pendingUnIsolate' - > - >(() => { + const pendingIsolationActions = useMemo<{ + pendingIsolate: number; + pendingUnIsolate: number; + }>(() => { if (endpointPendingActions?.data.length) { const pendingActions = endpointPendingActions.data[0].pending_actions; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx index 2ffcf76b8ba01..b56746e7890a6 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx @@ -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(({ endpointId }) refetchInterval: 10000, }); - const pendingActionRequests = useMemo< - Pick, '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 ; } @@ -90,10 +74,8 @@ export const HeaderEndpointInfo = memo(({ endpointId }) - diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 39bd07d071974..7030f13bdd0f9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -45,7 +45,7 @@ import { fleetGetPackagePoliciesListHttpMock, } from '../../mocks'; -type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{ +export type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{ metadataList: () => MetadataListResponse; metadataDetails: () => HostInfo; }>; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 29f0d81b96a97..8ad781c60dd20 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -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, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 6431bde743483..1b4c716c37462 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -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) => state.hosts; @@ -47,6 +46,9 @@ export const listError = (state: Immutable) => state.error; export const detailsData = (state: Immutable) => state.endpointDetails.hostDetails.details; +export const fullDetailsHostInfo = (state: Immutable) => + state.endpointDetails.hostInfo; + export const detailsLoading = (state: Immutable): boolean => state.endpointDetails.hostDetails.detailsLoading; @@ -266,53 +268,32 @@ export const getEndpointPendingActionsState = ( return state.endpointPendingActions; }; +export const getMetadataTransformStats = (state: Immutable) => + state.metadataTransformStats; + +export const metadataTransformStats = (state: Immutable) => + isLoadedResourceState(state.metadataTransformStats) ? state.metadataTransformStats.data : []; + +export const isMetadataTransformStatsLoading = (state: Immutable) => + isLoadingResourceState(state.metadataTransformStats); + /** - * Returns a function (callback) that can be used to retrieve the props for the `EndpointHostIsolationStatus` - * component for a given Endpoint + * 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 getEndpointHostIsolationStatusPropsCallback: ( +export const getEndpointPendingActionsCallback: ( state: Immutable -) => (endpoint: HostMetadata) => EndpointHostIsolationStatusProps = createSelector( +) => (endpointId: string) => EndpointPendingActions['pending_actions'] = createSelector( getEndpointPendingActionsState, (pendingActionsState) => { - return (endpoint: HostMetadata) => { - let pendingIsolate = 0; - let pendingUnIsolate = 0; - let pendingKillProcess = 0; - let pendingSuspendProcess = 0; - let pendingRunningProcesses = 0; + return (endpointId: string) => { + let response: EndpointPendingActions['pending_actions'] = {}; 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; - } + response = pendingActionsState.data.get(endpointId) ?? {}; } - return { - isIsolated: isEndpointHostIsolated(endpoint), - pendingActions: { - pendingIsolate, - pendingUnIsolate, - pendingKillProcess, - pendingSuspendProcess, - pendingRunningProcesses, - }, - }; + return response; }; } ); - -export const getMetadataTransformStats = (state: Immutable) => - state.metadataTransformStats; - -export const metadataTransformStats = (state: Immutable) => - isLoadedResourceState(state.metadataTransformStats) ? state.metadataTransformStats.data : []; - -export const isMetadataTransformStatsLoading = (state: Immutable) => - isLoadingResourceState(state.metadataTransformStats); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 8d7e6b0c4d10b..cdd5020226697 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -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; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx deleted file mode 100644 index c4270e8736e83..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx +++ /dev/null @@ -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>; - let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; - let renderResult: ReturnType; - let httpMocks: ReturnType; - 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(); - 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'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx deleted file mode 100644 index 494545b237052..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx +++ /dev/null @@ -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( - ({ endpointMetadata, hostStatus }) => { - const getEndpointIsolationStatusProps = useEndpointSelector( - getEndpointHostIsolationStatusPropsCallback - ); - - return ( - - - - - - - - - ); - } -); - -EndpointAgentStatus.displayName = 'EndpointAgentStatus'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx index d142e1385e80d..b33f98078b9fb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx @@ -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( /> ), - description: , + description: hostInfo ? ( + + ) : ( + <> + ), }, { title: ( @@ -214,7 +228,15 @@ export const EndpointDetailsContent = memo( ), }, ]; - }, [details, hostStatus, policyStatus, policyStatusClickHandler, policyInfo, missingPolicies]); + }, [ + details, + getHostPendingActions, + hostInfo, + missingPolicies, + policyInfo, + policyStatus, + policyStatusClickHandler, + ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 095f6ce65c9b3..95f63266d4778 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -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 ( - + ); }, }, @@ -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) { diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx index d7591ed353fa9..cc24f8b0dbeba 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx @@ -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', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx index 43be986d78500..f62fa5627ebdf 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx @@ -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(({ contextID, data }) => { { title: i18n.FLEET_AGENT_STATUS, description: - data != null && data.elasticAgentStatus ? ( - <> - - - + data != null && data.hostInfo ? ( + ) : ( getEmptyTagValue() ), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx deleted file mode 100644 index 5ef421d057a4a..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx +++ /dev/null @@ -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 ( - - {agentStatus !== undefined ? ( - - - - ) : ( - -

{EMPTY_STATUS}

-
- )} - - - -
- ); - } -); - -AgentStatuses.displayName = 'AgentStatuses'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 1fe2a6b658791..bf216c55f721f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -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 ( - ); } else if ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts index d303fe45bba53..e58a4ceefd46c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts @@ -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} ', - }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index aa07acd20c896..66f36a9052bb5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -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, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b8e6993bf9d6a..368dcb4607aea 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27995,7 +27995,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}}", @@ -28071,7 +28070,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.", @@ -30395,15 +30393,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", @@ -31219,7 +31208,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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7d3317df1bf37..cd89375c208cd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27974,7 +27974,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 {#個のエンドポイント}}を表示中", @@ -28050,7 +28049,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}\"がイベントフィルターリストに追加されました。", @@ -30374,15 +30372,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": "開く", @@ -31198,7 +31187,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": "ホスト分離例外を削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dce328063bf26..036e5a1cf7ed0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27990,7 +27990,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 {# 个终端}}", @@ -28066,7 +28065,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}”已添加到事件筛选列表。", @@ -30390,15 +30388,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": "打开", @@ -31214,7 +31203,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": "删除主机隔离例外",