diff --git a/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts b/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts index d5d4154f985e9..40e58613c69e0 100644 --- a/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts +++ b/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts @@ -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({}); diff --git a/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts b/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts index 3c9cc15ea167e..68f8275d71434 100644 --- a/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts +++ b/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts @@ -17,8 +17,8 @@ import { CrowdstrikeGetTokenResponseSchema, CrowdstrikeGetAgentsResponseSchema, RelaxedCrowdstrikeBaseApiResponseSchema, - CrowdstrikeInitRTRResponseSchema, CrowdstrikeInitRTRParamsSchema, + CrowdstrikeExecuteRTRResponseSchema, } from './schema'; export type CrowdstrikeConfig = TypeOf; @@ -35,9 +35,10 @@ export type CrowdstrikeGetAgentOnlineStatusResponse = TypeOf< typeof CrowdstrikeGetAgentOnlineStatusResponseSchema >; export type CrowdstrikeGetTokenResponse = TypeOf; -export type CrowdstrikeInitRTRResponse = TypeOf; export type CrowdstrikeHostActionsParams = TypeOf; export type CrowdstrikeActionParams = TypeOf; export type CrowdstrikeInitRTRParams = TypeOf; + +export type CrowdStrikeExecuteRTRResponse = TypeOf; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts index 14b38b414eb3b..9bc53c58aa198 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts @@ -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 => { - // 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( { - 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 { @@ -351,7 +344,6 @@ export class CrowdstrikeConnector extends SubActionConnector< payload: { command: string; endpoint_ids: string[]; - overwriteUrl?: 'batchAdminExecuteRTR'; }, connectorUsageCollector: ConnectorUsageCollector ): Promise { diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts index 56b1fafdb5a71..3a765e0c83134 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts @@ -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; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts index dfa88941b34e0..95c035e866884 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts @@ -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'; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts index 707be0a4d1e65..ece0a9501e3fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts @@ -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 +): 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]); diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts index 131a8d0c6df5c..e3c47102ce719 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts @@ -97,7 +97,8 @@ export interface ResponseActionScanOutputContent { } export interface ResponseActionRunScriptOutputContent { - output: string; + stdout: string; + stderr: string; code: string; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts index 6929452e5d9e9..50885a9db6b6a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts @@ -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. diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx index 43ff3ffca5a62..09d1e45f24c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx @@ -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 + | ActionDetails + | ActionDetails >; 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( ({ action, @@ -33,6 +39,8 @@ export const ExecuteActionHostResponse = memo( canAccessFileDownloadLink, textSize = 's', 'data-test-subj': dataTestSubj, + hideFile, + hideContext, }) => { const outputContent = useMemo( () => @@ -44,21 +52,24 @@ export const ExecuteActionHostResponse = memo( return ( <> - - - - + {!hideFile && ( + + + + + )} {outputContent && ( )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx index f24f18e149393..4e6161b8767d9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx @@ -98,6 +98,7 @@ interface ShellInfoContentProps { textSize?: 's' | 'xs'; title: string; } + const ShellInfoContent = memo(({ content, textSize, title }) => ( @@ -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( - ({ 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 - - - + {!hideContext && ( + + + + )} {outputContent.stderr.length > 0 && ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx new file mode 100644 index 0000000000000..e2ed358455027 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx @@ -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(() => { + 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 ( + + + + ); +}); +RunScriptActionResult.displayName = 'RunScriptActionResult'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index 8c99186f69d93..22d138dea3586 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -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), diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx index cba1d0aee41b4..53f2c89ac84fc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx @@ -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 ( + + {action.agents.map((agentId) => ( +
+ {OUTPUT_MESSAGES.wasSuccessful(command)} + +
+ ))} +
+ ); + } + + // CrowdStrike Isolate/Release actions if (action.agentType === 'crowdstrike') { return <>{OUTPUT_MESSAGES.submittedSuccessfully(command)}; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.test.ts b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.test.ts new file mode 100644 index 0000000000000..39eb3728162dc --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.test.ts @@ -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; + 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); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.ts b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.ts new file mode 100644 index 0000000000000..f4dd937fe3008 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.ts @@ -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( + (runScriptActionReqBody) => { + return KibanaServices.get().http.post(RUN_SCRIPT_ROUTE, { + body: JSON.stringify(runScriptActionReqBody), + version: '2023-10-31', + }); + }, + options + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts b/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts index 256484f8d0e92..e8307895c3c4c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts @@ -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; + + runscript: () => ActionDetailsApiResponse; }>; export const responseActionsHttpMocks = httpHandlerMockFactory([ @@ -273,6 +278,25 @@ export const responseActionsHttpMocks = httpHandlerMockFactory => { + const generator = new EndpointActionGenerator('seed'); + const response = generator.generateActionDetails< + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters + >({ + command: 'runscript', + }); + return { data: response }; }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts index 1987a019f8887..d54df140b3b4c 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts @@ -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({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts index 0c505b12c129d..25fcb30be4949 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts @@ -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 > { - // 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 | 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; + } 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, + doc: LogsEndpointAction + ): Promise { + 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 | undefined, + actionResponse: ActionTypeExecutorResult, doc: LogsEndpointAction ): Promise { const options = { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.test.ts new file mode 100644 index 0000000000000..f961c7a3d1d6b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.test.ts @@ -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 '); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.ts new file mode 100644 index 0000000000000..2ec2ec2bb0cf8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.ts @@ -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(' ')}`; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts index 6360ceba71cef..00b4774d9489c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts @@ -248,7 +248,7 @@ const createRunScriptOptionsMock = ( const options: RunScriptActionRequestBody = { ...createNoParamsResponseActionOptionsMock(), parameters: { - Raw: 'ls', + raw: 'ls', }, }; return merge(options, overrides);