Skip to content

Commit

Permalink
Kill Process action UI (#135260)
Browse files Browse the repository at this point in the history
* [Security Solution] Kill process action UI
  • Loading branch information
kevinlog authored Jul 5, 2022
1 parent 0618dd9 commit 96048fa
Show file tree
Hide file tree
Showing 23 changed files with 1,021 additions and 210 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ActionStatusRequestSchema,
NoParametersRequestSchema,
ResponseActionBodySchema,
KillOrSuspendProcessRequestSchema,
} from '../schema/actions';

export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';
Expand Down Expand Up @@ -217,6 +218,8 @@ export type HostIsolationRequestBody = TypeOf<typeof NoParametersRequestSchema.b

export type ResponseActionRequestBody = TypeOf<typeof ResponseActionBodySchema>;

export type KillProcessRequestBody = TypeOf<typeof KillOrSuspendProcessRequestSchema.body>;

export interface HostIsolationResponse {
action: string;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ResponseActionApiResponse> => {
return KibanaServices.get().http.post<ResponseActionApiResponse>(KILL_PROCESS_ROUTE, {
body: JSON.stringify(params),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,17 @@ export const BadArgument = memo<CommandExecutionComponentProps<{}, { errorMessag
<div className="eui-displayInlineBlock">
<CommandInputUsage commandDef={command.commandDefinition} />
</div>
{'. '}
<EuiText size="s" className="eui-displayInlineBlock">
<FormattedMessage
id="xpack.securitySolution.console.badArgument.helpMessage"
defaultMessage="Enter {helpCmd} for further assistance."
values={{
helpCmd: <EuiCode>{`${command.commandDefinition.name} --help`}</EuiCode>,
}}
/>
</EuiText>
<div>
<EuiText size="s" className="eui-displayInlineBlock">
<FormattedMessage
id="xpack.securitySolution.console.badArgument.helpMessage"
defaultMessage="Enter {helpCmd} for further assistance."
values={{
helpCmd: <EuiCode>{`${command.commandDefinition.name} --help`}</EuiCode>,
}}
/>
</EuiText>
</div>
</>
</UnsupportedMessageCallout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import React, { memo, useMemo } from 'react';
import {
EuiBadge,
EuiCode,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
Expand All @@ -17,35 +16,55 @@ 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';
import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj';

export const CommandInputUsage = memo<Pick<CommandUsageProps, 'commandDef'>>(({ commandDef }) => {
const usageHelp = useMemo(() => {
return getArgumentsForCommand(commandDef);
return getArgumentsForCommand(commandDef).map((usage) => {
return (
<EuiText size="s">
<EuiBadge>{commandDef.name}</EuiBadge>
<ConsoleCodeBlock>{usage}</ConsoleCodeBlock>
</EuiText>
);
});
}, [commandDef]);

return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText size="s">
<FormattedMessage
id="xpack.securitySolution.console.commandUsage.inputUsage"
defaultMessage="Usage:"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow>
<code>
<>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiBadge>{commandDef.name}</EuiBadge>
<EuiCode transparentBackground={true}>{usageHelp}</EuiCode>
<FormattedMessage
id="xpack.securitySolution.console.commandUsage.inputUsage"
defaultMessage="Usage:"
/>
</EuiText>
</code>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<code>{usageHelp}</code>
</EuiFlexItem>
</EuiFlexGroup>
{commandDef.exampleUsage && (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText size="s">
<FormattedMessage
id="xpack.securitySolution.console.commandUsage.exampleUsage"
defaultMessage="Example:"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow>
<ConsoleCodeBlock>{commandDef.exampleUsage}</ConsoleCodeBlock>
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
);
});
CommandInputUsage.displayName = 'CommandInputUsage';
Expand All @@ -57,17 +76,50 @@ export interface CommandUsageProps {
export const CommandUsage = memo<CommandUsageProps>(({ 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',
Expand All @@ -79,13 +131,13 @@ export const CommandUsage = memo<CommandUsageProps>(({ commandDef }) => {
<EuiPanel color="transparent" data-test-subj={getTestId('commandUsage')}>
<EuiText>{commandDef.about}</EuiText>
<CommandInputUsage commandDef={commandDef} />
{hasArgs && (
{commandOptions.required && commandOptions.required.length > 0 && (
<>
<EuiSpacer />
<EuiText>
<FormattedMessage
id="xpack.securitySolution.console.commandUsage.optionsLabel"
defaultMessage="Options:"
id="xpack.securitySolution.console.commandUsage.requiredLabel"
defaultMessage="Required parameters:"
/>
{commandDef.mustHaveArgs && commandDef.args && hasArgs && (
<EuiText size="s" color="subdued">
Expand All @@ -101,7 +153,51 @@ export const CommandUsage = memo<CommandUsageProps>(({ 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 && (
<>
<EuiSpacer />
<EuiText>
<FormattedMessage
id="xpack.securitySolution.console.commandUsage.exclusiveOr"
defaultMessage="Include only one of the following required parameters:"
/>
</EuiText>
{commandDef.args && (
<EuiDescriptionList
compressed
type="column"
className="descriptionList-20_80"
listItems={commandOptions.exclusiveOr}
descriptionProps={additionalProps}
titleProps={additionalProps}
data-test-subj={getTestId('commandUsage-options')}
/>
)}
</>
)}
{commandOptions.optional && commandOptions.optional.length > 0 && (
<>
<EuiSpacer />
<EuiText>
<FormattedMessage
id="xpack.securitySolution.console.commandUsage.optionalLabel"
defaultMessage="Optional parameters:"
/>
</EuiText>
{commandDef.args && (
<EuiDescriptionList
compressed
type="column"
className="descriptionList-20_80"
listItems={commandOptions.optional}
descriptionProps={additionalProps}
titleProps={additionalProps}
data-test-subj={getTestId('commandUsage-options')}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { EuiCode } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';

export const ConsoleCodeBlock = euiStyled(EuiCode).attrs({ transparentBackground: true })``;
Loading

0 comments on commit 96048fa

Please sign in to comment.