mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Responder] Improved UI for responder capabilities check (#138662)
This commit is contained in:
parent
ea929a0eb8
commit
0b8e2700fd
20 changed files with 353 additions and 115 deletions
|
@ -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];
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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';
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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} />,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue