Skip to content

Commit

Permalink
[Security Solution][Endpoint] Display better success and failure mess…
Browse files Browse the repository at this point in the history
…ages for kill suspend process actions (#137353)

* add map with endpoint action response code and associated i18n message

* Add component to handle getting a failure message from a completed action

* Add component to handle getting a failure message from a completed action

* Correct type definition for ActionResponseOutput

* New ActionSuccess component + use it in kill/suspend process

* Change default failure message

* add some jsdocs to the endpoint codes
  • Loading branch information
paul-tavares authored Jul 28, 2022
1 parent a40d241 commit cb4d6aa
Show file tree
Hide file tree
Showing 16 changed files with 304 additions and 71 deletions.
26 changes: 20 additions & 6 deletions x-pack/plugins/security_solution/common/endpoint/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';
/** The output provided by some of the Endpoint responses */
export interface ActionResponseOutput<TOutputContent extends object = object> {
type: 'json' | 'text';
content: {
entries: TOutputContent[];
};
content: TOutputContent;
}

export interface ProcessesEntry {
Expand All @@ -30,6 +28,24 @@ export interface ProcessesEntry {
user: string;
}

export interface GetProcessesActionOutputContent {
entries: ProcessesEntry[];
}

export interface SuspendProcessActionOutputContent {
code: string;
command?: string;
pid?: number;
entity_id?: string;
}

export interface KillProcessActionOutputContent {
code: string;
command?: string;
pid?: number;
entity_id?: string;
}

export const RESPONSE_ACTION_COMMANDS = [
'isolate',
'unisolate',
Expand Down Expand Up @@ -275,9 +291,7 @@ export interface ActionDetails<TOutputContent extends object = object> {
startedAt: string;
/** The date when the action was completed (a response by the endpoint (not fleet) was received) */
completedAt: string | undefined;
/**
* The output data from an action
*/
/** The output data from an action stored in an object where the key is the agent id */
outputs?: Record<string, ActionResponseOutput<TOutputContent>>;
/** user that created the action */
createdBy: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@

export { Console } from './console';
export { ConsoleManager, useConsoleManager } from './components/console_manager';
export type { CommandDefinition, Command, ConsoleProps } from './types';
export type {
CommandDefinition,
Command,
ConsoleProps,
CommandExecutionComponentProps,
} from './types';
export type {
ConsoleRegistrationInterface,
ManagedConsoleExtensionComponentProps,
RegisteredConsoleClient,
ConsoleManagerClient,
} from './components/console_manager/types';
export type {
CommandExecutionResultProps,
CommandExecutionResultComponent,
} from './components/command_execution_result';
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { memo, useMemo } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { endpointActionResponseCodes } from '../endpoint_responder/endpoint_action_response_codes';
import type { ActionDetails, MaybeImmutable } from '../../../../common/endpoint/types';

interface EndpointActionFailureMessageProps {
action: MaybeImmutable<ActionDetails<{ code?: string }>>;
}

export const EndpointActionFailureMessage = memo<EndpointActionFailureMessageProps>(
({ action }) => {
return useMemo(() => {
if (!action.isCompleted || action.wasSuccessful) {
return null;
}

const errors: string[] = [];

// Determine if each endpoint returned a response code and if so,
// see if we have a localized message for it
if (action.outputs) {
for (const agent of action.agents) {
const endpointAgentOutput = action.outputs[agent];

if (
endpointAgentOutput &&
endpointAgentOutput.type === 'json' &&
endpointAgentOutput.content.code &&
endpointActionResponseCodes[endpointAgentOutput.content.code]
) {
errors.push(endpointActionResponseCodes[endpointAgentOutput.content.code]);
}
}
}

if (!errors.length) {
if (action.errors) {
errors.push(...action.errors);
} else {
errors.push(
i18n.translate('xpack.securitySolution.endpointActionFailureMessage.unknownFailure', {
defaultMessage: 'Action failed',
})
);
}
}

return (
<>
<FormattedMessage
id="xpack.securitySolution.endpointResponseActions.actionError.errorMessage"
defaultMessage="The following { errorCount, plural, =1 {error was} other {errors were}} encountered:"
values={{ errorCount: errors.length }}
/>
<EuiSpacer size="s" />
<div>{errors.join(' | ')}</div>
</>
);
}, [action]);
}
);
EndpointActionFailureMessage.displayName = 'EndpointActionFailureMessage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export { EndpointActionFailureMessage } from './endpoint_action_failure_message';
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,19 @@
*/

import React, { memo } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EndpointActionFailureMessage } from '../endpoint_action_failure_message';
import type { CommandExecutionResultComponent } from '../console/components/command_execution_result';
import type { ImmutableArray } from '../../../../common/endpoint/types';
import type { ActionDetails, MaybeImmutable } from '../../../../common/endpoint/types';

export const ActionError = memo<{
errors: ImmutableArray<string>;
title?: string;
action: MaybeImmutable<ActionDetails>;
ResultComponent: CommandExecutionResultComponent;
title?: string;
dataTestSubj?: string;
}>(({ title, dataTestSubj, errors, ResultComponent }) => {
}>(({ title, dataTestSubj, action, ResultComponent }) => {
return (
<ResultComponent showAs="failure" title={title} data-test-subj={dataTestSubj}>
<FormattedMessage
id="xpack.securitySolution.endpointResponseActions.actionError.errorMessage"
defaultMessage="The following errors were encountered:"
/>
<EuiSpacer size="s" />
<div>{errors.join(' | ')}</div>
<EndpointActionFailureMessage action={action} />
</ResultComponent>
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { memo, useMemo } from 'react';
import { endpointActionResponseCodes } from './endpoint_action_response_codes';
import type { ActionDetails, MaybeImmutable } from '../../../../common/endpoint/types';
import type { CommandExecutionResultComponent, CommandExecutionResultProps } from '../console';

export interface ActionSuccessProps extends CommandExecutionResultProps {
action: MaybeImmutable<ActionDetails<{ code?: string }>>;
ResultComponent: CommandExecutionResultComponent;
}

/**
* Display generic success message for all actions
*/
export const ActionSuccess = memo<ActionSuccessProps>(
({ action, ResultComponent, title: _title, ...props }) => {
const title = useMemo(() => {
if (_title) {
return _title;
}

const firstAgentId = action.agents[0];
const actionOutputCode = action.outputs?.[firstAgentId]?.content?.code;

return actionOutputCode ? endpointActionResponseCodes[actionOutputCode] : undefined;
}, [_title, action.agents, action.outputs]);

return <ResultComponent {...props} title={title} />;
}
);
ActionSuccess.displayName = 'ActionSuccess';
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 { i18n } from '@kbn/i18n';

const CODES = Object.freeze({
// -----------------------------------------------------------------
// SUSPEND-PROCESS CODES
// -----------------------------------------------------------------
/**
* Code will be used whenever you provide an entity_id or pid that isn't found.
* suspend_process will always be an error because the process was not found to be suspended
*/
'ra_suspend-process_error_not-found': i18n.translate(
'xpack.securitySolution.endpointActionResponseCodes.suspendProcess.notFoundError',
{ defaultMessage: 'The provided process was not found' }
),

/**
* Code will be used when the provided process can not be killed (for stability reasons).
* Example: This occurs if you try to kill Endpoint Security
*/
'ra_suspend-process_error_not-permitted': i18n.translate(
'xpack.securitySolution.endpointActionResponseCodes.suspendProcess.notPermittedSuccess',
{ defaultMessage: 'The provided process cannot be suspended' }
),

// -----------------------------------------------------------------
// KILL-PROCESS CODES
// -----------------------------------------------------------------
/**
* Code will be used whenever you provide an entity_id that isn't found. Since entity_id is
* unique, we can guarantee that it was legitimately not found and not just that the process
* was already killed.
*/
'ra_kill-process_error_not-found': i18n.translate(
'xpack.securitySolution.endpointActionResponseCodes.killProcess.notFoundError',
{ defaultMessage: 'The provided process was not found' }
),

/**
* Code will be used whenever you provide a pid that isn't found. Since pid is reused, we aren't
* sure if the process was already killed or just wasn't found. In either case, a process with
* that pid will no longer be running.
*/
'ra_kill-process_success_no-action': i18n.translate(
'xpack.securitySolution.endpointActionResponseCodes.killProcess.noActionSuccess',
{ defaultMessage: 'Action completed. The provided process was not found or already killed' }
),

/**
* Code will be used when the provided process can not be killed (for stability reasons).
* Example: This occurs if you try to kill Endpoint Security
*/
'ra_kill-process_error_not-permitted': i18n.translate(
'xpack.securitySolution.endpointActionResponseCodes.killProcess.notPermittedSuccess',
{ defaultMessage: 'The provided process cannot be killed' }
),
});

/**
* A map of possible code's that can be returned from the endpoint for response actions
*/
export const endpointActionResponseCodes: Readonly<Record<string | keyof typeof CODES, string>> =
CODES;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { EuiBasicTable } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import type { ActionDetails, ProcessesEntry } from '../../../../common/endpoint/types';
import type {
ActionDetails,
GetProcessesActionOutputContent,
} from '../../../../common/endpoint/types';
import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details';
import type { EndpointCommandDefinitionMeta } from './types';
import type { CommandExecutionComponentProps } from '../console/types';
Expand Down Expand Up @@ -46,7 +49,7 @@ export const GetProcessesActionResult = memo<
{
actionId?: string;
actionRequestSent?: boolean;
completedActionDetails?: ActionDetails<ProcessesEntry>;
completedActionDetails?: ActionDetails<GetProcessesActionOutputContent>;
apiError?: IHttpFetchError;
},
EndpointCommandDefinitionMeta
Expand All @@ -66,10 +69,13 @@ export const GetProcessesActionResult = memo<
error: processesActionRequestError,
} = useSendGetEndpointProcessesRequest();

const { data: actionDetails } = useGetActionDetails<ProcessesEntry>(actionId ?? '-', {
enabled: Boolean(actionId) && isPending,
refetchInterval: isPending ? 3000 : false,
});
const { data: actionDetails } = useGetActionDetails<GetProcessesActionOutputContent>(
actionId ?? '-',
{
enabled: Boolean(actionId) && isPending,
refetchInterval: isPending ? 3000 : false,
}
);

// Send get processes request if not yet done
useEffect(() => {
Expand Down Expand Up @@ -201,7 +207,7 @@ export const GetProcessesActionResult = memo<
{ defaultMessage: 'Get processes action failed' }
)}
dataTestSubj={'getProcessesErrorCallout'}
errors={completedActionDetails?.errors}
action={completedActionDetails}
ResultComponent={ResultComponent}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const IsolateActionResult = memo<ActionRequestComponentProps>(
return (
<ActionError
dataTestSubj={'isolateErrorCallout'}
errors={completedActionDetails?.errors}
action={completedActionDetails}
ResultComponent={ResultComponent}
/>
);
Expand Down
Loading

0 comments on commit cb4d6aa

Please sign in to comment.