diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ed4759629b65..726e9c2223d3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1565,6 +1565,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ ## Security Solution sub teams - security-defend-workflows /x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/common/lib/endpoint*/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/common/components/agents/ @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-defend-workflows /x-pack/plugins/security_solution/common/endpoint/ @elastic/security-defend-workflows /x-pack/plugins/security_solution/server/endpoint/ @elastic/security-defend-workflows diff --git a/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_response_action_status.tsx b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_response_action_status.tsx new file mode 100644 index 000000000000..72c3258a6286 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_response_action_status.tsx @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui'; +import type { EndpointPendingActions } from '../../../../../common/endpoint/types'; +import type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants'; +import { RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP } from '../../../../../common/endpoint/service/response_actions/constants'; +import { ISOLATED_LABEL, ISOLATING_LABEL, RELEASING_LABEL } from './endpoint/endpoint_agent_status'; +import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator'; + +const TOOLTIP_CONTENT_STYLES: React.CSSProperties = Object.freeze({ width: 150 }); + +interface AgentResponseActionsStatusProps { + /** The host's individual pending action list as return by the pending action summary api */ + pendingActions: EndpointPendingActions['pending_actions']; + /** Is host currently isolated */ + isIsolated: boolean; + 'data-test-subj'?: string; +} + +export const AgentResponseActionsStatus = memo( + ({ pendingActions, isIsolated, 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + interface PendingActionsState { + actionList: Array<{ label: string; count: number }>; + totalPending: number; + wasReleasing: boolean; + wasIsolating: boolean; + hasMultipleActionTypesPending: boolean; + hasPendingIsolate: boolean; + hasPendingUnIsolate: boolean; + } + + const { + totalPending, + actionList, + wasReleasing, + wasIsolating, + hasMultipleActionTypesPending, + hasPendingIsolate, + hasPendingUnIsolate, + } = useMemo(() => { + const list: Array<{ label: string; count: number }> = []; + let actionTotal = 0; + const pendingActionEntries = Object.entries(pendingActions); + const actionTypesCount = pendingActionEntries.length; + + pendingActionEntries.sort().forEach(([actionName, actionCount]) => { + actionTotal += actionCount; + + list.push({ + count: actionCount, + label: + RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP[ + actionName as ResponseActionsApiCommandNames + ] ?? actionName, + }); + }); + + const pendingIsolate = pendingActions.isolate ?? 0; + const pendingUnIsolate = pendingActions.unisolate ?? 0; + + return { + actionList: list, + totalPending: actionTotal, + wasReleasing: pendingIsolate === 0 && pendingUnIsolate > 0, + wasIsolating: pendingIsolate > 0 && pendingUnIsolate === 0, + hasMultipleActionTypesPending: actionTypesCount > 1, + hasPendingIsolate: pendingIsolate > 0, + hasPendingUnIsolate: pendingUnIsolate > 0, + }; + }, [pendingActions]); + + const badgeDisplayValue = useMemo(() => { + return hasPendingIsolate ? ( + ISOLATING_LABEL + ) : hasPendingUnIsolate ? ( + RELEASING_LABEL + ) : isIsolated ? ( + ISOLATED_LABEL + ) : ( + + ); + }, [hasPendingIsolate, hasPendingUnIsolate, isIsolated, totalPending]); + + const isolatedBadge = useMemo(() => { + return ( + + {ISOLATED_LABEL} + + ); + }, [dataTestSubj]); + + // If nothing is pending + if (totalPending === 0) { + // and host is either releasing and or currently released, then render nothing + if ((!wasIsolating && wasReleasing) || !isIsolated) { + return null; + } + // else host was isolating or is isolated, then show isolation badge + else if ((!isIsolated && wasIsolating && !wasReleasing) || isIsolated) { + return isolatedBadge; + } + } + + // If there are different types of action pending + // --OR-- + // the only type of actions pending is NOT isolate/release, + // then show a summary with tooltip + if (hasMultipleActionTypesPending || (!hasPendingIsolate && !hasPendingUnIsolate)) { + return ( + + +
+ +
+ {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} + + + ); + } +); +AgentResponseActionsStatus.displayName = 'AgentResponseActionsStatus'; diff --git a/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_status.test.tsx new file mode 100644 index 000000000000..47210272781c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_status.test.tsx @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { AgentStatus } from './agent_status'; +import { + useAgentStatusHook, + useGetAgentStatus, +} from '../../../../management/hooks/agents/use_get_agent_status'; +import { + RESPONSE_ACTION_AGENT_TYPE, + type ResponseActionAgentType, +} from '../../../../../common/endpoint/service/response_actions/constants'; +import type { AppContextTestRender } from '../../../mock/endpoint'; +import { createAppRootMockRenderer } from '../../../mock/endpoint'; +import { HostStatus } from '../../../../../common/endpoint/types'; + +jest.mock('../../../hooks/use_experimental_features'); +jest.mock('../../../../management/hooks/agents/use_get_agent_status'); + +const getAgentStatusMock = useGetAgentStatus as jest.Mock; +const useAgentStatusHookMock = useAgentStatusHook as jest.Mock; + +describe('AgentStatus component', () => { + let render: (agentType?: ResponseActionAgentType) => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + const agentId = 'agent-id-1234'; + const baseData = { + agentId, + found: true, + isolated: false, + lastSeen: new Date().toISOString(), + pendingActions: {}, + status: HostStatus.HEALTHY, + }; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + render = (agentType?: ResponseActionAgentType) => + (renderResult = mockedContext.render( + + )); + + getAgentStatusMock.mockReturnValue({ data: {} }); + useAgentStatusHookMock.mockImplementation(() => useGetAgentStatus); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe.each(RESPONSE_ACTION_AGENT_TYPE)('`%s` agentType', (agentType) => { + it('should show agent health status info', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { ...baseData, agentType, status: HostStatus.OFFLINE }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.queryByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Offline'); + expect(actionStatusBadge).toBeFalsy(); + }); + + it('should show agent health status info and Isolated status', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + isolated: true, + }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.getByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Healthy'); + expect(actionStatusBadge.textContent).toEqual('Isolated'); + }); + + it('should show agent health status info and Releasing status', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + isolated: true, + pendingActions: { + unisolate: 1, + }, + }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.getByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Healthy'); + expect(actionStatusBadge.textContent).toEqual('Releasing'); + }); + + it('should show agent health status info and Isolating status', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + pendingActions: { + isolate: 1, + }, + }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.getByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Healthy'); + expect(actionStatusBadge.textContent).toEqual('Isolating'); + }); + + it('should show agent health status info and Releasing status also when multiple actions are pending', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + isolated: true, + pendingActions: { + unisolate: 1, + execute: 1, + 'kill-process': 1, + }, + }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.getByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Healthy'); + expect(actionStatusBadge.textContent).toEqual('Releasing'); + }); + + it('should show agent health status info and Isolating status also when multiple actions are pending', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + pendingActions: { + isolate: 1, + execute: 1, + 'kill-process': 1, + }, + }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.getByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Healthy'); + expect(actionStatusBadge.textContent).toEqual('Isolating'); + }); + + it('should show agent health status info and pending action status when not isolating/releasing', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + pendingActions: { + 'kill-process': 1, + 'running-processes': 1, + }, + }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.getByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Healthy'); + expect(actionStatusBadge.textContent).toEqual('2 actions pending'); + }); + + it('should show agent health status info and Isolated when pending actions', () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + isolated: true, + pendingActions: { + 'kill-process': 1, + 'running-processes': 1, + }, + }, + }, + isLoading: false, + isFetched: true, + }); + + render(agentType); + const statusBadge = renderResult.getByTestId('test-agentStatus'); + const actionStatusBadge = renderResult.getByTestId('test-actionStatuses'); + + expect(statusBadge.textContent).toEqual('Healthy'); + expect(actionStatusBadge.textContent).toEqual('Isolated'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/sentinel_one_agent_status.tsx b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_status.tsx similarity index 50% rename from x-pack/plugins/security_solution/public/detections/components/host_isolation/sentinel_one_agent_status.tsx rename to x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_status.tsx index 98d5161843a5..05c5941e4575 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/sentinel_one_agent_status.tsx +++ b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/agent_status.tsx @@ -8,15 +8,14 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; -import { getAgentStatusText } from '../../../common/components/endpoint/agent_status_text'; -import { HOST_STATUS_TO_BADGE_COLOR } from '../../../management/pages/endpoint_hosts/view/host_constants'; -import { useAgentStatusHook } from './use_sentinelone_host_isolation'; -import { - ISOLATED_LABEL, - ISOLATING_LABEL, - RELEASING_LABEL, -} from '../../../common/components/endpoint/endpoint_agent_status'; +import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants'; +import type { EndpointPendingActions } from '../../../../../common/endpoint/types'; +import { useAgentStatusHook } from '../../../../management/hooks/agents/use_get_agent_status'; +import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator'; +import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../management/pages/endpoint_hosts/view/host_constants'; +import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; +import { getAgentStatusText } from '../agent_status_text'; +import { AgentResponseActionsStatus } from './agent_response_action_status'; export enum SENTINEL_ONE_NETWORK_STATUS { CONNECTING = 'connecting', @@ -31,37 +30,39 @@ const EuiFlexGroupStyled = styled(EuiFlexGroup)` } `; -export const SentinelOneAgentStatus = React.memo( - ({ agentId, 'data-test-subj': dataTestSubj }: { agentId: string; 'data-test-subj'?: string }) => { +export const AgentStatus = React.memo( + ({ + agentId, + agentType, + 'data-test-subj': dataTestSubj, + }: { + agentId: string; + agentType: ResponseActionAgentType; + 'data-test-subj'?: string; + }) => { + const getTestId = useTestIdGenerator(dataTestSubj); const useAgentStatus = useAgentStatusHook(); const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled( 'sentinelOneManualHostActionsEnabled' ); - const { data, isLoading, isFetched } = useAgentStatus([agentId], 'sentinel_one', { + const { data, isLoading, isFetched } = useAgentStatus([agentId], agentType, { enabled: sentinelOneManualHostActionsEnabled, }); const agentStatus = data?.[`${agentId}`]; + const isCurrentlyIsolated = Boolean(agentStatus?.isolated); + const pendingActions = agentStatus?.pendingActions; - const label = useMemo(() => { - const currentNetworkStatus = agentStatus?.isolated; - const pendingActions = agentStatus?.pendingActions; - - if (pendingActions) { - if (pendingActions.isolate > 0) { - return ISOLATING_LABEL; - } - - if (pendingActions.unisolate > 0) { - return RELEASING_LABEL; - } + const [hasPendingActions, hostPendingActions] = useMemo< + [boolean, EndpointPendingActions['pending_actions']] + >(() => { + if (!pendingActions) { + return [false, {}]; } - if (currentNetworkStatus) { - return ISOLATED_LABEL; - } - }, [agentStatus?.isolated, agentStatus?.pendingActions]); + return [Object.keys(pendingActions).length > 0, pendingActions]; + }, [pendingActions]); return ( {getAgentStatusText(agentStatus.status)} @@ -82,11 +84,13 @@ export const SentinelOneAgentStatus = React.memo( '-' )} - {isFetched && !isLoading && label && ( + {(isCurrentlyIsolated || hasPendingActions) && ( - - <>{label} - + )} @@ -94,4 +98,4 @@ export const SentinelOneAgentStatus = React.memo( } ); -SentinelOneAgentStatus.displayName = 'SentinelOneAgentStatus'; +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/agents/agent_status/endpoint/endpoint_agent_status.test.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx rename to x-pack/plugins/security_solution/public/common/components/agents/agent_status/endpoint/endpoint_agent_status.test.tsx index 7fa169b32d34..58ef96a42b93 100644 --- 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/agents/agent_status/endpoint/endpoint_agent_status.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { AppContextTestRender } from '../../../mock/endpoint'; -import { createAppRootMockRenderer } from '../../../mock/endpoint'; +import type { AppContextTestRender } from '../../../../mock/endpoint'; +import { createAppRootMockRenderer } from '../../../../mock/endpoint'; import type { EndpointAgentStatusByIdProps, EndpointAgentStatusProps, @@ -15,18 +15,18 @@ import { EndpointAgentStatus, EndpointAgentStatusById } from './endpoint_agent_s import type { EndpointPendingActions, HostInfoInterface, -} from '../../../../../common/endpoint/types'; -import { HostStatus } from '../../../../../common/endpoint/types'; +} from '../../../../../../common/endpoint/types'; +import { HostStatus } from '../../../../../../common/endpoint/types'; import React from 'react'; -import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator'; -import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; -import { composeHttpHandlerMocks } from '../../../mock/endpoint/http_handler_mock_factory'; -import type { EndpointMetadataHttpMocksInterface } from '../../../../management/pages/endpoint_hosts/mocks'; -import { endpointMetadataHttpMocks } from '../../../../management/pages/endpoint_hosts/mocks'; -import type { ResponseActionsHttpMocksInterface } from '../../../../management/mocks/response_actions_http_mocks'; -import { responseActionsHttpMocks } from '../../../../management/mocks/response_actions_http_mocks'; +import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator'; +import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; +import { composeHttpHandlerMocks } from '../../../../mock/endpoint/http_handler_mock_factory'; +import type { EndpointMetadataHttpMocksInterface } from '../../../../../management/pages/endpoint_hosts/mocks'; +import { endpointMetadataHttpMocks } from '../../../../../management/pages/endpoint_hosts/mocks'; +import type { ResponseActionsHttpMocksInterface } from '../../../../../management/mocks/response_actions_http_mocks'; +import { responseActionsHttpMocks } from '../../../../../management/mocks/response_actions_http_mocks'; import { waitFor, within, fireEvent } from '@testing-library/react'; -import { getEmptyValue } from '../../empty_value'; +import { getEmptyValue } from '../../../empty_value'; import { clone, set } from 'lodash'; type AgentStatusApiMocksInterface = EndpointMetadataHttpMocksInterface & diff --git a/x-pack/plugins/security_solution/public/common/components/agents/agent_status/endpoint/endpoint_agent_status.tsx b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/endpoint/endpoint_agent_status.tsx new file mode 100644 index 000000000000..85568daa312b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/endpoint/endpoint_agent_status.tsx @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { DEFAULT_POLL_INTERVAL } from '../../../../../management/common/constants'; +import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../../management/pages/endpoint_hosts/view/host_constants'; +import { getEmptyValue } from '../../../empty_value'; + +import { useGetEndpointPendingActionsSummary } from '../../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary'; +import { useTestIdGenerator } from '../../../../../management/hooks/use_test_id_generator'; +import type { EndpointPendingActions, HostInfo } from '../../../../../../common/endpoint/types'; +import { useGetEndpointDetails } from '../../../../../management/hooks'; +import { getAgentStatusText } from '../../agent_status_text'; +import { AgentResponseActionsStatus } from '../agent_response_action_status'; + +export const ISOLATING_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating', + { defaultMessage: 'Isolating' } +); +export const RELEASING_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating', + { defaultMessage: 'Releasing' } +); +export const ISOLATED_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.agentAndActionsStatus.isolated', + { defaultMessage: 'Isolated' } +); + +const EuiFlexGroupStyled = styled(EuiFlexGroup)` + .isolation-status { + margin-left: ${({ theme }) => theme.eui.euiSizeS}; + } +`; + +export interface EndpointAgentStatusProps { + endpointHostInfo: HostInfo; + /** + * If set to `true` (Default), then the endpoint isolation state and response actions count + * will be kept up to date by querying the API periodically. + * Only used if `pendingActions` is not defined. + */ + autoRefresh?: boolean; + /** + * The pending actions for the host (as return by the pending actions summary api). + * If undefined, then this component will call the API to retrieve that list of pending actions. + * NOTE: if this prop is defined, it will invalidate `autoRefresh` prop. + */ + pendingActions?: EndpointPendingActions['pending_actions']; + 'data-test-subj'?: string; +} + +/** + * Displays the status of an Endpoint agent along with its Isolation state or the number of pending + * response actions against it. + * + * TIP: if you only have the Endpoint's `agent.id`, then consider using `EndpointAgentStatusById`, + * which will call the needed APIs to get the information necessary to display the status. + */ + +// TODO: used by `EndpointAgentStatusById` +// remove usage/code when `agentStatusClientEnabled` FF is enabled and removed +export const EndpointAgentStatus = memo( + ({ 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 + */ + autoRefresh?: boolean; + 'data-test-subj'?: string; +} + +/** + * Given an Endpoint Agent Id, it will make the necessary API calls and then display the agent + * status using the `` component. + * + * NOTE: if the `HostInfo` is already available, consider using `` component + * instead in order to avoid duplicate API calls. + */ +export const EndpointAgentStatusById = memo( + ({ endpointAgentId, autoRefresh, 'data-test-subj': dataTestSubj }) => { + const { data } = useGetEndpointDetails(endpointAgentId, { + refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false, + }); + + if (!data) { + return ( + +

{getEmptyValue()}

+
+ ); + } + + return ( + + ); + } +); +EndpointAgentStatusById.displayName = 'EndpointAgentStatusById'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/index.ts b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/index.ts similarity index 60% rename from x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/index.ts rename to x-pack/plugins/security_solution/public/common/components/agents/agent_status/index.ts index 1d94de32e333..f6c67097ef46 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/agents/agent_status/index.ts @@ -5,5 +5,6 @@ * 2.0. */ -export * from './endpoint_agent_status'; -export type { EndpointAgentStatusProps } from './endpoint_agent_status'; +export * from './endpoint/endpoint_agent_status'; +export type { EndpointAgentStatusProps } from './endpoint/endpoint_agent_status'; +export * from './agent_status'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status_text.ts b/x-pack/plugins/security_solution/public/common/components/agents/agent_status_text.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/endpoint/agent_status_text.ts rename to x-pack/plugins/security_solution/public/common/components/agents/agent_status_text.ts 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 deleted file mode 100644 index c9aa9b3bfffa..000000000000 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useMemo } from 'react'; -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTextColor, - EuiToolTip, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { DEFAULT_POLL_INTERVAL } from '../../../../management/common/constants'; -import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../management/pages/endpoint_hosts/view/host_constants'; -import { getEmptyValue } from '../../empty_value'; -import { - RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, - type ResponseActionsApiCommandNames, -} from '../../../../../common/endpoint/service/response_actions/constants'; -import { useGetEndpointPendingActionsSummary } from '../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary'; -import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator'; -import type { EndpointPendingActions, HostInfo } from '../../../../../common/endpoint/types'; -import { useGetEndpointDetails } from '../../../../management/hooks'; -import { getAgentStatusText } from '../agent_status_text'; - -const TOOLTIP_CONTENT_STYLES: React.CSSProperties = Object.freeze({ width: 150 }); -export const ISOLATING_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating', - { defaultMessage: 'Isolating' } -); -export const RELEASING_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating', - { defaultMessage: 'Releasing' } -); -export const ISOLATED_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.agentAndActionsStatus.isolated', - { defaultMessage: 'Isolated' } -); - -const EuiFlexGroupStyled = styled(EuiFlexGroup)` - .isolation-status { - margin-left: ${({ theme }) => theme.eui.euiSizeS}; - } -`; - -export interface EndpointAgentStatusProps { - endpointHostInfo: HostInfo; - /** - * If set to `true` (Default), then the endpoint isolation state and response actions count - * will be kept up to date by querying the API periodically. - * Only used if `pendingActions` is not defined. - */ - autoRefresh?: boolean; - /** - * The pending actions for the host (as return by the pending actions summary api). - * If undefined, then this component will call the API to retrieve that list of pending actions. - * NOTE: if this prop is defined, it will invalidate `autoRefresh` prop. - */ - pendingActions?: EndpointPendingActions['pending_actions']; - 'data-test-subj'?: string; -} - -/** - * Displays the status of an Endpoint agent along with its Isolation state or the number of pending - * response actions against it. - * - * TIP: if you only have the Endpoint's `agent.id`, then consider using `EndpointAgentStatusById`, - * which will call the needed APIs to get the information necessary to display the status. - */ -export const EndpointAgentStatus = memo( - ({ 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 - */ - autoRefresh?: boolean; - 'data-test-subj'?: string; -} - -/** - * Given an Endpoint Agent Id, it will make the necessary API calls and then display the agent - * status using the `` component. - * - * NOTE: if the `HostInfo` is already available, consider using `` component - * instead in order to avoid duplicate API calls. - */ -export const EndpointAgentStatusById = memo( - ({ endpointAgentId, autoRefresh, 'data-test-subj': dataTestSubj }) => { - const { data } = useGetEndpointDetails(endpointAgentId, { - refetchInterval: autoRefresh ? 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); - - 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_COMMAND_TO_CONSOLE_COMMAND_MAP[ - actionName as ResponseActionsApiCommandNames - ] ?? actionName, - }); - }); - - const pendingIsolate = pendingActions.isolate ?? 0; - const pendingUnIsolate = pendingActions.unisolate ?? 0; - - return { - actionList: list, - totalPending: actionTotal, - wasReleasing: pendingIsolate === 0 && pendingUnIsolate > 0, - wasIsolating: pendingIsolate > 0 && pendingUnIsolate === 0, - hasMultipleActionTypesPending: actionTypesCount > 1, - hasPendingIsolate: pendingIsolate > 0, - hasPendingUnIsolate: pendingUnIsolate > 0, - }; - }, [pendingActions]); - - const badgeDisplayValue = useMemo(() => { - return hasPendingIsolate ? ( - ISOLATING_LABEL - ) : hasPendingUnIsolate ? ( - RELEASING_LABEL - ) : isIsolated ? ( - ISOLATED_LABEL - ) : ( - - ); - }, [hasPendingIsolate, hasPendingUnIsolate, isIsolated, totalPending]); - - const isolatedBadge = useMemo(() => { - return ( - - {ISOLATED_LABEL} - - ); - }, [dataTestSubj]); - - // If nothing is pending - if (totalPending === 0) { - // and host is either releasing and or currently released, then render nothing - if ((!wasIsolating && wasReleasing) || !isIsolated) { - return null; - } - // else host was isolating or is isolated, then show isolation badge - else if ((!isIsolated && wasIsolating && !wasReleasing) || isIsolated) { - return isolatedBadge; - } - } - - // If there are different types of action pending - // --OR-- - // the only type of actions pending is NOT isolate/release, - // then show a summary with tooltip - if (hasMultipleActionTypesPending || (!hasPendingIsolate && !hasPendingUnIsolate)) { - return ( - - -
- -
- {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/detections/components/endpoint_responder/use_responder_action_data.ts b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts index bf31ad493bbb..820837393461 100644 --- a/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts +++ b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts @@ -7,6 +7,7 @@ import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import type { Platform } from '../../../management/components/endpoint_responder/components/header_info/platforms'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check'; import type { ThirdPartyAgentInfo } from '../../../../common/types'; @@ -165,6 +166,7 @@ export const useResponderActionData = ({ agentType: 'endpoint', capabilities: (hostInfo.metadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [], hostName: hostInfo.metadata.host.name, + platform: hostInfo.metadata.host.os.name.toLowerCase() as Platform, }); } if (onClick) onClick(); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.test.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.test.tsx index 891d85175aa4..f263d80ea92d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.test.tsx @@ -14,10 +14,10 @@ import { useAgentStatusHook, useGetAgentStatus, useGetSentinelOneAgentStatus, -} from './use_sentinelone_host_isolation'; +} from '../../../management/hooks/agents/use_get_agent_status'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; -jest.mock('./use_sentinelone_host_isolation'); +jest.mock('../../../management/hooks/agents/use_get_agent_status'); jest.mock('../../../common/hooks/use_experimental_features'); type AgentType = 'endpoint' | 'sentinel_one' | 'crowdstrike'; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx index 9c3e187b45d4..8b054ec811a9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx @@ -25,7 +25,7 @@ import { ISOLATE_HOST, UNISOLATE_HOST } from './translations'; import { getFieldValue } from './helpers'; import { useUserPrivileges } from '../../../common/components/user_privileges'; import type { AlertTableContextMenuItem } from '../alerts_table/types'; -import { useAgentStatusHook } from './use_sentinelone_host_isolation'; +import { useAgentStatusHook } from '../../../management/hooks/agents/use_get_agent_status'; interface UseHostIsolationActionProps { closePopover: () => void; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx index 5c2446a6b407..b54d195abb27 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx @@ -23,11 +23,11 @@ import { useAgentStatusHook, useGetAgentStatus, useGetSentinelOneAgentStatus, -} from '../../../../detections/components/host_isolation/use_sentinelone_host_isolation'; +} from '../../../../management/hooks/agents/use_get_agent_status'; import { type ExpandableFlyoutApi, useExpandableFlyoutApi } from '@kbn/expandable-flyout'; jest.mock('../../../../management/hooks'); -jest.mock('../../../../detections/components/host_isolation/use_sentinelone_host_isolation'); +jest.mock('../../../../management/hooks/agents/use_get_agent_status'); jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn(), @@ -54,9 +54,11 @@ const panelContextValue = { const renderHighlightedFieldsCell = (values: string[], field: string) => render( - - - + + + + + ); describe('', () => { @@ -65,7 +67,11 @@ describe('', () => { }); it('should render a basic cell', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); expect(getByTestId(HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID)).toBeInTheDocument(); }); @@ -108,7 +114,7 @@ describe('', () => { expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument(); }); - // TODO: 8.15 simplify when `agentStatusClientEnabled` FF is enabled/removed + // TODO: 8.15 simplify when `agentStatusClientEnabled` FF is enabled and removed it.each(Object.keys(hooksToMock))( 'should render SentinelOne agent status cell if field is agent.status and `origialField` is `observer.serial_number` with %s hook', (hookName) => { @@ -135,7 +141,11 @@ describe('', () => { ); it('should not render if values is null', () => { - const { container } = render(); + const { container } = render( + + + + ); expect(container).toBeEmptyDOMElement(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx index 2ae899109416..11031638d67f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx @@ -6,12 +6,15 @@ */ import type { VFC } from 'react'; -import React, { useCallback } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { EuiFlexItem, EuiLink } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { SentinelOneAgentStatus } from '../../../../detections/components/host_isolation/sentinel_one_agent_status'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../common/utils/sentinelone_alert_check'; -import { EndpointAgentStatusById } from '../../../../common/components/endpoint/endpoint_agent_status'; +import { + AgentStatus, + EndpointAgentStatusById, +} from '../../../../common/components/agents/agent_status'; import { useRightPanelContext } from '../context'; import { AGENT_STATUS_FIELD_NAME, @@ -77,41 +80,74 @@ export interface HighlightedFieldsCellProps { values: string[] | null | undefined; } +const FieldsAgentStatus = memo( + ({ + value, + isSentinelOneAgentIdField, + }: { + value: string | undefined; + isSentinelOneAgentIdField: boolean; + }) => { + const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled'); + if (isSentinelOneAgentIdField || agentStatusClientEnabled) { + return ( + + ); + } else { + // TODO: remove usage of `EndpointAgentStatusById` when `agentStatusClientEnabled` FF is enabled and removed + return ( + + ); + } + } +); + +FieldsAgentStatus.displayName = 'FieldsAgentStatus'; + /** + * console.log('c::*, values != null * Renders a component in the highlighted fields table cell based on the field name */ export const HighlightedFieldsCell: VFC = ({ values, field, originalField, -}) => ( - <> - {values != null && - values.map((value, i) => { - return ( - - {field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? ( - - ) : field === AGENT_STATUS_FIELD_NAME && - originalField === SENTINEL_ONE_AGENT_ID_FIELD ? ( - - ) : field === AGENT_STATUS_FIELD_NAME ? ( - - ) : ( - {value} - )} - - ); - })} - -); +}) => { + const isSentinelOneAgentIdField = useMemo( + () => originalField === SENTINEL_ONE_AGENT_ID_FIELD, + [originalField] + ); + + return ( + <> + {values != null && + values.map((value, i) => { + return ( + + {field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? ( + + ) : field === AGENT_STATUS_FIELD_NAME ? ( + + ) : ( + {value} + )} + + ); + })} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx index fd9b8c744a7b..7334cf9d7e8f 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx @@ -10,7 +10,7 @@ import { EuiHealth } from '@elastic/eui'; import type { EntityTableRows } from '../../shared/components/entity_table/types'; import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; -import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status'; +import { EndpointAgentStatus } from '../../../../common/components/agents/agent_status'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import type { HostItem } from '../../../../../common/search_strategy'; import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy'; 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 2de29f621c7e..3f4b46b318b8 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 @@ -19,7 +19,7 @@ import type { CommandExecutionComponentProps } from '../../console/types'; import { FormattedError } from '../../formatted_error'; import { ConsoleCodeBlock } from '../../console/components/console_code_block'; import { POLICY_STATUS_TO_TEXT } from '../../../pages/endpoint_hosts/view/host_constants'; -import { getAgentStatusText } from '../../../../common/components/endpoint/agent_status_text'; +import { getAgentStatusText } from '../../../../common/components/agents/agent_status_text'; export const EndpointStatusActionResult = memo< CommandExecutionComponentProps< diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/agent_info/agent_info.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/agent_info/agent_info.test.tsx new file mode 100644 index 000000000000..272c7bafce4d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/agent_info/agent_info.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; +import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint'; +import { AgentInfo } from './agent_info'; +import { + useAgentStatusHook, + useGetAgentStatus, +} from '../../../../../hooks/agents/use_get_agent_status'; +import type { ResponseActionAgentType } from '../../../../../../../common/endpoint/service/response_actions/constants'; +import { RESPONSE_ACTION_AGENT_TYPE } from '../../../../../../../common/endpoint/service/response_actions/constants'; +import type { Platform } from '../platforms'; +import { HostStatus } from '../../../../../../../common/endpoint/types'; + +jest.mock('../../../../../hooks/agents/use_get_agent_status'); + +const getAgentStatusMock = useGetAgentStatus as jest.Mock; +const useAgentStatusHookMock = useAgentStatusHook as jest.Mock; + +describe('Responder header Agent Info', () => { + let render: ( + agentType?: ResponseActionAgentType, + platform?: Platform + ) => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + const agentId = 'agent-id-1234'; + const baseData = { + agentId, + found: true, + isolated: false, + lastSeen: new Date().toISOString(), + pendingActions: {}, + status: HostStatus.HEALTHY, + }; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + render = (agentType?: ResponseActionAgentType, platform?: Platform) => + (renderResult = mockedContext.render( + + )); + + getAgentStatusMock.mockReturnValue({ data: {} }); + useAgentStatusHookMock.mockImplementation(() => useGetAgentStatus); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe.each(RESPONSE_ACTION_AGENT_TYPE)('`%s` agentType', (agentType) => { + it('should show endpoint name', async () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { ...baseData, agentType, status: HostStatus.OFFLINE }, + }, + isLoading: false, + isFetched: true, + }); + render(agentType); + + const name = await renderResult.findByTestId('responderHeaderHostName'); + expect(name.textContent).toBe('test-agent'); + }); + + it('should show agent and isolation status', async () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { + ...baseData, + agentType, + status: HostStatus.HEALTHY, + pendingActions: { isolate: 1 }, + }, + }, + isLoading: false, + isFetched: true, + }); + render(agentType); + + const agentStatus = await renderResult.findByTestId( + `responderHeader-${agentType}-agentIsolationStatus` + ); + expect(agentStatus.textContent).toBe(`HealthyIsolating`); + }); + + it('should show last checkin time', async () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { ...baseData, agentType, status: HostStatus.HEALTHY }, + }, + isLoading: false, + isFetched: true, + }); + render(agentType); + + const lastUpdated = await renderResult.findByTestId('responderHeaderLastSeen'); + expect(lastUpdated).toBeTruthy(); + }); + + it('should show platform icon', async () => { + getAgentStatusMock.mockReturnValue({ + data: { + [agentId]: { ...baseData, agentType, status: HostStatus.OFFLINE }, + }, + isLoading: false, + isFetched: true, + }); + render(agentType); + + const platformIcon = await renderResult.findByTestId('responderHeaderHostPlatformIcon'); + expect(platformIcon).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/agent_info/agent_info.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/agent_info/agent_info.tsx new file mode 100644 index 000000000000..4c0bf3ca511a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/agent_info/agent_info.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { AgentStatus } from '../../../../../../common/components/agents/agent_status'; +import { useAgentStatusHook } from '../../../../../hooks/agents/use_get_agent_status'; +import type { ThirdPartyAgentInfo } from '../../../../../../../common/types'; +import { HeaderAgentInfo } from '../header_agent_info'; +import type { Platform } from '../platforms'; + +interface AgentInfoProps { + agentId: ThirdPartyAgentInfo['agent']['id']; + agentType: ThirdPartyAgentInfo['agent']['type']; + platform: ThirdPartyAgentInfo['host']['os']['family']; + hostName: ThirdPartyAgentInfo['host']['name']; +} + +export const AgentInfo = memo(({ agentId, platform, hostName, agentType }) => { + const getAgentStatus = useAgentStatusHook(); + const { data } = getAgentStatus([agentId], agentType); + const agentStatus = data?.[agentId]; + const lastCheckin = agentStatus ? agentStatus.lastSeen : ''; + + return ( + + + + ); +}); + +AgentInfo.displayName = 'AgentInfo'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx index ab9a0ff4a3f6..e9d73b3d62d6 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import { EuiSkeletonText } from '@elastic/eui'; -import { EndpointAgentStatus } from '../../../../../../common/components/endpoint/endpoint_agent_status'; +import { EndpointAgentStatus } from '../../../../../../common/components/agents/agent_status'; import { HeaderAgentInfo } from '../header_agent_info'; import { useGetEndpointDetails } from '../../../../../hooks'; import type { Platform } from '../platforms'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/sentinel_one/header_sentinel_one_info.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/sentinel_one/header_sentinel_one_info.tsx index 6a369abb05f5..212f66b6fe6e 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/sentinel_one/header_sentinel_one_info.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/sentinel_one/header_sentinel_one_info.tsx @@ -6,21 +6,22 @@ */ import React, { memo } from 'react'; +import { AgentStatus } from '../../../../../../common/components/agents/agent_status'; +import { useAgentStatusHook } from '../../../../../hooks/agents/use_get_agent_status'; import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features'; -import { useAgentStatusHook } from '../../../../../../detections/components/host_isolation/use_sentinelone_host_isolation'; -import { SentinelOneAgentStatus } from '../../../../../../detections/components/host_isolation/sentinel_one_agent_status'; import type { ThirdPartyAgentInfo } from '../../../../../../../common/types'; import { HeaderAgentInfo } from '../header_agent_info'; import type { Platform } from '../platforms'; interface HeaderSentinelOneInfoProps { agentId: ThirdPartyAgentInfo['agent']['id']; + agentType: ThirdPartyAgentInfo['agent']['type']; platform: ThirdPartyAgentInfo['host']['os']['family']; hostName: ThirdPartyAgentInfo['host']['name']; } export const HeaderSentinelOneInfo = memo( - ({ agentId, platform, hostName }) => { + ({ agentId, agentType, platform, hostName }) => { const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( 'sentinelOneManualHostActionsEnabled' ); @@ -35,8 +36,9 @@ export const HeaderSentinelOneInfo = memo( hostName={hostName} lastCheckin={lastCheckin} > - diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.test.tsx index 12593f6320ee..c28c15c226a8 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.test.tsx @@ -15,13 +15,13 @@ import { useAgentStatusHook, useGetAgentStatus, useGetSentinelOneAgentStatus, -} from '../../../../detections/components/host_isolation/use_sentinelone_host_isolation'; +} from '../../../hooks/agents/use_get_agent_status'; import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details'; import { mockEndpointDetailsApiResult } from '../../../pages/endpoint_hosts/store/mock_endpoint_result_list'; import { OfflineCallout } from './offline_callout'; jest.mock('../../../hooks/endpoint/use_get_endpoint_details'); -jest.mock('../../../../detections/components/host_isolation/use_sentinelone_host_isolation'); +jest.mock('../../../hooks/agents/use_get_agent_status'); const getEndpointDetails = useGetEndpointDetails as jest.Mock; const getSentinelOneAgentStatus = useGetSentinelOneAgentStatus as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.tsx index cae6396885c1..b630af895932 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/offline_callout.tsx @@ -10,7 +10,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useAgentStatusHook } from '../../../../detections/components/host_isolation/use_sentinelone_host_isolation'; +import { useAgentStatusHook } from '../../../hooks/agents/use_get_agent_status'; import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants'; import { useGetEndpointDetails } from '../../../hooks'; import { HostStatus } from '../../../../../common/endpoint/types'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/index.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/index.ts index e2d4906069a4..2cc5321ff6a2 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/index.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/index.ts @@ -8,4 +8,5 @@ export { getEndpointConsoleCommands } from './lib/console_commands_definition'; export { ActionLogButton } from './components/action_log_button'; export { HeaderEndpointInfo } from './components/header_info/endpoint/header_endpoint_info'; +export { AgentInfo } from './components/header_info/agent_info/agent_info'; export { OfflineCallout } from './components/offline_callout'; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_sentinelone_host_isolation.tsx b/x-pack/plugins/security_solution/public/management/hooks/agents/use_get_agent_status.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/detections/components/host_isolation/use_sentinelone_host_isolation.tsx rename to x-pack/plugins/security_solution/public/management/hooks/agents/use_get_agent_status.tsx index d62d68a2534f..066c9a3c5321 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_sentinelone_host_isolation.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/agents/use_get_agent_status.tsx @@ -47,6 +47,7 @@ export const useGetSentinelOneAgentStatus = ( }); }; +// 8.14, 8.15 used for fetching agent status export const useGetAgentStatus = ( agentIds: string[], agentType: string, diff --git a/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx b/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx index 15186ddc486f..8e0e1d213a73 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx @@ -12,7 +12,7 @@ import { useLicense } from '../../common/hooks/use_license'; import type { MaybeImmutable } from '../../../common/endpoint/types'; import type { EndpointCapabilities } from '../../../common/endpoint/service/response_actions/constants'; import { type ResponseActionAgentType } from '../../../common/endpoint/service/response_actions/constants'; -import { HeaderSentinelOneInfo } from '../components/endpoint_responder/components/header_info/sentinel_one/header_sentinel_one_info'; +import { AgentInfo } from '../components/endpoint_responder/components/header_info/agent_info/agent_info'; import { useUserPrivileges } from '../../common/components/user_privileges'; import { @@ -33,6 +33,7 @@ export interface BasicConsoleProps { hostName: string; /** Required for Endpoint agents. */ capabilities: MaybeImmutable; + platform: string; } type ResponderInfoProps = @@ -41,7 +42,6 @@ type ResponderInfoProps = }) | (BasicConsoleProps & { agentType: Exclude; - platform: string; }); export const useWithShowResponder = (): ShowResponseActionsConsole => { @@ -51,10 +51,11 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => { const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( 'responseActionsSentinelOneV1Enabled' ); + const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled'); return useCallback( (props: ResponderInfoProps) => { - const { agentId, agentType, capabilities, hostName } = props; + const { agentId, agentType, capabilities, hostName, platform } = props; // If no authz, just exit and log something to the console if (agentType === 'endpoint' && !endpointPrivileges.canAccessResponseConsole) { window.console.error(new Error(`Access denied to ${agentType} response actions console`)); @@ -81,18 +82,21 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => { 'data-test-subj': `${agentType}ResponseActionsConsole`, storagePrefix: 'xpack.securitySolution.Responder', TitleComponent: () => { - if (agentType === 'endpoint') { - return ; - } - if (agentType === 'sentinel_one') { + if (agentStatusClientEnabled || agentType !== 'endpoint') { return ( - ); } + // TODO: 8.15 remove this if block when agentStatusClientEnabled is enabled/removed + if (agentType === 'endpoint') { + return ; + } + return null; }, }; @@ -104,6 +108,7 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => { agentId, hostName, capabilities, + platform, }, consoleProps, PageTitleComponent: () => { @@ -139,6 +144,12 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => { .show(); } }, - [endpointPrivileges, isEnterpriseLicense, isSentinelOneV1Enabled, consoleManager] + [ + endpointPrivileges, + isEnterpriseLicense, + consoleManager, + agentStatusClientEnabled, + isSentinelOneV1Enabled, + ] ); }; 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 7285239b398a..60ccec200d3c 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,7 +17,11 @@ import { } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EndpointAgentStatus } from '../../../../../common/components/endpoint/endpoint_agent_status'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { + AgentStatus, + EndpointAgentStatus, +} from '../../../../../common/components/agents/agent_status'; import { isPolicyOutOfDate } from '../../utils'; import type { HostInfo } from '../../../../../../common/endpoint/types'; import { useEndpointSelector } from '../hooks'; @@ -54,6 +58,7 @@ interface EndpointDetailsContentProps { export const EndpointDetailsContent = memo( ({ hostInfo, policyInfo }) => { + const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled'); const queryParams = useEndpointSelector(uiQueryParams); const policyStatus = useMemo( () => hostInfo.metadata.Endpoint.policy.applied.status, @@ -95,7 +100,10 @@ export const EndpointDetailsContent = memo( /> ), - description: ( + // TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed + description: agentStatusClientEnabled ? ( + + ) : ( ( }, ]; }, [ + agentStatusClientEnabled, hostInfo, getHostPendingActions, missingPolicies, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 5003aeae0884..75aeaf45a9a8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -8,6 +8,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; +import type { Platform } from '../../../../components/endpoint_responder/components/header_info/platforms'; import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { useWithShowResponder } from '../../../../hooks'; @@ -136,6 +137,7 @@ export const useEndpointActionItems = ( capabilities: (endpointMetadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [], hostName: endpointMetadata.host.name, + platform: endpointMetadata.host.os.name.toLowerCase() as Platform, }); }, children: ( 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 d049eb6ab9e5..440ad6a7560a 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 @@ -10,13 +10,13 @@ import styled from 'styled-components'; import type { CriteriaWithPagination } from '@elastic/eui'; import { EuiBasicTable, - EuiEmptyPrompt, - EuiLoadingLogo, type EuiBasicTableColumn, + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiHealth, EuiHorizontalRule, + EuiLoadingLogo, type EuiSelectableProps, EuiSpacer, EuiSuperDatePicker, @@ -32,10 +32,14 @@ import type { AgentPolicyDetailsDeployAgentAction, CreatePackagePolicyRouteState, } from '@kbn/fleet-plugin/public'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { TransformFailedCallout } from './components/transform_failed_callout'; import type { EndpointIndexUIQueryParams } from '../types'; import { EndpointListNavLink } from './components/endpoint_list_nav_link'; -import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status'; +import { + AgentStatus, + EndpointAgentStatus, +} from '../../../../common/components/agents/agent_status'; import { EndpointDetailsFlyout } from './details'; import * as selectors from '../store/selectors'; import { getEndpointPendingActionsCallback } from '../store/selectors'; @@ -78,6 +82,7 @@ const StyledDatePicker = styled.div` `; interface GetEndpointListColumnsProps { + agentStatusClientEnabled: boolean; canReadPolicyManagement: boolean; backToEndpointList: PolicyDetailsRouteState['backLink']; getHostPendingActions: ReturnType; @@ -102,6 +107,7 @@ const columnWidths: Record< }; const getEndpointListColumns = ({ + agentStatusClientEnabled, canReadPolicyManagement, backToEndpointList, getHostPendingActions, @@ -152,7 +158,10 @@ const getEndpointListColumns = ({ }), sortable: true, render: (hostStatus: HostInfo['host_status'], endpointInfo) => { - return ( + // TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed + return agentStatusClientEnabled ? ( + + ) : ( { + const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled'); + const history = useHistory(); const { listData, @@ -528,6 +539,7 @@ export const EndpointList = () => { const columns = useMemo( () => getEndpointListColumns({ + agentStatusClientEnabled, canReadPolicyManagement, backToEndpointList, getAppUrl, @@ -536,6 +548,7 @@ export const EndpointList = () => { search, }), [ + agentStatusClientEnabled, backToEndpointList, canReadPolicyManagement, getAppUrl, 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 0215ca442e22..ea6639ac8bcb 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,7 +9,11 @@ import { EuiHealth } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { + AgentStatus, + EndpointAgentStatus, +} from '../../../../common/components/agents/agent_status'; import { OverviewDescriptionList } from '../../../../common/components/overview_description_list'; import type { DescriptionList } from '../../../../../common/utility_types'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; @@ -25,6 +29,7 @@ interface Props { } export const EndpointOverview = React.memo(({ contextID, data, scopeId }) => { + const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled'); const getDefaultRenderer = useCallback( (fieldName: string, fieldData: EndpointFields, attrName: string) => ( (({ contextID, data, scopeId }) { title: i18n.FLEET_AGENT_STATUS, description: + // TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed data != null && data.hostInfo ? ( - + agentStatusClientEnabled ? ( + + ) : ( + + ) ) : ( getEmptyTagValue() ), }, ], ]; - }, [data, getDefaultRenderer]); + }, [agentStatusClientEnabled, data, getDefaultRenderer]); return ( <> 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 1b8c453fcf1f..46a3301d5256 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 @@ -13,14 +13,17 @@ import { isEmpty, isNumber } from 'lodash/fp'; import React from 'react'; import { css } from '@emotion/css'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import type { BrowserField } from '../../../../../common/containers/source'; import { ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY, } from '../../../../../../common/field_maps/field_names'; import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../../common/utils/sentinelone_alert_check'; -import { SentinelOneAgentStatus } from '../../../../../detections/components/host_isolation/sentinel_one_agent_status'; -import { EndpointAgentStatusById } from '../../../../../common/components/endpoint/endpoint_agent_status'; +import { + AgentStatus, + EndpointAgentStatusById, +} from '../../../../../common/components/agents/agent_status'; import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { Bytes, BYTES_FORMAT } from './bytes'; @@ -104,6 +107,8 @@ const FormattedFieldValueComponent: React.FC<{ value, linkValue, }) => { + const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled'); + if (isObjectArray || asPlainText) { return {value}; } else if (fieldType === IP_FIELD_TYPE) { @@ -273,7 +278,7 @@ const FormattedFieldValueComponent: React.FC<{ fieldName === AGENT_STATUS_FIELD_NAME && fieldFromBrowserField?.name === SENTINEL_ONE_AGENT_ID_FIELD ) { - return ; + return ; } else if (fieldName === ALERT_HOST_CRITICALITY || fieldName === ALERT_USER_CRITICALITY) { return ( ); } else if (fieldName === AGENT_STATUS_FIELD_NAME) { - return ( + return agentStatusClientEnabled ? ( + + ) : (