mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security solution][Endpoint] Enable the new agent status API/Component (#186669)
## Summary PR enables the use of the new Agent Status API and associated UI components, including: - Deleted feature key `agentStatusClientEnabled` - Removal of several modules that are now absolute - cleaned up code in several components and tests to now only use `AgentStatus` component (handles all `agentType`s) - Adjust `AgentStatus` component to also accept `statusInfo` as a prop - Needed in order to enable more efficient use of this component on a list - API call to get agent status will not be made when prop is used
This commit is contained in:
parent
8ada1ac8d2
commit
e5d37fca63
60 changed files with 928 additions and 1969 deletions
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { merge } from 'lodash';
|
||||
import type { DeepPartial } from 'utility-types';
|
||||
import type { AgentStatusRecords, AgentStatusApiResponse, AgentStatusInfo } from '../../../types';
|
||||
import { HostStatus } from '../../../types';
|
||||
|
||||
const generateAgentStatusMock = (overrides: DeepPartial<AgentStatusInfo> = {}): AgentStatusInfo => {
|
||||
return merge(
|
||||
{
|
||||
agentId: 'abfe4a35-d5b4-42a0-a539-bd054c791769',
|
||||
agentType: 'endpoint',
|
||||
found: true,
|
||||
isolated: false,
|
||||
lastSeen: new Date().toISOString(),
|
||||
pendingActions: {},
|
||||
status: HostStatus.HEALTHY,
|
||||
},
|
||||
overrides
|
||||
) as AgentStatusInfo;
|
||||
};
|
||||
|
||||
const generateAgentStatusRecordsMock = (
|
||||
overrides: DeepPartial<AgentStatusRecords> = {}
|
||||
): AgentStatusRecords => {
|
||||
return merge(
|
||||
{ 'abfe4a35-d5b4-42a0-a539-bd054c791769': generateAgentStatusMock() },
|
||||
overrides
|
||||
) as AgentStatusRecords;
|
||||
};
|
||||
|
||||
const generateAgentStatusApiResponseMock = (
|
||||
overrides: DeepPartial<AgentStatusApiResponse> = {}
|
||||
): AgentStatusApiResponse => {
|
||||
return merge({ data: generateAgentStatusRecordsMock() }, overrides);
|
||||
};
|
||||
|
||||
export const agentStatusMocks = Object.freeze({
|
||||
generateAgentStatus: generateAgentStatusMock,
|
||||
generateAgentStatusRecords: generateAgentStatusRecordsMock,
|
||||
generateAgentStatusApiResponse: generateAgentStatusApiResponseMock,
|
||||
});
|
|
@ -11,22 +11,20 @@ import type {
|
|||
ResponseActionsApiCommandNames,
|
||||
} from '../service/response_actions/constants';
|
||||
|
||||
export interface AgentStatusRecords {
|
||||
[agentId: string]: {
|
||||
agentId: string;
|
||||
agentType: ResponseActionAgentType;
|
||||
found: boolean;
|
||||
isolated: boolean;
|
||||
lastSeen: string; // ISO date
|
||||
pendingActions: Record<ResponseActionsApiCommandNames | string, number>;
|
||||
status: HostStatus;
|
||||
};
|
||||
export interface AgentStatusInfo {
|
||||
agentId: string;
|
||||
agentType: ResponseActionAgentType;
|
||||
found: boolean;
|
||||
isolated: boolean;
|
||||
lastSeen: string; // ISO date
|
||||
pendingActions: Record<ResponseActionsApiCommandNames | string, number>;
|
||||
status: HostStatus;
|
||||
}
|
||||
|
||||
// TODO: 8.15 remove when `agentStatusClientEnabled` is enabled/removed
|
||||
export interface AgentStatusInfo {
|
||||
[agentId: string]: AgentStatusRecords[string] & {
|
||||
isPendingUninstall: boolean;
|
||||
isUninstalled: boolean;
|
||||
};
|
||||
export interface AgentStatusRecords {
|
||||
[agentId: string]: AgentStatusInfo;
|
||||
}
|
||||
|
||||
export interface AgentStatusApiResponse {
|
||||
data: AgentStatusRecords;
|
||||
}
|
||||
|
|
|
@ -81,13 +81,6 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
/** Enables the `get-file` response action for SentinelOne */
|
||||
responseActionsSentinelOneGetFileEnabled: false,
|
||||
|
||||
/**
|
||||
* 8.15
|
||||
* Enables use of agent status service to get agent status information
|
||||
* for endpoint and third-party agents.
|
||||
*/
|
||||
agentStatusClientEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables the ability to send Response actions to Crowdstrike and persist the results
|
||||
* in ES.
|
||||
|
|
|
@ -12,7 +12,6 @@ export * from './detail_panel';
|
|||
export * from './header_actions';
|
||||
export * from './session_view';
|
||||
export * from './bulk_actions';
|
||||
export * from './third_party_agent';
|
||||
|
||||
export const FILTER_OPEN: Status = 'open';
|
||||
export const FILTER_CLOSED: Status = 'closed';
|
||||
|
|
|
@ -1,24 +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 { ResponseActionAgentType } from '../../endpoint/service/response_actions/constants';
|
||||
|
||||
export interface ThirdPartyAgentInfo {
|
||||
agent: {
|
||||
id: string;
|
||||
type: ResponseActionAgentType;
|
||||
};
|
||||
host: {
|
||||
name: string;
|
||||
os: {
|
||||
name: string;
|
||||
family: string;
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
lastCheckin: string;
|
||||
}
|
|
@ -8,10 +8,10 @@
|
|||
import React, { memo, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui';
|
||||
import { ISOLATED_LABEL, ISOLATING_LABEL, RELEASING_LABEL } from './translations';
|
||||
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 });
|
||||
|
|
|
@ -7,57 +7,79 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import type { AgentStatusProps } from './agent_status';
|
||||
import { AgentStatus } from './agent_status';
|
||||
import {
|
||||
useAgentStatusHook,
|
||||
useGetAgentStatus,
|
||||
} from '../../../../../management/hooks/agents/use_get_agent_status';
|
||||
import { useGetAgentStatus as _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 type { AgentStatusInfo } from '../../../../../../common/endpoint/types';
|
||||
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;
|
||||
const useGetAgentStatusMock = _useGetAgentStatus as jest.Mock;
|
||||
|
||||
describe('AgentStatus component', () => {
|
||||
let render: (agentType?: ResponseActionAgentType) => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
const agentId = 'agent-id-1234';
|
||||
const baseData = {
|
||||
agentId,
|
||||
found: true,
|
||||
isolated: false,
|
||||
lastSeen: new Date().toISOString(),
|
||||
pendingActions: {},
|
||||
status: HostStatus.HEALTHY,
|
||||
};
|
||||
let baseData: AgentStatusInfo;
|
||||
let agentId: string;
|
||||
let statusInfoProp: AgentStatusProps['statusInfo'];
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
render = (agentType?: ResponseActionAgentType) =>
|
||||
(renderResult = mockedContext.render(
|
||||
<AgentStatus agentId={agentId} agentType={agentType || 'endpoint'} data-test-subj="test" />
|
||||
));
|
||||
render = (agentType: ResponseActionAgentType = 'endpoint') => {
|
||||
baseData.agentType = agentType;
|
||||
|
||||
getAgentStatusMock.mockReturnValue({ data: {} });
|
||||
useAgentStatusHookMock.mockImplementation(() => useGetAgentStatus);
|
||||
return (renderResult = mockedContext.render(
|
||||
<AgentStatus
|
||||
agentId={agentId}
|
||||
agentType={agentType}
|
||||
statusInfo={statusInfoProp}
|
||||
data-test-subj="test"
|
||||
/>
|
||||
));
|
||||
};
|
||||
useGetAgentStatusMock.mockReturnValue({ data: {} });
|
||||
baseData = {
|
||||
agentId,
|
||||
found: true,
|
||||
isolated: false,
|
||||
lastSeen: new Date().toISOString(),
|
||||
pendingActions: {},
|
||||
status: HostStatus.HEALTHY,
|
||||
agentType: 'endpoint',
|
||||
};
|
||||
agentId = 'agent-id-1234';
|
||||
statusInfoProp = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call the API when `agentId` is provided and no `statusInfo` prop', () => {
|
||||
render();
|
||||
|
||||
expect(useGetAgentStatusMock).toHaveBeenCalledWith(agentId, 'endpoint', { enabled: true });
|
||||
});
|
||||
|
||||
it('should NOT call the API when `statusInfo` prop is provided', () => {
|
||||
statusInfoProp = baseData;
|
||||
render();
|
||||
|
||||
expect(useGetAgentStatusMock).toHaveBeenCalledWith(agentId, 'endpoint', { enabled: false });
|
||||
});
|
||||
|
||||
describe.each(RESPONSE_ACTION_AGENT_TYPE)('`%s` agentType', (agentType) => {
|
||||
it('should show agent health status info', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: { ...baseData, agentType, status: HostStatus.OFFLINE },
|
||||
},
|
||||
|
@ -74,7 +96,7 @@ describe('AgentStatus component', () => {
|
|||
});
|
||||
|
||||
it('should show agent health status info and Isolated status', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: {
|
||||
...baseData,
|
||||
|
@ -95,7 +117,7 @@ describe('AgentStatus component', () => {
|
|||
});
|
||||
|
||||
it('should show agent health status info and Releasing status', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: {
|
||||
...baseData,
|
||||
|
@ -119,7 +141,7 @@ describe('AgentStatus component', () => {
|
|||
});
|
||||
|
||||
it('should show agent health status info and Isolating status', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: {
|
||||
...baseData,
|
||||
|
@ -142,7 +164,7 @@ describe('AgentStatus component', () => {
|
|||
});
|
||||
|
||||
it('should show agent health status info and Releasing status also when multiple actions are pending', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: {
|
||||
...baseData,
|
||||
|
@ -168,7 +190,7 @@ describe('AgentStatus component', () => {
|
|||
});
|
||||
|
||||
it('should show agent health status info and Isolating status also when multiple actions are pending', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: {
|
||||
...baseData,
|
||||
|
@ -193,7 +215,7 @@ describe('AgentStatus component', () => {
|
|||
});
|
||||
|
||||
it('should show agent health status info and pending action status when not isolating/releasing', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: {
|
||||
...baseData,
|
||||
|
@ -217,7 +239,7 @@ describe('AgentStatus component', () => {
|
|||
});
|
||||
|
||||
it('should show agent health status info and Isolated when pending actions', () => {
|
||||
getAgentStatusMock.mockReturnValue({
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
data: {
|
||||
[agentId]: {
|
||||
...baseData,
|
||||
|
|
|
@ -8,20 +8,14 @@
|
|||
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { getAgentStatusText } from './translations';
|
||||
import { getEmptyTagValue } from '../../../empty_value';
|
||||
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 { useGetAgentStatus } 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',
|
||||
CONNECTED = 'connected',
|
||||
DISCONNECTING = 'disconnecting',
|
||||
DISCONNECTED = 'disconnected',
|
||||
}
|
||||
import type { AgentStatusInfo } from '../../../../../../common/endpoint/types';
|
||||
|
||||
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
|
||||
.isolation-status {
|
||||
|
@ -29,42 +23,61 @@ const EuiFlexGroupStyled = styled(EuiFlexGroup)`
|
|||
}
|
||||
`;
|
||||
|
||||
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();
|
||||
export interface AgentStatusProps {
|
||||
agentType: ResponseActionAgentType;
|
||||
/**
|
||||
* The agent id for which the status will be displayed. An API call will be made to retrieve the
|
||||
* status. If using this component on a List view, use `statusInfo` prop instead and make API
|
||||
* call to retrieve all statuses of displayed agents at the view level in order to keep API calls
|
||||
* to a minimum
|
||||
*
|
||||
* NOTE: will be ignored if `statusInfo` prop is defined!
|
||||
*/
|
||||
agentId?: string;
|
||||
/**
|
||||
* The status info for the agent. When both `agentId` and `agentInfo` are defined, `agentInfo` will
|
||||
* be used and `agentId` ignored.
|
||||
*/
|
||||
statusInfo?: AgentStatusInfo;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const sentinelOneManualHostActionsEnabled = useIsExperimentalFeatureEnabled(
|
||||
'sentinelOneManualHostActionsEnabled'
|
||||
);
|
||||
const responseActionsCrowdstrikeManualHostIsolationEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
||||
);
|
||||
const { data, isLoading, isFetched } = useAgentStatus([agentId], agentType, {
|
||||
enabled:
|
||||
sentinelOneManualHostActionsEnabled || responseActionsCrowdstrikeManualHostIsolationEnabled,
|
||||
/**
|
||||
* Display the agent status of a host that supports response actions.
|
||||
*
|
||||
* IMPORTANT: If using this component on a list view, ensure that `statusInfo` prop is used instead
|
||||
* of `agentId` in order to ensure API calls are kept to a minimum and the list view
|
||||
* remains more performant.
|
||||
*/
|
||||
export const AgentStatus = React.memo(
|
||||
({ agentId, agentType, statusInfo, 'data-test-subj': dataTestSubj }: AgentStatusProps) => {
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
const enableApiCall = useMemo(() => {
|
||||
return !statusInfo || !agentId;
|
||||
}, [agentId, statusInfo]);
|
||||
const { data, isLoading, isFetched } = useGetAgentStatus(agentId ?? '', agentType, {
|
||||
enabled: enableApiCall,
|
||||
});
|
||||
const agentStatus = data?.[`${agentId}`];
|
||||
const agentStatus: AgentStatusInfo | undefined = useMemo(() => {
|
||||
if (statusInfo) {
|
||||
return statusInfo;
|
||||
}
|
||||
return data?.[agentId ?? ''];
|
||||
}, [agentId, data, statusInfo]);
|
||||
|
||||
const isCurrentlyIsolated = Boolean(agentStatus?.isolated);
|
||||
const pendingActions = agentStatus?.pendingActions;
|
||||
|
||||
const [hasPendingActions, hostPendingActions] = useMemo<
|
||||
[boolean, EndpointPendingActions['pending_actions']]
|
||||
[boolean, AgentStatusInfo['pendingActions']]
|
||||
>(() => {
|
||||
const pendingActions = agentStatus?.pendingActions;
|
||||
|
||||
if (!pendingActions) {
|
||||
return [false, {}];
|
||||
}
|
||||
|
||||
return [Object.keys(pendingActions).length > 0, pendingActions];
|
||||
}, [pendingActions]);
|
||||
}, [agentStatus?.pendingActions]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroupStyled
|
||||
|
@ -83,7 +96,7 @@ export const AgentStatus = React.memo(
|
|||
{getAgentStatusText(agentStatus.status)}
|
||||
</EuiBadge>
|
||||
) : (
|
||||
'-'
|
||||
getEmptyTagValue()
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{(isCurrentlyIsolated || hasPendingActions) && (
|
||||
|
|
|
@ -1,366 +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 '../../../../../mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../../../mock/endpoint';
|
||||
import type {
|
||||
EndpointAgentStatusByIdProps,
|
||||
EndpointAgentStatusProps,
|
||||
} from './endpoint_agent_status';
|
||||
import { EndpointAgentStatus, EndpointAgentStatusById } from './endpoint_agent_status';
|
||||
import type {
|
||||
EndpointPendingActions,
|
||||
HostInfoInterface,
|
||||
} from '../../../../../../../common/endpoint/types';
|
||||
import { HostStatus } from '../../../../../../../common/endpoint/types';
|
||||
import React from 'react';
|
||||
import { EndpointActionGenerator } from '../../../../../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||
import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data';
|
||||
import { composeHttpHandlerMocks } from '../../../../../mock/endpoint/http_handler_mock_factory';
|
||||
import type { EndpointMetadataHttpMocksInterface } from '../../../../../../management/pages/endpoint_hosts/mocks';
|
||||
import { endpointMetadataHttpMocks } from '../../../../../../management/pages/endpoint_hosts/mocks';
|
||||
import type { ResponseActionsHttpMocksInterface } from '../../../../../../management/mocks/response_actions_http_mocks';
|
||||
import { responseActionsHttpMocks } from '../../../../../../management/mocks/response_actions_http_mocks';
|
||||
import { waitFor, within, fireEvent } from '@testing-library/react';
|
||||
import { getEmptyValue } from '../../../../empty_value';
|
||||
import { clone, set } from 'lodash';
|
||||
|
||||
type AgentStatusApiMocksInterface = EndpointMetadataHttpMocksInterface &
|
||||
ResponseActionsHttpMocksInterface;
|
||||
|
||||
// API mocks composed from the endpoint metadata API mock and the response actions API mocks
|
||||
const agentStatusApiMocks = composeHttpHandlerMocks<AgentStatusApiMocksInterface>([
|
||||
endpointMetadataHttpMocks,
|
||||
responseActionsHttpMocks,
|
||||
]);
|
||||
|
||||
describe('When showing Endpoint Agent Status', () => {
|
||||
const ENDPOINT_ISOLATION_OBJ_PATH = 'metadata.Endpoint.state.isolation';
|
||||
|
||||
let appTestContext: AppContextTestRender;
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let endpointDetails: HostInfoInterface;
|
||||
let actionsSummary: EndpointPendingActions;
|
||||
let apiMocks: ReturnType<typeof agentStatusApiMocks>;
|
||||
|
||||
const triggerTooltip = () => {
|
||||
fireEvent.mouseOver(renderResult.getByTestId('test-actionStatuses-tooltipTrigger'));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
appTestContext = createAppRootMockRenderer();
|
||||
apiMocks = agentStatusApiMocks(appTestContext.coreStart.http);
|
||||
|
||||
const actionGenerator = new EndpointActionGenerator('seed');
|
||||
|
||||
actionsSummary = actionGenerator.generateAgentPendingActionsSummary();
|
||||
actionsSummary.pending_actions = {};
|
||||
apiMocks.responseProvider.agentPendingActionsSummary.mockImplementation(() => {
|
||||
return {
|
||||
data: [actionsSummary],
|
||||
};
|
||||
});
|
||||
|
||||
const metadataGenerator = new EndpointDocGenerator('seed');
|
||||
|
||||
endpointDetails = {
|
||||
metadata: metadataGenerator.generateHostMetadata(),
|
||||
host_status: HostStatus.HEALTHY,
|
||||
} as HostInfoInterface;
|
||||
apiMocks.responseProvider.metadataDetails.mockImplementation(() => endpointDetails);
|
||||
});
|
||||
|
||||
describe('and using `EndpointAgentStatus` component', () => {
|
||||
let renderProps: EndpointAgentStatusProps;
|
||||
|
||||
beforeEach(() => {
|
||||
renderProps = {
|
||||
'data-test-subj': 'test',
|
||||
endpointHostInfo: endpointDetails,
|
||||
};
|
||||
|
||||
render = () => {
|
||||
renderResult = appTestContext.render(<EndpointAgentStatus {...renderProps} />);
|
||||
return renderResult;
|
||||
};
|
||||
});
|
||||
|
||||
it('should display status', () => {
|
||||
const { getByTestId } = render();
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('Healthy');
|
||||
});
|
||||
|
||||
it('should display status and isolated', () => {
|
||||
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
|
||||
const { getByTestId } = render();
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
|
||||
});
|
||||
|
||||
it('should display status and isolated and display other pending actions in tooltip', async () => {
|
||||
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
|
||||
actionsSummary.pending_actions = {
|
||||
'get-file': 2,
|
||||
execute: 6,
|
||||
};
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
|
||||
|
||||
triggerTooltip();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
|
||||
.textContent
|
||||
).toEqual('Pending actions:execute6get-file2');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display status and action count', async () => {
|
||||
actionsSummary.pending_actions = {
|
||||
'get-file': 2,
|
||||
execute: 6,
|
||||
};
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('Healthy8 actions pending');
|
||||
});
|
||||
|
||||
it('should display status and isolating', async () => {
|
||||
actionsSummary.pending_actions = {
|
||||
isolate: 1,
|
||||
};
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyIsolating');
|
||||
});
|
||||
|
||||
it('should display status and isolating and have tooltip with other pending actions', async () => {
|
||||
actionsSummary.pending_actions = {
|
||||
isolate: 1,
|
||||
'kill-process': 1,
|
||||
};
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyIsolating');
|
||||
|
||||
triggerTooltip();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
|
||||
.textContent
|
||||
).toEqual('Pending actions:isolate1kill-process1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display status and releasing', async () => {
|
||||
actionsSummary.pending_actions = {
|
||||
unisolate: 1,
|
||||
};
|
||||
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyReleasing');
|
||||
});
|
||||
|
||||
it('should display status and releasing and show other pending actions in tooltip', async () => {
|
||||
actionsSummary.pending_actions = {
|
||||
unisolate: 1,
|
||||
'kill-process': 1,
|
||||
};
|
||||
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyReleasing');
|
||||
|
||||
triggerTooltip();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
|
||||
.textContent
|
||||
).toEqual('Pending actions:kill-process1release1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show individual action count in tooltip (including unknown actions) sorted asc', async () => {
|
||||
actionsSummary.pending_actions = {
|
||||
isolate: 1,
|
||||
'get-file': 2,
|
||||
execute: 6,
|
||||
'kill-process': 1,
|
||||
foo: 2,
|
||||
};
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyIsolating');
|
||||
|
||||
triggerTooltip();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(renderResult.baseElement).getByTestId('test-actionStatuses-tooltipContent')
|
||||
.textContent
|
||||
).toEqual('Pending actions:execute6foo2get-file2isolate1kill-process1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should still display status and isolation state if action summary api fails', async () => {
|
||||
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
|
||||
apiMocks.responseProvider.agentPendingActionsSummary.mockImplementation(() => {
|
||||
throw new Error('test error');
|
||||
});
|
||||
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
|
||||
});
|
||||
|
||||
describe('and `autoRefresh` prop is set to true', () => {
|
||||
beforeEach(() => {
|
||||
renderProps.autoRefresh = true;
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should keep actions up to date when autoRefresh is true', async () => {
|
||||
apiMocks.responseProvider.agentPendingActionsSummary.mockReturnValueOnce({
|
||||
data: [actionsSummary],
|
||||
});
|
||||
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.agentPendingActionsSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual('Healthy');
|
||||
|
||||
apiMocks.responseProvider.agentPendingActionsSummary.mockReturnValueOnce({
|
||||
data: [
|
||||
{
|
||||
...actionsSummary,
|
||||
pending_actions: {
|
||||
'kill-process': 2,
|
||||
'running-processes': 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('test').textContent).toEqual('Healthy4 actions pending');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('And when using EndpointAgentStatusById', () => {
|
||||
let renderProps: EndpointAgentStatusByIdProps;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
renderProps = {
|
||||
'data-test-subj': 'test',
|
||||
endpointAgentId: '123',
|
||||
};
|
||||
|
||||
render = () => {
|
||||
renderResult = appTestContext.render(<EndpointAgentStatusById {...renderProps} />);
|
||||
return renderResult;
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should display status and isolated', async () => {
|
||||
set(endpointDetails, ENDPOINT_ISOLATION_OBJ_PATH, true);
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('test').textContent).toEqual('HealthyIsolated');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display empty value if API call to host metadata fails', async () => {
|
||||
apiMocks.responseProvider.metadataDetails.mockImplementation(() => {
|
||||
throw new Error('test error');
|
||||
});
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.metadataDetails).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('test').textContent).toEqual(getEmptyValue());
|
||||
});
|
||||
|
||||
it('should keep agent status up to date when autoRefresh is true', async () => {
|
||||
renderProps.autoRefresh = 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,168 +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 } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DEFAULT_POLL_INTERVAL } from '../../../../../../management/common/constants';
|
||||
import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../../../management/pages/endpoint_hosts/view/host_constants';
|
||||
import { getEmptyValue } from '../../../../empty_value';
|
||||
|
||||
import { useGetEndpointPendingActionsSummary } from '../../../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary';
|
||||
import { useTestIdGenerator } from '../../../../../../management/hooks/use_test_id_generator';
|
||||
import type { EndpointPendingActions, HostInfo } from '../../../../../../../common/endpoint/types';
|
||||
import { useGetEndpointDetails } from '../../../../../../management/hooks';
|
||||
import { getAgentStatusText } from '../../agent_status_text';
|
||||
import { AgentResponseActionsStatus } from '../agent_response_action_status';
|
||||
|
||||
export const ISOLATING_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.agentAndActionsStatus.isIsolating',
|
||||
{ defaultMessage: 'Isolating' }
|
||||
);
|
||||
export const RELEASING_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.agentAndActionsStatus.isUnIsolating',
|
||||
{ defaultMessage: 'Releasing' }
|
||||
);
|
||||
export const ISOLATED_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.agentAndActionsStatus.isolated',
|
||||
{ defaultMessage: 'Isolated' }
|
||||
);
|
||||
|
||||
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
|
||||
.isolation-status {
|
||||
margin-left: ${({ theme }) => theme.eui.euiSizeS};
|
||||
}
|
||||
`;
|
||||
|
||||
export interface EndpointAgentStatusProps {
|
||||
endpointHostInfo: HostInfo;
|
||||
/**
|
||||
* If set to `true` (Default), then the endpoint isolation state and response actions count
|
||||
* will be kept up to date by querying the API periodically.
|
||||
* Only used if `pendingActions` is not defined.
|
||||
*/
|
||||
autoRefresh?: boolean;
|
||||
/**
|
||||
* The pending actions for the host (as return by the pending actions summary api).
|
||||
* If undefined, then this component will call the API to retrieve that list of pending actions.
|
||||
* NOTE: if this prop is defined, it will invalidate `autoRefresh` prop.
|
||||
*/
|
||||
pendingActions?: EndpointPendingActions['pending_actions'];
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the status of an Endpoint agent along with its Isolation state or the number of pending
|
||||
* response actions against it.
|
||||
*
|
||||
* TIP: if you only have the Endpoint's `agent.id`, then consider using `EndpointAgentStatusById`,
|
||||
* which will call the needed APIs to get the information necessary to display the status.
|
||||
*/
|
||||
|
||||
// TODO: used by `EndpointAgentStatusById`
|
||||
// remove usage/code when `agentStatusClientEnabled` FF is enabled and removed
|
||||
export const EndpointAgentStatus = memo<EndpointAgentStatusProps>(
|
||||
({ endpointHostInfo, autoRefresh = true, pendingActions, 'data-test-subj': dataTestSubj }) => {
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
const { data: endpointPendingActions } = useGetEndpointPendingActionsSummary(
|
||||
[endpointHostInfo.metadata.agent.id],
|
||||
{
|
||||
refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false,
|
||||
enabled: !pendingActions,
|
||||
}
|
||||
);
|
||||
|
||||
const [hasPendingActions, hostPendingActions] = useMemo<
|
||||
[boolean, EndpointPendingActions['pending_actions']]
|
||||
>(() => {
|
||||
if (!endpointPendingActions && !pendingActions) {
|
||||
return [false, {}];
|
||||
}
|
||||
|
||||
const pending = pendingActions
|
||||
? pendingActions
|
||||
: endpointPendingActions?.data[0].pending_actions ?? {};
|
||||
|
||||
return [Object.keys(pending).length > 0, pending];
|
||||
}, [endpointPendingActions, pendingActions]);
|
||||
|
||||
const status = endpointHostInfo.host_status;
|
||||
const isIsolated = Boolean(endpointHostInfo.metadata.Endpoint.state?.isolation);
|
||||
|
||||
return (
|
||||
<EuiFlexGroupStyled
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge
|
||||
color={status != null ? HOST_STATUS_TO_BADGE_COLOR[status] : 'warning'}
|
||||
data-test-subj={getTestId('agentStatus')}
|
||||
className="eui-textTruncate"
|
||||
>
|
||||
{getAgentStatusText(status)}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
{(isIsolated || hasPendingActions) && (
|
||||
<EuiFlexItem grow={false} className="eui-textTruncate isolation-status">
|
||||
<AgentResponseActionsStatus
|
||||
data-test-subj={getTestId('actionStatuses')}
|
||||
isIsolated={isIsolated}
|
||||
pendingActions={hostPendingActions}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroupStyled>
|
||||
);
|
||||
}
|
||||
);
|
||||
EndpointAgentStatus.displayName = 'EndpointAgentStatus';
|
||||
|
||||
export interface EndpointAgentStatusByIdProps {
|
||||
endpointAgentId: string;
|
||||
/**
|
||||
* If set to `true` (Default), then the endpoint status and isolation/action counts will
|
||||
* be kept up to date by querying the API periodically
|
||||
*/
|
||||
autoRefresh?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an Endpoint Agent Id, it will make the necessary API calls and then display the agent
|
||||
* status using the `<EndpointAgentStatus />` component.
|
||||
*
|
||||
* NOTE: if the `HostInfo` is already available, consider using `<EndpointAgentStatus/>` component
|
||||
* instead in order to avoid duplicate API calls.
|
||||
*/
|
||||
export const EndpointAgentStatusById = memo<EndpointAgentStatusByIdProps>(
|
||||
({ endpointAgentId, autoRefresh, 'data-test-subj': dataTestSubj }) => {
|
||||
const { data } = useGetEndpointDetails(endpointAgentId, {
|
||||
refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false,
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<EuiText size="xs" data-test-subj={dataTestSubj}>
|
||||
<p>{getEmptyValue()}</p>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EndpointAgentStatus
|
||||
endpointHostInfo={data}
|
||||
data-test-subj={dataTestSubj}
|
||||
autoRefresh={autoRefresh}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
EndpointAgentStatusById.displayName = 'EndpointAgentStatusById';
|
|
@ -5,6 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './endpoint/endpoint_agent_status';
|
||||
export type { EndpointAgentStatusProps } from './endpoint/endpoint_agent_status';
|
||||
export * from './agent_status';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { HostStatus } from '../../../../../common/endpoint/types';
|
||||
import type { HostStatus } from '../../../../../../common/endpoint/types';
|
||||
|
||||
export const getAgentStatusText = (hostStatus: HostStatus) => {
|
||||
return i18n.translate('xpack.securitySolution.endpoint.list.hostStatusValue', {
|
||||
|
@ -15,3 +15,15 @@ export const getAgentStatusText = (hostStatus: HostStatus) => {
|
|||
values: { hostStatus },
|
||||
});
|
||||
};
|
||||
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' }
|
||||
);
|
|
@ -5,140 +5,188 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import type { UseHostIsolationActionProps } from './use_host_isolation_action';
|
||||
import { useHostIsolationAction } from './use_host_isolation_action';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { AppContextTestRender, UserPrivilegesMockSetter } from '../../../../mock/endpoint';
|
||||
import { createAppRootMockRenderer, endpointAlertDataMock } from '../../../../mock/endpoint';
|
||||
import { agentStatusGetHttpMock } from '../../../../../management/mocks';
|
||||
import { useUserPrivileges as _useUserPrivileges } from '../../../user_privileges';
|
||||
import type { AlertTableContextMenuItem } from '../../../../../detections/components/alerts_table/types';
|
||||
import type { ResponseActionsApiCommandNames } from '../../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { agentStatusMocks } from '../../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
|
||||
import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
|
||||
import type React from 'react';
|
||||
import {
|
||||
useAgentStatusHook,
|
||||
useGetAgentStatus,
|
||||
useGetSentinelOneAgentStatus,
|
||||
} from '../../../../../management/hooks/agents/use_get_agent_status';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../hooks/use_experimental_features';
|
||||
import type { ResponseActionAgentType } from '../../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { ExperimentalFeaturesService as ExperimentalFeaturesServiceMock } from '../../../../experimental_features_service';
|
||||
import { endpointAlertDataMock } from '../../../../mock/endpoint';
|
||||
HOST_ENDPOINT_UNENROLLED_TOOLTIP,
|
||||
LOADING_ENDPOINT_DATA_TOOLTIP,
|
||||
NOT_FROM_ENDPOINT_HOST_TOOLTIP,
|
||||
} from '../..';
|
||||
import { HostStatus } from '../../../../../../common/endpoint/types';
|
||||
|
||||
jest.mock('../../../../../management/hooks/agents/use_get_agent_status');
|
||||
jest.mock('../../../../hooks/use_experimental_features');
|
||||
jest.mock('../../../../experimental_features_service');
|
||||
jest.mock('../../../user_privileges');
|
||||
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
const useGetSentinelOneAgentStatusMock = useGetSentinelOneAgentStatus as jest.Mock;
|
||||
const useGetAgentStatusMock = useGetAgentStatus as jest.Mock;
|
||||
const useAgentStatusHookMock = useAgentStatusHook as jest.Mock;
|
||||
const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;
|
||||
|
||||
describe('useHostIsolationAction', () => {
|
||||
const setFeatureFlags = (isEnabled: boolean = true): void => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(isEnabled);
|
||||
(ExperimentalFeaturesServiceMock.get as jest.Mock).mockReturnValue({
|
||||
responseActionsSentinelOneV1Enabled: isEnabled,
|
||||
responseActionsCrowdstrikeManualHostIsolationEnabled: isEnabled,
|
||||
});
|
||||
let appContextMock: AppContextTestRender;
|
||||
let hookProps: UseHostIsolationActionProps;
|
||||
let apiMock: ReturnType<typeof agentStatusGetHttpMock>;
|
||||
let authMockSetter: UserPrivilegesMockSetter;
|
||||
|
||||
const buildExpectedMenuItemResult = (
|
||||
overrides: Partial<AlertTableContextMenuItem> = {}
|
||||
): AlertTableContextMenuItem => {
|
||||
return {
|
||||
'data-test-subj': 'isolate-host-action-item',
|
||||
disabled: false,
|
||||
key: 'isolate-host-action-item',
|
||||
name: ISOLATE_HOST,
|
||||
onClick: expect.any(Function),
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const createReactQueryWrapper = () => {
|
||||
const queryClient = new QueryClient();
|
||||
const wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
return wrapper;
|
||||
const render = () => {
|
||||
return appContextMock.renderHook(() => useHostIsolationAction(hookProps));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
appContextMock = createAppRootMockRenderer();
|
||||
authMockSetter = appContextMock.getUserPrivilegesMockSetter(useUserPrivilegesMock);
|
||||
hookProps = {
|
||||
closePopover: jest.fn(),
|
||||
detailsData: endpointAlertDataMock.generateEndpointAlertDetailsItemData(),
|
||||
isHostIsolationPanelOpen: false,
|
||||
onAddIsolationStatusClick: jest.fn(),
|
||||
};
|
||||
apiMock = agentStatusGetHttpMock(appContextMock.coreStart.http);
|
||||
appContextMock.setExperimentalFlag({
|
||||
responseActionsSentinelOneV1Enabled: true,
|
||||
responseActionsCrowdstrikeManualHostIsolationEnabled: true,
|
||||
});
|
||||
authMockSetter.set({
|
||||
canIsolateHost: true,
|
||||
canUnIsolateHost: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
authMockSetter.reset();
|
||||
});
|
||||
|
||||
it.each<ResponseActionsApiCommandNames>(['isolate', 'unisolate'])(
|
||||
'should return menu item for displaying %s',
|
||||
async (command) => {
|
||||
if (command === 'unisolate') {
|
||||
apiMock.responseProvider.getAgentStatus.mockReturnValue({
|
||||
data: {
|
||||
'abfe4a35-d5b4-42a0-a539-bd054c791769': agentStatusMocks.generateAgentStatus({
|
||||
isolated: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { result, waitForValueToChange } = render();
|
||||
await waitForValueToChange(() => result.current);
|
||||
|
||||
expect(result.current).toEqual([
|
||||
buildExpectedMenuItemResult({
|
||||
...(command === 'unisolate' ? { name: UNISOLATE_HOST } : {}),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it('should call `closePopover` callback when menu item `onClick` is called', async () => {
|
||||
const { result, waitForValueToChange } = render();
|
||||
await waitForValueToChange(() => result.current);
|
||||
result.current[0].onClick!({} as unknown as React.MouseEvent);
|
||||
|
||||
expect(hookProps.closePopover).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT return the menu item for Events', () => {
|
||||
useAgentStatusHookMock.mockImplementation(() => {
|
||||
return jest.fn(() => {
|
||||
return { data: {} };
|
||||
});
|
||||
hookProps.detailsData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType('foo', {
|
||||
'kibana.alert.rule.uuid': undefined,
|
||||
});
|
||||
setFeatureFlags(true);
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return useHostIsolationAction({
|
||||
closePopover: jest.fn(),
|
||||
detailsData: endpointAlertDataMock.generateAlertDetailsItemDataForAgentType('foo', {
|
||||
'kibana.alert.rule.uuid': undefined,
|
||||
}),
|
||||
isHostIsolationPanelOpen: false,
|
||||
onAddIsolationStatusClick: jest.fn(),
|
||||
});
|
||||
},
|
||||
{ wrapper: createReactQueryWrapper() }
|
||||
);
|
||||
const { result } = render();
|
||||
|
||||
expect(result.current).toHaveLength(0);
|
||||
});
|
||||
|
||||
// FIXME:PT refactor describe below - its not actually testing the component! Tests seem to be for `useAgentStatusHook()`
|
||||
describe.each([
|
||||
['useGetSentinelOneAgentStatus', useGetSentinelOneAgentStatusMock],
|
||||
['useGetAgentStatus', useGetAgentStatusMock],
|
||||
])('works with %s hook', (name, hook) => {
|
||||
const render = (agentTypeAlert: ResponseActionAgentType) =>
|
||||
renderHook(
|
||||
() =>
|
||||
useHostIsolationAction({
|
||||
closePopover: jest.fn(),
|
||||
detailsData:
|
||||
endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentTypeAlert),
|
||||
isHostIsolationPanelOpen: false,
|
||||
onAddIsolationStatusClick: jest.fn(),
|
||||
it('should NOT return menu item if user does not have authz', async () => {
|
||||
authMockSetter.set({
|
||||
canIsolateHost: false,
|
||||
canUnIsolateHost: false,
|
||||
});
|
||||
const { result } = render();
|
||||
|
||||
expect(result.current).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should NOT attempt to get Agent status if host does not support response actions', async () => {
|
||||
hookProps.detailsData = [];
|
||||
render();
|
||||
|
||||
expect(apiMock.responseProvider.getAgentStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return disabled menu item while loading agent status', async () => {
|
||||
const { result } = render();
|
||||
|
||||
expect(result.current).toEqual([
|
||||
buildExpectedMenuItemResult({
|
||||
disabled: true,
|
||||
toolTipContent: LOADING_ENDPOINT_DATA_TOOLTIP,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(['endpoint', 'non-endpoint'])(
|
||||
'should return disabled menu item if %s host agent is unenrolled',
|
||||
async (type) => {
|
||||
apiMock.responseProvider.getAgentStatus.mockReturnValue({
|
||||
data: {
|
||||
'abfe4a35-d5b4-42a0-a539-bd054c791769': agentStatusMocks.generateAgentStatus({
|
||||
status: HostStatus.UNENROLLED,
|
||||
}),
|
||||
{
|
||||
wrapper: createReactQueryWrapper(),
|
||||
}
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
useAgentStatusHookMock.mockImplementation(() => hook);
|
||||
setFeatureFlags(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(ExperimentalFeaturesServiceMock.get as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
it(`${name} is invoked as 'enabled' when SentinelOne alert and FF enabled`, () => {
|
||||
render('sentinel_one');
|
||||
|
||||
expect(hook).toHaveBeenCalledWith(['abfe4a35-d5b4-42a0-a539-bd054c791769'], 'sentinel_one', {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
it(`${name} is invoked as 'enabled' when Crowdstrike alert and FF enabled`, () => {
|
||||
render('crowdstrike');
|
||||
if (type === 'non-endpoint') {
|
||||
hookProps.detailsData = endpointAlertDataMock.generateSentinelOneAlertDetailsItemData();
|
||||
}
|
||||
const { result, waitForValueToChange } = render();
|
||||
await waitForValueToChange(() => result.current);
|
||||
|
||||
expect(hook).toHaveBeenCalledWith(['abfe4a35-d5b4-42a0-a539-bd054c791769'], 'crowdstrike', {
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
expect(result.current).toEqual([
|
||||
buildExpectedMenuItemResult({
|
||||
disabled: true,
|
||||
toolTipContent:
|
||||
type === 'endpoint' ? HOST_ENDPOINT_UNENROLLED_TOOLTIP : NOT_FROM_ENDPOINT_HOST_TOOLTIP,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it(`${name} is invoked as 'disabled' when SentinelOne alert and FF disabled`, () => {
|
||||
setFeatureFlags(false);
|
||||
render('sentinel_one');
|
||||
it('should call isolate API when agent is currently NOT isolated', async () => {
|
||||
const { result, waitForValueToChange } = render();
|
||||
await waitForValueToChange(() => result.current);
|
||||
result.current[0].onClick!({} as unknown as React.MouseEvent);
|
||||
|
||||
expect(hook).toHaveBeenCalledWith(['abfe4a35-d5b4-42a0-a539-bd054c791769'], 'sentinel_one', {
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
expect(hookProps.onAddIsolationStatusClick).toHaveBeenCalledWith('isolateHost');
|
||||
});
|
||||
|
||||
it(`${name} is invoked as 'disabled' when Crowdstrike alert and FF disabled`, () => {
|
||||
setFeatureFlags(false);
|
||||
render('crowdstrike');
|
||||
it('should call un-isolate API when agent is currently isolated', async () => {
|
||||
apiMock.responseProvider.getAgentStatus.mockReturnValue(
|
||||
agentStatusMocks.generateAgentStatusApiResponse({
|
||||
data: { 'abfe4a35-d5b4-42a0-a539-bd054c791769': { isolated: true } },
|
||||
})
|
||||
);
|
||||
const { result, waitForValueToChange } = render();
|
||||
await waitForValueToChange(() => result.current);
|
||||
result.current[0].onClick!({} as unknown as React.MouseEvent);
|
||||
|
||||
expect(hook).toHaveBeenCalledWith(['abfe4a35-d5b4-42a0-a539-bd054c791769'], 'crowdstrike', {
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it(`${name} is invoked as 'disabled' when endpoint alert`, () => {
|
||||
render('endpoint');
|
||||
|
||||
expect(hook).toHaveBeenCalledWith(['abfe4a35-d5b4-42a0-a539-bd054c791769'], 'endpoint', {
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
expect(hookProps.onAddIsolationStatusClick).toHaveBeenCalledWith('unisolateHost');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,16 +12,13 @@ import {
|
|||
NOT_FROM_ENDPOINT_HOST_TOOLTIP,
|
||||
} from '../../responder';
|
||||
import { useAlertResponseActionsSupport } from '../../../../hooks/endpoint/use_alert_response_actions_support';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../hooks/use_experimental_features';
|
||||
import type { AgentStatusInfo } from '../../../../../../common/endpoint/types';
|
||||
import { HostStatus } from '../../../../../../common/endpoint/types';
|
||||
import { useEndpointHostIsolationStatus } from './use_host_isolation_status';
|
||||
import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
|
||||
import { useUserPrivileges } from '../../../user_privileges';
|
||||
import type { AlertTableContextMenuItem } from '../../../../../detections/components/alerts_table/types';
|
||||
import { useAgentStatusHook } from '../../../../../management/hooks/agents/use_get_agent_status';
|
||||
import { useGetAgentStatus } from '../../../../../management/hooks/agents/use_get_agent_status';
|
||||
|
||||
interface UseHostIsolationActionProps {
|
||||
export interface UseHostIsolationActionProps {
|
||||
closePopover: () => void;
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
|
@ -44,87 +41,39 @@ export const useHostIsolationAction = ({
|
|||
agentSupport: { isolate: isolationSupported },
|
||||
},
|
||||
} = useAlertResponseActionsSupport(detailsData);
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
const useAgentStatus = useAgentStatusHook();
|
||||
const { canIsolateHost, canUnIsolateHost } = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
const isEndpointAgent = useMemo(() => {
|
||||
return agentType === 'endpoint';
|
||||
}, [agentType]);
|
||||
|
||||
const {
|
||||
loading: loadingHostIsolationStatus,
|
||||
isIsolated,
|
||||
agentStatus,
|
||||
capabilities,
|
||||
} = useEndpointHostIsolationStatus({
|
||||
agentId,
|
||||
agentType,
|
||||
const { data, isLoading, isFetched } = useGetAgentStatus(agentId, agentType, {
|
||||
enabled: hostSupportsResponseActions,
|
||||
});
|
||||
|
||||
const { data: externalAgentData } = useAgentStatus([agentId], agentType, {
|
||||
enabled: hostSupportsResponseActions && !isEndpointAgent,
|
||||
});
|
||||
|
||||
const externalAgentStatus = externalAgentData?.[agentId];
|
||||
|
||||
const isHostIsolated = useMemo((): boolean => {
|
||||
if (!isEndpointAgent) {
|
||||
return Boolean(externalAgentStatus?.isolated);
|
||||
}
|
||||
|
||||
return isIsolated;
|
||||
}, [isEndpointAgent, isIsolated, externalAgentStatus?.isolated]);
|
||||
const agentStatus = data?.[agentId];
|
||||
|
||||
const doesHostSupportIsolation = useMemo(() => {
|
||||
// With Elastic Defend Endpoint, we check that the actual `endpoint` agent on
|
||||
// this host reported that capability
|
||||
if (agentType === 'endpoint') {
|
||||
return capabilities.includes('isolation');
|
||||
}
|
||||
return hostSupportsResponseActions && isolationSupported;
|
||||
}, [hostSupportsResponseActions, isolationSupported]);
|
||||
|
||||
return Boolean(externalAgentStatus?.found && isolationSupported);
|
||||
}, [agentType, externalAgentStatus?.found, isolationSupported, capabilities]);
|
||||
const isHostIsolated = useMemo(() => {
|
||||
return Boolean(agentStatus?.isolated);
|
||||
}, [agentStatus?.isolated]);
|
||||
|
||||
const isolateHostHandler = useCallback(() => {
|
||||
closePopover();
|
||||
if (!isHostIsolated) {
|
||||
onAddIsolationStatusClick('isolateHost');
|
||||
} else {
|
||||
onAddIsolationStatusClick('unisolateHost');
|
||||
|
||||
if (doesHostSupportIsolation) {
|
||||
if (!isHostIsolated) {
|
||||
onAddIsolationStatusClick('isolateHost');
|
||||
} else {
|
||||
onAddIsolationStatusClick('unisolateHost');
|
||||
}
|
||||
}
|
||||
}, [closePopover, isHostIsolated, onAddIsolationStatusClick]);
|
||||
}, [closePopover, doesHostSupportIsolation, isHostIsolated, onAddIsolationStatusClick]);
|
||||
|
||||
const isHostAgentUnEnrolled = useMemo<boolean>(() => {
|
||||
if (!hostSupportsResponseActions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isEndpointAgent) {
|
||||
return agentStatus === HostStatus.UNENROLLED;
|
||||
}
|
||||
|
||||
// NON-Endpoint agent types
|
||||
// 8.15 use FF for computing if action is enabled
|
||||
if (agentStatusClientEnabled) {
|
||||
return externalAgentStatus?.status === HostStatus.UNENROLLED;
|
||||
}
|
||||
|
||||
// else use the old way
|
||||
if (!externalAgentStatus) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { isUninstalled, isPendingUninstall } = externalAgentStatus as AgentStatusInfo[string];
|
||||
|
||||
return isUninstalled || isPendingUninstall;
|
||||
}, [
|
||||
hostSupportsResponseActions,
|
||||
isEndpointAgent,
|
||||
agentStatusClientEnabled,
|
||||
externalAgentStatus,
|
||||
agentStatus,
|
||||
]);
|
||||
return (
|
||||
!hostSupportsResponseActions ||
|
||||
!agentStatus?.found ||
|
||||
agentStatus.status === HostStatus.UNENROLLED
|
||||
);
|
||||
}, [hostSupportsResponseActions, agentStatus]);
|
||||
|
||||
return useMemo<AlertTableContextMenuItem[]>(() => {
|
||||
// If not an Alert OR user has no Authz, then don't show the menu item at all
|
||||
|
@ -147,14 +96,15 @@ export const useHostIsolationAction = ({
|
|||
// support response actions, then show that as the tooltip. Else, just show the normal "enroll" message
|
||||
menuItem.toolTipContent =
|
||||
agentType && unsupportedReason ? unsupportedReason : NOT_FROM_ENDPOINT_HOST_TOOLTIP;
|
||||
} else if (isEndpointAgent && loadingHostIsolationStatus) {
|
||||
} else if (isLoading || !isFetched) {
|
||||
menuItem.disabled = true;
|
||||
menuItem.toolTipContent = LOADING_ENDPOINT_DATA_TOOLTIP;
|
||||
} else if (isHostAgentUnEnrolled) {
|
||||
menuItem.disabled = true;
|
||||
menuItem.toolTipContent = isEndpointAgent
|
||||
? HOST_ENDPOINT_UNENROLLED_TOOLTIP
|
||||
: NOT_FROM_ENDPOINT_HOST_TOOLTIP;
|
||||
menuItem.toolTipContent =
|
||||
agentType === 'endpoint'
|
||||
? HOST_ENDPOINT_UNENROLLED_TOOLTIP
|
||||
: NOT_FROM_ENDPOINT_HOST_TOOLTIP;
|
||||
}
|
||||
|
||||
return [menuItem];
|
||||
|
@ -167,8 +117,8 @@ export const useHostIsolationAction = ({
|
|||
isHostIsolationPanelOpen,
|
||||
isolateHostHandler,
|
||||
doesHostSupportIsolation,
|
||||
isEndpointAgent,
|
||||
loadingHostIsolationStatus,
|
||||
isLoading,
|
||||
isFetched,
|
||||
agentType,
|
||||
unsupportedReason,
|
||||
]);
|
||||
|
|
|
@ -107,25 +107,6 @@ describe('use responder action data hooks', () => {
|
|||
|
||||
expect(onClickMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and agentType is NOT Endpoint', () => {
|
||||
beforeEach(() => {
|
||||
alertDetailItemData = endpointAlertDataMock.generateSentinelOneAlertDetailsItemData();
|
||||
});
|
||||
|
||||
it('should show action when agentType is supported', () => {
|
||||
expect(renderHook().result.current).toEqual(getExpectedResponderActionData());
|
||||
});
|
||||
|
||||
it('should NOT call the endpoint host metadata api', () => {
|
||||
renderHook();
|
||||
const wasMetadataApiCalled = appContextMock.coreStart.http.get.mock.calls.some(([path]) => {
|
||||
return (path as unknown as string).includes(HOST_METADATA_LIST_ROUTE);
|
||||
});
|
||||
|
||||
expect(wasMetadataApiCalled).toBe(false);
|
||||
});
|
||||
|
||||
it.each([...RESPONSE_ACTION_AGENT_TYPE])(
|
||||
'should show action disabled with tooltip for %s if agent id field is missing',
|
||||
|
@ -150,6 +131,25 @@ describe('use responder action data hooks', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('and agentType is NOT Endpoint', () => {
|
||||
beforeEach(() => {
|
||||
alertDetailItemData = endpointAlertDataMock.generateSentinelOneAlertDetailsItemData();
|
||||
});
|
||||
|
||||
it('should show action when agentType is supported', () => {
|
||||
expect(renderHook().result.current).toEqual(getExpectedResponderActionData());
|
||||
});
|
||||
|
||||
it('should NOT call the endpoint host metadata api', () => {
|
||||
renderHook();
|
||||
const wasMetadataApiCalled = appContextMock.coreStart.http.get.mock.calls.some(([path]) => {
|
||||
return (path as unknown as string).includes(HOST_METADATA_LIST_ROUTE);
|
||||
});
|
||||
|
||||
expect(wasMetadataApiCalled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and agentType IS Endpoint', () => {
|
||||
let metadataApiMocks: ReturnType<typeof endpointMetadataHttpMocks>;
|
||||
|
||||
|
|
|
@ -133,8 +133,6 @@ const useResponderDataForEndpointHost = (
|
|||
endpointAgentId: string,
|
||||
enabled: boolean = true
|
||||
): ResponderDataForEndpointHost => {
|
||||
// FIXME:PT is this the correct API to call? or should we call the agent status api instead
|
||||
|
||||
const {
|
||||
data: endpointHostInfo,
|
||||
isFetching,
|
||||
|
|
|
@ -21,6 +21,8 @@ import { ACTION_STATUS_ROUTE } from '../../../../../common/endpoint/constants';
|
|||
export const fetchPendingActionsByAgentId = (
|
||||
agentIds: PendingActionsRequestQuery['agent_ids']
|
||||
): Promise<PendingActionsResponse> => {
|
||||
// FIXME:PT Delete method now that we are using new internal API (team issue: 9783)
|
||||
|
||||
return KibanaServices.get().http.get<PendingActionsResponse>(ACTION_STATUS_ROUTE, {
|
||||
version: '2023-10-31',
|
||||
query: {
|
||||
|
|
|
@ -32,7 +32,7 @@ export const PanelHeader: FC = () => {
|
|||
const title = (
|
||||
<EuiFlexGroup responsive gutterSize="s">
|
||||
<EuiFlexItem grow={false} data-test-subj="flyoutHostIsolationHeaderTitle">
|
||||
{isolateAction === 'isolateHost' ? <>{ISOLATE_HOST}</> : <>{UNISOLATE_HOST}</>}
|
||||
{isolateAction === 'isolateHost' ? ISOLATE_HOST : UNISOLATE_HOST}
|
||||
</EuiFlexItem>
|
||||
{showTechPreviewBadge && (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -18,14 +18,8 @@ import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys';
|
|||
import { LeftPanelInsightsTab } from '../../left';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
|
||||
import { useGetEndpointDetails } from '../../../../management/hooks';
|
||||
import {
|
||||
useAgentStatusHook,
|
||||
useGetAgentStatus,
|
||||
useGetSentinelOneAgentStatus,
|
||||
} from '../../../../management/hooks/agents/use_get_agent_status';
|
||||
import { useGetAgentStatus } from '../../../../management/hooks/agents/use_get_agent_status';
|
||||
import { type ExpandableFlyoutApi, useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
|
||||
jest.mock('../../../../management/hooks');
|
||||
jest.mock('../../../../management/hooks/agents/use_get_agent_status');
|
||||
|
@ -35,13 +29,7 @@ jest.mock('@kbn/expandable-flyout', () => ({
|
|||
ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}</>,
|
||||
}));
|
||||
|
||||
const useGetSentinelOneAgentStatusMock = useGetSentinelOneAgentStatus as jest.Mock;
|
||||
const useGetAgentStatusMock = useGetAgentStatus as jest.Mock;
|
||||
const useAgentStatusHookMock = useAgentStatusHook as jest.Mock;
|
||||
const hooksToMock: Record<string, jest.Mock> = {
|
||||
useGetSentinelOneAgentStatus: useGetSentinelOneAgentStatusMock,
|
||||
useGetAgentStatus: useGetAgentStatusMock,
|
||||
};
|
||||
|
||||
const flyoutContextValue = {
|
||||
openLeftPanel: jest.fn(),
|
||||
|
@ -105,7 +93,10 @@ describe('<HighlightedFieldsCell />', () => {
|
|||
});
|
||||
|
||||
it('should render agent status cell if field is `agent.status`', () => {
|
||||
(useGetEndpointDetails as jest.Mock).mockReturnValue({});
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
isFetched: true,
|
||||
isLoading: false,
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<HighlightedFieldsCell values={['value']} field={'agent.status'} />
|
||||
|
@ -115,55 +106,43 @@ describe('<HighlightedFieldsCell />', () => {
|
|||
expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 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 'originalField' is ${RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one} with %s hook`,
|
||||
(hookName) => {
|
||||
const hook = hooksToMock[hookName];
|
||||
useAgentStatusHookMock.mockImplementation(() => hook);
|
||||
it('should render SentinelOne agent status cell if field is agent.status and `originalField` is `observer.serial_number`', () => {
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
isFetched: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
(hook as jest.Mock).mockReturnValue({
|
||||
isFetched: true,
|
||||
isLoading: false,
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<HighlightedFieldsCell
|
||||
values={['value']}
|
||||
field={'agent.status'}
|
||||
originalField="observer.serial_number"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<HighlightedFieldsCell
|
||||
values={['value']}
|
||||
field={'agent.status'}
|
||||
originalField={RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
it.each(Object.keys(hooksToMock))(
|
||||
`should render Crowdstrike agent status cell if field is agent.status and 'originalField' is ${RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike} with %s hook`,
|
||||
(hookName) => {
|
||||
const hook = hooksToMock[hookName];
|
||||
useAgentStatusHookMock.mockImplementation(() => hook);
|
||||
it('should render Crowdstrike agent status cell if field is agent.status and `originalField` is `crowdstrike.event.DeviceId`', () => {
|
||||
useGetAgentStatusMock.mockReturnValue({
|
||||
isFetched: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
(hook as jest.Mock).mockReturnValue({
|
||||
isFetched: true,
|
||||
isLoading: false,
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<HighlightedFieldsCell
|
||||
values={['value']}
|
||||
field={'agent.status'}
|
||||
originalField="crowdstrike.event.DeviceId"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<HighlightedFieldsCell
|
||||
values={['value']}
|
||||
field={'agent.status'}
|
||||
originalField={RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
it('should not render if values is null', () => {
|
||||
const { container } = render(
|
||||
<TestProviders>
|
||||
|
|
|
@ -6,15 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { VFC } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import {
|
||||
AgentStatus,
|
||||
EndpointAgentStatusById,
|
||||
} from '../../../../common/components/endpoint/agents/agent_status';
|
||||
import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
import {
|
||||
AGENT_STATUS_FIELD_NAME,
|
||||
|
@ -81,33 +77,7 @@ export interface HighlightedFieldsCellProps {
|
|||
values: string[] | null | undefined;
|
||||
}
|
||||
|
||||
const FieldsAgentStatus = memo(
|
||||
({ value, agentType }: { value: string | undefined; agentType: ResponseActionAgentType }) => {
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
if (agentType !== 'endpoint' || agentStatusClientEnabled) {
|
||||
return (
|
||||
<AgentStatus
|
||||
agentId={String(value ?? '')}
|
||||
agentType={agentType}
|
||||
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// TODO: remove usage of `EndpointAgentStatusById` when `agentStatusClientEnabled` FF is enabled and removed
|
||||
return (
|
||||
<EndpointAgentStatusById
|
||||
endpointAgentId={String(value ?? '')}
|
||||
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
FieldsAgentStatus.displayName = 'FieldsAgentStatus';
|
||||
|
||||
/**
|
||||
* console.log('c::*, values != null
|
||||
* Renders a component in the highlighted fields table cell based on the field name
|
||||
*/
|
||||
export const HighlightedFieldsCell: VFC<HighlightedFieldsCellProps> = ({
|
||||
|
@ -146,7 +116,11 @@ export const HighlightedFieldsCell: VFC<HighlightedFieldsCellProps> = ({
|
|||
{field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? (
|
||||
<LinkFieldCell value={value} />
|
||||
) : field === AGENT_STATUS_FIELD_NAME ? (
|
||||
<FieldsAgentStatus value={value} agentType={agentType} />
|
||||
<AgentStatus
|
||||
agentId={String(value ?? '')}
|
||||
agentType={agentType}
|
||||
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
|
||||
/>
|
||||
) : (
|
||||
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
|
||||
)}
|
||||
|
|
|
@ -11,6 +11,8 @@ import React from 'react';
|
|||
import { mockObservedHostData } from '../../mocks';
|
||||
import { policyFields } from './endpoint_policy_fields';
|
||||
|
||||
jest.mock('../../../../management/hooks/agents/use_get_agent_status');
|
||||
|
||||
const TestWrapper = ({ el }: { el: JSX.Element | undefined }) => <>{el}</>;
|
||||
|
||||
jest.mock(
|
||||
|
|
|
@ -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/agents/agent_status';
|
||||
import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
import type { HostItem } from '../../../../../common/search_strategy';
|
||||
import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy';
|
||||
|
@ -57,8 +57,9 @@ export const policyFields: EntityTableRows<ObservedEntityData<HostItem>> = [
|
|||
label: i18n.FLEET_AGENT_STATUS,
|
||||
render: (hostData: ObservedEntityData<HostItem>) =>
|
||||
hostData.details.endpoint?.hostInfo ? (
|
||||
<EndpointAgentStatus
|
||||
endpointHostInfo={hostData.details.endpoint?.hostInfo}
|
||||
<AgentStatus
|
||||
agentId={hostData.details.endpoint?.hostInfo.metadata.agent.id}
|
||||
agentType="endpoint"
|
||||
data-test-subj="endpointHostAgentStatus"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -41,5 +41,5 @@ export const MANAGEMENT_DEFAULT_SORT_ORDER = 'desc';
|
|||
export const MANAGEMENT_DEFAULT_SORT_FIELD = 'created_at';
|
||||
|
||||
// --[ DEFAULTS ]---------------------------------------------------------------------------
|
||||
/** The default polling interval to start all polling pages */
|
||||
/** The default polling interval for API calls that require a refresh interval */
|
||||
export const DEFAULT_POLL_INTERVAL = 10000;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { EuiDescriptionList } from '@elastic/eui';
|
|||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { getAgentStatusText } from '../../../../common/components/endpoint/agents/agent_status/translations';
|
||||
import type { HostInfo, PendingActionsResponse } from '../../../../../common/endpoint/types';
|
||||
import type { EndpointCommandDefinitionMeta } from '../types';
|
||||
import { useGetEndpointPendingActionsSummary } from '../../../hooks/response_actions/use_get_endpoint_pending_actions_summary';
|
||||
|
@ -19,7 +20,6 @@ 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/agents/agent_status_text';
|
||||
|
||||
export const EndpointStatusActionResult = memo<
|
||||
CommandExecutionComponentProps<
|
||||
|
@ -28,6 +28,7 @@ export const EndpointStatusActionResult = memo<
|
|||
apiCalled?: boolean;
|
||||
endpointDetails?: HostInfo;
|
||||
detailsFetchError?: IHttpFetchError;
|
||||
// FIXME:PT remove this and use new API/TYpe (team issue: 9783)
|
||||
endpointPendingActions?: PendingActionsResponse;
|
||||
},
|
||||
EndpointCommandDefinitionMeta
|
||||
|
|
|
@ -10,10 +10,7 @@ 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 { 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';
|
||||
|
@ -22,7 +19,6 @@ 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: (
|
||||
|
@ -54,7 +50,6 @@ describe('Responder header Agent Info', () => {
|
|||
));
|
||||
|
||||
getAgentStatusMock.mockReturnValue({ data: {} });
|
||||
useAgentStatusHookMock.mockImplementation(() => useGetAgentStatus);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -6,22 +6,21 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import type { ResponseActionAgentType } from '../../../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { AgentStatus } from '../../../../../../common/components/endpoint/agents/agent_status';
|
||||
import { useAgentStatusHook } from '../../../../../hooks/agents/use_get_agent_status';
|
||||
import type { ThirdPartyAgentInfo } from '../../../../../../../common/types';
|
||||
import { useGetAgentStatus } from '../../../../../hooks/agents/use_get_agent_status';
|
||||
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'];
|
||||
agentId: string;
|
||||
agentType: ResponseActionAgentType;
|
||||
platform: string;
|
||||
hostName: string;
|
||||
}
|
||||
|
||||
export const AgentInfo = memo<AgentInfoProps>(({ agentId, platform, hostName, agentType }) => {
|
||||
const getAgentStatus = useAgentStatusHook();
|
||||
const { data } = getAgentStatus([agentId], agentType);
|
||||
const { data } = useGetAgentStatus(agentId, agentType);
|
||||
const agentStatus = data?.[agentId];
|
||||
const lastCheckin = agentStatus ? agentStatus.lastSeen : '';
|
||||
|
||||
|
|
|
@ -10,15 +10,17 @@ import { EndpointActionGenerator } from '../../../../../../../common/endpoint/da
|
|||
import type { HostInfo } from '../../../../../../../common/endpoint/types';
|
||||
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
|
||||
import { useGetEndpointDetails } from '../../../../../hooks/endpoint/use_get_endpoint_details';
|
||||
import { useGetEndpointDetails as _useGetEndpointDetails } from '../../../../../hooks/endpoint/use_get_endpoint_details';
|
||||
import { useGetEndpointPendingActionsSummary } from '../../../../../hooks/response_actions/use_get_endpoint_pending_actions_summary';
|
||||
import { mockEndpointDetailsApiResult } from '../../../../../pages/endpoint_hosts/store/mock_endpoint_result_list';
|
||||
import { HeaderEndpointInfo } from './header_endpoint_info';
|
||||
import { agentStatusGetHttpMock } from '../../../../../mocks';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
jest.mock('../../../../../hooks/endpoint/use_get_endpoint_details');
|
||||
jest.mock('../../../../../hooks/response_actions/use_get_endpoint_pending_actions_summary');
|
||||
|
||||
const getEndpointDetails = useGetEndpointDetails as jest.Mock;
|
||||
const useGetEndpointDetailsMock = _useGetEndpointDetails as jest.Mock;
|
||||
const getPendingActions = useGetEndpointPendingActionsSummary as jest.Mock;
|
||||
|
||||
describe('Responder header endpoint info', () => {
|
||||
|
@ -27,12 +29,12 @@ describe('Responder header endpoint info', () => {
|
|||
let mockedContext: AppContextTestRender;
|
||||
let endpointDetails: HostInfo;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
render = () =>
|
||||
(renderResult = mockedContext.render(<HeaderEndpointInfo endpointId={'1234'} />));
|
||||
endpointDetails = mockEndpointDetailsApiResult();
|
||||
getEndpointDetails.mockReturnValue({ data: endpointDetails });
|
||||
useGetEndpointDetailsMock.mockReturnValue({ data: endpointDetails });
|
||||
getPendingActions.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
|
@ -42,7 +44,11 @@ describe('Responder header endpoint info', () => {
|
|||
],
|
||||
},
|
||||
});
|
||||
const apiMock = agentStatusGetHttpMock(mockedContext.coreStart.http);
|
||||
render();
|
||||
await waitFor(() => {
|
||||
expect(apiMock.responseProvider.getAgentStatus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -56,7 +62,7 @@ describe('Responder header endpoint info', () => {
|
|||
const agentStatus = await renderResult.findByTestId(
|
||||
'responderHeaderEndpointAgentIsolationStatus'
|
||||
);
|
||||
expect(agentStatus.textContent).toBe(`UnhealthyIsolating`);
|
||||
expect(agentStatus.textContent).toBe(`Healthy`);
|
||||
});
|
||||
it('should show last checkin time', async () => {
|
||||
const lastUpdated = await renderResult.findByTestId('responderHeaderLastSeen');
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiSkeletonText } from '@elastic/eui';
|
||||
import { EndpointAgentStatus } from '../../../../../../common/components/endpoint/agents/agent_status';
|
||||
import { AgentStatus } from '../../../../../../common/components/endpoint/agents/agent_status';
|
||||
import { HeaderAgentInfo } from '../header_agent_info';
|
||||
import { useGetEndpointDetails } from '../../../../../hooks';
|
||||
import type { Platform } from '../platforms';
|
||||
|
@ -35,8 +35,9 @@ export const HeaderEndpointInfo = memo<HeaderEndpointInfoProps>(({ endpointId })
|
|||
hostName={endpointDetails.metadata.host.name}
|
||||
lastCheckin={endpointDetails.last_checkin}
|
||||
>
|
||||
<EndpointAgentStatus
|
||||
endpointHostInfo={endpointDetails}
|
||||
<AgentStatus
|
||||
agentId={endpointId}
|
||||
agentType="endpoint"
|
||||
data-test-subj="responderHeaderEndpointAgentIsolationStatus"
|
||||
/>
|
||||
</HeaderAgentInfo>
|
||||
|
|
|
@ -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 { AgentStatus } from '../../../../../../common/components/endpoint/agents/agent_status';
|
||||
import { useAgentStatusHook } from '../../../../../hooks/agents/use_get_agent_status';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features';
|
||||
import type { ThirdPartyAgentInfo } from '../../../../../../../common/types';
|
||||
import { HeaderAgentInfo } from '../header_agent_info';
|
||||
import type { Platform } from '../platforms';
|
||||
|
||||
interface HeaderSentinelOneInfoProps {
|
||||
agentId: ThirdPartyAgentInfo['agent']['id'];
|
||||
agentType: ThirdPartyAgentInfo['agent']['type'];
|
||||
platform: ThirdPartyAgentInfo['host']['os']['family'];
|
||||
hostName: ThirdPartyAgentInfo['host']['name'];
|
||||
}
|
||||
|
||||
export const HeaderSentinelOneInfo = memo<HeaderSentinelOneInfoProps>(
|
||||
({ agentId, agentType, platform, hostName }) => {
|
||||
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
|
||||
'sentinelOneManualHostActionsEnabled'
|
||||
);
|
||||
const getAgentStatus = useAgentStatusHook();
|
||||
const { data } = getAgentStatus([agentId], 'sentinel_one', { enabled: isSentinelOneV1Enabled });
|
||||
const agentStatus = data?.[agentId];
|
||||
const lastCheckin = agentStatus ? agentStatus.lastSeen : '';
|
||||
|
||||
return (
|
||||
<HeaderAgentInfo
|
||||
platform={platform.toLowerCase() as Platform}
|
||||
hostName={hostName}
|
||||
lastCheckin={lastCheckin}
|
||||
>
|
||||
<AgentStatus
|
||||
agentId={agentId}
|
||||
agentType={agentType}
|
||||
data-test-subj="responderHeaderSentinelOneAgentIsolationStatus"
|
||||
/>
|
||||
</HeaderAgentInfo>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
HeaderSentinelOneInfo.displayName = 'HeaderSentinelOneInfo';
|
|
@ -8,101 +8,62 @@
|
|||
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { RESPONSE_ACTION_AGENT_TYPE } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import React from 'react';
|
||||
import type { HostInfo } from '../../../../../common/endpoint/types';
|
||||
import { HostStatus } from '../../../../../common/endpoint/types';
|
||||
import type { AppContextTestRender } from '../../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../../common/mock/endpoint';
|
||||
import {
|
||||
useAgentStatusHook,
|
||||
useGetAgentStatus,
|
||||
useGetSentinelOneAgentStatus,
|
||||
} 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('../../../hooks/agents/use_get_agent_status');
|
||||
|
||||
const getEndpointDetails = useGetEndpointDetails as jest.Mock;
|
||||
const getSentinelOneAgentStatus = useGetSentinelOneAgentStatus as jest.Mock;
|
||||
const getAgentStatus = useGetAgentStatus as jest.Mock;
|
||||
const useAgentStatusHookMock = useAgentStatusHook as jest.Mock;
|
||||
import { agentStatusGetHttpMock } from '../../../mocks';
|
||||
import { agentStatusMocks } from '../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
describe('Responder offline callout', () => {
|
||||
// TODO: 8.15 remove the sentinelOneAgentStatus hook when `agentStatusClientEnabled` is enabled and removed
|
||||
describe.each([
|
||||
[useGetSentinelOneAgentStatus, getSentinelOneAgentStatus],
|
||||
[useGetAgentStatus, getAgentStatus],
|
||||
])('works with %s hook', (hook, mockHook) => {
|
||||
let render: (agentType?: ResponseActionAgentType) => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
let endpointDetails: HostInfo;
|
||||
let render: (agentType?: ResponseActionAgentType) => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
let apiMocks: ReturnType<typeof agentStatusGetHttpMock>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
render = (agentType?: ResponseActionAgentType) =>
|
||||
(renderResult = mockedContext.render(
|
||||
<OfflineCallout
|
||||
endpointId={'1234'}
|
||||
agentType={agentType || 'endpoint'}
|
||||
hostName="Host name"
|
||||
/>
|
||||
));
|
||||
endpointDetails = mockEndpointDetailsApiResult();
|
||||
getEndpointDetails.mockReturnValue({ data: endpointDetails });
|
||||
mockHook.mockReturnValue({ data: {} });
|
||||
useAgentStatusHookMock.mockImplementation(() => hook);
|
||||
render();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(
|
||||
'should be visible when agent type is %s and host is offline',
|
||||
(agentType) => {
|
||||
if (agentType === 'endpoint') {
|
||||
getEndpointDetails.mockReturnValue({
|
||||
data: { ...endpointDetails, host_status: HostStatus.OFFLINE },
|
||||
});
|
||||
} else {
|
||||
mockHook.mockReturnValue({
|
||||
data: {
|
||||
'1234': {
|
||||
status: HostStatus.OFFLINE,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
render(agentType);
|
||||
const callout = renderResult.queryByTestId('offlineCallout');
|
||||
expect(callout).toBeTruthy();
|
||||
}
|
||||
);
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(
|
||||
'should not be visible when agent type is %s and host is online',
|
||||
(agentType) => {
|
||||
if (agentType === 'endpoint') {
|
||||
getEndpointDetails.mockReturnValue({
|
||||
data: { ...endpointDetails, host_status: HostStatus.HEALTHY },
|
||||
});
|
||||
} else {
|
||||
mockHook.mockReturnValue({
|
||||
data: {
|
||||
'1234': {
|
||||
status: HostStatus.HEALTHY,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
render(agentType);
|
||||
const callout = renderResult.queryByTestId('offlineCallout');
|
||||
expect(callout).toBeFalsy();
|
||||
}
|
||||
);
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
apiMocks = agentStatusGetHttpMock(mockedContext.coreStart.http);
|
||||
render = (agentType?: ResponseActionAgentType) =>
|
||||
(renderResult = mockedContext.render(
|
||||
<OfflineCallout
|
||||
endpointId={'abfe4a35-d5b4-42a0-a539-bd054c791769'}
|
||||
agentType={agentType || 'endpoint'}
|
||||
hostName="Host name"
|
||||
/>
|
||||
));
|
||||
});
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(
|
||||
'should be visible when agent type is %s and host is offline',
|
||||
async (agentType) => {
|
||||
apiMocks.responseProvider.getAgentStatus.mockReturnValue({
|
||||
data: {
|
||||
'abfe4a35-d5b4-42a0-a539-bd054c791769': agentStatusMocks.generateAgentStatus({
|
||||
agentType,
|
||||
status: HostStatus.OFFLINE,
|
||||
}),
|
||||
},
|
||||
});
|
||||
render(agentType);
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.getAgentStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(renderResult.getByTestId('offlineCallout')).toBeTruthy();
|
||||
}
|
||||
);
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(
|
||||
'should NOT be visible when agent type is %s and host is online',
|
||||
async (agentType) => {
|
||||
render(agentType);
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.getAgentStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(renderResult.queryByTestId('offlineCallout')).toBeNull();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,15 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isAgentTypeAndActionSupported } from '../../../../common/lib/endpoint';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useAgentStatusHook } from '../../../hooks/agents/use_get_agent_status';
|
||||
import { useGetAgentStatus } 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';
|
||||
|
||||
interface OfflineCalloutProps {
|
||||
|
@ -23,45 +20,9 @@ interface OfflineCalloutProps {
|
|||
}
|
||||
|
||||
export const OfflineCallout = memo<OfflineCalloutProps>(({ agentType, endpointId, hostName }) => {
|
||||
const isEndpointAgent = agentType === 'endpoint';
|
||||
const isSentinelOneAgent = agentType === 'sentinel_one';
|
||||
const isCrowdstrikeAgent = agentType === 'crowdstrike';
|
||||
const getAgentStatus = useAgentStatusHook();
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
const { data } = useGetAgentStatus(endpointId, agentType);
|
||||
|
||||
const isAgentTypeEnabled = useMemo(() => {
|
||||
return isAgentTypeAndActionSupported(agentType);
|
||||
}, [agentType]);
|
||||
|
||||
const { data: endpointDetails } = useGetEndpointDetails(endpointId, {
|
||||
refetchInterval: 10000,
|
||||
enabled: isEndpointAgent && !agentStatusClientEnabled,
|
||||
});
|
||||
|
||||
const { data } = getAgentStatus([endpointId], agentType, {
|
||||
enabled:
|
||||
(isEndpointAgent && agentStatusClientEnabled) || (!isEndpointAgent && isAgentTypeEnabled),
|
||||
});
|
||||
const showOfflineCallout = useMemo(
|
||||
() =>
|
||||
(isEndpointAgent && endpointDetails?.host_status === HostStatus.OFFLINE) ||
|
||||
(isSentinelOneAgent && data?.[endpointId].status === HostStatus.OFFLINE) ||
|
||||
(isCrowdstrikeAgent && data?.[endpointId].status === HostStatus.OFFLINE),
|
||||
[
|
||||
data,
|
||||
endpointDetails?.host_status,
|
||||
endpointId,
|
||||
isEndpointAgent,
|
||||
isCrowdstrikeAgent,
|
||||
isSentinelOneAgent,
|
||||
]
|
||||
);
|
||||
|
||||
if ((isEndpointAgent && !endpointDetails) || (isAgentTypeEnabled && !data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showOfflineCallout) {
|
||||
if (data?.[endpointId].status === HostStatus.OFFLINE) {
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { agentStatusMocks } from '../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
|
||||
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import type { AgentStatusRecords } from '../../../../../common/endpoint/types';
|
||||
|
||||
const useGetAgentStatusMock = jest.fn(
|
||||
(agentIds: string[] | string, agentType: ResponseActionAgentType) => {
|
||||
const agentsIdList = Array.isArray(agentIds) ? agentIds : [agentIds];
|
||||
|
||||
return {
|
||||
data: agentsIdList.reduce<AgentStatusRecords>((acc, agentId) => {
|
||||
acc[agentId] = agentStatusMocks.generateAgentStatus({ agentType, agentId });
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
isLoading: false,
|
||||
isFetched: true,
|
||||
isFetching: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export { useGetAgentStatusMock as useGetAgentStatus };
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { useGetAgentStatus } from './use_get_agent_status';
|
||||
import { agentStatusGetHttpMock } from '../../mocks';
|
||||
import { AGENT_STATUS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
|
||||
|
||||
describe('useGetAgentStatus hook', () => {
|
||||
let httpMock: AppContextTestRender['coreStart']['http'];
|
||||
let agentIdsProp: Parameters<typeof useGetAgentStatus>[0];
|
||||
let optionsProp: Parameters<typeof useGetAgentStatus>[2];
|
||||
let apiMock: ReturnType<typeof agentStatusGetHttpMock>;
|
||||
let renderHook: () => RenderHookResult<unknown, ReturnType<typeof useGetAgentStatus>>;
|
||||
|
||||
beforeEach(() => {
|
||||
const appTestContext = createAppRootMockRenderer();
|
||||
|
||||
httpMock = appTestContext.coreStart.http;
|
||||
apiMock = agentStatusGetHttpMock(httpMock);
|
||||
renderHook = () => {
|
||||
return appTestContext.renderHook<unknown, ReturnType<typeof useGetAgentStatus>>(() =>
|
||||
useGetAgentStatus(agentIdsProp, 'endpoint', optionsProp)
|
||||
);
|
||||
};
|
||||
agentIdsProp = '1-2-3';
|
||||
optionsProp = undefined;
|
||||
});
|
||||
|
||||
it('should accept a single agent id (string)', () => {
|
||||
renderHook();
|
||||
|
||||
expect(httpMock.get).toHaveBeenCalledWith(AGENT_STATUS_ROUTE, {
|
||||
query: { agentIds: ['1-2-3'], agentType: 'endpoint' },
|
||||
version: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept multiple agent ids (array)', () => {
|
||||
agentIdsProp = ['1', '2', '3'];
|
||||
renderHook();
|
||||
|
||||
expect(httpMock.get).toHaveBeenCalledWith(AGENT_STATUS_ROUTE, {
|
||||
query: { agentIds: ['1', '2', '3'], agentType: 'endpoint' },
|
||||
version: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should only use agentIds that are not empty strings', () => {
|
||||
agentIdsProp = ['', '1', ''];
|
||||
renderHook();
|
||||
|
||||
expect(httpMock.get).toHaveBeenCalledWith(AGENT_STATUS_ROUTE, {
|
||||
query: { agentIds: ['1'], agentType: 'endpoint' },
|
||||
version: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return expected data', async () => {
|
||||
const { result, waitForValueToChange } = renderHook();
|
||||
await waitForValueToChange(() => result.current);
|
||||
|
||||
expect(result.current.data).toEqual({
|
||||
'1-2-3': {
|
||||
agentId: '1-2-3',
|
||||
agentType: 'endpoint',
|
||||
found: true,
|
||||
isolated: false,
|
||||
lastSeen: expect.any(String),
|
||||
pendingActions: {},
|
||||
status: 'healthy',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT call agent status api if list of agent ids is empty', async () => {
|
||||
agentIdsProp = ['', ' '];
|
||||
const { result, waitForValueToChange } = renderHook();
|
||||
await waitForValueToChange(() => result.current);
|
||||
|
||||
expect(result.current.data).toEqual({});
|
||||
expect(apiMock.responseProvider.getAgentStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { SentinelOneGetAgentsResponse } from '@kbn/stack-connectors-plugin/common/sentinelone/types';
|
||||
import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common';
|
||||
import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import { DEFAULT_POLL_INTERVAL } from '../../common/constants';
|
||||
import { AGENT_STATUS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import type { AgentStatusRecords, AgentStatusApiResponse } from '../../../../common/endpoint/types';
|
||||
import { useHttp } from '../../../common/lib/kibana';
|
||||
|
||||
interface ErrorType {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
meta: ActionTypeExecutorResult<SentinelOneGetAgentsResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the status of a supported host's agent type
|
||||
* @param agentIds
|
||||
* @param agentType
|
||||
* @param options
|
||||
*/
|
||||
export const useGetAgentStatus = (
|
||||
agentIds: string[] | string,
|
||||
agentType: ResponseActionAgentType,
|
||||
options: Omit<UseQueryOptions<AgentStatusRecords, IHttpFetchError<ErrorType>>, 'queryFn'> = {}
|
||||
): UseQueryResult<AgentStatusRecords, IHttpFetchError<ErrorType>> => {
|
||||
const http = useHttp();
|
||||
const agentIdList = (Array.isArray(agentIds) ? agentIds : [agentIds]).filter(
|
||||
(agentId) => agentId.trim().length
|
||||
);
|
||||
|
||||
return useQuery<AgentStatusRecords, IHttpFetchError<ErrorType>>({
|
||||
queryKey: ['get-agent-status', agentIdList],
|
||||
refetchInterval: DEFAULT_POLL_INTERVAL,
|
||||
...options,
|
||||
queryFn: () => {
|
||||
if (agentIdList.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return http
|
||||
.get<AgentStatusApiResponse>(AGENT_STATUS_ROUTE, {
|
||||
version: '1',
|
||||
query: {
|
||||
agentIds: agentIdList,
|
||||
agentType,
|
||||
},
|
||||
})
|
||||
.then((response) => response.data);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,83 +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 { SentinelOneGetAgentsResponse } from '@kbn/stack-connectors-plugin/common/sentinelone/types';
|
||||
import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { AGENT_STATUS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import type { AgentStatusInfo, AgentStatusRecords } from '../../../../common/endpoint/types';
|
||||
import { useHttp } from '../../../common/lib/kibana';
|
||||
|
||||
interface ErrorType {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
meta: ActionTypeExecutorResult<SentinelOneGetAgentsResponse>;
|
||||
}
|
||||
|
||||
// TODO: 8.15: Remove `useGetSentinelOneAgentStatus` function when `agentStatusClientEnabled` is enabled/removed
|
||||
export const useGetSentinelOneAgentStatus = (
|
||||
agentIds: string[],
|
||||
agentType?: string,
|
||||
options: UseQueryOptions<AgentStatusInfo, IHttpFetchError<ErrorType>> = {}
|
||||
): UseQueryResult<AgentStatusInfo, IHttpFetchError<ErrorType>> => {
|
||||
const http = useHttp();
|
||||
|
||||
return useQuery<AgentStatusInfo, IHttpFetchError<ErrorType>>({
|
||||
queryKey: ['get-agent-status', agentIds],
|
||||
refetchInterval: 5000,
|
||||
...options,
|
||||
enabled: agentType === 'sentinel_one',
|
||||
queryFn: () =>
|
||||
http
|
||||
.get<{ data: AgentStatusInfo }>(AGENT_STATUS_ROUTE, {
|
||||
version: '1',
|
||||
query: {
|
||||
agentIds,
|
||||
// 8.13 sentinel_one support via internal API
|
||||
agentType: agentType ? agentType : 'sentinel_one',
|
||||
},
|
||||
})
|
||||
.then((response) => response.data),
|
||||
});
|
||||
};
|
||||
|
||||
// 8.14, 8.15 used for fetching agent status
|
||||
export const useGetAgentStatus = (
|
||||
agentIds: string[],
|
||||
agentType: string,
|
||||
options: UseQueryOptions<AgentStatusRecords, IHttpFetchError<ErrorType>> = {}
|
||||
): UseQueryResult<AgentStatusRecords, IHttpFetchError<ErrorType>> => {
|
||||
const http = useHttp();
|
||||
|
||||
return useQuery<AgentStatusRecords, IHttpFetchError<ErrorType>>({
|
||||
queryKey: ['get-agent-status', agentIds],
|
||||
// TODO: remove this refetchInterval and instead override it where called, via options.
|
||||
refetchInterval: 5000,
|
||||
...options,
|
||||
queryFn: () =>
|
||||
http
|
||||
.get<{ data: AgentStatusRecords }>(AGENT_STATUS_ROUTE, {
|
||||
version: '1',
|
||||
query: {
|
||||
agentIds: agentIds.filter((agentId) => agentId.trim().length),
|
||||
agentType,
|
||||
},
|
||||
})
|
||||
.then((response) => response.data),
|
||||
});
|
||||
};
|
||||
|
||||
export const useAgentStatusHook = ():
|
||||
| typeof useGetAgentStatus
|
||||
| typeof useGetSentinelOneAgentStatus => {
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
// 8.15 use agent status client hook if `agentStatusClientEnabled` FF enabled
|
||||
return !agentStatusClientEnabled ? useGetSentinelOneAgentStatus : useGetAgentStatus;
|
||||
};
|
|
@ -18,7 +18,6 @@ import { useUserPrivileges } from '../../common/components/user_privileges';
|
|||
import {
|
||||
ActionLogButton,
|
||||
getEndpointConsoleCommands,
|
||||
HeaderEndpointInfo,
|
||||
OfflineCallout,
|
||||
} from '../components/endpoint_responder';
|
||||
import { useConsoleManager } from '../components/console';
|
||||
|
@ -54,7 +53,6 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
|
|||
const responseActionsCrowdstrikeManualHostIsolationEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
||||
);
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
|
||||
return useCallback(
|
||||
(props: ResponderInfoProps) => {
|
||||
|
@ -89,22 +87,14 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
|
|||
'data-test-subj': `${agentType}ResponseActionsConsole`,
|
||||
storagePrefix: 'xpack.securitySolution.Responder',
|
||||
TitleComponent: () => {
|
||||
if (agentStatusClientEnabled || agentType !== 'endpoint') {
|
||||
return (
|
||||
<AgentInfo
|
||||
agentId={agentId}
|
||||
agentType={agentType}
|
||||
hostName={hostName}
|
||||
platform={platform}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// TODO: 8.15 remove this if block when agentStatusClientEnabled is enabled/removed
|
||||
if (agentType === 'endpoint') {
|
||||
return <HeaderEndpointInfo endpointId={agentId} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<AgentInfo
|
||||
agentId={agentId}
|
||||
agentType={agentType}
|
||||
hostName={hostName}
|
||||
platform={platform}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -157,7 +147,6 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => {
|
|||
endpointPrivileges,
|
||||
isEnterpriseLicense,
|
||||
consoleManager,
|
||||
agentStatusClientEnabled,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { HttpFetchOptionsWithPath } from '@kbn/core-http-browser';
|
||||
import type { Mutable } from 'utility-types';
|
||||
import { agentStatusMocks } from '../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
|
||||
import type { EndpointAgentStatusRequestQueryParams } from '../../../common/api/endpoint/agent/get_agent_status_route';
|
||||
import type { ResponseProvidersInterface } from '../../common/mock/endpoint';
|
||||
import { httpHandlerMockFactory } from '../../common/mock/endpoint/http_handler_mock_factory';
|
||||
import type { AgentStatusApiResponse, AgentStatusRecords } from '../../../common/endpoint/types';
|
||||
import { AGENT_STATUS_ROUTE } from '../../../common/endpoint/constants';
|
||||
|
||||
export type AgentStatusHttpMocksInterface = ResponseProvidersInterface<{
|
||||
getAgentStatus: (options: HttpFetchOptionsWithPath) => AgentStatusApiResponse;
|
||||
}>;
|
||||
|
||||
export const agentStatusGetHttpMock = httpHandlerMockFactory<AgentStatusHttpMocksInterface>([
|
||||
{
|
||||
id: 'getAgentStatus',
|
||||
method: 'get',
|
||||
path: AGENT_STATUS_ROUTE,
|
||||
handler: (options): AgentStatusApiResponse => {
|
||||
const queryOptions = options.query as Mutable<EndpointAgentStatusRequestQueryParams>;
|
||||
const agentType = queryOptions.agentType || 'endpoint';
|
||||
const agentIds = Array.isArray(queryOptions.agentIds)
|
||||
? queryOptions.agentIds
|
||||
: [queryOptions.agentIds];
|
||||
|
||||
return {
|
||||
data: agentIds.reduce<AgentStatusRecords>((acc, agentId) => {
|
||||
acc[agentId] = agentStatusMocks.generateAgentStatus({ agentId, agentType });
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
|
@ -8,3 +8,4 @@
|
|||
export * from './fleet_mocks';
|
||||
export * from './trusted_apps_http_mocks';
|
||||
export * from './exceptions_list_http_mocks';
|
||||
export * from './agent_status_http_mocks';
|
||||
|
|
|
@ -141,10 +141,6 @@ export type EndpointIsolationRequestStateChange = Action<'endpointIsolationReque
|
|||
payload: EndpointState['isolationRequestState'];
|
||||
};
|
||||
|
||||
export type EndpointPendingActionsStateChanged = Action<'endpointPendingActionsStateChanged'> & {
|
||||
payload: EndpointState['endpointPendingActions'];
|
||||
};
|
||||
|
||||
export type LoadMetadataTransformStats = Action<'loadMetadataTransformStats'>;
|
||||
|
||||
export type MetadataTransformStatsChanged = Action<'metadataTransformStatsChanged'> & {
|
||||
|
@ -173,7 +169,6 @@ export type EndpointAction =
|
|||
| ServerFailedToReturnEndpointsTotal
|
||||
| EndpointIsolationRequest
|
||||
| EndpointIsolationRequestStateChange
|
||||
| EndpointPendingActionsStateChanged
|
||||
| LoadMetadataTransformStats
|
||||
| MetadataTransformStatsChanged
|
||||
| ServerFinishedInitialization;
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from '../../../../../common/endpoint/constants';
|
||||
import type { Immutable } from '../../../../../common/endpoint/types';
|
||||
import { DEFAULT_POLL_INTERVAL } from '../../../common/constants';
|
||||
import { createLoadedResourceState, createUninitialisedResourceState } from '../../../state';
|
||||
import { createUninitialisedResourceState } from '../../../state';
|
||||
import type { EndpointState } from '../types';
|
||||
|
||||
export const initialEndpointPageState = (): Immutable<EndpointState> => {
|
||||
|
@ -41,7 +41,6 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => {
|
|||
endpointsTotal: 0,
|
||||
endpointsTotalError: undefined,
|
||||
isolationRequestState: createUninitialisedResourceState(),
|
||||
endpointPendingActions: createLoadedResourceState(new Map()),
|
||||
metadataTransformStats: createUninitialisedResourceState(),
|
||||
isInitialized: false,
|
||||
};
|
||||
|
|
|
@ -72,10 +72,6 @@ describe('EndpointList store concerns', () => {
|
|||
isolationRequestState: {
|
||||
type: 'UninitialisedResourceState',
|
||||
},
|
||||
endpointPendingActions: {
|
||||
data: new Map(),
|
||||
type: 'LoadedResourceState',
|
||||
},
|
||||
metadataTransformStats: createUninitialisedResourceState(),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -138,7 +138,6 @@ describe('endpoint list middleware', () => {
|
|||
|
||||
await Promise.all([
|
||||
waitForAction('serverReturnedEndpointList'),
|
||||
waitForAction('endpointPendingActionsStateChanged'),
|
||||
waitForAction('serverReturnedMetadataPatterns'),
|
||||
waitForAction('serverCancelledPolicyItemsLoading'),
|
||||
waitForAction('serverReturnedEndpointExistValue'),
|
||||
|
@ -236,42 +235,6 @@ describe('endpoint list middleware', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('handle Endpoint Pending Actions state actions', () => {
|
||||
let mockedApis: ReturnType<typeof endpointPageHttpMock>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedApis = endpointPageHttpMock(fakeHttpServices);
|
||||
});
|
||||
|
||||
it('should include all agents ids from the list when calling API', async () => {
|
||||
const loadingPendingActions = waitForAction('endpointPendingActionsStateChanged', {
|
||||
validate: (action) => isLoadedResourceState(action.payload),
|
||||
});
|
||||
|
||||
dispatchUserChangedUrlToEndpointList();
|
||||
await loadingPendingActions;
|
||||
|
||||
expect(mockedApis.responseProvider.pendingActions).toHaveBeenCalledWith({
|
||||
path: expect.any(String),
|
||||
version: '2023-10-31',
|
||||
query: {
|
||||
agent_ids: [
|
||||
'0dc3661d-6e67-46b0-af39-6f12b025fcb0',
|
||||
'fe16dda9-7f34-434c-9824-b4844880f410',
|
||||
'f412728b-929c-48d5-bdb6-5a1298e3e607',
|
||||
'd0405ddc-1e7c-48f0-93d7-d55f954bd745',
|
||||
'46d78dd2-aedf-4d3f-b3a9-da445f1fd25f',
|
||||
'5aafa558-26b8-4bb4-80e2-ac0644d77a3f',
|
||||
'edac2c58-1748-40c3-853c-8fab48c333d7',
|
||||
'06b7223a-bb2a-428a-9021-f1c0d2267ada',
|
||||
'b8daa43b-7f73-4684-9221-dbc8b769405e',
|
||||
'fbc06310-7d41-46b8-a5ea-ceed8a993b1a',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles metadata transform stats actions', () => {
|
||||
const dispatchLoadTransformStats = () => {
|
||||
dispatch({
|
||||
|
|
|
@ -30,7 +30,6 @@ import type {
|
|||
ResponseActionApiResponse,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint/endpoint_isolation';
|
||||
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint/endpoint_pending_actions';
|
||||
import type { ImmutableMiddlewareAPI, ImmutableMiddlewareFactory } from '../../../../common/store';
|
||||
import type { AppAction } from '../../../../common/store/actions';
|
||||
import { sendGetEndpointSpecificPackagePolicies } from '../../../services/policies/policies';
|
||||
|
@ -45,13 +44,7 @@ import {
|
|||
sendGetEndpointSecurityPackage,
|
||||
} from '../../../services/policies/ingest';
|
||||
import type { GetPolicyListResponse } from '../../policy/types';
|
||||
import type {
|
||||
AgentIdsPendingActions,
|
||||
EndpointState,
|
||||
PolicyIds,
|
||||
TransformStats,
|
||||
TransformStatsResponse,
|
||||
} from '../types';
|
||||
import type { EndpointState, PolicyIds, TransformStats, TransformStatsResponse } from '../types';
|
||||
import type { EndpointPackageInfoStateChanged } from './action';
|
||||
import {
|
||||
endpointPackageInfo,
|
||||
|
@ -62,7 +55,6 @@ import {
|
|||
getMetadataTransformStats,
|
||||
isMetadataTransformStatsLoading,
|
||||
isOnEndpointPage,
|
||||
listData,
|
||||
nonExistingPolicies,
|
||||
patterns,
|
||||
searchBarQuery,
|
||||
|
@ -301,47 +293,6 @@ async function getEndpointPackageInfo(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves the Endpoint pending actions for all the existing endpoints being displayed on the list
|
||||
* or the details tab.
|
||||
*
|
||||
* @param store
|
||||
*/
|
||||
const loadEndpointsPendingActions = async ({
|
||||
getState,
|
||||
dispatch,
|
||||
}: EndpointPageStore): Promise<void> => {
|
||||
const state = getState();
|
||||
const listEndpoints = listData(state);
|
||||
const agentsIds = new Set<string>();
|
||||
|
||||
for (const endpointInfo of listEndpoints) {
|
||||
agentsIds.add(endpointInfo.metadata.elastic.agent.id);
|
||||
}
|
||||
|
||||
if (agentsIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: pendingActions } = await fetchPendingActionsByAgentId(Array.from(agentsIds));
|
||||
const agentIdToPendingActions: AgentIdsPendingActions = new Map();
|
||||
|
||||
for (const pendingAction of pendingActions) {
|
||||
agentIdToPendingActions.set(pendingAction.agent_id, pendingAction.pending_actions);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'endpointPendingActionsStateChanged',
|
||||
payload: createLoadedResourceState(agentIdToPendingActions),
|
||||
});
|
||||
} catch (error) {
|
||||
// TODO should handle the error instead of logging it to the browser
|
||||
// Also this is an anti-pattern we shouldn't use
|
||||
logError(error);
|
||||
}
|
||||
};
|
||||
|
||||
async function endpointListMiddleware({
|
||||
store,
|
||||
coreStart,
|
||||
|
@ -380,8 +331,6 @@ async function endpointListMiddleware({
|
|||
payload: endpointResponse,
|
||||
});
|
||||
|
||||
loadEndpointsPendingActions(store);
|
||||
|
||||
dispatchIngestPolicies({ http: coreStart.http, hosts: endpointResponse.data, store });
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
|
|
|
@ -5,11 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
EndpointPackageInfoStateChanged,
|
||||
EndpointPendingActionsStateChanged,
|
||||
MetadataTransformStatsChanged,
|
||||
} from './action';
|
||||
import type { EndpointPackageInfoStateChanged, MetadataTransformStatsChanged } from './action';
|
||||
import {
|
||||
getCurrentIsolationRequestState,
|
||||
hasSelectedEndpoint,
|
||||
|
@ -29,19 +25,6 @@ type CaseReducer<T extends AppAction> = (
|
|||
action: Immutable<T>
|
||||
) => Immutable<EndpointState>;
|
||||
|
||||
const handleEndpointPendingActionsStateChanged: CaseReducer<EndpointPendingActionsStateChanged> = (
|
||||
state,
|
||||
action
|
||||
) => {
|
||||
if (isOnEndpointPage(state)) {
|
||||
return {
|
||||
...state,
|
||||
endpointPendingActions: action.payload,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const handleEndpointPackageInfoStateChanged: CaseReducer<EndpointPackageInfoStateChanged> = (
|
||||
state,
|
||||
action
|
||||
|
@ -109,8 +92,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
...state,
|
||||
patternsError: action.payload,
|
||||
};
|
||||
} else if (action.type === 'endpointPendingActionsStateChanged') {
|
||||
return handleEndpointPendingActionsStateChanged(state, action);
|
||||
} else if (action.type === 'serverReturnedPoliciesForOnboarding') {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -11,11 +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 {
|
||||
EndpointPendingActions,
|
||||
EndpointSortableField,
|
||||
Immutable,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import type { EndpointSortableField, Immutable } from '../../../../../common/endpoint/types';
|
||||
import type { EndpointIndexUIQueryParams, EndpointState } from '../types';
|
||||
import { extractListPaginationParams } from '../../../common/routing';
|
||||
import {
|
||||
|
@ -31,7 +27,6 @@ import {
|
|||
} from '../../../state';
|
||||
|
||||
import type { ServerApiError } from '../../../../common/types';
|
||||
import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
|
||||
|
||||
export const listData = (state: Immutable<EndpointState>) => state.hosts;
|
||||
|
||||
|
@ -234,17 +229,6 @@ export const getIsolationRequestError: (
|
|||
}
|
||||
});
|
||||
|
||||
export const getIsOnEndpointDetailsActivityLog: (state: Immutable<EndpointState>) => boolean =
|
||||
createSelector(uiQueryParams, (searchParams) => {
|
||||
return searchParams.show === EndpointDetailsTabsTypes.activityLog;
|
||||
});
|
||||
|
||||
export const getEndpointPendingActionsState = (
|
||||
state: Immutable<EndpointState>
|
||||
): Immutable<EndpointState['endpointPendingActions']> => {
|
||||
return state.endpointPendingActions;
|
||||
};
|
||||
|
||||
export const getMetadataTransformStats = (state: Immutable<EndpointState>) =>
|
||||
state.metadataTransformStats;
|
||||
|
||||
|
@ -253,24 +237,3 @@ export const metadataTransformStats = (state: Immutable<EndpointState>) =>
|
|||
|
||||
export const isMetadataTransformStatsLoading = (state: Immutable<EndpointState>) =>
|
||||
isLoadingResourceState(state.metadataTransformStats);
|
||||
|
||||
/**
|
||||
* Returns a function (callback) that can be used to retrieve the list of pending actions against
|
||||
* an endpoint currently displayed in the endpoint list
|
||||
*/
|
||||
export const getEndpointPendingActionsCallback: (
|
||||
state: Immutable<EndpointState>
|
||||
) => (endpointId: string) => EndpointPendingActions['pending_actions'] = createSelector(
|
||||
getEndpointPendingActionsState,
|
||||
(pendingActionsState) => {
|
||||
return (endpointId: string) => {
|
||||
let response: EndpointPendingActions['pending_actions'] = {};
|
||||
|
||||
if (isLoadedResourceState(pendingActionsState)) {
|
||||
response = pendingActionsState.data.get(endpointId) ?? {};
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -71,12 +71,6 @@ export interface EndpointState {
|
|||
endpointsTotalError?: ServerApiError;
|
||||
/** Host isolation request state for a single endpoint */
|
||||
isolationRequestState: AsyncResourceState<ResponseActionApiResponse>;
|
||||
/**
|
||||
* Holds a map of `agentId` to `EndpointPendingActions` that is used by both the list and details view
|
||||
* Getting pending endpoint actions is "supplemental" data, so there is no need to show other Async
|
||||
* states other than Loaded
|
||||
*/
|
||||
endpointPendingActions: AsyncResourceState<AgentIdsPendingActions>;
|
||||
// Metadata transform stats to checking transform state
|
||||
metadataTransformStats: AsyncResourceState<TransformStats[]>;
|
||||
isInitialized: boolean;
|
||||
|
|
|
@ -17,18 +17,10 @@ import {
|
|||
import React, { memo, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { isPolicyOutOfDate } from '../../utils';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import {
|
||||
AgentStatus,
|
||||
EndpointAgentStatus,
|
||||
} from '../../../../../common/components/endpoint/agents/agent_status';
|
||||
import { AgentStatus } from '../../../../../common/components/endpoint/agents/agent_status';
|
||||
import type { HostInfo } from '../../../../../../common/endpoint/types';
|
||||
import { useEndpointSelector } from '../hooks';
|
||||
import {
|
||||
getEndpointPendingActionsCallback,
|
||||
nonExistingPolicies,
|
||||
uiQueryParams,
|
||||
} from '../../store/selectors';
|
||||
import { nonExistingPolicies, 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';
|
||||
|
@ -50,13 +42,11 @@ interface EndpointDetailsContentProps {
|
|||
|
||||
export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
|
||||
({ hostInfo, policyInfo }) => {
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
const queryParams = useEndpointSelector(uiQueryParams);
|
||||
const policyStatus = useMemo(
|
||||
() => hostInfo.metadata.Endpoint.policy.applied.status,
|
||||
[hostInfo]
|
||||
);
|
||||
const getHostPendingActions = useEndpointSelector(getEndpointPendingActionsCallback);
|
||||
const missingPolicies = useEndpointSelector(nonExistingPolicies);
|
||||
|
||||
const policyResponseRoutePath = useMemo(() => {
|
||||
|
@ -92,15 +82,7 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
|
|||
/>
|
||||
</ColumnTitle>
|
||||
),
|
||||
// TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed
|
||||
description: agentStatusClientEnabled ? (
|
||||
<AgentStatus agentId={hostInfo.metadata.agent.id} agentType="endpoint" />
|
||||
) : (
|
||||
<EndpointAgentStatus
|
||||
pendingActions={getHostPendingActions(hostInfo.metadata.agent.id)}
|
||||
endpointHostInfo={hostInfo}
|
||||
/>
|
||||
),
|
||||
description: <AgentStatus agentId={hostInfo.metadata.agent.id} agentType="endpoint" />,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
|
@ -198,15 +180,7 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
|
|||
),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
hostInfo,
|
||||
agentStatusClientEnabled,
|
||||
getHostPendingActions,
|
||||
policyInfo,
|
||||
missingPolicies,
|
||||
policyStatus,
|
||||
policyStatusClickHandler,
|
||||
]);
|
||||
}, [hostInfo, policyInfo, missingPolicies, policyStatus, policyStatusClickHandler]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
@ -55,6 +55,8 @@ import { getUserPrivilegesMockDefaultValue } from '../../../../common/components
|
|||
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../../../../common/components/user_privileges/endpoint/mocks';
|
||||
import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details';
|
||||
import { useGetAgentStatus as _useGetAgentStatus } from '../../../hooks/agents/use_get_agent_status';
|
||||
import { agentStatusMocks } from '../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
|
||||
|
||||
const mockUserPrivileges = useUserPrivileges as jest.Mock;
|
||||
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
|
||||
|
@ -80,6 +82,9 @@ jest.mock('../../../services/policies/ingest', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../hooks/agents/use_get_agent_status');
|
||||
const useGetAgentStatusMock = _useGetAgentStatus as jest.Mock;
|
||||
|
||||
const mockUseUiSetting$ = useUiSetting$ as jest.Mock;
|
||||
const timepickerRanges = [
|
||||
{
|
||||
|
@ -325,6 +330,18 @@ describe('when on the endpoint list page', () => {
|
|||
endpointsResults: hostListData,
|
||||
endpointPackagePolicies: ingestPackagePolicies,
|
||||
});
|
||||
|
||||
useGetAgentStatusMock.mockImplementation((agentId, agentType) => {
|
||||
return {
|
||||
data: {
|
||||
[agentId]: agentStatusMocks.generateAgentStatus({
|
||||
agentType,
|
||||
}),
|
||||
},
|
||||
isLoading: false,
|
||||
isFetched: true,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
|
@ -347,18 +364,19 @@ describe('when on the endpoint list page', () => {
|
|||
const total = await renderResult.findByTestId('endpointListTableTotal');
|
||||
expect(total.textContent).toEqual('Showing 5 endpoints');
|
||||
});
|
||||
it('should display correct status', async () => {
|
||||
it('should agent status', async () => {
|
||||
const renderResult = render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
||||
const hostStatuses = await renderResult.findAllByTestId('rowHostStatus');
|
||||
|
||||
expect(hostStatuses[0].textContent).toEqual('Unhealthy');
|
||||
expect(hostStatuses[0].textContent).toEqual('Healthy');
|
||||
expect(hostStatuses[1].textContent).toEqual('Healthy');
|
||||
expect(hostStatuses[2].textContent).toEqual('Offline');
|
||||
expect(hostStatuses[3].textContent).toEqual('Updating');
|
||||
expect(hostStatuses[4].textContent).toEqual('Inactive');
|
||||
expect(hostStatuses[2].textContent).toEqual('Healthy');
|
||||
expect(hostStatuses[3].textContent).toEqual('Healthy');
|
||||
expect(hostStatuses[4].textContent).toEqual('Healthy');
|
||||
});
|
||||
|
||||
it('should display correct policy status', async () => {
|
||||
|
|
|
@ -33,21 +33,19 @@ import type {
|
|||
CreatePackagePolicyRouteState,
|
||||
} from '@kbn/fleet-plugin/public';
|
||||
import { isPolicyOutOfDate } from '../utils';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useGetAgentStatus } from '../../../hooks/agents/use_get_agent_status';
|
||||
import { TransformFailedCallout } from './components/transform_failed_callout';
|
||||
import type { EndpointIndexUIQueryParams } from '../types';
|
||||
import { EndpointListNavLink } from './components/endpoint_list_nav_link';
|
||||
import {
|
||||
AgentStatus,
|
||||
EndpointAgentStatus,
|
||||
} from '../../../../common/components/endpoint/agents/agent_status';
|
||||
import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status';
|
||||
import { EndpointDetailsFlyout } from './details';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { getEndpointPendingActionsCallback, nonExistingPolicies } from '../store/selectors';
|
||||
import { nonExistingPolicies } from '../store/selectors';
|
||||
import { useEndpointSelector } from './hooks';
|
||||
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
|
||||
import type { CreateStructuredSelector } from '../../../../common/store';
|
||||
import type {
|
||||
AgentStatusRecords,
|
||||
HostInfo,
|
||||
HostInfoInterface,
|
||||
Immutable,
|
||||
|
@ -81,13 +79,12 @@ const StyledDatePicker = styled.div`
|
|||
`;
|
||||
|
||||
interface GetEndpointListColumnsProps {
|
||||
agentStatusClientEnabled: boolean;
|
||||
missingPolicies: ReturnType<typeof nonExistingPolicies>;
|
||||
backToEndpointList: PolicyDetailsRouteState['backLink'];
|
||||
getHostPendingActions: ReturnType<typeof getEndpointPendingActionsCallback>;
|
||||
queryParams: Immutable<EndpointIndexUIQueryParams>;
|
||||
search: string;
|
||||
getAppUrl: ReturnType<typeof useAppUrl>['getAppUrl'];
|
||||
agentStatusRecords: AgentStatusRecords;
|
||||
}
|
||||
|
||||
const columnWidths: Record<
|
||||
|
@ -106,13 +103,12 @@ const columnWidths: Record<
|
|||
};
|
||||
|
||||
const getEndpointListColumns = ({
|
||||
agentStatusClientEnabled,
|
||||
missingPolicies,
|
||||
backToEndpointList,
|
||||
getHostPendingActions,
|
||||
queryParams,
|
||||
search,
|
||||
getAppUrl,
|
||||
agentStatusRecords,
|
||||
}: GetEndpointListColumnsProps): Array<EuiBasicTableColumn<Immutable<HostInfo>>> => {
|
||||
const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpoint.list.lastActive', {
|
||||
defaultMessage: 'Last active',
|
||||
|
@ -156,13 +152,10 @@ const getEndpointListColumns = ({
|
|||
}),
|
||||
sortable: true,
|
||||
render: (hostStatus: HostInfo['host_status'], endpointInfo) => {
|
||||
// TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed
|
||||
return agentStatusClientEnabled ? (
|
||||
<AgentStatus agentId={endpointInfo.metadata.agent.id} agentType="endpoint" />
|
||||
) : (
|
||||
<EndpointAgentStatus
|
||||
endpointHostInfo={endpointInfo}
|
||||
pendingActions={getHostPendingActions(endpointInfo.metadata.agent.id)}
|
||||
return (
|
||||
<AgentStatus
|
||||
statusInfo={agentStatusRecords[endpointInfo.metadata.agent.id]}
|
||||
agentType="endpoint"
|
||||
data-test-subj="rowHostStatus"
|
||||
/>
|
||||
);
|
||||
|
@ -323,8 +316,6 @@ const stateHandleDeployEndpointsClick: AgentPolicyDetailsDeployAgentAction = {
|
|||
};
|
||||
|
||||
export const EndpointList = () => {
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
|
||||
const history = useHistory();
|
||||
const {
|
||||
listData,
|
||||
|
@ -349,7 +340,6 @@ export const EndpointList = () => {
|
|||
isInitialized,
|
||||
} = useEndpointSelector(selector);
|
||||
const missingPolicies = useEndpointSelector(nonExistingPolicies);
|
||||
const getHostPendingActions = useEndpointSelector(getEndpointPendingActionsCallback);
|
||||
const {
|
||||
canReadEndpointList,
|
||||
canAccessFleet,
|
||||
|
@ -509,26 +499,23 @@ export const EndpointList = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const { data: agentStatusRecords } = useGetAgentStatus(
|
||||
listData.map((rowItem) => rowItem.metadata.agent.id),
|
||||
'endpoint',
|
||||
{ enabled: hasListData }
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getEndpointListColumns({
|
||||
agentStatusClientEnabled,
|
||||
backToEndpointList,
|
||||
getAppUrl,
|
||||
missingPolicies,
|
||||
getHostPendingActions,
|
||||
queryParams,
|
||||
search,
|
||||
agentStatusRecords: agentStatusRecords ?? {},
|
||||
}),
|
||||
[
|
||||
agentStatusClientEnabled,
|
||||
backToEndpointList,
|
||||
getAppUrl,
|
||||
getHostPendingActions,
|
||||
missingPolicies,
|
||||
queryParams,
|
||||
search,
|
||||
]
|
||||
[agentStatusRecords, backToEndpointList, getAppUrl, missingPolicies, queryParams, search]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
|
|
|
@ -14,9 +14,12 @@ import { TestProviders } from '../../../../common/mock';
|
|||
import { EndpointOverview } from '.';
|
||||
import type { EndpointFields } from '../../../../../common/search_strategy/security_solution/hosts';
|
||||
import { EndpointMetadataGenerator } from '../../../../../common/endpoint/data_generators/endpoint_metadata_generator';
|
||||
import { set } from 'lodash';
|
||||
import { useGetAgentStatus as _useGetAgentStatus } from '../../../../management/hooks/agents/use_get_agent_status';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../management/hooks/agents/use_get_agent_status');
|
||||
|
||||
const useGetAgentStatusMock = _useGetAgentStatus as jest.Mock;
|
||||
|
||||
describe('EndpointOverview Component', () => {
|
||||
let endpointData: EndpointFields;
|
||||
|
@ -59,7 +62,7 @@ describe('EndpointOverview Component', () => {
|
|||
endpointData?.hostInfo?.metadata.Endpoint.policy.applied.status
|
||||
);
|
||||
expect(findData.at(2).text()).toContain(endpointData?.hostInfo?.metadata.agent.version); // contain because drag adds a space
|
||||
expect(findData.at(3).text()).toEqual('HealthyIsolated');
|
||||
expect(findData.at(3).text()).toEqual('Healthy');
|
||||
});
|
||||
|
||||
test('it renders with null data', () => {
|
||||
|
@ -71,19 +74,10 @@ describe('EndpointOverview Component', () => {
|
|||
});
|
||||
|
||||
test('it shows isolation status', () => {
|
||||
set(endpointData.hostInfo ?? {}, 'metadata.Endpoint.state.isolation', true);
|
||||
const status = useGetAgentStatusMock(endpointData.hostInfo?.metadata.agent.id, 'endpoint');
|
||||
status.data[endpointData.hostInfo!.metadata.agent.id].isolated = true;
|
||||
useGetAgentStatusMock.mockReturnValue(status);
|
||||
render();
|
||||
expect(findData.at(3).text()).toEqual('HealthyIsolated');
|
||||
});
|
||||
|
||||
// FIXME: un-skip once pending isolation status are supported again
|
||||
test.skip.each([
|
||||
['isolate', 'Isolating'],
|
||||
['unisolate', 'Releasing'],
|
||||
])('it shows pending %s status', (action, expectedLabel) => {
|
||||
set(endpointData.hostInfo ?? {}, 'metadata.Endpoint.state.isolation', true);
|
||||
endpointData.pendingActions![action] = 1;
|
||||
render();
|
||||
expect(findData.at(3).text()).toEqual(`Healthy${expectedLabel}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,11 +9,7 @@ import { EuiHealth } from '@elastic/eui';
|
|||
import { getOr } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import {
|
||||
AgentStatus,
|
||||
EndpointAgentStatus,
|
||||
} from '../../../../common/components/endpoint/agents/agent_status';
|
||||
import { AgentStatus } from '../../../../common/components/endpoint/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';
|
||||
|
@ -29,7 +25,6 @@ interface Props {
|
|||
}
|
||||
|
||||
export const EndpointOverview = React.memo<Props>(({ contextID, data, scopeId }) => {
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
const getDefaultRenderer = useCallback(
|
||||
(fieldName: string, fieldData: EndpointFields, attrName: string) => (
|
||||
<DefaultFieldRenderer
|
||||
|
@ -82,23 +77,15 @@ export const EndpointOverview = React.memo<Props>(({ contextID, data, scopeId })
|
|||
{
|
||||
title: i18n.FLEET_AGENT_STATUS,
|
||||
description:
|
||||
// TODO: 8.15 remove `EndpointAgentStatus` when `agentStatusClientEnabled` FF is enabled and removed
|
||||
data != null && data.hostInfo ? (
|
||||
agentStatusClientEnabled ? (
|
||||
<AgentStatus agentId={data.hostInfo.metadata.agent.id} agentType="endpoint" />
|
||||
) : (
|
||||
<EndpointAgentStatus
|
||||
endpointHostInfo={data.hostInfo}
|
||||
data-test-subj="endpointHostAgentStatus"
|
||||
/>
|
||||
)
|
||||
<AgentStatus agentId={data.hostInfo.metadata.agent.id} agentType="endpoint" />
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
),
|
||||
},
|
||||
],
|
||||
];
|
||||
}, [agentStatusClientEnabled, data, getDefaultRenderer]);
|
||||
}, [data, getDefaultRenderer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -2,12 +2,7 @@
|
|||
|
||||
exports[`Netflow renders correctly against snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c12 svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.c10,
|
||||
.c10,
|
||||
.c10 * {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
|
@ -111,6 +106,11 @@ tr:hover .c2:focus::before {
|
|||
vertical-align: top;
|
||||
}
|
||||
|
||||
.c12 svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
|
@ -13,16 +13,12 @@ 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 {
|
||||
AgentStatus,
|
||||
EndpointAgentStatusById,
|
||||
} from '../../../../../common/components/endpoint/agents/agent_status';
|
||||
import { AgentStatus } from '../../../../../common/components/endpoint/agents/agent_status';
|
||||
import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants';
|
||||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
import { Bytes, BYTES_FORMAT } from './bytes';
|
||||
|
@ -107,8 +103,6 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
value,
|
||||
linkValue,
|
||||
}) => {
|
||||
const agentStatusClientEnabled = useIsExperimentalFeatureEnabled('agentStatusClientEnabled');
|
||||
|
||||
if (isObjectArray || asPlainText) {
|
||||
return <span data-test-subj={`formatted-field-${fieldName}`}>{value}</span>;
|
||||
} else if (fieldType === IP_FIELD_TYPE) {
|
||||
|
@ -292,17 +286,12 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
/>
|
||||
);
|
||||
} else if (fieldName === AGENT_STATUS_FIELD_NAME) {
|
||||
return agentStatusClientEnabled ? (
|
||||
return (
|
||||
<AgentStatus
|
||||
agentId={String(value ?? '')}
|
||||
agentType="endpoint"
|
||||
data-test-subj="endpointHostAgentStatus"
|
||||
/>
|
||||
) : (
|
||||
<EndpointAgentStatusById
|
||||
endpointAgentId={String(value ?? '')}
|
||||
data-test-subj="endpointHostAgentStatus"
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
[
|
||||
|
|
|
@ -16,11 +16,6 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = `
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.c14 svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.c12,
|
||||
.c12 * {
|
||||
display: inline-block;
|
||||
|
@ -125,6 +120,11 @@ tr:hover .c4:focus::before {
|
|||
vertical-align: top;
|
||||
}
|
||||
|
||||
.c14 svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ import { createCasesClientMock } from '@kbn/cases-plugin/server/client/mocks';
|
|||
import type { AddVersionOpts, VersionedRouteConfig } from '@kbn/core-http-server';
|
||||
import { unsecuredActionsClientMock } from '@kbn/actions-plugin/server/unsecured_actions_client/unsecured_actions_client.mock';
|
||||
import type { PluginStartContract } from '@kbn/actions-plugin/server';
|
||||
import type { Mutable } from 'utility-types';
|
||||
import { responseActionsClientMock } from '../services/actions/clients/mocks';
|
||||
import { getEndpointAuthzInitialStateMock } from '../../../common/endpoint/service/authz/mocks';
|
||||
import { createMockConfig, requestContextMock } from '../../lib/detection_engine/routes/__mocks__';
|
||||
|
@ -264,7 +265,7 @@ export interface HttpApiTestSetupMock<P = any, Q = any, B = any> {
|
|||
httpResponseMock: ReturnType<typeof httpServerMock.createResponseFactory>;
|
||||
httpHandlerContextMock: ReturnType<typeof requestContextMock.convertContext>;
|
||||
getEsClientMock: (type?: 'internalUser' | 'currentUser') => ElasticsearchClientMock;
|
||||
createRequestMock: (options?: RequestFixtureOptions<P, Q, B>) => KibanaRequest<P, Q, B>;
|
||||
createRequestMock: (options?: RequestFixtureOptions<P, Q, B>) => Mutable<KibanaRequest<P, Q, B>>;
|
||||
/** Retrieves the handler that was registered with the `router` for a given `method` and `path` */
|
||||
getRegisteredRouteHandler: (method: RouterMethod, path: string) => RequestHandler;
|
||||
/** Retrieves the route handler configuration that was registered with the router */
|
||||
|
@ -334,7 +335,9 @@ export const createHttpApiTestSetupMock = <P = any, Q = any, B = any>(): HttpApi
|
|||
httpHandlerContextMock,
|
||||
httpResponseMock,
|
||||
|
||||
createRequestMock: (options: RequestFixtureOptions<P, Q, B> = {}): KibanaRequest<P, Q, B> => {
|
||||
createRequestMock: (
|
||||
options: RequestFixtureOptions<P, Q, B> = {}
|
||||
): Mutable<KibanaRequest<P, Q, B>> => {
|
||||
return httpServerMock.createKibanaRequest<P, Q, B>(options);
|
||||
},
|
||||
|
||||
|
|
|
@ -12,12 +12,33 @@ import { registerAgentStatusRoute } from './agent_status_handler';
|
|||
import { AGENT_STATUS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import { CustomHttpRequestError } from '../../../utils/custom_http_request_error';
|
||||
import type { EndpointAgentStatusRequestQueryParams } from '../../../../common/api/endpoint/agent/get_agent_status_route';
|
||||
import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import { RESPONSE_ACTION_AGENT_TYPE } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import type { ExperimentalFeatures } from '../../../../common';
|
||||
import { agentServiceMocks as mockAgentService } from '../../services/agent/mocks';
|
||||
import { getAgentStatusClient as _getAgentStatusClient } from '../../services';
|
||||
import type { DeepMutable } from '../../../../common/endpoint/types';
|
||||
|
||||
jest.mock('../../services', () => {
|
||||
const realModule = jest.requireActual('../../services');
|
||||
|
||||
return {
|
||||
...realModule,
|
||||
getAgentStatusClient: jest.fn((agentType: ResponseActionAgentType) => {
|
||||
return mockAgentService.createClient(agentType);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const getAgentStatusClientMock = _getAgentStatusClient as jest.Mock;
|
||||
|
||||
describe('Agent Status API route handler', () => {
|
||||
let apiTestSetup: HttpApiTestSetupMock<never, EndpointAgentStatusRequestQueryParams>;
|
||||
let apiTestSetup: HttpApiTestSetupMock<never, DeepMutable<EndpointAgentStatusRequestQueryParams>>;
|
||||
let httpRequestMock: ReturnType<
|
||||
HttpApiTestSetupMock<never, EndpointAgentStatusRequestQueryParams>['createRequestMock']
|
||||
HttpApiTestSetupMock<
|
||||
never,
|
||||
DeepMutable<EndpointAgentStatusRequestQueryParams>
|
||||
>['createRequestMock']
|
||||
>;
|
||||
let httpHandlerContextMock: HttpApiTestSetupMock<
|
||||
never,
|
||||
|
@ -43,60 +64,55 @@ describe('Agent Status API route handler', () => {
|
|||
apiTestSetup.endpointAppContextMock.experimentalFeatures = {
|
||||
...apiTestSetup.endpointAppContextMock.experimentalFeatures,
|
||||
responseActionsSentinelOneV1Enabled: true,
|
||||
responseActionsCrowdstrikeManualHostIsolationEnabled: false,
|
||||
agentStatusClientEnabled: false,
|
||||
responseActionsCrowdstrikeManualHostIsolationEnabled: true,
|
||||
};
|
||||
|
||||
registerAgentStatusRoute(apiTestSetup.routerMock, apiTestSetup.endpointAppContextMock);
|
||||
});
|
||||
|
||||
it('should error if the sentinel_one feature flag is turned off', async () => {
|
||||
apiTestSetup.endpointAppContextMock.experimentalFeatures = {
|
||||
...apiTestSetup.endpointAppContextMock.experimentalFeatures,
|
||||
responseActionsSentinelOneV1Enabled: false,
|
||||
responseActionsCrowdstrikeManualHostIsolationEnabled: false,
|
||||
};
|
||||
it.each`
|
||||
agentType | featureFlag
|
||||
${'sentinel_one'} | ${'responseActionsSentinelOneV1Enabled'}
|
||||
${'crowdstrike'} | ${'responseActionsCrowdstrikeManualHostIsolationEnabled'}
|
||||
`(
|
||||
'should error if the $agentType feature flag ($featureFlag) is turned off',
|
||||
async ({
|
||||
agentType,
|
||||
featureFlag,
|
||||
}: {
|
||||
agentType: ResponseActionAgentType;
|
||||
featureFlag: keyof ExperimentalFeatures;
|
||||
}) => {
|
||||
apiTestSetup.endpointAppContextMock.experimentalFeatures = {
|
||||
...apiTestSetup.endpointAppContextMock.experimentalFeatures,
|
||||
[featureFlag]: false,
|
||||
};
|
||||
httpRequestMock.query.agentType = agentType;
|
||||
|
||||
await apiTestSetup
|
||||
.getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1')
|
||||
.routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock);
|
||||
|
||||
expect(httpResponseMock.customError).toHaveBeenCalledWith({
|
||||
statusCode: 400,
|
||||
body: expect.any(CustomHttpRequestError),
|
||||
});
|
||||
});
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)('should accept agent type of %s', async (agentType) => {
|
||||
// @ts-expect-error `query.*` is not mutable
|
||||
httpRequestMock.query.agentType = agentType;
|
||||
// TODO TC: Temporary workaround to catch thrown error while Crowdstrike status is not yet supported
|
||||
try {
|
||||
await apiTestSetup
|
||||
.getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1')
|
||||
.routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock);
|
||||
} catch (error) {
|
||||
if (agentType === 'crowdstrike') {
|
||||
expect(error.message).toBe('Agent type [crowdstrike] does not support agent status');
|
||||
}
|
||||
}
|
||||
if (agentType !== 'crowdstrike') {
|
||||
expect(httpResponseMock.ok).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept agent type of `endpoint` when FF is disabled', async () => {
|
||||
apiTestSetup.endpointAppContextMock.experimentalFeatures = {
|
||||
...apiTestSetup.endpointAppContextMock.experimentalFeatures,
|
||||
responseActionsSentinelOneV1Enabled: false,
|
||||
};
|
||||
// @ts-expect-error `query.*` is not mutable
|
||||
httpRequestMock.query.agentType = 'endpoint';
|
||||
expect(httpResponseMock.customError).toHaveBeenCalledWith({
|
||||
statusCode: 400,
|
||||
body: expect.any(CustomHttpRequestError),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)('should accept agent type of %s', async (agentType) => {
|
||||
httpRequestMock.query.agentType = agentType;
|
||||
await apiTestSetup
|
||||
.getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1')
|
||||
.routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock);
|
||||
|
||||
expect(httpResponseMock.ok).toHaveBeenCalled();
|
||||
expect(getAgentStatusClientMock).toHaveBeenCalledWith(agentType, {
|
||||
esClient: (await httpHandlerContextMock.core).elasticsearch.client.asInternalUser,
|
||||
soClient: (await httpHandlerContextMock.core).savedObjects.client,
|
||||
connectorActionsClient: (await httpHandlerContextMock.actions).getActionsClient(),
|
||||
endpointService: apiTestSetup.endpointAppContextMock.service,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return status code 200 with expected payload', async () => {
|
||||
|
@ -109,43 +125,21 @@ describe('Agent Status API route handler', () => {
|
|||
data: {
|
||||
one: {
|
||||
agentType: 'sentinel_one',
|
||||
found: false,
|
||||
found: true,
|
||||
agentId: 'one',
|
||||
isUninstalled: false,
|
||||
isPendingUninstall: false,
|
||||
isolated: false,
|
||||
lastSeen: '',
|
||||
pendingActions: {
|
||||
execute: 0,
|
||||
'get-file': 0,
|
||||
isolate: 0,
|
||||
'kill-process': 0,
|
||||
'running-processes': 0,
|
||||
'suspend-process': 0,
|
||||
unisolate: 0,
|
||||
upload: 0,
|
||||
},
|
||||
status: 'unenrolled',
|
||||
lastSeen: expect.any(String),
|
||||
pendingActions: {},
|
||||
status: 'healthy',
|
||||
},
|
||||
two: {
|
||||
agentType: 'sentinel_one',
|
||||
found: false,
|
||||
found: true,
|
||||
agentId: 'two',
|
||||
isUninstalled: false,
|
||||
isPendingUninstall: false,
|
||||
isolated: false,
|
||||
lastSeen: '',
|
||||
pendingActions: {
|
||||
execute: 0,
|
||||
'get-file': 0,
|
||||
isolate: 0,
|
||||
'kill-process': 0,
|
||||
'running-processes': 0,
|
||||
'suspend-process': 0,
|
||||
unisolate: 0,
|
||||
upload: 0,
|
||||
},
|
||||
status: 'unenrolled',
|
||||
lastSeen: expect.any(String),
|
||||
pendingActions: {},
|
||||
status: 'healthy',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import type { RequestHandler } from '@kbn/core/server';
|
||||
import { getSentinelOneAgentStatus } from '../../services/agent/agent_status';
|
||||
import { errorHandler } from '../error_handler';
|
||||
import type { EndpointAgentStatusRequestQueryParams } from '../../../../common/api/endpoint/agent/get_agent_status_route';
|
||||
import { EndpointAgentStatusRequestSchema } from '../../../../common/api/endpoint/agent/get_agent_status_route';
|
||||
|
@ -59,6 +58,10 @@ export const getAgentStatusRouteHandler = (
|
|||
const { agentType = 'endpoint', agentIds: _agentIds } = request.query;
|
||||
const agentIds = Array.isArray(_agentIds) ? _agentIds : [_agentIds];
|
||||
|
||||
logger.debug(
|
||||
`Retrieving status for: agentType [${agentType}], agentIds: [${agentIds.join(', ')}]`
|
||||
);
|
||||
|
||||
// Note: because our API schemas are defined as module static variables (as opposed to a
|
||||
// `getter` function), we need to include this additional validation here, since
|
||||
// `agent_type` is included in the schema independent of the feature flag
|
||||
|
@ -77,36 +80,17 @@ export const getAgentStatusRouteHandler = (
|
|||
|
||||
const esClient = (await context.core).elasticsearch.client.asInternalUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const connectorActionsClient = (await context.actions).getActionsClient();
|
||||
const agentStatusClient = getAgentStatusClient(agentType, {
|
||||
esClient,
|
||||
soClient,
|
||||
connectorActionsClient,
|
||||
endpointService: endpointContext.service,
|
||||
connectorActionsClient:
|
||||
agentType === 'crowdstrike' ? (await context.actions).getActionsClient() : undefined,
|
||||
});
|
||||
|
||||
// 8.15: use the new `agentStatusClientEnabled` FF enabled
|
||||
const data = endpointContext.experimentalFeatures.agentStatusClientEnabled
|
||||
? await agentStatusClient.getAgentStatuses(agentIds)
|
||||
: agentType === 'sentinel_one'
|
||||
? await getSentinelOneAgentStatus({
|
||||
agentType,
|
||||
agentIds,
|
||||
logger,
|
||||
connectorActionsClient: (await context.actions).getActionsClient(),
|
||||
})
|
||||
: [];
|
||||
|
||||
logger.debug(
|
||||
`Retrieving status for: agentType [${agentType}], agentIds: [${agentIds.join(', ')}]`
|
||||
);
|
||||
const data = await agentStatusClient.getAgentStatuses(agentIds);
|
||||
|
||||
try {
|
||||
return response.ok({
|
||||
body: {
|
||||
data,
|
||||
},
|
||||
});
|
||||
return response.ok({ body: { data } });
|
||||
} catch (e) {
|
||||
return errorHandler(logger, response, e);
|
||||
}
|
||||
|
|
|
@ -1,184 +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 { GetAgentStatusOptions } from './agent_status';
|
||||
import { getSentinelOneAgentStatus, SENTINEL_ONE_NETWORK_STATUS } from './agent_status';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { sentinelOneMock } from '../actions/clients/sentinelone/mocks';
|
||||
import { responseActionsClientMock } from '../actions/clients/mocks';
|
||||
|
||||
describe('Endpoint Get Agent Status service', () => {
|
||||
let agentStatusOptions: GetAgentStatusOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
agentStatusOptions = {
|
||||
agentType: 'sentinel_one',
|
||||
agentIds: ['1', '2'],
|
||||
logger: loggingSystemMock.create().get('getSentinelOneAgentStatus'),
|
||||
connectorActionsClient: sentinelOneMock.createConnectorActionsClient(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should throw error if unable to access stack connectors', async () => {
|
||||
(agentStatusOptions.connectorActionsClient.getAll as jest.Mock).mockImplementation(async () => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
const getStatusResponsePromise = getSentinelOneAgentStatus(agentStatusOptions);
|
||||
|
||||
await expect(getStatusResponsePromise).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unable to retrieve list of stack connectors: boom'
|
||||
);
|
||||
await expect(getStatusResponsePromise).rejects.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
|
||||
it('should throw error if no SentinelOne connector is registered', async () => {
|
||||
(agentStatusOptions.connectorActionsClient.getAll as jest.Mock).mockResolvedValue([]);
|
||||
const getStatusResponsePromise = getSentinelOneAgentStatus(agentStatusOptions);
|
||||
|
||||
await expect(getStatusResponsePromise).rejects.toHaveProperty(
|
||||
'message',
|
||||
'No SentinelOne stack connector found'
|
||||
);
|
||||
await expect(getStatusResponsePromise).rejects.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
|
||||
it('should send api request to SentinelOne', async () => {
|
||||
await getSentinelOneAgentStatus(agentStatusOptions);
|
||||
|
||||
expect(agentStatusOptions.connectorActionsClient.execute).toHaveBeenCalledWith({
|
||||
actionId: 's1-connector-instance-id',
|
||||
params: {
|
||||
subAction: 'getAgents',
|
||||
subActionParams: {
|
||||
uuids: '1,2',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if api call to SentinelOne failed', async () => {
|
||||
(agentStatusOptions.connectorActionsClient.execute as jest.Mock).mockResolvedValue(
|
||||
responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
status: 'error',
|
||||
serviceMessage: 'boom',
|
||||
})
|
||||
);
|
||||
const getStatusResponsePromise = getSentinelOneAgentStatus(agentStatusOptions);
|
||||
|
||||
await expect(getStatusResponsePromise).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Attempt retrieve agent information from to SentinelOne failed: boom'
|
||||
);
|
||||
await expect(getStatusResponsePromise).rejects.toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
||||
it('should return expected output', async () => {
|
||||
agentStatusOptions.agentIds = ['aaa', 'bbb', 'ccc', 'invalid'];
|
||||
(agentStatusOptions.connectorActionsClient.execute as jest.Mock).mockResolvedValue(
|
||||
responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: sentinelOneMock.createGetAgentsResponse([
|
||||
sentinelOneMock.createSentinelOneAgentDetails({
|
||||
networkStatus: SENTINEL_ONE_NETWORK_STATUS.DISCONNECTED, // Isolated
|
||||
uuid: 'aaa',
|
||||
}),
|
||||
sentinelOneMock.createSentinelOneAgentDetails({
|
||||
networkStatus: SENTINEL_ONE_NETWORK_STATUS.DISCONNECTING, // Releasing
|
||||
uuid: 'bbb',
|
||||
}),
|
||||
sentinelOneMock.createSentinelOneAgentDetails({
|
||||
networkStatus: SENTINEL_ONE_NETWORK_STATUS.CONNECTING, // isolating
|
||||
uuid: 'ccc',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
|
||||
await expect(getSentinelOneAgentStatus(agentStatusOptions)).resolves.toEqual({
|
||||
aaa: {
|
||||
agentType: 'sentinel_one',
|
||||
found: true,
|
||||
agentId: 'aaa',
|
||||
isUninstalled: false,
|
||||
isPendingUninstall: false,
|
||||
isolated: true,
|
||||
lastSeen: '2023-12-26T21:35:35.986596Z',
|
||||
pendingActions: {
|
||||
execute: 0,
|
||||
'get-file': 0,
|
||||
isolate: 0,
|
||||
'kill-process': 0,
|
||||
'running-processes': 0,
|
||||
'suspend-process': 0,
|
||||
unisolate: 0,
|
||||
upload: 0,
|
||||
},
|
||||
status: 'healthy',
|
||||
},
|
||||
bbb: {
|
||||
agentType: 'sentinel_one',
|
||||
found: true,
|
||||
agentId: 'bbb',
|
||||
isUninstalled: false,
|
||||
isPendingUninstall: false,
|
||||
isolated: false,
|
||||
lastSeen: '2023-12-26T21:35:35.986596Z',
|
||||
pendingActions: {
|
||||
execute: 0,
|
||||
'get-file': 0,
|
||||
isolate: 1,
|
||||
'kill-process': 0,
|
||||
'running-processes': 0,
|
||||
'suspend-process': 0,
|
||||
unisolate: 0,
|
||||
upload: 0,
|
||||
},
|
||||
status: 'healthy',
|
||||
},
|
||||
ccc: {
|
||||
agentType: 'sentinel_one',
|
||||
found: true,
|
||||
agentId: 'ccc',
|
||||
isUninstalled: false,
|
||||
isPendingUninstall: false,
|
||||
isolated: false,
|
||||
lastSeen: '2023-12-26T21:35:35.986596Z',
|
||||
pendingActions: {
|
||||
execute: 0,
|
||||
'get-file': 0,
|
||||
isolate: 0,
|
||||
'kill-process': 0,
|
||||
'running-processes': 0,
|
||||
'suspend-process': 0,
|
||||
unisolate: 1,
|
||||
upload: 0,
|
||||
},
|
||||
status: 'healthy',
|
||||
},
|
||||
invalid: {
|
||||
agentType: 'sentinel_one',
|
||||
found: false,
|
||||
agentId: 'invalid',
|
||||
isUninstalled: false,
|
||||
isPendingUninstall: false,
|
||||
isolated: false,
|
||||
lastSeen: '',
|
||||
pendingActions: {
|
||||
execute: 0,
|
||||
'get-file': 0,
|
||||
isolate: 0,
|
||||
'kill-process': 0,
|
||||
'running-processes': 0,
|
||||
'suspend-process': 0,
|
||||
unisolate: 0,
|
||||
upload: 0,
|
||||
},
|
||||
status: 'unenrolled',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,137 +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 { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
|
||||
import {
|
||||
SENTINELONE_CONNECTOR_ID,
|
||||
SUB_ACTION,
|
||||
} from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { keyBy, merge } from 'lodash';
|
||||
import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common';
|
||||
import type { SentinelOneGetAgentsResponse } from '@kbn/stack-connectors-plugin/common/sentinelone/types';
|
||||
import { stringify } from '../../utils/stringify';
|
||||
import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import type { AgentStatusInfo } from '../../../../common/endpoint/types';
|
||||
import { HostStatus } from '../../../../common/endpoint/types';
|
||||
import { CustomHttpRequestError } from '../../../utils/custom_http_request_error';
|
||||
|
||||
export interface GetAgentStatusOptions {
|
||||
// NOTE: only sentinel_one currently supported
|
||||
agentType: ResponseActionAgentType;
|
||||
agentIds: string[];
|
||||
connectorActionsClient: ActionsClient;
|
||||
logger: Logger;
|
||||
}
|
||||
export const getSentinelOneAgentStatus = async ({
|
||||
agentType,
|
||||
agentIds,
|
||||
connectorActionsClient,
|
||||
logger,
|
||||
}: GetAgentStatusOptions): Promise<AgentStatusInfo> => {
|
||||
let connectorList: ConnectorWithExtraFindData[] = [];
|
||||
|
||||
try {
|
||||
connectorList = await connectorActionsClient.getAll();
|
||||
} catch (err) {
|
||||
throw new CustomHttpRequestError(
|
||||
`Unable to retrieve list of stack connectors: ${err.message}`,
|
||||
// failure here is likely due to Authz, but because we don't have a good way to determine that,
|
||||
// the `statusCode` below is set to `400` instead of `401`.
|
||||
400,
|
||||
err
|
||||
);
|
||||
}
|
||||
const connector = connectorList.find(({ actionTypeId, isDeprecated, isMissingSecrets }) => {
|
||||
return actionTypeId === SENTINELONE_CONNECTOR_ID && !isDeprecated && !isMissingSecrets;
|
||||
});
|
||||
|
||||
if (!connector) {
|
||||
throw new CustomHttpRequestError(`No SentinelOne stack connector found`, 400, connectorList);
|
||||
}
|
||||
|
||||
logger.debug(`Using SentinelOne stack connector: ${connector.name} (${connector.id})`);
|
||||
|
||||
const agentDetailsResponse = (await connectorActionsClient.execute({
|
||||
actionId: connector.id,
|
||||
params: {
|
||||
subAction: SUB_ACTION.GET_AGENTS,
|
||||
subActionParams: {
|
||||
uuids: agentIds.filter((agentId) => agentId.trim().length).join(','),
|
||||
},
|
||||
},
|
||||
})) as ActionTypeExecutorResult<SentinelOneGetAgentsResponse>;
|
||||
|
||||
if (agentDetailsResponse.status === 'error') {
|
||||
logger.error(stringify(agentDetailsResponse));
|
||||
|
||||
throw new CustomHttpRequestError(
|
||||
`Attempt retrieve agent information from to SentinelOne failed: ${
|
||||
agentDetailsResponse.serviceMessage || agentDetailsResponse.message
|
||||
}`,
|
||||
500,
|
||||
agentDetailsResponse
|
||||
);
|
||||
}
|
||||
|
||||
const agentDetailsById = keyBy(agentDetailsResponse.data?.data, 'uuid');
|
||||
|
||||
logger.debug(`Response from SentinelOne API:\n${stringify(agentDetailsById)}`);
|
||||
|
||||
return agentIds.reduce<AgentStatusInfo>((acc, agentId) => {
|
||||
const thisAgentDetails = agentDetailsById[agentId];
|
||||
const thisAgentStatus = {
|
||||
agentType,
|
||||
agentId,
|
||||
found: false,
|
||||
isolated: false,
|
||||
isPendingUninstall: false,
|
||||
isUninstalled: false,
|
||||
lastSeen: '',
|
||||
pendingActions: {
|
||||
execute: 0,
|
||||
upload: 0,
|
||||
unisolate: 0,
|
||||
isolate: 0,
|
||||
'get-file': 0,
|
||||
'kill-process': 0,
|
||||
'suspend-process': 0,
|
||||
'running-processes': 0,
|
||||
},
|
||||
status: HostStatus.UNENROLLED,
|
||||
};
|
||||
|
||||
if (thisAgentDetails) {
|
||||
merge(thisAgentStatus, {
|
||||
found: true,
|
||||
lastSeen: thisAgentDetails.updatedAt,
|
||||
isPendingUninstall: thisAgentDetails.isPendingUninstall,
|
||||
isUninstalled: thisAgentDetails.isUninstalled,
|
||||
isolated: thisAgentDetails.networkStatus === SENTINEL_ONE_NETWORK_STATUS.DISCONNECTED,
|
||||
status: !thisAgentDetails.isActive ? HostStatus.OFFLINE : HostStatus.HEALTHY,
|
||||
pendingActions: {
|
||||
isolate:
|
||||
thisAgentDetails.networkStatus === SENTINEL_ONE_NETWORK_STATUS.DISCONNECTING ? 1 : 0,
|
||||
unisolate:
|
||||
thisAgentDetails.networkStatus === SENTINEL_ONE_NETWORK_STATUS.CONNECTING ? 1 : 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
acc[agentId] = thisAgentStatus;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export enum SENTINEL_ONE_NETWORK_STATUS {
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
DISCONNECTING = 'disconnecting',
|
||||
DISCONNECTED = 'disconnected',
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import type { AgentStatusRecords } from '../../../../common/endpoint/types';
|
||||
import { agentStatusMocks } from '../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
|
||||
import type { AgentStatusClientInterface } from '..';
|
||||
|
||||
const createClientMock = (
|
||||
agentType: ResponseActionAgentType = 'endpoint'
|
||||
): jest.Mocked<AgentStatusClientInterface> => {
|
||||
return {
|
||||
getAgentStatuses: jest.fn(async (agentIds) => {
|
||||
return agentIds.reduce<AgentStatusRecords>((acc, agentId) => {
|
||||
acc[agentId] = agentStatusMocks.generateAgentStatus({ agentId, agentType });
|
||||
return acc;
|
||||
}, {});
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const agentServiceMocks = Object.freeze({
|
||||
...agentStatusMocks,
|
||||
createClient: createClientMock,
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue