Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -8,31 +8,30 @@
import type { RequestHandler } from '@kbn/core/server';
import type { TypeOf } from '@kbn/config-schema';

import { responseActionsWithLegacyActionProperty } from '../../services/actions/constants';
import { stringify } from '../../utils/stringify';
import { getResponseActionsClient, NormalizedExternalConnectorClient } from '../../services';
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 +41,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 +365,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 +497,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