mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[EDR Workflows] CrowdStrike RunScript: Log Actions and UI Output (#204044)](https://github.com/elastic/kibana/pull/204044) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Tomasz Ciecierski","email":"tomasz.ciecierski@elastic.co"},"sourceCommit":{"committedDate":"2024-12-17T15:53:03Z","message":"[EDR Workflows] CrowdStrike RunScript: Log Actions and UI Output (#204044)","sha":"a7addbadd394b33fa3212e57f55be251c1a3a371","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["v9.0.0","Team:Defend Workflows","release_note:feature","backport:version","v8.18.0"],"number":204044,"url":"https://github.com/elastic/kibana/pull/204044","mergeCommit":{"message":"[EDR Workflows] CrowdStrike RunScript: Log Actions and UI Output (#204044)","sha":"a7addbadd394b33fa3212e57f55be251c1a3a371"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204044","number":204044,"mergeCommit":{"message":"[EDR Workflows] CrowdStrike RunScript: Log Actions and UI Output (#204044)","sha":"a7addbadd394b33fa3212e57f55be251c1a3a371"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
2735f3cfa3
commit
8e8fdee03f
21 changed files with 546 additions and 65 deletions
|
@ -318,16 +318,16 @@ export const CrowdstrikeExecuteRTRResponseSchema = schema.object(
|
|||
schema.string(),
|
||||
schema.object(
|
||||
{
|
||||
session_id: schema.maybe(schema.string()),
|
||||
task_id: schema.maybe(schema.string()),
|
||||
complete: schema.maybe(schema.boolean()),
|
||||
stdout: schema.maybe(schema.string()),
|
||||
stderr: schema.maybe(schema.string()),
|
||||
base_command: schema.maybe(schema.string()),
|
||||
aid: schema.maybe(schema.string()),
|
||||
errors: schema.maybe(schema.arrayOf(schema.any())),
|
||||
query_time: schema.maybe(schema.number()),
|
||||
offline_queued: schema.maybe(schema.boolean()),
|
||||
session_id: schema.string(),
|
||||
task_id: schema.string(),
|
||||
complete: schema.boolean(),
|
||||
stdout: schema.string(),
|
||||
stderr: schema.string(),
|
||||
base_command: schema.string(),
|
||||
aid: schema.string(),
|
||||
errors: schema.arrayOf(schema.any()),
|
||||
query_time: schema.number(),
|
||||
offline_queued: schema.boolean(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
|
@ -337,9 +337,9 @@ export const CrowdstrikeExecuteRTRResponseSchema = schema.object(
|
|||
),
|
||||
meta: schema.object(
|
||||
{
|
||||
query_time: schema.maybe(schema.number()),
|
||||
powered_by: schema.maybe(schema.string()),
|
||||
trace_id: schema.maybe(schema.string()),
|
||||
query_time: schema.number(),
|
||||
powered_by: schema.string(),
|
||||
trace_id: schema.string(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
|
@ -348,7 +348,5 @@ export const CrowdstrikeExecuteRTRResponseSchema = schema.object(
|
|||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export type CrowdStrikeExecuteRTRResponse = typeof CrowdstrikeExecuteRTRResponseSchema;
|
||||
|
||||
// TODO: will be part of a next PR
|
||||
export const CrowdstrikeGetScriptsParamsSchema = schema.any({});
|
||||
|
|
|
@ -17,8 +17,8 @@ import {
|
|||
CrowdstrikeGetTokenResponseSchema,
|
||||
CrowdstrikeGetAgentsResponseSchema,
|
||||
RelaxedCrowdstrikeBaseApiResponseSchema,
|
||||
CrowdstrikeInitRTRResponseSchema,
|
||||
CrowdstrikeInitRTRParamsSchema,
|
||||
CrowdstrikeExecuteRTRResponseSchema,
|
||||
} from './schema';
|
||||
|
||||
export type CrowdstrikeConfig = TypeOf<typeof CrowdstrikeConfigSchema>;
|
||||
|
@ -35,9 +35,10 @@ export type CrowdstrikeGetAgentOnlineStatusResponse = TypeOf<
|
|||
typeof CrowdstrikeGetAgentOnlineStatusResponseSchema
|
||||
>;
|
||||
export type CrowdstrikeGetTokenResponse = TypeOf<typeof CrowdstrikeGetTokenResponseSchema>;
|
||||
export type CrowdstrikeInitRTRResponse = TypeOf<typeof CrowdstrikeInitRTRResponseSchema>;
|
||||
|
||||
export type CrowdstrikeHostActionsParams = TypeOf<typeof CrowdstrikeHostActionsParamsSchema>;
|
||||
|
||||
export type CrowdstrikeActionParams = TypeOf<typeof CrowdstrikeActionParamsSchema>;
|
||||
export type CrowdstrikeInitRTRParams = TypeOf<typeof CrowdstrikeInitRTRParamsSchema>;
|
||||
|
||||
export type CrowdStrikeExecuteRTRResponse = TypeOf<typeof CrowdstrikeExecuteRTRResponseSchema>;
|
||||
|
|
|
@ -22,6 +22,7 @@ import type {
|
|||
CrowdstrikeGetTokenResponse,
|
||||
CrowdstrikeGetAgentOnlineStatusResponse,
|
||||
RelaxedCrowdstrikeBaseApiResponse,
|
||||
CrowdStrikeExecuteRTRResponse,
|
||||
} from '../../../common/crowdstrike/types';
|
||||
import {
|
||||
CrowdstrikeHostActionsParamsSchema,
|
||||
|
@ -31,7 +32,6 @@ import {
|
|||
CrowdstrikeRTRCommandParamsSchema,
|
||||
CrowdstrikeExecuteRTRResponseSchema,
|
||||
CrowdstrikeGetScriptsParamsSchema,
|
||||
CrowdStrikeExecuteRTRResponse,
|
||||
CrowdstrikeApiDoNotValidateResponsesSchema,
|
||||
CrowdstrikeGetTokenResponseSchema,
|
||||
} from '../../../common/crowdstrike/schema';
|
||||
|
@ -292,15 +292,9 @@ export class CrowdstrikeConnector extends SubActionConnector<
|
|||
payload: {
|
||||
command: string;
|
||||
endpoint_ids: string[];
|
||||
overwriteUrl?: 'batchExecuteRTR' | 'batchActiveResponderExecuteRTR' | 'batchAdminExecuteRTR';
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<CrowdStrikeExecuteRTRResponse> => {
|
||||
// Some commands are only available in specific API endpoints, however there's an additional requirement check for the command's argument
|
||||
// Eg. runscript command is available with the batchExecuteRTR endpoint, but if it goes with --Raw parameter, it should go to batchAdminExecuteRTR endpoint
|
||||
// This overwrite value will be coming from kibana response actions api
|
||||
const csUrl = payload.overwriteUrl ? this.urls[payload.overwriteUrl] : url;
|
||||
|
||||
const batchId = await this.crowdStrikeSessionManager.initializeSession(
|
||||
{ endpoint_ids: payload.endpoint_ids },
|
||||
connectorUsageCollector
|
||||
|
@ -313,7 +307,7 @@ export class CrowdstrikeConnector extends SubActionConnector<
|
|||
}
|
||||
return await this.crowdstrikeApiRequest<CrowdStrikeExecuteRTRResponse>(
|
||||
{
|
||||
url: csUrl,
|
||||
url,
|
||||
method: 'post',
|
||||
data: {
|
||||
base_command: baseCommand,
|
||||
|
@ -335,7 +329,6 @@ export class CrowdstrikeConnector extends SubActionConnector<
|
|||
payload: {
|
||||
command: string;
|
||||
endpoint_ids: string[];
|
||||
overwriteUrl?: 'batchActiveResponderExecuteRTR' | 'batchAdminExecuteRTR';
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<CrowdStrikeExecuteRTRResponse> {
|
||||
|
@ -351,7 +344,6 @@ export class CrowdstrikeConnector extends SubActionConnector<
|
|||
payload: {
|
||||
command: string;
|
||||
endpoint_ids: string[];
|
||||
overwriteUrl?: 'batchAdminExecuteRTR';
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<CrowdStrikeExecuteRTRResponse> {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { GetProcessesRouteRequestSchema } from '../response_actions/running_proc
|
|||
import { KillProcessRouteRequestSchema } from '../response_actions/kill_process';
|
||||
import { SuspendProcessRouteRequestSchema } from '../response_actions/suspend_process';
|
||||
import { UploadActionRequestSchema } from '../response_actions/upload';
|
||||
import { RunScriptActionRequestSchema } from '../response_actions/run_script';
|
||||
|
||||
export const ResponseActionBodySchema = schema.oneOf([
|
||||
IsolateRouteRequestSchema.body,
|
||||
|
@ -28,6 +29,7 @@ export const ResponseActionBodySchema = schema.oneOf([
|
|||
ExecuteActionRequestSchema.body,
|
||||
UploadActionRequestSchema.body,
|
||||
ScanActionRequestSchema.body,
|
||||
RunScriptActionRequestSchema.body,
|
||||
]);
|
||||
|
||||
export type ResponseActionsRequestBody = TypeOf<typeof ResponseActionBodySchema>;
|
||||
|
|
|
@ -26,27 +26,27 @@ export const RunScriptActionRequestSchema = {
|
|||
/**
|
||||
* The script to run
|
||||
*/
|
||||
Raw: schema.maybe(NonEmptyString),
|
||||
raw: schema.maybe(NonEmptyString),
|
||||
/**
|
||||
* The path to the script on the host to run
|
||||
*/
|
||||
HostPath: schema.maybe(NonEmptyString),
|
||||
hostPath: schema.maybe(NonEmptyString),
|
||||
/**
|
||||
* The path to the script in the cloud to run
|
||||
*/
|
||||
CloudFile: schema.maybe(NonEmptyString),
|
||||
cloudFile: schema.maybe(NonEmptyString),
|
||||
/**
|
||||
* The command line to run
|
||||
*/
|
||||
CommandLine: schema.maybe(NonEmptyString),
|
||||
commandLine: schema.maybe(NonEmptyString),
|
||||
/**
|
||||
* The max timeout value before the command is killed. Number represents milliseconds
|
||||
*/
|
||||
Timeout: schema.maybe(schema.number({ min: 1 })),
|
||||
timeout: schema.maybe(schema.number({ min: 1 })),
|
||||
},
|
||||
{
|
||||
validate: (params) => {
|
||||
if (!params.Raw && !params.HostPath && !params.CloudFile) {
|
||||
if (!params.raw && !params.hostPath && !params.cloudFile) {
|
||||
return 'At least one of Raw, HostPath, or CloudFile must be provided';
|
||||
}
|
||||
},
|
||||
|
|
|
@ -15,6 +15,8 @@ import type {
|
|||
ResponseActionUploadOutputContent,
|
||||
ResponseActionUploadParameters,
|
||||
GetProcessesActionOutputContent,
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters,
|
||||
} from '../../types';
|
||||
import { RESPONSE_ACTION_AGENT_TYPE, RESPONSE_ACTION_TYPE } from './constants';
|
||||
|
||||
|
@ -47,6 +49,15 @@ export const isProcessesAction = (
|
|||
return action.command === 'running-processes';
|
||||
};
|
||||
|
||||
export const isRunScriptAction = (
|
||||
action: MaybeImmutable<SomeObjectWithCommand>
|
||||
): action is ActionDetails<
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters
|
||||
> => {
|
||||
return action.command === 'runscript';
|
||||
};
|
||||
|
||||
// type guards to ensure only the matching string values are attached to the types filter type
|
||||
export const isAgentType = (type: string): type is (typeof RESPONSE_ACTION_AGENT_TYPE)[number] =>
|
||||
RESPONSE_ACTION_AGENT_TYPE.includes(type as (typeof RESPONSE_ACTION_AGENT_TYPE)[number]);
|
||||
|
|
|
@ -97,7 +97,8 @@ export interface ResponseActionScanOutputContent {
|
|||
}
|
||||
|
||||
export interface ResponseActionRunScriptOutputContent {
|
||||
output: string;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -279,7 +279,7 @@ Command Examples for Running Scripts:
|
|||
|
||||
3. Executes a raw script provided entirely within the "--Raw" flag.
|
||||
|
||||
runscript --Raw="Get-ChildItem."
|
||||
runscript --Raw=\`\`\`Get-ChildItem.\`\`\`
|
||||
|
||||
4. Executes a script located on the remote host at the specified path with the provided command-line arguments.
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import type {
|
|||
ActionDetails,
|
||||
MaybeImmutable,
|
||||
ResponseActionExecuteOutputContent,
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters,
|
||||
ResponseActionsExecuteParameters,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { EXECUTE_FILE_LINK_TITLE } from '../endpoint_response_actions_list/translations';
|
||||
|
@ -18,14 +20,18 @@ import { ExecuteActionHostResponseOutput } from './execute_action_host_response_
|
|||
|
||||
export interface ExecuteActionHostResponseProps {
|
||||
action: MaybeImmutable<
|
||||
ActionDetails<ResponseActionExecuteOutputContent, ResponseActionsExecuteParameters>
|
||||
| ActionDetails<ResponseActionExecuteOutputContent, ResponseActionsExecuteParameters>
|
||||
| ActionDetails<ResponseActionRunScriptOutputContent, ResponseActionRunScriptParameters>
|
||||
>;
|
||||
agentId?: string;
|
||||
canAccessFileDownloadLink: boolean;
|
||||
'data-test-subj'?: string;
|
||||
textSize?: 'xs' | 's';
|
||||
hideFile?: boolean;
|
||||
hideContext?: boolean;
|
||||
}
|
||||
|
||||
// Note: also used for RunScript command
|
||||
export const ExecuteActionHostResponse = memo<ExecuteActionHostResponseProps>(
|
||||
({
|
||||
action,
|
||||
|
@ -33,6 +39,8 @@ export const ExecuteActionHostResponse = memo<ExecuteActionHostResponseProps>(
|
|||
canAccessFileDownloadLink,
|
||||
textSize = 's',
|
||||
'data-test-subj': dataTestSubj,
|
||||
hideFile,
|
||||
hideContext,
|
||||
}) => {
|
||||
const outputContent = useMemo(
|
||||
() =>
|
||||
|
@ -44,21 +52,24 @@ export const ExecuteActionHostResponse = memo<ExecuteActionHostResponseProps>(
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<ResponseActionFileDownloadLink
|
||||
action={action}
|
||||
buttonTitle={EXECUTE_FILE_LINK_TITLE}
|
||||
canAccessFileDownloadLink={canAccessFileDownloadLink}
|
||||
data-test-subj={`${dataTestSubj}-getExecuteLink`}
|
||||
textSize={textSize}
|
||||
/>
|
||||
<EuiSpacer size="xxl" />
|
||||
</EuiFlexItem>
|
||||
{!hideFile && (
|
||||
<EuiFlexItem>
|
||||
<ResponseActionFileDownloadLink
|
||||
action={action}
|
||||
buttonTitle={EXECUTE_FILE_LINK_TITLE}
|
||||
canAccessFileDownloadLink={canAccessFileDownloadLink}
|
||||
data-test-subj={`${dataTestSubj}-getExecuteLink`}
|
||||
textSize={textSize}
|
||||
/>
|
||||
<EuiSpacer size="xxl" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{outputContent && (
|
||||
<ExecuteActionHostResponseOutput
|
||||
outputContent={outputContent}
|
||||
data-test-subj={`${dataTestSubj}-executeResponseOutput`}
|
||||
textSize={textSize}
|
||||
hideContext={hideContext}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -98,6 +98,7 @@ interface ShellInfoContentProps {
|
|||
textSize?: 's' | 'xs';
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ShellInfoContent = memo<ShellInfoContentProps>(({ content, textSize, title }) => (
|
||||
<StyledEuiText size={textSize}>
|
||||
<strong>
|
||||
|
@ -178,10 +179,12 @@ export interface ExecuteActionHostResponseOutputProps {
|
|||
outputContent: ResponseActionExecuteOutputContent;
|
||||
'data-test-subj'?: string;
|
||||
textSize?: 's' | 'xs';
|
||||
hideContext?: boolean;
|
||||
}
|
||||
|
||||
// Note: also used for RunScript command
|
||||
export const ExecuteActionHostResponseOutput = memo<ExecuteActionHostResponseOutputProps>(
|
||||
({ outputContent, 'data-test-subj': dataTestSubj, textSize = 'xs' }) => {
|
||||
({ outputContent, 'data-test-subj': dataTestSubj, textSize = 'xs', hideContext }) => {
|
||||
const contextContent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
|
@ -216,14 +219,16 @@ export const ExecuteActionHostResponseOutput = memo<ExecuteActionHostResponseOut
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<ExecutionActionOutputAccordion
|
||||
content={contextContent}
|
||||
data-test-subj={`${dataTestSubj}-context`}
|
||||
textSize={textSize}
|
||||
type="context"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{!hideContext && (
|
||||
<EuiFlexItem>
|
||||
<ExecutionActionOutputAccordion
|
||||
content={contextContent}
|
||||
data-test-subj={`${dataTestSubj}-context`}
|
||||
textSize={textSize}
|
||||
type="context"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
{outputContent.stderr.length > 0 && (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ExecuteActionHostResponse } from '../../endpoint_execute_action';
|
||||
import { useSendRunScriptEndpoint } from '../../../hooks/response_actions/use_send_run_script_endpoint_request';
|
||||
import type { RunScriptActionRequestBody } from '../../../../../common/api/endpoint';
|
||||
import { useConsoleActionSubmitter } from '../hooks/use_console_action_submitter';
|
||||
import type {
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import type { ActionRequestComponentProps } from '../types';
|
||||
|
||||
export const RunScriptActionResult = memo<
|
||||
ActionRequestComponentProps<
|
||||
{
|
||||
Raw?: string;
|
||||
HostPath?: string;
|
||||
CloudFile?: string;
|
||||
CommandLine?: string;
|
||||
Timeout?: number;
|
||||
comment?: string;
|
||||
},
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters
|
||||
>
|
||||
>(({ command, setStore, store, status, setStatus, ResultComponent }) => {
|
||||
const actionCreator = useSendRunScriptEndpoint();
|
||||
const actionRequestBody = useMemo<undefined | RunScriptActionRequestBody>(() => {
|
||||
const { endpointId, agentType } = command.commandDefinition?.meta ?? {};
|
||||
|
||||
if (!endpointId) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
agent_type: agentType,
|
||||
endpoint_ids: [endpointId],
|
||||
parameters: {
|
||||
raw: command.args.args.Raw?.[0],
|
||||
hostPath: command.args.args.HostPath?.[0],
|
||||
cloudFile: command.args.args.CloudFile?.[0],
|
||||
commandLine: command.args.args.CommandLine?.[0],
|
||||
timeout: command.args.args.Timeout?.[0],
|
||||
},
|
||||
comment: command.args.args?.comment?.[0],
|
||||
};
|
||||
}, [command]);
|
||||
|
||||
const { result, actionDetails: completedActionDetails } = useConsoleActionSubmitter<
|
||||
RunScriptActionRequestBody,
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters
|
||||
>({
|
||||
ResultComponent,
|
||||
setStore,
|
||||
store,
|
||||
status,
|
||||
setStatus,
|
||||
actionCreator,
|
||||
actionRequestBody,
|
||||
dataTestSubj: 'runscript',
|
||||
});
|
||||
|
||||
if (!completedActionDetails || !completedActionDetails.wasSuccessful) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResultComponent
|
||||
data-test-subj="executeSuccess"
|
||||
showAs="success"
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.endpointResponseActions.runScriptAction.successTitle',
|
||||
{ defaultMessage: 'RunScript was successful.' }
|
||||
)}
|
||||
>
|
||||
<ExecuteActionHostResponse
|
||||
action={completedActionDetails}
|
||||
canAccessFileDownloadLink={true}
|
||||
agentId={command.commandDefinition?.meta?.endpointId}
|
||||
textSize="s"
|
||||
data-test-subj="console"
|
||||
hideFile={true}
|
||||
hideContext={true}
|
||||
/>
|
||||
</ResultComponent>
|
||||
);
|
||||
});
|
||||
RunScriptActionResult.displayName = 'RunScriptActionResult';
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RunScriptActionResult } from '../command_render_components/run_script_action';
|
||||
import type { CommandArgDefinition } from '../../console/types';
|
||||
import { isAgentTypeAndActionSupported } from '../../../../common/lib/endpoint';
|
||||
import { getRbacControl } from '../../../../../common/endpoint/service/response_actions/utils';
|
||||
|
@ -531,14 +532,14 @@ export const getEndpointConsoleCommands = ({
|
|||
aboutInfo: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about,
|
||||
isSupported: doesEndpointSupportCommand('runscript'),
|
||||
}),
|
||||
RenderComponent: () => null,
|
||||
RenderComponent: RunScriptActionResult,
|
||||
meta: {
|
||||
agentType,
|
||||
endpointId: endpointAgentId,
|
||||
capabilities: endpointCapabilities,
|
||||
privileges: endpointPrivileges,
|
||||
},
|
||||
exampleUsage: `runscript --Raw="Get-ChildItem ." --CommandLine=""`,
|
||||
exampleUsage: `runscript --Raw=\`\`\`Get-ChildItem .\`\`\` --CommandLine=""`,
|
||||
helpUsage: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.helpUsage,
|
||||
exampleInstruction: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about,
|
||||
validate: capabilitiesAndPrivilegesValidator(agentType),
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
isExecuteAction,
|
||||
isGetFileAction,
|
||||
isProcessesAction,
|
||||
isRunScriptAction,
|
||||
isUploadAction,
|
||||
} from '../../../../../common/endpoint/service/response_actions/type_guards';
|
||||
import { EndpointUploadActionResult } from '../../endpoint_upload_action_result';
|
||||
|
@ -209,6 +210,30 @@ const OutputContent = memo<{
|
|||
);
|
||||
}
|
||||
|
||||
if (isRunScriptAction(action)) {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" data-test-subj={getTestId('runScriptDetails')}>
|
||||
{action.agents.map((agentId) => (
|
||||
<div key={agentId}>
|
||||
{OUTPUT_MESSAGES.wasSuccessful(command)}
|
||||
<ExecuteActionHostResponse
|
||||
action={action}
|
||||
agentId={agentId}
|
||||
canAccessFileDownloadLink={
|
||||
canAccessEndpointActionsLogManagement || canReadActionsLogManagement
|
||||
}
|
||||
textSize="xs"
|
||||
data-test-subj={getTestId('actionsLogTray')}
|
||||
hideFile={true}
|
||||
hideContext={true}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
// CrowdStrike Isolate/Release actions
|
||||
if (action.agentType === 'crowdstrike') {
|
||||
return <>{OUTPUT_MESSAGES.submittedSuccessfully(command)}</>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 as _useMutation } from '@tanstack/react-query';
|
||||
import type { AppContextTestRender } from '../../../common/mock/endpoint';
|
||||
import type { RenderHookResult } from '@testing-library/react';
|
||||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
|
||||
import { RUN_SCRIPT_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import type { RunScriptActionRequestBody } from '../../../../common/api/endpoint';
|
||||
import type {
|
||||
RunScriptRequestCustomOptions,
|
||||
UseSendRunScriptRequestResult,
|
||||
} from './use_send_run_script_endpoint_request';
|
||||
import { useSendRunScriptEndpoint } from './use_send_run_script_endpoint_request';
|
||||
|
||||
const useMutationMock = _useMutation as jest.Mock;
|
||||
|
||||
jest.mock('@tanstack/react-query', () => {
|
||||
const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
|
||||
|
||||
return {
|
||||
...actualReactQueryModule,
|
||||
useMutation: jest.fn((...args) => actualReactQueryModule.useMutation(...args)),
|
||||
};
|
||||
});
|
||||
|
||||
const runScriptPayload: RunScriptActionRequestBody = {
|
||||
endpoint_ids: ['test-endpoint-id'],
|
||||
agent_type: 'crowdstrike',
|
||||
parameters: { raw: 'ls' },
|
||||
};
|
||||
|
||||
describe('When using the `useSendRunScriptRequest()` hook', () => {
|
||||
let customOptions: RunScriptRequestCustomOptions;
|
||||
let http: AppContextTestRender['coreStart']['http'];
|
||||
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
|
||||
let renderHook: () => RenderHookResult<
|
||||
UseSendRunScriptRequestResult,
|
||||
RunScriptRequestCustomOptions
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
const testContext = createAppRootMockRenderer();
|
||||
|
||||
http = testContext.coreStart.http;
|
||||
apiMocks = responseActionsHttpMocks(http);
|
||||
customOptions = {};
|
||||
|
||||
renderHook = () => {
|
||||
return testContext.renderHook(() => useSendRunScriptEndpoint(customOptions));
|
||||
};
|
||||
});
|
||||
|
||||
it('should call the `runScript` API with correct payload', async () => {
|
||||
const {
|
||||
result: {
|
||||
current: { mutateAsync },
|
||||
},
|
||||
} = renderHook();
|
||||
await mutateAsync(runScriptPayload);
|
||||
|
||||
expect(apiMocks.responseProvider.runscript).toHaveBeenCalledWith({
|
||||
body: JSON.stringify(runScriptPayload),
|
||||
path: RUN_SCRIPT_ROUTE,
|
||||
version: '2023-10-31',
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow custom options to be passed to ReactQuery', async () => {
|
||||
customOptions.mutationKey = ['pqr-abc'];
|
||||
customOptions.cacheTime = 10;
|
||||
renderHook();
|
||||
|
||||
expect(useMutationMock).toHaveBeenCalledWith(expect.any(Function), customOptions);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { RunScriptActionRequestBody } from '../../../../common/api/endpoint';
|
||||
import { KibanaServices } from '../../../common/lib/kibana';
|
||||
import { RUN_SCRIPT_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import type { ResponseActionApiResponse } from '../../../../common/endpoint/types';
|
||||
|
||||
export type RunScriptRequestCustomOptions = UseMutationOptions<
|
||||
ResponseActionApiResponse,
|
||||
IHttpFetchError,
|
||||
RunScriptActionRequestBody
|
||||
>;
|
||||
|
||||
export type UseSendRunScriptRequestResult = UseMutationResult<
|
||||
ResponseActionApiResponse,
|
||||
IHttpFetchError,
|
||||
RunScriptActionRequestBody
|
||||
>;
|
||||
|
||||
export const useSendRunScriptEndpoint = (
|
||||
options?: RunScriptRequestCustomOptions
|
||||
): UseSendRunScriptRequestResult => {
|
||||
return useMutation<ResponseActionApiResponse, IHttpFetchError, RunScriptActionRequestBody>(
|
||||
(runScriptActionReqBody) => {
|
||||
return KibanaServices.get().http.post<ResponseActionApiResponse>(RUN_SCRIPT_ROUTE, {
|
||||
body: JSON.stringify(runScriptActionReqBody),
|
||||
version: '2023-10-31',
|
||||
});
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
|
@ -17,6 +17,7 @@ import {
|
|||
GET_PROCESSES_ROUTE,
|
||||
ISOLATE_HOST_ROUTE_V2,
|
||||
KILL_PROCESS_ROUTE,
|
||||
RUN_SCRIPT_ROUTE,
|
||||
SCAN_ROUTE,
|
||||
SUSPEND_PROCESS_ROUTE,
|
||||
UNISOLATE_HOST_ROUTE_V2,
|
||||
|
@ -42,6 +43,8 @@ import type {
|
|||
ResponseActionScanParameters,
|
||||
ResponseActionUploadOutputContent,
|
||||
ResponseActionUploadParameters,
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters,
|
||||
} from '../../../common/endpoint/types';
|
||||
|
||||
export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{
|
||||
|
@ -73,6 +76,8 @@ export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{
|
|||
>;
|
||||
|
||||
scan: () => ActionDetailsApiResponse<ResponseActionScanOutputContent>;
|
||||
|
||||
runscript: () => ActionDetailsApiResponse<ResponseActionRunScriptOutputContent>;
|
||||
}>;
|
||||
|
||||
export const responseActionsHttpMocks = httpHandlerMockFactory<ResponseActionsHttpMocksInterface>([
|
||||
|
@ -273,6 +278,25 @@ export const responseActionsHttpMocks = httpHandlerMockFactory<ResponseActionsHt
|
|||
command: 'scan',
|
||||
});
|
||||
|
||||
return { data: response };
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'runscript',
|
||||
path: RUN_SCRIPT_ROUTE,
|
||||
method: 'post',
|
||||
handler: (): ActionDetailsApiResponse<
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters
|
||||
> => {
|
||||
const generator = new EndpointActionGenerator('seed');
|
||||
const response = generator.generateActionDetails<
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters
|
||||
>({
|
||||
command: 'runscript',
|
||||
});
|
||||
|
||||
return { data: response };
|
||||
},
|
||||
},
|
||||
|
|
|
@ -27,6 +27,7 @@ import type {
|
|||
ResponseActionGetFileParameters,
|
||||
EndpointActionResponseDataOutput,
|
||||
ResponseActionScanOutputContent,
|
||||
ResponseActionRunScriptOutputContent,
|
||||
} from '../../../common/endpoint/types';
|
||||
import { getFileDownloadId } from '../../../common/endpoint/service/response_actions/get_file_download_id';
|
||||
import {
|
||||
|
@ -138,6 +139,15 @@ export const sendEndpointActionResponse = async (
|
|||
.content as unknown as ResponseActionExecuteOutputContent
|
||||
).stderr = 'execute command timed out';
|
||||
}
|
||||
if (
|
||||
endpointResponse.EndpointActions.data.command === 'runscript' &&
|
||||
endpointResponse.EndpointActions.data.output
|
||||
) {
|
||||
(
|
||||
endpointResponse.EndpointActions.data.output
|
||||
.content as unknown as ResponseActionRunScriptOutputContent
|
||||
).stderr = 'runscript command timed out';
|
||||
}
|
||||
}
|
||||
|
||||
await esClient.index({
|
||||
|
|
|
@ -11,8 +11,13 @@ import {
|
|||
CROWDSTRIKE_CONNECTOR_ID,
|
||||
} from '@kbn/stack-connectors-plugin/common/crowdstrike/constants';
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { CrowdstrikeBaseApiResponse } from '@kbn/stack-connectors-plugin/common/crowdstrike/types';
|
||||
import type {
|
||||
CrowdstrikeBaseApiResponse,
|
||||
CrowdStrikeExecuteRTRResponse,
|
||||
} from '@kbn/stack-connectors-plugin/common/crowdstrike/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { mapParametersToCrowdStrikeArguments } from './utils';
|
||||
import type { CrowdstrikeActionRequestCommonMeta } from '../../../../../../common/endpoint/types/crowdstrike';
|
||||
import type {
|
||||
CommonResponseActionMethodOptions,
|
||||
|
@ -305,15 +310,99 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
): Promise<
|
||||
ActionDetails<ResponseActionRunScriptOutputContent, ResponseActionRunScriptParameters>
|
||||
> {
|
||||
// TODO: just a placeholder for now
|
||||
return Promise.resolve({ output: 'runscript', code: 200 }) as never as ActionDetails<
|
||||
ResponseActionRunScriptOutputContent,
|
||||
ResponseActionRunScriptParameters
|
||||
>;
|
||||
const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions = {
|
||||
...actionRequest,
|
||||
...this.getMethodOptions(options),
|
||||
command: 'runscript',
|
||||
};
|
||||
|
||||
let actionResponse: ActionTypeExecutorResult<CrowdStrikeExecuteRTRResponse> | undefined;
|
||||
if (!reqIndexOptions.error) {
|
||||
let error = (await this.validateRequest(reqIndexOptions)).error;
|
||||
if (!error) {
|
||||
if (!reqIndexOptions.actionId) {
|
||||
reqIndexOptions.actionId = uuidv4();
|
||||
}
|
||||
|
||||
try {
|
||||
actionResponse = (await this.sendAction(SUB_ACTION.EXECUTE_ADMIN_RTR, {
|
||||
actionParameters: { comment: this.buildExternalComment(reqIndexOptions) },
|
||||
command: mapParametersToCrowdStrikeArguments('runscript', actionRequest.parameters),
|
||||
endpoint_ids: actionRequest.endpoint_ids,
|
||||
})) as ActionTypeExecutorResult<CrowdStrikeExecuteRTRResponse>;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
|
||||
reqIndexOptions.error = error?.message;
|
||||
|
||||
if (!this.options.isAutomated && error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const actionRequestDoc = await this.writeActionRequestToEndpointIndex(reqIndexOptions);
|
||||
|
||||
// Ensure actionResponse is assigned before using it
|
||||
if (actionResponse) {
|
||||
await this.completeCrowdstrikeBatchAction(actionResponse, actionRequestDoc);
|
||||
}
|
||||
|
||||
await this.updateCases({
|
||||
command: reqIndexOptions.command,
|
||||
caseIds: reqIndexOptions.case_ids,
|
||||
alertIds: reqIndexOptions.alert_ids,
|
||||
actionId: actionRequestDoc.EndpointActions.action_id,
|
||||
hosts: actionRequest.endpoint_ids.map((agentId) => {
|
||||
return {
|
||||
hostId: agentId,
|
||||
hostname: actionRequestDoc.EndpointActions.data.hosts?.[agentId].name ?? '',
|
||||
};
|
||||
}),
|
||||
comment: reqIndexOptions.comment,
|
||||
});
|
||||
|
||||
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
|
||||
}
|
||||
|
||||
private async completeCrowdstrikeBatchAction(
|
||||
actionResponse: ActionTypeExecutorResult<CrowdStrikeExecuteRTRResponse>,
|
||||
doc: LogsEndpointAction
|
||||
): Promise<void> {
|
||||
const agentId = doc.agent.id as string;
|
||||
const stdout = actionResponse.data?.combined.resources[agentId].stdout || '';
|
||||
const stderr = actionResponse.data?.combined.resources[agentId].stderr || '';
|
||||
const error = actionResponse.data?.combined.resources[agentId].errors?.[0];
|
||||
const options = {
|
||||
actionId: doc.EndpointActions.action_id,
|
||||
agentId,
|
||||
data: {
|
||||
...doc.EndpointActions.data,
|
||||
output: {
|
||||
content: {
|
||||
stdout,
|
||||
stderr,
|
||||
code: '200',
|
||||
},
|
||||
type: 'text' as const,
|
||||
},
|
||||
},
|
||||
...(error
|
||||
? {
|
||||
error: {
|
||||
code: error.code,
|
||||
message: `Crowdstrike action failed: ${error.message}`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
await this.writeActionResponseToEndpointIndex(options);
|
||||
}
|
||||
|
||||
private async completeCrowdstrikeAction(
|
||||
actionResponse: ActionTypeExecutorResult<CrowdstrikeBaseApiResponse> | undefined,
|
||||
actionResponse: ActionTypeExecutorResult<CrowdstrikeBaseApiResponse>,
|
||||
doc: LogsEndpointAction
|
||||
): Promise<void> {
|
||||
const options = {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { mapParametersToCrowdStrikeArguments } from './utils';
|
||||
|
||||
describe('mapParametersToCrowdStrikeArguments', () => {
|
||||
it('returns command with single word parameter as is', () => {
|
||||
const result = mapParametersToCrowdStrikeArguments('runscript', { raw: 'echo Hello' });
|
||||
expect(result).toBe('runscript --Raw=```echo Hello```');
|
||||
});
|
||||
|
||||
it('wraps multi-word parameter in triple backticks', () => {
|
||||
const result = mapParametersToCrowdStrikeArguments('runscript', {
|
||||
commandLine: 'echo Hello World',
|
||||
});
|
||||
expect(result).toBe('runscript --CommandLine=```echo Hello World```');
|
||||
});
|
||||
|
||||
it('leaves parameter already wrapped in triple backticks unchanged', () => {
|
||||
const result = mapParametersToCrowdStrikeArguments('runscript', {
|
||||
commandLine: '```echo Hello World```',
|
||||
});
|
||||
expect(result).toBe('runscript --CommandLine=```echo Hello World```');
|
||||
});
|
||||
|
||||
it('trims spaces from parameter values', () => {
|
||||
const result = mapParametersToCrowdStrikeArguments('runscript', { raw: ' echo Hello ' });
|
||||
expect(result).toBe('runscript --Raw=```echo Hello```');
|
||||
});
|
||||
|
||||
it('handles multiple parameters correctly', () => {
|
||||
const result = mapParametersToCrowdStrikeArguments('runscript', {
|
||||
raw: 'echo Hello',
|
||||
commandLine: 'echo Hello World',
|
||||
hostPath: '/home/user',
|
||||
cloudFile: 'file.txt',
|
||||
});
|
||||
expect(result).toBe(
|
||||
'runscript --Raw=```echo Hello``` --CommandLine=```echo Hello World``` --HostPath=/home/user --CloudFile=file.txt'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns command with no parameters correctly', () => {
|
||||
const result = mapParametersToCrowdStrikeArguments('runscript', {});
|
||||
expect(result).toBe('runscript ');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { upperFirst } from 'lodash';
|
||||
import type { RunScriptActionRequestBody } from '../../../../../../common/api/endpoint';
|
||||
|
||||
export const mapParametersToCrowdStrikeArguments = (
|
||||
commandName: string,
|
||||
parameters: RunScriptActionRequestBody['parameters']
|
||||
): string => {
|
||||
// Map each parameter to the required syntax and join them with spaces
|
||||
// In short: this function has to transform the parameters object into a string that can be used as a CS command
|
||||
// One word commands eg. 'ls' can go as it is, but if there are more elements eg. 'ls -l', they have to be wrapped in triple backticks
|
||||
const commandParts = Object.entries(parameters).map(([key, value]) => {
|
||||
// Check and process the parameter value
|
||||
let sanitizedValue;
|
||||
if (typeof value === 'string') {
|
||||
if (/^```.*```$/.test(value)) {
|
||||
// If already wrapped in triple backticks, leave unchanged
|
||||
sanitizedValue = value;
|
||||
} else {
|
||||
const strippedValue = value.trim(); // Remove spaces at the beginning and end
|
||||
if (strippedValue.split(/\s+/).length === 1) {
|
||||
// If it's a single element (no spaces), use it as-is
|
||||
sanitizedValue = strippedValue;
|
||||
} else {
|
||||
// If it contains multiple elements (spaces), wrap in ```
|
||||
sanitizedValue = `\`\`\`${strippedValue}\`\`\``;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sanitizedValue = value;
|
||||
}
|
||||
return `--${upperFirst(key)}=${sanitizedValue}`;
|
||||
});
|
||||
|
||||
// Combine the base command with the constructed parameters
|
||||
return `${commandName} ${commandParts.join(' ')}`;
|
||||
};
|
|
@ -248,7 +248,7 @@ const createRunScriptOptionsMock = (
|
|||
const options: RunScriptActionRequestBody = {
|
||||
...createNoParamsResponseActionOptionsMock(),
|
||||
parameters: {
|
||||
Raw: 'ls',
|
||||
raw: 'ls',
|
||||
},
|
||||
};
|
||||
return merge(options, overrides);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue