[Security Solution][Responder] Improved UI for responder capabilities check (#138662)

This commit is contained in:
Candace Park 2022-08-26 11:55:13 -04:00 committed by GitHub
parent ea929a0eb8
commit 0b8e2700fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 353 additions and 115 deletions

View file

@ -75,11 +75,24 @@ export const ENDPOINT_DEFAULT_PAGE = 0;
export const ENDPOINT_DEFAULT_PAGE_SIZE = 10;
/**
* The list of capabilities, reported by the endpoint in the metadata document, that are
* needed in order for the Responder UI to be accessible
* The list of possible capabilities, reported by the endpoint in the metadata document
*/
export const RESPONDER_CAPABILITIES = [
'isolation',
'kill_process',
'suspend_process',
'running_processes',
] as const;
export type ResponderCapabilities = typeof RESPONDER_CAPABILITIES[number];
/** The list of possible responder command names **/
export const RESPONDER_COMMANDS = [
'isolate',
'release',
'kill-process',
'suspend-process',
'processes',
] as const;
export type ResponderCommands = typeof RESPONDER_COMMANDS[number];

View file

@ -1,17 +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 { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
import type { HostMetadata, MaybeImmutable } from '../../../../common/endpoint/types';
export const useDoesEndpointSupportResponder = (
endpointMetadata: MaybeImmutable<HostMetadata> | undefined
): boolean => {
return RESPONDER_CAPABILITIES.every((capability) =>
endpointMetadata?.Endpoint.capabilities?.includes(capability)
);
};

View file

@ -12,8 +12,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useGetEndpointDetails, useWithShowEndpointResponder } from '../../../management/hooks';
import { HostStatus } from '../../../../common/endpoint/types';
import { useDoesEndpointSupportResponder } from '../../../common/hooks/endpoint/use_does_endpoint_support_responder';
import { UPGRADE_ENDPOINT_FOR_RESPONDER } from '../../../common/translations';
export const NOT_FROM_ENDPOINT_HOST_TOOLTIP = i18n.translate(
'xpack.securitySolution.endpoint.detections.takeAction.responseActionConsole.notSupportedTooltip',
@ -49,18 +47,11 @@ export const ResponderContextMenuItem = memo<ResponderContextMenuItemProps>(
error,
} = useGetEndpointDetails(endpointId, { enabled: Boolean(endpointId) });
const isResponderCapabilitiesEnabled = useDoesEndpointSupportResponder(
endpointHostInfo?.metadata
);
const [isDisabled, tooltip]: [disabled: boolean, tooltip: ReactNode] = useMemo(() => {
if (!endpointId) {
return [true, NOT_FROM_ENDPOINT_HOST_TOOLTIP];
}
if (endpointHostInfo && !isResponderCapabilitiesEnabled) {
return [true, UPGRADE_ENDPOINT_FOR_RESPONDER];
}
// Still loading Endpoint host info
if (isFetching) {
return [true, LOADING_ENDPOINT_DATA_TOOLTIP];
@ -82,7 +73,7 @@ export const ResponderContextMenuItem = memo<ResponderContextMenuItemProps>(
}
return [false, undefined];
}, [endpointHostInfo, endpointId, error, isFetching, isResponderCapabilitiesEnabled]);
}, [endpointHostInfo, endpointId, error, isFetching]);
const handleResponseActionsClick = useCallback(() => {
if (endpointHostInfo) showEndpointResponseActionsConsole(endpointHostInfo.metadata);

View file

@ -40,30 +40,24 @@ export const BadArgument = memo<CommandExecutionComponentProps<{}, { errorMessag
}
data-test-subj={getTestId('badArgument')}
>
<>
<div data-test-subj={getTestId('badArgument-message')}>{store.errorMessage}</div>
<EuiSpacer size="s" />
<div>
<CommandInputUsage commandDef={command.commandDefinition} />
</div>
<div>
<ConsoleCodeBlock>
<EuiSpacer size="m" />
<FormattedMessage
id="xpack.securitySolution.console.badArgument.helpMessage"
defaultMessage="Enter {helpCmd} for further assistance."
values={{
helpCmd: (
<ConsoleCodeBlock
bold
inline
>{`${command.commandDefinition.name} --help`}</ConsoleCodeBlock>
),
}}
/>
</ConsoleCodeBlock>
</div>
</>
<div data-test-subj={getTestId('badArgument-message')}>{store.errorMessage}</div>
<EuiSpacer size="s" />
<CommandInputUsage commandDef={command.commandDefinition} />
<ConsoleCodeBlock>
<EuiSpacer size="m" />
<FormattedMessage
id="xpack.securitySolution.console.badArgument.helpMessage"
defaultMessage="Enter {helpCmd} for further assistance."
values={{
helpCmd: (
<ConsoleCodeBlock
bold
inline
>{`${command.commandDefinition.name} --help`}</ConsoleCodeBlock>
),
}}
/>
</ConsoleCodeBlock>
</UnsupportedMessageCallout>
);
}

View file

@ -155,7 +155,7 @@ export const CommandList = memo<CommandListProps>(({ commands, display = 'defaul
(
commandsByGroup: CommandDefinition[]
): Array<{
[key: string]: { name: string; about: React.ElementType | string };
[key: string]: { name: string; about: React.ReactNode | string };
}> => {
if (commandsByGroup[0].helpGroupLabel === HELP_GROUPS.supporting.label) {
return [...COMMON_ARGS, ...commandsByGroup].map((command) => ({
@ -201,15 +201,23 @@ export const CommandList = memo<CommandListProps>(({ commands, display = 'defaul
command.RenderComponent && (
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.translate(
'xpack.securitySolution.console.commandList.addButtonTooltip',
{ defaultMessage: 'Add to text bar' }
)}
content={
command.helpDisabled === true
? i18n.translate(
'xpack.securitySolution.console.commandList.disabledButtonTooltip',
{ defaultMessage: 'Unsupported command' }
)
: i18n.translate(
'xpack.securitySolution.console.commandList.addButtonTooltip',
{ defaultMessage: 'Add to text bar' }
)
}
>
<EuiButtonIcon
iconType="plusInCircle"
aria-label={`updateTextInputCommand-${command.name}`}
onClick={updateInputText(`${commandNameWithArgs} `)}
isDisabled={command.helpDisabled === true}
/>
</EuiToolTip>
</EuiFlexItem>

View file

@ -226,7 +226,7 @@ describe('When a Console command is entered by the user', () => {
const cmd1Definition = commands.find((command) => command.name === 'cmd1');
if (!cmd1Definition) {
throw new Error('cmd1 defintion not fount');
throw new Error('cmd1 defintion not found');
}
cmd1Definition.validate = () => 'command is invalid';
@ -235,7 +235,7 @@ describe('When a Console command is entered by the user', () => {
enterCommand('cmd1');
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual(
expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual(
'command is invalid'
);
});

View file

@ -25,6 +25,7 @@ import type { ParsedCommandInterface } from '../../../service/parsed_command_inp
import { parseCommandInput } from '../../../service/parsed_command_input';
import { UnknownCommand } from '../../unknown_comand';
import { BadArgument } from '../../bad_argument';
import { ValidationError } from '../../validation_error';
import type { Command, CommandDefinition, CommandExecutionComponentProps } from '../../../types';
const toCliArgumentOption = (argName: string) => `--${argName}`;
@ -449,7 +450,7 @@ export const handleExecuteCommand: ConsoleStoreReducer<
return updateStateWithNewCommandHistoryItem(
state,
createCommandHistoryEntry(
cloneCommandDefinitionWithNewRenderComponent(command, BadArgument),
cloneCommandDefinitionWithNewRenderComponent(command, ValidationError),
createCommandExecutionState({
errorMessage: validationResult,
}),

View file

@ -0,0 +1,65 @@
/*
* 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 { ReactNode } from 'react';
import React, { memo, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer } from '@elastic/eui';
import { UnsupportedMessageCallout } from './unsupported_message_callout';
import type { CommandExecutionComponentProps } from '../types';
import { CommandInputUsage } from './command_usage';
import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
import { ConsoleCodeBlock } from './console_code_block';
/**
* Shows a validation error. The error message needs to be defined via the Command defintion's
* `validate`
*/
export const ValidationError = memo<
CommandExecutionComponentProps<{}, { errorMessage: ReactNode }>
>(({ command, setStatus, store }) => {
const getTestId = useTestIdGenerator(useDataTestSubj());
useEffect(() => {
setStatus('success');
}, [setStatus]);
return (
<UnsupportedMessageCallout
header={
<ConsoleCodeBlock textColor="error">
<FormattedMessage
id="xpack.securitySolution.console.validationError.title"
defaultMessage="Unsupported action"
/>
</ConsoleCodeBlock>
}
data-test-subj={getTestId('validationError')}
>
<div data-test-subj={getTestId('validationError-message')}>{store.errorMessage}</div>
<EuiSpacer size="s" />
<CommandInputUsage commandDef={command.commandDefinition} />
<ConsoleCodeBlock>
<EuiSpacer size="m" />
<FormattedMessage
id="xpack.securitySolution.console.validationError.helpMessage"
defaultMessage="Enter {helpCmd} for further assistance."
values={{
helpCmd: (
<ConsoleCodeBlock
bold
inline
>{`${command.commandDefinition.name} --help`}</ConsoleCodeBlock>
),
}}
/>
</ConsoleCodeBlock>
</UnsupportedMessageCallout>
);
});
ValidationError.displayName = 'ValidationError';

View file

@ -7,7 +7,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ComponentType } from 'react';
import type { ComponentType, ReactNode } from 'react';
import type { CommonProps } from '@elastic/eui';
import type { CommandExecutionResultComponent } from './components/command_execution_result';
import type { CommandExecutionState } from './components/console_state/types';
@ -35,7 +35,7 @@ export interface CommandArgs {
export interface CommandDefinition<TMeta = any> {
name: string;
about: ComponentType | string;
about: ReactNode;
/**
* The Component that will be used to render the Command
*/
@ -53,6 +53,10 @@ export interface CommandDefinition<TMeta = any> {
* the console's built in output.
*/
HelpComponent?: CommandExecutionComponent;
/**
* If defined, the button to add to the text bar will be disabled.
*/
helpDisabled?: boolean;
/**
* A store for any data needed when the command is executed.
* The entire `CommandDefinition` is passed along to the component

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import type { CommandDefinition } from '../console';
import type { Command, CommandDefinition } from '../console';
import { IsolateActionResult } from './isolate_action';
import { ReleaseActionResult } from './release_action';
import { KillProcessActionResult } from './kill_process_action';
@ -14,6 +14,13 @@ import { SuspendProcessActionResult } from './suspend_process_action';
import { EndpointStatusActionResult } from './status_action';
import { GetProcessesActionResult } from './get_processes_action';
import type { ParsedArgData } from '../console/service/parsed_command_input';
import type { ImmutableArray } from '../../../../common/endpoint/types';
import { UPGRADE_ENDPOINT_FOR_RESPONDER } from '../../../common/translations';
import type {
ResponderCapabilities,
ResponderCommands,
} from '../../../../common/endpoint/constants';
import { getCommandAboutInfo } from './get_command_about_info';
const emptyArgumentValidator = (argData: ParsedArgData): true | string => {
if (argData?.length > 0 && argData[0]?.trim().length > 0) {
@ -38,6 +45,27 @@ const pidValidator = (argData: ParsedArgData): true | string => {
}
};
const commandToCapabilitiesMap = new Map<ResponderCommands, ResponderCapabilities>([
['isolate', 'isolation'],
['release', 'isolation'],
['kill-process', 'kill_process'],
['suspend-process', 'suspend_process'],
['processes', 'running_processes'],
]);
const capabilitiesValidator = (command: Command): true | string => {
const endpointCapabilities: ResponderCapabilities[] = command.commandDefinition.meta.capabilities;
const responderCapability = commandToCapabilitiesMap.get(
command.commandDefinition.name as ResponderCommands
);
if (responderCapability) {
if (endpointCapabilities.includes(responderCapability)) {
return true;
}
}
return UPGRADE_ENDPOINT_FOR_RESPONDER;
};
const HELP_GROUPS = Object.freeze({
responseActions: {
position: 0,
@ -62,21 +90,37 @@ const COMMENT_ARG_ABOUT = i18n.translate(
{ defaultMessage: 'A comment to go along with the action' }
);
export const getEndpointResponseActionsConsoleCommands = (
endpointAgentId: string
): CommandDefinition[] => {
export const getEndpointResponseActionsConsoleCommands = ({
endpointAgentId,
endpointCapabilities,
}: {
endpointAgentId: string;
endpointCapabilities: ImmutableArray<string>;
}): CommandDefinition[] => {
const doesEndpointSupportCommand = (commandName: ResponderCommands) => {
const responderCapability = commandToCapabilitiesMap.get(commandName);
if (responderCapability) {
return endpointCapabilities.includes(responderCapability);
}
return false;
};
return [
{
name: 'isolate',
about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.isolate.about', {
defaultMessage: 'Isolate the host',
about: getCommandAboutInfo({
aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.isolate.about', {
defaultMessage: 'Isolate the host',
}),
isSupported: doesEndpointSupportCommand('isolate'),
}),
RenderComponent: IsolateActionResult,
meta: {
endpointId: endpointAgentId,
capabilities: endpointCapabilities,
},
exampleUsage: 'isolate --comment "isolate this host"',
exampleInstruction: ENTER_OR_ADD_COMMENT_ARG_INSTRUCTION,
validate: capabilitiesValidator,
args: {
comment: {
required: false,
@ -87,18 +131,24 @@ export const getEndpointResponseActionsConsoleCommands = (
helpGroupLabel: HELP_GROUPS.responseActions.label,
helpGroupPosition: HELP_GROUPS.responseActions.position,
helpCommandPosition: 0,
helpDisabled: doesEndpointSupportCommand('isolate') === false,
},
{
name: 'release',
about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.release.about', {
defaultMessage: 'Release the host',
about: getCommandAboutInfo({
aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.release.about', {
defaultMessage: 'Release the host',
}),
isSupported: doesEndpointSupportCommand('release'),
}),
RenderComponent: ReleaseActionResult,
meta: {
endpointId: endpointAgentId,
capabilities: endpointCapabilities,
},
exampleUsage: 'release --comment "release this host"',
exampleInstruction: ENTER_OR_ADD_COMMENT_ARG_INSTRUCTION,
validate: capabilitiesValidator,
args: {
comment: {
required: false,
@ -109,18 +159,27 @@ export const getEndpointResponseActionsConsoleCommands = (
helpGroupLabel: HELP_GROUPS.responseActions.label,
helpGroupPosition: HELP_GROUPS.responseActions.position,
helpCommandPosition: 1,
helpDisabled: doesEndpointSupportCommand('release') === false,
},
{
name: 'kill-process',
about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.killProcess.about', {
defaultMessage: 'Kill/terminate a process',
about: getCommandAboutInfo({
aboutInfo: i18n.translate(
'xpack.securitySolution.endpointConsoleCommands.killProcess.about',
{
defaultMessage: 'Kill/terminate a process',
}
),
isSupported: doesEndpointSupportCommand('kill-process'),
}),
RenderComponent: KillProcessActionResult,
meta: {
endpointId: endpointAgentId,
capabilities: endpointCapabilities,
},
exampleUsage: 'kill-process --pid 123 --comment "kill this process"',
exampleInstruction: ENTER_PID_OR_ENTITY_ID_INSTRUCTION,
validate: capabilitiesValidator,
mustHaveArgs: true,
args: {
comment: {
@ -153,18 +212,27 @@ export const getEndpointResponseActionsConsoleCommands = (
helpGroupLabel: HELP_GROUPS.responseActions.label,
helpGroupPosition: HELP_GROUPS.responseActions.position,
helpCommandPosition: 4,
helpDisabled: doesEndpointSupportCommand('kill-process') === false,
},
{
name: 'suspend-process',
about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.suspendProcess.about', {
defaultMessage: 'Temporarily suspend a process',
about: getCommandAboutInfo({
aboutInfo: i18n.translate(
'xpack.securitySolution.endpointConsoleCommands.suspendProcess.about',
{
defaultMessage: 'Temporarily suspend a process',
}
),
isSupported: doesEndpointSupportCommand('suspend-process'),
}),
RenderComponent: SuspendProcessActionResult,
meta: {
endpointId: endpointAgentId,
capabilities: endpointCapabilities,
},
exampleUsage: 'suspend-process --pid 123 --comment "suspend this process"',
exampleInstruction: ENTER_PID_OR_ENTITY_ID_INSTRUCTION,
validate: capabilitiesValidator,
mustHaveArgs: true,
args: {
comment: {
@ -200,6 +268,7 @@ export const getEndpointResponseActionsConsoleCommands = (
helpGroupLabel: HELP_GROUPS.responseActions.label,
helpGroupPosition: HELP_GROUPS.responseActions.position,
helpCommandPosition: 5,
helpDisabled: doesEndpointSupportCommand('suspend-process') === false,
},
{
name: 'status',
@ -216,15 +285,23 @@ export const getEndpointResponseActionsConsoleCommands = (
},
{
name: 'processes',
about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.processes.about', {
defaultMessage: 'Show all running processes',
about: getCommandAboutInfo({
aboutInfo: i18n.translate(
'xpack.securitySolution.endpointConsoleCommands.processes.about',
{
defaultMessage: 'Show all running processes',
}
),
isSupported: doesEndpointSupportCommand('processes'),
}),
RenderComponent: GetProcessesActionResult,
meta: {
endpointId: endpointAgentId,
capabilities: endpointCapabilities,
},
exampleUsage: 'processes --comment "get the processes"',
exampleInstruction: ENTER_OR_ADD_COMMENT_ARG_INSTRUCTION,
validate: capabilitiesValidator,
args: {
comment: {
required: false,
@ -235,6 +312,7 @@ export const getEndpointResponseActionsConsoleCommands = (
helpGroupLabel: HELP_GROUPS.responseActions.label,
helpGroupPosition: HELP_GROUPS.responseActions.position,
helpCommandPosition: 3,
helpDisabled: doesEndpointSupportCommand('processes') === false,
},
];
};

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiIconTip } from '@elastic/eui';
const UNSUPPORTED_COMMAND_INFO = i18n.translate(
'xpack.securitySolution.endpointConsoleCommands.suspendProcess.unsupportedCommandInfo',
{
defaultMessage:
'This version of the Endpoint does not support this command. Upgrade your Agent in Fleet to use the latest response actions.',
}
);
const DisabledTooltip = React.memo(() => {
return <EuiIconTip content={UNSUPPORTED_COMMAND_INFO} type="alert" color="danger" />;
});
DisabledTooltip.displayName = 'DisabledTooltip';
export const getCommandAboutInfo = ({
aboutInfo,
isSupported,
}: {
aboutInfo: string;
isSupported: boolean;
}) => {
return isSupported ? (
aboutInfo
) : (
<>
{aboutInfo} <DisabledTooltip />
</>
);
};

View file

@ -16,9 +16,13 @@ import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_a
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import { enterConsoleCommand } from '../console/mocks';
import { waitFor } from '@testing-library/react';
import type { ResponderCapabilities } from '../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
describe('When using processes action from response actions console', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
let render: (
capabilities?: ResponderCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
let consoleManagerMockAccess: ReturnType<
@ -30,14 +34,17 @@ describe('When using processes action from response actions console', () => {
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async () => {
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {
return {
consoleProps: {
'data-test-subj': 'test',
commands: getEndpointResponseActionsConsoleCommands('a.b.c'),
commands: getEndpointResponseActionsConsoleCommands({
endpointAgentId: 'a.b.c',
endpointCapabilities: [...capabilities],
}),
},
};
}}
@ -53,6 +60,15 @@ describe('When using processes action from response actions console', () => {
};
});
it('should show an error if the `running_processes` capability is not present in the endpoint', async () => {
await render([]);
enterConsoleCommand(renderResult, 'processes');
expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual(
'The current version of the Agent does not support this feature. Upgrade your Agent through Fleet to use this feature and new response actions such as killing and suspending processes.'
);
});
it('should call `running-procs` api when command is entered', async () => {
await render();
enterConsoleCommand(renderResult, 'processes');

View file

@ -17,9 +17,13 @@ import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mock
import { enterConsoleCommand } from '../console/mocks';
import { waitFor } from '@testing-library/react';
import { getDeferred } from '../mocks';
import type { ResponderCapabilities } from '../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
describe('When using isolate action from response actions console', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
let render: (
capabilities?: ResponderCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
let consoleManagerMockAccess: ReturnType<
@ -31,14 +35,17 @@ describe('When using isolate action from response actions console', () => {
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async () => {
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {
return {
consoleProps: {
'data-test-subj': 'test',
commands: getEndpointResponseActionsConsoleCommands('a.b.c'),
commands: getEndpointResponseActionsConsoleCommands({
endpointAgentId: 'a.b.c',
endpointCapabilities: [...capabilities],
}),
},
};
}}
@ -54,6 +61,15 @@ describe('When using isolate action from response actions console', () => {
};
});
it('should show an error if the `isolation` capability is not present in the endpoint', async () => {
await render([]);
enterConsoleCommand(renderResult, 'isolate');
expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual(
'The current version of the Agent does not support this feature. Upgrade your Agent through Fleet to use this feature and new response actions such as killing and suspending processes.'
);
});
it('should call `isolate` api when command is entered', async () => {
await render();
enterConsoleCommand(renderResult, 'isolate');

View file

@ -16,9 +16,13 @@ import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_a
import { enterConsoleCommand } from '../console/mocks';
import { waitFor } from '@testing-library/react';
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import type { ResponderCapabilities } from '../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
describe('When using the kill-process action from response actions console', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
let render: (
capabilities?: ResponderCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
let consoleManagerMockAccess: ReturnType<
@ -30,14 +34,17 @@ describe('When using the kill-process action from response actions console', ()
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async () => {
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {
return {
consoleProps: {
'data-test-subj': 'test',
commands: getEndpointResponseActionsConsoleCommands('a.b.c'),
commands: getEndpointResponseActionsConsoleCommands({
endpointAgentId: 'a.b.c',
endpointCapabilities: [...capabilities],
}),
},
};
}}
@ -53,6 +60,15 @@ describe('When using the kill-process action from response actions console', ()
};
});
it('should show an error if the `kill_process` capability is not present in the endpoint', async () => {
await render([]);
enterConsoleCommand(renderResult, 'kill-process --pid 123');
expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual(
'The current version of the Agent does not support this feature. Upgrade your Agent through Fleet to use this feature and new response actions such as killing and suspending processes.'
);
});
it('should call `kill-process` api when command is entered', async () => {
await render();
enterConsoleCommand(renderResult, 'kill-process --pid 123');

View file

@ -17,9 +17,13 @@ import { enterConsoleCommand } from '../console/mocks';
import { waitFor } from '@testing-library/react';
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import { getDeferred } from '../mocks';
import type { ResponderCapabilities } from '../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
describe('When using the release action from response actions console', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
let render: (
capabilities?: ResponderCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
let consoleManagerMockAccess: ReturnType<
@ -31,14 +35,17 @@ describe('When using the release action from response actions console', () => {
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async () => {
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {
return {
consoleProps: {
'data-test-subj': 'test',
commands: getEndpointResponseActionsConsoleCommands('a.b.c'),
commands: getEndpointResponseActionsConsoleCommands({
endpointAgentId: 'a.b.c',
endpointCapabilities: [...capabilities],
}),
},
};
}}
@ -54,6 +61,15 @@ describe('When using the release action from response actions console', () => {
};
});
it('should show an error if the `isolation` capability is not present in the endpoint', async () => {
await render([]);
enterConsoleCommand(renderResult, 'release');
expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual(
'The current version of the Agent does not support this feature. Upgrade your Agent through Fleet to use this feature and new response actions such as killing and suspending processes.'
);
});
it('should call `release` api when command is entered', async () => {
await render();
enterConsoleCommand(renderResult, 'release');

View file

@ -16,9 +16,13 @@ import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_a
import { enterConsoleCommand } from '../console/mocks';
import { waitFor } from '@testing-library/react';
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import type { ResponderCapabilities } from '../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
describe('When using the suspend-process action from response actions console', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
let render: (
capabilities?: ResponderCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
let consoleManagerMockAccess: ReturnType<
@ -30,14 +34,17 @@ describe('When using the suspend-process action from response actions console',
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async () => {
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {
return {
consoleProps: {
'data-test-subj': 'test',
commands: getEndpointResponseActionsConsoleCommands('a.b.c'),
commands: getEndpointResponseActionsConsoleCommands({
endpointAgentId: 'a.b.c',
endpointCapabilities: [...capabilities],
}),
},
};
}}
@ -53,6 +60,15 @@ describe('When using the suspend-process action from response actions console',
};
});
it('should show an error if the `suspend_process` capability is not present in the endpoint', async () => {
await render([]);
enterConsoleCommand(renderResult, 'suspend-process --pid 123');
expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual(
'The current version of the Agent does not support this feature. Upgrade your Agent through Fleet to use this feature and new response actions such as killing and suspending processes.'
);
});
it('should call `suspend-process` api when command is entered', async () => {
await render();
enterConsoleCommand(renderResult, 'suspend-process --pid 123');

View file

@ -48,7 +48,10 @@ export const useWithShowEndpointResponder = (): ShowEndpointResponseActionsConso
endpoint: endpointMetadata,
},
consoleProps: {
commands: getEndpointResponseActionsConsoleCommands(endpointAgentId),
commands: getEndpointResponseActionsConsoleCommands({
endpointAgentId,
endpointCapabilities: endpointMetadata.Endpoint.capabilities ?? [],
}),
'data-test-subj': 'endpointResponseActionsConsole',
TitleComponent: () => <HeaderEndpointInfo endpointId={endpointAgentId} />,
},

View file

@ -21,8 +21,6 @@ import type { ContextMenuItemNavByRouterProps } from '../../../../components/con
import { isEndpointHostIsolated } from '../../../../../common/utils/validators';
import { useLicense } from '../../../../../common/hooks/use_license';
import { isIsolationSupported } from '../../../../../../common/endpoint/service/host_isolation/utils';
import { useDoesEndpointSupportResponder } from '../../../../../common/hooks/endpoint/use_does_endpoint_support_responder';
import { UPGRADE_ENDPOINT_FOR_RESPONDER } from '../../../../../common/translations';
interface Options {
isEndpointList: boolean;
@ -45,7 +43,6 @@ export const useEndpointActionItems = (
'responseActionsConsoleEnabled'
);
const canAccessResponseConsole = useUserPrivileges().endpointPrivileges.canAccessResponseConsole;
const isResponderCapabilitiesEnabled = useDoesEndpointSupportResponder(endpointMetadata);
return useMemo<ContextMenuItemNavByRouterProps[]>(() => {
if (endpointMetadata) {
@ -128,7 +125,6 @@ export const useEndpointActionItems = (
'data-test-subj': 'console',
icon: 'console',
key: 'consoleLink',
disabled: !isResponderCapabilitiesEnabled,
onClick: (ev: React.MouseEvent) => {
ev.preventDefault();
showEndpointResponseActionsConsole(endpointMetadata);
@ -139,9 +135,6 @@ export const useEndpointActionItems = (
defaultMessage="Respond"
/>
),
toolTipContent: !isResponderCapabilitiesEnabled
? UPGRADE_ENDPOINT_FOR_RESPONDER
: '',
},
]
: []),
@ -264,6 +257,5 @@ export const useEndpointActionItems = (
isResponseActionsConsoleEnabled,
showEndpointResponseActionsConsole,
options?.isEndpointList,
isResponderCapabilitiesEnabled,
]);
};

View file

@ -1116,19 +1116,6 @@ describe('when on the endpoint list page', () => {
expect(responderButton).not.toHaveAttribute('disabled');
});
it('disables the Responder option and shows a tooltip when no processes capabilities are present in the endpoint', async () => {
const firstActionButton = (await renderResult.findAllByTestId('endpointTableRowActions'))[0];
const secondActionButton = (await renderResult.findAllByTestId('endpointTableRowActions'))[1];
reactTestingLibrary.act(() => {
// close any open action menus
userEvent.click(firstActionButton);
userEvent.click(secondActionButton);
});
const responderButton = await renderResult.findByTestId('console');
expect(responderButton).toHaveAttribute('disabled');
});
it('navigates to the Actions log flyout', async () => {
const actionsLink = await renderResult.findByTestId('actionsLink');

View file

@ -323,7 +323,7 @@ export const EndpointList = () => {
const setTableRowProps = useCallback((endpoint: HostInfo) => {
return {
'data-endpoint-Id': endpoint.metadata.agent.id,
'data-endpoint-id': endpoint.metadata.agent.id,
};
}, []);