From 96048faa8bdc35fc2d05061eb0233d9479e05f1e Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 5 Jul 2022 10:41:41 -0400 Subject: [PATCH] Kill Process action UI (#135260) * [Security Solution] Kill process action UI --- .../common/endpoint/types/actions.ts | 3 + .../common/lib/process_actions/index.ts | 22 ++ .../console/components/bad_argument.tsx | 21 +- .../command_input/command_input.test.tsx | 6 +- .../command_input/hooks/use_input_hints.ts | 15 +- .../console/components/command_usage.tsx | 152 +++++++-- .../console/components/console_code_block.tsx | 11 + .../handle_execute_command.test.tsx | 46 ++- .../handle_execute_command.tsx | 56 +++- .../management/components/console/mocks.tsx | 42 +++ .../console/service/builtin_commands.tsx | 2 +- .../service/parse_command_input.test.ts | 289 ++++++++++-------- .../console/service/parsed_command_input.ts | 43 ++- .../management/components/console/types.ts | 40 +-- ...point_response_actions_console_commands.ts | 62 ++++ .../kill_process_action.test.tsx | 237 ++++++++++++++ .../kill_process_action.tsx | 127 ++++++++ .../response_actions_list.tsx | 8 +- .../use_send_kill_process_endpoint_request.ts | 33 ++ .../mocks/response_actions_http_mocks.ts | 13 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 23 files changed, 1021 insertions(+), 210 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts create mode 100644 x-pack/plugins/security_solution/public/management/components/console/components/console_code_block.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx create mode 100644 x-pack/plugins/security_solution/public/management/hooks/endpoint/use_send_kill_process_endpoint_request.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index b4e8a333fd5c5..24404ce1637e5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -10,6 +10,7 @@ import { ActionStatusRequestSchema, NoParametersRequestSchema, ResponseActionBodySchema, + KillOrSuspendProcessRequestSchema, } from '../schema/actions'; export type ISOLATION_ACTIONS = 'isolate' | 'unisolate'; @@ -217,6 +218,8 @@ export type HostIsolationRequestBody = TypeOf; +export type KillProcessRequestBody = TypeOf; + export interface HostIsolationResponse { action: string; } diff --git a/x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts b/x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts new file mode 100644 index 0000000000000..02e98d099a7aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { + KillProcessRequestBody, + ResponseActionApiResponse, +} from '../../../../common/endpoint/types'; +import { KibanaServices } from '../kibana'; +import { KILL_PROCESS_ROUTE } from '../../../../common/endpoint/constants'; + +/** Kills a process specified by pid or entity id on a host running Endpoint Security */ +export const killProcess = async ( + params: KillProcessRequestBody +): Promise => { + return KibanaServices.get().http.post(KILL_PROCESS_ROUTE, { + body: JSON.stringify(params), + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx index b353a6714841e..a1e8c5327c93c 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx @@ -47,16 +47,17 @@ export const BadArgument = memo - {'. '} - - {`${command.commandDefinition.name} --help`}, - }} - /> - +
+ + {`${command.commandDefinition.name} --help`}, + }} + /> + +
); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx index 15af8febb09d9..a689433baf4da 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx @@ -81,14 +81,14 @@ describe('When entering data into the Console input', () => { render(); enterCommand('cmd2 ', { inputOnly: true }); - expect(getFooterText()).toEqual('Hint: cmd2 --file [--ext --bad]'); + expect(getFooterText()).toEqual('cmd2 --file [--ext --bad]'); }); it('should display hint when an unknown command is typed', () => { render(); enterCommand('abc ', { inputOnly: true }); - expect(getFooterText()).toEqual('Hint: unknown command abc'); + expect(getFooterText()).toEqual('Unknown command abc'); }); it('should display the input history popover when UP key is pressed', async () => { @@ -233,7 +233,7 @@ describe('When entering data into the Console input', () => { expect(getUserInputText()).toEqual('c'); expect(getRightOfCursorText()).toEqual('md1 '); - expect(getFooterText()).toEqual('Hint: cmd1 '); + expect(getFooterText()).toEqual('cmd1 '); }); it('should return original cursor position if input history is closed with no selection', async () => { diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/hooks/use_input_hints.ts b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/hooks/use_input_hints.ts index cdd3810c941f9..8084fb9803bc1 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/hooks/use_input_hints.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/hooks/use_input_hints.ts @@ -16,13 +16,13 @@ import { useWithCommandList } from '../../../hooks/state_selectors/use_with_comm const UNKNOWN_COMMAND_HINT = (commandName: string) => i18n.translate('xpack.securitySolution.useInputHints.unknownCommand', { - defaultMessage: 'Hint: unknown command {commandName}', + defaultMessage: 'Unknown command {commandName}', values: { commandName }, }); const COMMAND_USAGE_HINT = (usage: string) => i18n.translate('xpack.securitySolution.useInputHints.commandUsage', { - defaultMessage: 'Hint: {usage}', + defaultMessage: '{usage}', values: { usage, }, @@ -51,9 +51,14 @@ export const useInputHints = () => { dispatch({ type: 'updateFooterContent', payload: { - value: COMMAND_USAGE_HINT( - `${commandEnteredDefinition.name} ${getArgumentsForCommand(commandEnteredDefinition)}` - ), + value: + commandEnteredDefinition.exampleUsage && commandEnteredDefinition.exampleInstruction + ? `${commandEnteredDefinition.exampleInstruction} Ex: [${commandEnteredDefinition.exampleUsage}]` + : COMMAND_USAGE_HINT( + `${commandEnteredDefinition.name} ${getArgumentsForCommand( + commandEnteredDefinition + )}` + ), }, }); } else { diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx index 01527fec4a730..da52453b5abc0 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx @@ -8,7 +8,6 @@ import React, { memo, useMemo } from 'react'; import { EuiBadge, - EuiCode, EuiDescriptionList, EuiFlexGroup, EuiFlexItem, @@ -17,6 +16,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { ConsoleCodeBlock } from './console_code_block'; import { getArgumentsForCommand } from '../service/parsed_command_input'; import { CommandDefinition } from '../types'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; @@ -24,28 +24,47 @@ import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; export const CommandInputUsage = memo>(({ commandDef }) => { const usageHelp = useMemo(() => { - return getArgumentsForCommand(commandDef); + return getArgumentsForCommand(commandDef).map((usage) => { + return ( + + {commandDef.name} + {usage} + + ); + }); }, [commandDef]); return ( - - - - - - - - + <> + + - {commandDef.name} - {usageHelp} + - - - + + + {usageHelp} + + + {commandDef.exampleUsage && ( + + + + + + + + {commandDef.exampleUsage} + + + )} + ); }); CommandInputUsage.displayName = 'CommandInputUsage'; @@ -57,17 +76,50 @@ export interface CommandUsageProps { export const CommandUsage = memo(({ commandDef }) => { const getTestId = useTestIdGenerator(useDataTestSubj()); const hasArgs = useMemo(() => Object.keys(commandDef.args ?? []).length > 0, [commandDef.args]); + + type CommandDetails = Array<{ + title: string; + description: string; + }>; + const commandOptions = useMemo(() => { - // `command.args` only here to silence TS check if (!hasArgs || !commandDef.args) { - return []; + return { + required: [], + exclusiveOr: [], + optional: [], + }; } - return Object.entries(commandDef.args).map(([option, { about: description }]) => ({ - title: `--${option}`, - description, - })); + const enteredCommands = Object.entries(commandDef.args).reduce<{ + required: CommandDetails; + exclusiveOr: CommandDetails; + optional: CommandDetails; + }>( + (acc, curr) => { + const item = { + title: `--${curr[0]}`, + description: curr[1].about, + }; + if (curr[1].required) { + acc.required.push(item); + } else if (curr[1].exclusiveOr) { + acc.exclusiveOr.push(item); + } else { + acc.optional.push(item); + } + + return acc; + }, + { + required: [], + exclusiveOr: [], + optional: [], + } + ); + return enteredCommands; }, [commandDef.args, hasArgs]); + const additionalProps = useMemo( () => ({ className: 'euiTruncateText', @@ -79,13 +131,13 @@ export const CommandUsage = memo(({ commandDef }) => { {commandDef.about} - {hasArgs && ( + {commandOptions.required && commandOptions.required.length > 0 && ( <> {commandDef.mustHaveArgs && commandDef.args && hasArgs && ( @@ -101,7 +153,51 @@ export const CommandUsage = memo(({ commandDef }) => { compressed type="column" className="descriptionList-20_80" - listItems={commandOptions} + listItems={commandOptions.required} + descriptionProps={additionalProps} + titleProps={additionalProps} + data-test-subj={getTestId('commandUsage-options')} + /> + )} + + )} + {commandOptions.exclusiveOr && commandOptions.exclusiveOr.length > 0 && ( + <> + + + + + {commandDef.args && ( + + )} + + )} + {commandOptions.optional && commandOptions.optional.length > 0 && ( + <> + + + + + {commandDef.args && ( + { }); }); - it('should clear the command output history when `clear` is entered', async () => { + it('should clear the command output history when `cls` is entered', async () => { render(); enterCommand('help'); enterCommand('help'); expect(renderResult.getByTestId('test-historyOutput').childElementCount).toBe(2); - enterCommand('clear'); + enterCommand('cls'); expect(renderResult.getByTestId('test-historyOutput').childElementCount).toBe(0); }); @@ -181,7 +181,7 @@ describe('When a Console command is entered by the user', () => { }); }); - it('should show error no options were provided, bug command requires some', async () => { + it('should show error no options were provided, but command requires some', async () => { render(); enterCommand('cmd2'); @@ -221,4 +221,44 @@ describe('When a Console command is entered by the user', () => { ); }); }); + + it('should show error no options were provided, but has exclusive or arguments', async () => { + render(); + enterCommand('cmd6'); + + await waitFor(() => { + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'This command supports only one of the following arguments: --foo, --bar' + ); + }); + }); + + it('should show error when it has multiple exclusive arguments', async () => { + render(); + enterCommand('cmd6 --foo 234 --bar 123'); + + await waitFor(() => { + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'This command supports only one of the following arguments: --foo, --bar' + ); + }); + }); + + it('should show success when one exlusive argument is used', async () => { + render(); + enterCommand('cmd6 --foo 234'); + + await waitFor(() => { + expect(renderResult.getByTestId('exec-output')).toBeTruthy(); + }); + }); + + it('should show success when the other exlusive argument is used', async () => { + render(); + enterCommand('cmd6 --bar 234'); + + await waitFor(() => { + expect(renderResult.getByTestId('exec-output')).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx index 7bc01704c61bf..c4072a1e9ffbb 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx @@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n'; import { v4 as uuidV4 } from 'uuid'; -import { EuiCode } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { ConsoleCodeBlock } from '../../console_code_block'; import { handleInputAreaState } from './handle_input_area_state'; import { HelpCommandArgument } from '../../builtin_commands/help_command_argument'; import { @@ -53,6 +53,21 @@ const getUnknownArguments = ( return response; }; +const getExclusiveOrArgs = (argDefinitions: CommandDefinition['args']): string[] => { + if (!argDefinitions) { + return []; + } + + const exclusiveOrArgs: string[] = []; + + return Object.entries(argDefinitions).reduce((acc, [argName, argDef]) => { + if (argDef.exclusiveOr) { + acc.push(argName); + } + return acc; + }, exclusiveOrArgs); +}; + const updateStateWithNewCommandHistoryItem = ( state: ConsoleDataState, newHistoryItem: ConsoleDataState['commandHistory'][number] @@ -133,6 +148,19 @@ export const handleExecuteCommand: ConsoleStoreReducer< commandDefinition, }; const requiredArgs = getRequiredArguments(commandDefinition.args); + const exclusiveOrArgs = getExclusiveOrArgs(commandDefinition.args); + + const exclusiveOrErrorMessage = ( + {exclusiveOrArgs.map(toCliArgumentOption).join(', ')} + ), + }} + /> + ); // If args were entered, then validate them if (parsedInput.hasArgs) { @@ -175,11 +203,11 @@ export const handleExecuteCommand: ConsoleStoreReducer< defaultMessage="The following {command} {countOfInvalidArgs, plural, =1 {argument is} other {arguments are}} not support by this command: {unknownArgs}" values={{ countOfInvalidArgs: unknownInputArgs.length, - command: {parsedInput.name}, + command: {parsedInput.name}, unknownArgs: ( - + {unknownInputArgs.map(toCliArgumentOption).join(', ')} - + ), }} /> @@ -209,6 +237,18 @@ export const handleExecuteCommand: ConsoleStoreReducer< } } + // Validate exclusiveOr arguments, can only have one. + const exclusiveArgsUsed = exclusiveOrArgs.filter((arg) => parsedInput.args[arg]); + if (exclusiveArgsUsed.length > 1) { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: exclusiveOrErrorMessage, + }), + }); + } + // Validate each argument given to the command for (const argName of Object.keys(parsedInput.args)) { const argDefinition = commandDefinition.args[argName]; @@ -284,6 +324,14 @@ export const handleExecuteCommand: ConsoleStoreReducer< ), }), }); + } else if (exclusiveOrArgs.length > 0) { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: exclusiveOrErrorMessage, + }), + }); } else if (commandDefinition.mustHaveArgs) { return updateStateWithNewCommandHistoryItem(state, { id: uuidV4(), diff --git a/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx b/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx index 788d3c616040a..ef550605714e1 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx @@ -194,6 +194,48 @@ export const getCommandListMock = (): CommandDefinition[] => { }, }, }, + { + name: 'cmd5', + about: 'has custom hint text', + RenderComponent: jest.fn(RenderComponent), + mustHaveArgs: true, + exampleUsage: 'cmd5 --foo 123', + exampleInstruction: 'Enter --foo to execute', + args: { + foo: { + about: 'foo stuff', + required: false, + allowMultiples: true, + }, + bar: { + about: 'bar stuff', + required: false, + allowMultiples: true, + }, + }, + }, + { + name: 'cmd6', + about: 'has custom hint text', + RenderComponent: jest.fn(RenderComponent), + mustHaveArgs: true, + exampleUsage: 'cmd6 --foo 123', + exampleInstruction: 'Enter --foo to execute', + args: { + foo: { + about: 'foo stuff', + required: false, + exclusiveOr: true, + allowMultiples: false, + }, + bar: { + about: 'bar stuff', + required: false, + exclusiveOr: true, + allowMultiples: false, + }, + }, + }, ]; return commands; diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx index 5869e3b4472cb..f0b2c58647805 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx @@ -20,7 +20,7 @@ export const getBuiltinCommands = (): CommandDefinition[] => { RenderComponent: HelpCommand, }, { - name: 'clear', + name: 'cls', about: i18n.translate('xpack.securitySolution.console.builtInCommands.clearAbout', { defaultMessage: 'Clear the console buffer', }), diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts b/x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts index ae463ac44a49b..1fad4b578d308 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts @@ -5,141 +5,164 @@ * 2.0. */ -import { parseCommandInput, ParsedCommandInterface } from './parsed_command_input'; - -describe('when using `parseCommandInput()`', () => { - const parsedCommandWith = ( - overrides: Partial> = {} - ): ParsedCommandInterface => { - return { - input: '', - name: 'foo', - args: {}, - hasArgs: Object.keys(overrides.args || {}).length > 0, - ...overrides, - } as ParsedCommandInterface; - }; - - it.each([ - ['foo', 'foo'], - [' foo', 'foo'], - [' foo ', 'foo'], - ['foo ', 'foo'], - [' foo-bar ', 'foo-bar'], - ])('should identify the command entered: [%s]', (input, expectedCommandName) => { - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - name: expectedCommandName, +import { + parseCommandInput, + ParsedCommandInterface, + parsedPidOrEntityIdParameter, +} from './parsed_command_input'; + +describe('when using parsed command input utils', () => { + describe('when using parseCommandInput()', () => { + const parsedCommandWith = ( + overrides: Partial> = {} + ): ParsedCommandInterface => { + return { + input: '', + name: 'foo', args: {}, - }) - ); + hasArgs: Object.keys(overrides.args || {}).length > 0, + ...overrides, + } as ParsedCommandInterface; + }; + + it.each([ + ['foo', 'foo'], + [' foo', 'foo'], + [' foo ', 'foo'], + ['foo ', 'foo'], + [' foo-bar ', 'foo-bar'], + ])('should identify the command entered: [%s]', (input, expectedCommandName) => { + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + name: expectedCommandName, + args: {}, + }) + ); + }); + + it('should parse arguments that the `--` prefix', () => { + const input = 'foo --one --two'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: [], + two: [], + }, + }) + ); + }); + + it('should parse arguments that have a single string value', () => { + const input = 'foo --one value --two=value2'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['value'], + two: ['value2'], + }, + }) + ); + }); + + it('should parse arguments that have multiple strings as the value', () => { + const input = 'foo --one value for one here --two=some more strings for 2'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['value for one here'], + two: ['some more strings for 2'], + }, + }) + ); + }); + + it('should parse arguments whose value is wrapped in quotes', () => { + const input = 'foo --one "value for one here" --two="some more strings for 2"'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['value for one here'], + two: ['some more strings for 2'], + }, + }) + ); + }); + + it('should parse arguments that can be used multiple times', () => { + const input = 'foo --one 1 --one 11 --two=2 --two=22'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['1', '11'], + two: ['2', '22'], + }, + }) + ); + }); + + it('should parse arguments whose value has `--` in it (must be escaped)', () => { + const input = 'foo --one something \\-\\- here --two="\\-\\-something \\-\\-'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['something -- here'], + two: ['--something --'], + }, + }) + ); + }); + + it('should parse arguments whose value has `=` in it', () => { + const input = 'foo --one =something \\-\\- here --two="=something=something else'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['=something -- here'], + two: ['=something=something else'], + }, + }) + ); + }); }); - it('should parse arguments that the `--` prefix', () => { - const input = 'foo --one --two'; - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - args: { - one: [], - two: [], - }, - }) - ); - }); - - it('should parse arguments that have a single string value', () => { - const input = 'foo --one value --two=value2'; - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - args: { - one: ['value'], - two: ['value2'], - }, - }) - ); - }); - - it('should parse arguments that have multiple strings as the value', () => { - const input = 'foo --one value for one here --two=some more strings for 2'; - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - args: { - one: ['value for one here'], - two: ['some more strings for 2'], - }, - }) - ); - }); - - it('should parse arguments whose value is wrapped in quotes', () => { - const input = 'foo --one "value for one here" --two="some more strings for 2"'; - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - args: { - one: ['value for one here'], - two: ['some more strings for 2'], - }, - }) - ); - }); - - it('should parse arguments that can be used multiple times', () => { - const input = 'foo --one 1 --one 11 --two=2 --two=22'; - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - args: { - one: ['1', '11'], - two: ['2', '22'], - }, - }) - ); - }); - - it('should parse arguments whose value has `--` in it (must be escaped)', () => { - const input = 'foo --one something \\-\\- here --two="\\-\\-something \\-\\-'; - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - args: { - one: ['something -- here'], - two: ['--something --'], - }, - }) - ); - }); - - it('should parse arguments whose value has `=` in it', () => { - const input = 'foo --one =something \\-\\- here --two="=something=something else'; - const parsedCommand = parseCommandInput(input); - - expect(parsedCommand).toEqual( - parsedCommandWith({ - input, - args: { - one: ['=something -- here'], - two: ['=something=something else'], - }, - }) - ); + describe('when using parsedPidOrEntityIdParameter()', () => { + it('should parse a pid as a number and return proper params', () => { + const parameters = parsedPidOrEntityIdParameter({ pid: ['123'] }); + expect(parameters).toEqual({ pid: 123 }); + }); + + it('should parse an entity id correctly and return proper params', () => { + const parameters = parsedPidOrEntityIdParameter({ entityId: ['123qwe'] }); + expect(parameters).toEqual({ entity_id: '123qwe' }); + }); + + it('should return undefined if no params are defined', () => { + const parameters = parsedPidOrEntityIdParameter({}); + expect(parameters).toEqual(undefined); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts b/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts index 83c16d6225534..0507337230765 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts @@ -8,6 +8,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { CommandDefinition } from '..'; +import type { EndpointActionDataParameterTypes } from '../../../../../common/endpoint/types'; export type ParsedArgData = string[]; @@ -135,9 +136,10 @@ export const getCommandNameFromTextInput = (input: string): string => { return trimmedInput.substring(0, firstSpacePosition); }; -export const getArgumentsForCommand = (command: CommandDefinition): string => { +export const getArgumentsForCommand = (command: CommandDefinition): string[] => { let requiredArgs = ''; let optionalArgs = ''; + const exclusiveOrArgs = []; if (command.args) { for (const [argName, argDefinition] of Object.entries(command.args)) { @@ -146,6 +148,8 @@ export const getArgumentsForCommand = (command: CommandDefinition): string => { requiredArgs += ' '; } requiredArgs += `--${argName}`; + } else if (argDefinition.exclusiveOr) { + exclusiveOrArgs.push(`--${argName}`); } else { if (optionalArgs.length) { optionalArgs += ' '; @@ -155,5 +159,40 @@ export const getArgumentsForCommand = (command: CommandDefinition): string => { } } - return `${requiredArgs} ${optionalArgs.length > 0 ? `[${optionalArgs}]` : ''}`.trim(); + const buildArgumentText = ({ + required, + exclusive, + optional, + }: { + required?: string; + exclusive?: string; + optional?: string; + }) => { + return `${required ? required : ''}${exclusive ? ` ${exclusive}` : ''} ${ + optional && optional.length > 0 ? `[${optional}]` : '' + }`.trim(); + }; + + return exclusiveOrArgs.length > 0 + ? exclusiveOrArgs.map((exclusiveArg) => { + return buildArgumentText({ + required: requiredArgs, + exclusive: exclusiveArg, + optional: optionalArgs, + }); + }) + : [buildArgumentText({ required: requiredArgs, optional: optionalArgs })]; +}; + +export const parsedPidOrEntityIdParameter = (parameters: { + pid?: ParsedArgData; + entityId?: ParsedArgData; +}): EndpointActionDataParameterTypes => { + if (parameters.pid) { + return { pid: Number(parameters.pid[0]) }; + } else if (parameters.entityId) { + return { entity_id: parameters.entityId[0] }; + } + + return undefined; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/types.ts b/x-pack/plugins/security_solution/public/management/components/console/types.ts index de2ec38b16015..4ef15b8a2a888 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/types.ts @@ -14,6 +14,25 @@ import type { CommandExecutionState } from './components/console_state/types'; import type { Immutable, MaybeImmutable } from '../../../../common/endpoint/types'; import type { ParsedArgData, ParsedCommandInterface } from './service/parsed_command_input'; +export interface CommandArgs { + [longName: string]: { + required: boolean; + allowMultiples: boolean; + exclusiveOr?: boolean; + about: string; + /** + * Validate the individual values given to this argument. + * Should return `true` if valid or a string with the error message + */ + validate?: (argData: ParsedArgData) => true | string; + + // Selector: Idea is that the schema can plugin in a rich component for the + // user to select something (ex. a file) + // FIXME: implement selector + selector?: ComponentType; + }; +} + export interface CommandDefinition { name: string; about: string; @@ -36,6 +55,9 @@ export interface CommandDefinition { /** If all args are optional, but at least one must be defined, set to true */ mustHaveArgs?: boolean; + exampleUsage?: string; + exampleInstruction?: string; + /** * Validate the command entered by the user. This is called only after the Console has ran * through all of its builtin validations (based on `CommandDefinition`). @@ -45,23 +67,7 @@ export interface CommandDefinition { validate?: (command: Command) => true | string; /** The list of arguments supported by this command */ - args?: { - [longName: string]: { - required: boolean; - allowMultiples: boolean; - about: string; - /** - * Validate the individual values given to this argument. - * Should return `true` if valid or a string with the error message - */ - validate?: (argData: ParsedArgData) => true | string; - - // Selector: Idea is that the schema can plugin in a rich component for the - // user to select something (ex. a file) - // FIXME: implement selector - selector?: ComponentType; - }; - }; + args?: CommandArgs; } /** diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/endpoint_response_actions_console_commands.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/endpoint_response_actions_console_commands.ts index 11e98e3a64d8d..9088e793b222f 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/endpoint_response_actions_console_commands.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/endpoint_response_actions_console_commands.ts @@ -9,8 +9,18 @@ import { i18n } from '@kbn/i18n'; import { CommandDefinition } from '../console'; import { IsolateActionResult } from './isolate_action'; import { ReleaseActionResult } from './release_action'; +import { KillProcessActionResult } from './kill_process_action'; import { EndpointStatusActionResult } from './status_action'; import { GetProcessesActionResult } from './get_processes_action'; +import type { ParsedArgData } from '../console/service/parsed_command_input'; + +const emptyArgumentValidator = (argData: ParsedArgData) => { + if (argData?.length > 0 && argData[0]?.trim().length > 0) { + return true; + } else { + return 'Argument cannot be empty'; + } +}; export const getEndpointResponseActionsConsoleCommands = ( endpointAgentId: string @@ -25,6 +35,8 @@ export const getEndpointResponseActionsConsoleCommands = ( meta: { endpointId: endpointAgentId, }, + exampleUsage: 'isolate --comment "isolate this host"', + exampleInstruction: 'Hit enter to execute or add an optional comment', args: { comment: { required: false, @@ -45,6 +57,31 @@ export const getEndpointResponseActionsConsoleCommands = ( meta: { endpointId: endpointAgentId, }, + exampleUsage: 'release --comment "isolate this host"', + exampleInstruction: 'Hit enter to execute or add an optional comment', + args: { + comment: { + required: false, + allowMultiples: false, + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.release.arg.comment', + { defaultMessage: 'A comment to go along with the action' } + ), + }, + }, + }, + { + name: 'kill-process', + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.killProcess.about', { + defaultMessage: 'Kill a running process', + }), + RenderComponent: KillProcessActionResult, + meta: { + endpointId: endpointAgentId, + }, + exampleUsage: 'kill-process --pid 123', + exampleInstruction: 'Enter a pid or an entity id to execute', + mustHaveArgs: true, args: { comment: { required: false, @@ -54,6 +91,29 @@ export const getEndpointResponseActionsConsoleCommands = ( { defaultMessage: 'A comment to go along with the action' } ), }, + pid: { + required: false, + allowMultiples: false, + exclusiveOr: true, + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.pid.arg.comment', { + defaultMessage: + 'A PID representing the process to kill. You can enter a pid or an entity id, but not both.', + }), + validate: emptyArgumentValidator, + }, + entityId: { + required: false, + allowMultiples: false, + exclusiveOr: true, + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.entityId.arg.comment', + { + defaultMessage: + 'An entity id representing the process to kill. You can enter a pid or an entity id, but not both.', + } + ), + validate: emptyArgumentValidator, + }, }, }, { @@ -75,6 +135,8 @@ export const getEndpointResponseActionsConsoleCommands = ( meta: { endpointId: endpointAgentId, }, + exampleUsage: 'processes --comment "get the processes"', + exampleInstruction: 'Hit enter to execute or add an optional comment', args: { comment: { required: false, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx new file mode 100644 index 0000000000000..caf582f6eb81a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx @@ -0,0 +1,237 @@ +/* + * 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 { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; +import { + ConsoleManagerTestComponent, + getConsoleManagerMockRenderResultQueriesAndActions, +} from '../console/components/console_manager/mocks'; +import React from 'react'; +import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_actions_console_commands'; +import { enterConsoleCommand } from '../console/mocks'; +import { waitFor } from '@testing-library/react'; +import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks'; + +describe('When using the kill-process action from response actions console', () => { + let render: () => Promise>; + let renderResult: ReturnType; + let apiMocks: ReturnType; + let consoleManagerMockAccess: ReturnType< + typeof getConsoleManagerMockRenderResultQueriesAndActions + >; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + + apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http); + + render = async () => { + renderResult = mockedContext.render( + { + return { + consoleProps: { + 'data-test-subj': 'test', + commands: getEndpointResponseActionsConsoleCommands('a.b.c'), + }, + }; + }} + /> + ); + + consoleManagerMockAccess = getConsoleManagerMockRenderResultQueriesAndActions(renderResult); + + await consoleManagerMockAccess.clickOnRegisterNewConsole(); + await consoleManagerMockAccess.openRunningConsole(); + + return renderResult; + }; + }); + + it('should call `kill-process` api when command is entered', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid 123'); + + await waitFor(() => { + expect(apiMocks.responseProvider.killProcess).toHaveBeenCalledTimes(1); + }); + }); + + it('should accept an optional `--comment`', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid 123 --comment "This is a comment"'); + + await waitFor(() => { + expect(apiMocks.responseProvider.killProcess).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.stringContaining('This is a comment'), + }) + ); + }); + }); + + it('should only accept one `--comment`', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid 123 --comment "one" --comment "two"'); + + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Argument can only be used once: --comment' + ); + }); + + it('should only accept one exclusive argument', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid 123 --entityId 123wer'); + + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'This command supports only one of the following arguments: --pid, --entityId' + ); + }); + + it('should check for at least one exclusive argument', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process'); + + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'This command supports only one of the following arguments: --pid, --entityId' + ); + }); + + it('should check the pid has a given value', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid'); + + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Invalid argument value: --pid. Argument cannot be empty' + ); + }); + + it('should check the pid has a non-empty value', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid " "'); + + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Invalid argument value: --pid. Argument cannot be empty' + ); + }); + + it('should check the entityId has a given value', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --entityId'); + + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Invalid argument value: --entityId. Argument cannot be empty' + ); + }); + + it('should check the entity id has a non-empty value', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --entityId " "'); + + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Invalid argument value: --entityId. Argument cannot be empty' + ); + }); + + it('should call the action status api after creating the `kill-process` request', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid 123'); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalled(); + }); + }); + + it('should show success when `kill-process` action completes with no errors when using `pid`', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid 123'); + + await waitFor(() => { + expect(renderResult.getByTestId('killProcessSuccessCallout')).toBeTruthy(); + }); + }); + + it('should show success when `kill-process` action completes with no errors when using `entityId`', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --entityId 123wer'); + + await waitFor(() => { + expect(renderResult.getByTestId('killProcessSuccessCallout')).toBeTruthy(); + }); + }); + + it('should show error if kill-process failed to complete successfully', async () => { + const pendingDetailResponse = apiMocks.responseProvider.actionDetails({ + path: '/api/endpoint/action/1.2.3', + }); + pendingDetailResponse.data.wasSuccessful = false; + pendingDetailResponse.data.errors = ['error one', 'error two']; + apiMocks.responseProvider.actionDetails.mockReturnValue(pendingDetailResponse); + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid 123'); + + await waitFor(() => { + expect(renderResult.getByTestId('killProcessErrorCallout').textContent).toMatch( + /error one \| error two/ + ); + }); + }); + + describe('and when console is closed (not terminated) and then reopened', () => { + beforeEach(() => { + const _render = render; + + render = async () => { + const response = await _render(); + enterConsoleCommand(response, 'kill-process --pid 123'); + + await waitFor(() => { + expect(apiMocks.responseProvider.killProcess).toHaveBeenCalledTimes(1); + }); + + // Hide the console + await consoleManagerMockAccess.hideOpenedConsole(); + + return response; + }; + }); + + it('should NOT send the `kill-process` request again', async () => { + await render(); + await consoleManagerMockAccess.openRunningConsole(); + + expect(apiMocks.responseProvider.killProcess).toHaveBeenCalledTimes(1); + }); + + it('should continue to check action status when still pending', async () => { + const pendingDetailResponse = apiMocks.responseProvider.actionDetails({ + path: '/api/endpoint/action/1.2.3', + }); + pendingDetailResponse.data.isCompleted = false; + apiMocks.responseProvider.actionDetails.mockReturnValue(pendingDetailResponse); + await render(); + + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(2); + + await consoleManagerMockAccess.openRunningConsole(); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(3); + }); + }); + + it('should display completion output if done (no additional API calls)', async () => { + await render(); + + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); + + await consoleManagerMockAccess.openRunningConsole(); + + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx new file mode 100644 index 0000000000000..5cb2944082657 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx @@ -0,0 +1,127 @@ +/* + * 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, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { ActionDetails } from '../../../../common/endpoint/types'; +import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; +import type { EndpointCommandDefinitionMeta } from './types'; +import { useSendKillProcessRequest } from '../../hooks/endpoint/use_send_kill_process_endpoint_request'; +import type { CommandExecutionComponentProps } from '../console/types'; +import { parsedPidOrEntityIdParameter } from '../console/service/parsed_command_input'; + +export const KillProcessActionResult = memo< + CommandExecutionComponentProps< + { comment?: string; pid?: number; entityId?: string }, + { + actionId?: string; + actionRequestSent?: boolean; + completedActionDetails?: ActionDetails; + }, + EndpointCommandDefinitionMeta + > +>(({ command, setStore, store, status, setStatus, ResultComponent }) => { + const endpointId = command.commandDefinition?.meta?.endpointId; + const { actionId, completedActionDetails } = store; + const isPending = status === 'pending'; + const actionRequestSent = Boolean(store.actionRequestSent); + + const killProcessApi = useSendKillProcessRequest(); + + const { data: actionDetails } = useGetActionDetails(actionId ?? '-', { + enabled: Boolean(actionId) && isPending, + refetchInterval: isPending ? 3000 : false, + }); + + // Send Kill request if not yet done + useEffect(() => { + const parameters = parsedPidOrEntityIdParameter(command.args.args); + + if (!actionRequestSent && endpointId && parameters) { + killProcessApi.mutate({ + endpoint_ids: [endpointId], + comment: command.args.args?.comment?.[0], + parameters, + }); + setStore((prevState) => { + return { ...prevState, actionRequestSent: true }; + }); + } + }, [actionRequestSent, command.args.args, endpointId, killProcessApi, setStore]); + + // If kill-process request was created, store the action id if necessary + useEffect(() => { + if (killProcessApi.isSuccess && actionId !== killProcessApi.data.data.id) { + setStore((prevState) => { + return { ...prevState, actionId: killProcessApi.data.data.id }; + }); + } + }, [ + actionId, + killProcessApi?.data?.data.id, + killProcessApi.isSuccess, + killProcessApi.error, + setStore, + ]); + + useEffect(() => { + if (actionDetails?.data.isCompleted) { + setStatus('success'); + setStore((prevState) => { + return { + ...prevState, + completedActionDetails: actionDetails.data, + }; + }); + } + }, [actionDetails?.data, setStatus, setStore]); + + // Show nothing if still pending + if (isPending) { + return ( + + + + ); + } + + // Show errors + if (completedActionDetails?.errors) { + return ( + + + + ); + } + + // Show Success + return ( + + ); +}); +KillProcessActionResult.displayName = 'KillProcessActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_list.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_list.tsx index ed806dec5cad3..65e7dc13b739d 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_list.tsx @@ -158,6 +158,12 @@ export const ResponseActionsList = memo< parameters, } = item; + const parametersList = parameters + ? Object.entries(parameters).map(([key, value]) => { + return `${key}:${value}`; + }) + : undefined; + const command = getCommand(_command); const descriptionListLeft = [ { @@ -177,7 +183,7 @@ export const ResponseActionsList = memo< }, { title: OUTPUT_MESSAGES.expandSection.parameters, - description: parameters ? parameters : emptyValue, + description: parametersList ? parametersList : emptyValue, }, ]; diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_send_kill_process_endpoint_request.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_send_kill_process_endpoint_request.ts new file mode 100644 index 0000000000000..f3051890871ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_send_kill_process_endpoint_request.ts @@ -0,0 +1,33 @@ +/* + * 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 { useMutation, UseMutationOptions, UseMutationResult } from 'react-query'; +import { HttpFetchError } from '@kbn/core/public'; +import type { + KillProcessRequestBody, + ResponseActionApiResponse, +} from '../../../../common/endpoint/types'; +import { killProcess } from '../../../common/lib/process_actions'; + +/** + * Create kill process requests + * @param customOptions + */ +export const useSendKillProcessRequest = ( + customOptions?: UseMutationOptions< + ResponseActionApiResponse, + HttpFetchError, + KillProcessRequestBody + > +): UseMutationResult => { + return useMutation( + (processData: KillProcessRequestBody) => { + return killProcess(processData); + }, + customOptions + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts b/x-pack/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts index 26606678020fc..25d5b9e01207d 100644 --- a/x-pack/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts +++ b/x-pack/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts @@ -14,6 +14,7 @@ import { ENDPOINTS_ACTION_LIST_ROUTE, ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, + KILL_PROCESS_ROUTE, } from '../../../common/endpoint/constants'; import { httpHandlerMockFactory, @@ -33,6 +34,8 @@ export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{ releaseHost: () => HostIsolationResponse; + killProcess: () => ActionDetailsApiResponse; + actionDetails: (options: HttpFetchOptionsWithPath) => ActionDetailsApiResponse; actionList: (options: HttpFetchOptionsWithPath) => ActionListApiResponse; @@ -59,6 +62,16 @@ export const responseActionsHttpMocks = httpHandlerMockFactory { + const generator = new EndpointActionGenerator('seed'); + const response = generator.generateActionDetails() as ActionDetails; + return { data: response }; + }, + }, { id: 'actionDetails', path: ACTION_DETAILS_ROUTE, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 54b0f83f8e36a..c0e84f4b21693 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -24592,7 +24592,6 @@ "xpack.securitySolution.console.commandList.footerText": "Pour plus d'informations sur les commandes ci-dessus, utilisez l'argument {helpOption}. Exemple : {cmdExample}", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "Remarque : au moins une option doit être utilisée.", "xpack.securitySolution.console.commandUsage.inputUsage": "Utilisation :", - "xpack.securitySolution.console.commandUsage.optionsLabel": "Options :", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "cet argument ne peut être utilisé qu’une fois : {argName}.", "xpack.securitySolution.console.commandValidation.invalidArgValue": "valeur d'argument non valide : {argName}. {error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "argument requis manquant : {argName}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9910013d2099e..74aabf615fb5a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24576,7 +24576,6 @@ "xpack.securitySolution.console.commandList.footerText": "上記のコマンドの詳細については、{helpOption}引数を使用してください。例:{cmdExample}", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "注記:1つ以上のオプションを使用する必要があります", "xpack.securitySolution.console.commandUsage.inputUsage": "使用方法:", - "xpack.securitySolution.console.commandUsage.optionsLabel": "オプション:", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "引数{argName}は一度だけ使用できます", "xpack.securitySolution.console.commandValidation.invalidArgValue": "無効な引数値:{argName}。{error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "不足している必須の引数:{argName}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 190f8b7a3dbb4..38e6fb9b55601 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24600,7 +24600,6 @@ "xpack.securitySolution.console.commandList.footerText": "有关上述命令的更多详情,请使用 {helpOption} 参数。示例:{cmdExample}", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "注意:必须至少使用一个选项", "xpack.securitySolution.console.commandUsage.inputUsage": "用法:", - "xpack.securitySolution.console.commandUsage.optionsLabel": "选项:", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "参数只能使用一次:{argName}", "xpack.securitySolution.console.commandValidation.invalidArgValue": "无效的参数值:{argName}。{error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "缺少所需参数:{argName}",