Skip to content

Commit

Permalink
[EDR Workflows] Add RunScript API route (supporting CrowdStrike) (ela…
Browse files Browse the repository at this point in the history
…stic#203101)

(cherry picked from commit e993f23)

# Conflicts:
#	x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts
  • Loading branch information
tomsonpl committed Dec 11, 2024
1 parent ab026f8 commit cd30795
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 15 deletions.
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 * from './run_script';
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import { BaseActionRequestSchema } from '../../common/base';

const { parameters, ...restBaseSchema } = BaseActionRequestSchema;
const NonEmptyString = schema.string({
minLength: 1,
validate: (value) => {
if (!value.trim().length) {
return 'Raw cannot be an empty string';
}
},
});
export const RunScriptActionRequestSchema = {
body: schema.object({
...restBaseSchema,
parameters: schema.object(
{
/**
* The script to run
*/
Raw: schema.maybe(NonEmptyString),
/**
* The path to the script on the host to run
*/
HostPath: schema.maybe(NonEmptyString),
/**
* The path to the script in the cloud to run
*/
CloudFile: schema.maybe(NonEmptyString),
/**
* The command line to run
*/
CommandLine: schema.maybe(NonEmptyString),
/**
* The max timeout value before the command is killed. Number represents milliseconds
*/
Timeout: schema.maybe(schema.number({ min: 1 })),
},
{
validate: (params) => {
if (!params.Raw && !params.HostPath && !params.CloudFile) {
return 'At least one of Raw, HostPath, or CloudFile must be provided';
}
},
}
),
}),
};

export type RunScriptActionRequestBody = TypeOf<typeof RunScriptActionRequestSchema.body>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
openapi: 3.0.0
info:
title: RunScript Action Schema
version: '2023-10-31'
paths:
/api/endpoint/action/runscript:
post:
summary: Run a script
operationId: RunScriptAction
description: Run a shell command on an endpoint.
x-codegen-enabled: true
x-labels: [ ess, serverless ]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RunScriptRouteRequestBody'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '../../../model/schema/common.schema.yaml#/components/schemas/SuccessResponse'

components:
schemas:
RunScriptRouteRequestBody:
allOf:
- $ref: '../../../model/schema/common.schema.yaml#/components/schemas/BaseActionSchema'
- type: object
required:
- parameters
properties:
parameters:
oneOf:
- type: object
properties:
Raw:
type: string
minLength: 1
description: Raw script content.
required:
- Raw
- type: object
properties:
HostPath:
type: string
minLength: 1
description: Absolute or relative path of script on host machine.
required:
- HostPath
- type: object
properties:
CloudFile:
type: string
minLength: 1
description: Script name in cloud storage.
required:
- CloudFile
- type: object
properties:
CommandLine:
type: string
minLength: 1
description: Command line arguments.
required:
- CommandLine
properties:
Timeout:
type: integer
minimum: 1
description: Timeout in seconds.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './actions/response_actions/get_file';
export * from './actions/response_actions/execute';
export * from './actions/response_actions/upload';
export * from './actions/response_actions/scan';
export * from './actions/response_actions/run_script';

export * from './metadata';

Expand Down
30 changes: 28 additions & 2 deletions x-pack/plugins/security_solution/common/endpoint/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export interface ResponseActionScanOutputContent {
code: string;
}

export interface ResponseActionRunScriptOutputContent {
output: string;
code: string;
}

export const ActivityLogItemTypes = {
ACTION: 'action' as const,
RESPONSE: 'response' as const,
Expand Down Expand Up @@ -216,13 +221,29 @@ export interface ResponseActionScanParameters {
path: string;
}

// Currently reflecting CrowdStrike's RunScript parameters
interface ActionsRunScriptParametersBase {
Raw?: string;
HostPath?: string;
CloudFile?: string;
CommandLine?: string;
Timeout?: number;
}

// Enforce at least one of the script parameters is required
export type ResponseActionRunScriptParameters = AtLeastOne<
ActionsRunScriptParametersBase,
'Raw' | 'HostPath' | 'CloudFile'
>;

export type EndpointActionDataParameterTypes =
| undefined
| ResponseActionParametersWithProcessData
| ResponseActionsExecuteParameters
| ResponseActionGetFileParameters
| ResponseActionUploadParameters
| ResponseActionScanParameters;
| ResponseActionScanParameters
| ResponseActionRunScriptParameters;

/** Output content of the different response actions */
export type EndpointActionResponseDataOutput =
Expand All @@ -233,7 +254,8 @@ export type EndpointActionResponseDataOutput =
| GetProcessesActionOutputContent
| SuspendProcessActionOutputContent
| KillProcessActionOutputContent
| ResponseActionScanOutputContent;
| ResponseActionScanOutputContent
| ResponseActionRunScriptOutputContent;

/**
* The data stored with each Response Action under `EndpointActions.data` property
Expand Down Expand Up @@ -571,3 +593,7 @@ export interface ResponseActionUploadOutputContent {
/** The free space available (after saving the file) of the drive where the file was saved to, In Bytes */
disk_free_space: number;
}

type AtLeastOne<T, K extends keyof T = keyof T> = K extends keyof T
? Required<Pick<T, K>> & Partial<Omit<T, K>>
: never;
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ export const getEndpointConsoleCommands = ({
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ const CODES = Object.freeze({
),

// Dev:
// scan success/competed
// scan success/completed
ra_scan_success_done: i18n.translate(
'xpack.securitySolution.endpointActionResponseCodes.scan.success',
{ defaultMessage: 'Scan complete' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const getConsoleHelpPanelResponseActionTestSubj = (): Record<
execute: 'endpointResponseActionsConsole-commandList-Responseactions-execute',
upload: 'endpointResponseActionsConsole-commandList-Responseactions-upload',
scan: 'endpointResponseActionsConsole-commandList-Responseactions-scan',
// Not implemented in Endpoint yet
// runscript: 'endpointResponseActionsConsole-commandList-Responseactions-runscript',
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,29 @@ import { getResponseActionsClient, NormalizedExternalConnectorClient } from '../
import type { ResponseActionsClient } from '../../services/actions/clients/lib/types';
import { CustomHttpRequestError } from '../../../utils/custom_http_request_error';
import type {
KillProcessRequestBody,
SuspendProcessRequestBody,
} from '../../../../common/api/endpoint';
ResponseActionAgentType,
ResponseActionsApiCommandNames,
} from '../../../../common/endpoint/service/response_actions/constants';
import type { RunScriptActionRequestBody } from '../../../../common/api/endpoint';
import {
EndpointActionGetFileSchema,
type ExecuteActionRequestBody,
ExecuteActionRequestSchema,
GetProcessesRouteRequestSchema,
IsolateRouteRequestSchema,
type KillProcessRequestBody,
KillProcessRouteRequestSchema,
type NoParametersRequestSchema,
type ResponseActionGetFileRequestBody,
type ResponseActionsRequestBody,
type ScanActionRequestBody,
ScanActionRequestSchema,
type SuspendProcessRequestBody,
SuspendProcessRouteRequestSchema,
UnisolateRouteRequestSchema,
type UploadActionApiRequestBody,
UploadActionRequestSchema,
RunScriptActionRequestSchema,
} from '../../../../common/api/endpoint';

import {
Expand All @@ -42,30 +46,33 @@ import {
ISOLATE_HOST_ROUTE,
ISOLATE_HOST_ROUTE_V2,
KILL_PROCESS_ROUTE,
RUN_SCRIPT_ROUTE,
SCAN_ROUTE,
SUSPEND_PROCESS_ROUTE,
UNISOLATE_HOST_ROUTE,
UNISOLATE_HOST_ROUTE_V2,
UPLOAD_ROUTE,
} from '../../../../common/endpoint/constants';
import type {
ActionDetails,
EndpointActionDataParameterTypes,
ResponseActionParametersWithProcessData,
ResponseActionsExecuteParameters,
ResponseActionScanParameters,
EndpointActionDataParameterTypes,
ActionDetails,
ResponseActionRunScriptParameters,
} from '../../../../common/endpoint/types';
import type {
ResponseActionAgentType,
ResponseActionsApiCommandNames,
} from '../../../../common/endpoint/service/response_actions/constants';
import type {
SecuritySolutionPluginRouter,
SecuritySolutionRequestHandlerContext,
} from '../../../types';
import type { EndpointAppContext } from '../../types';
import { withEndpointAuthz } from '../with_endpoint_authz';
import { stringify } from '../../utils/stringify';
import { errorHandler } from '../error_handler';
import { CustomHttpRequestError } from '../../../utils/custom_http_request_error';
import type { ResponseActionsClient } from '../../services';
import { getResponseActionsClient, NormalizedExternalConnectorClient } from '../../services';
import { responseActionsWithLegacyActionProperty } from '../../services/actions/constants';

export function registerResponseActionRoutes(
router: SecuritySolutionPluginRouter,
Expand Down Expand Up @@ -363,6 +370,33 @@ export function registerResponseActionRoutes(
responseActionRequestHandler<ResponseActionScanParameters>(endpointContext, 'scan')
)
);
router.versioned
.post({
access: 'public',
path: RUN_SCRIPT_ROUTE,
security: {
authz: {
requiredPrivileges: ['securitySolution'],
},
},
options: { authRequired: true },
})
.addVersion(
{
version: '2023-10-31',
validate: {
request: RunScriptActionRequestSchema,
},
},
withEndpointAuthz(
{ all: ['canWriteExecuteOperations'] },
logger,
responseActionRequestHandler<ResponseActionRunScriptParameters>(
endpointContext,
'runscript'
)
)
);
}

function responseActionRequestHandler<T extends EndpointActionDataParameterTypes>(
Expand Down Expand Up @@ -468,6 +502,8 @@ async function handleActionCreation(
return responseActionsClient.upload(body as UploadActionApiRequestBody);
case 'scan':
return responseActionsClient.scan(body as ScanActionRequestBody);
case 'runscript':
return responseActionsClient.runscript(body as RunScriptActionRequestBody);
default:
throw new CustomHttpRequestError(
`No handler found for response action command: [${command}]`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ import type {
EndpointActionDataParameterTypes,
EndpointActionResponseDataOutput,
LogsEndpointAction,
ResponseActionRunScriptOutputContent,
ResponseActionRunScriptParameters,
} from '../../../../../../common/endpoint/types';
import type {
IsolationRouteRequestBody,
RunScriptActionRequestBody,
UnisolationRouteRequestBody,
} from '../../../../../../common/api/endpoint';
import type {
Expand Down Expand Up @@ -296,6 +299,19 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
}

public async runscript(
actionRequest: RunScriptActionRequestBody,
options?: CommonResponseActionMethodOptions
): Promise<
ActionDetails<ResponseActionRunScriptOutputContent, ResponseActionRunScriptParameters>
> {
// TODO: just a placeholder for now
return Promise.resolve({ output: 'runscript', code: 200 }) as never as ActionDetails<
ResponseActionRunScriptOutputContent,
ResponseActionRunScriptParameters
>;
}

private async completeCrowdstrikeAction(
actionResponse: ActionTypeExecutorResult<CrowdstrikeBaseApiResponse> | undefined,
doc: LogsEndpointAction
Expand Down
Loading

0 comments on commit cd30795

Please sign in to comment.