From cd30795448bad90840c67dda137e71b08e08a62b Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Wed, 11 Dec 2024 09:18:35 +0100 Subject: [PATCH] [EDR Workflows] Add RunScript API route (supporting CrowdStrike) (#203101) (cherry picked from commit e993f2388111cf16798014e7c857f23df5ab7cdb) # Conflicts: # x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts --- .../response_actions/run_script/index.ts | 8 ++ .../response_actions/run_script/run_script.ts | 58 +++++++++++++++ .../run_script/run_script.yaml | 74 +++++++++++++++++++ .../common/api/endpoint/index.ts | 1 + .../common/endpoint/types/actions.ts | 30 +++++++- .../lib/console_commands_definition.ts | 2 +- .../lib/endpoint_action_response_codes.ts | 2 +- .../management/cypress/screens/responder.ts | 2 + .../routes/actions/response_actions.ts | 54 +++++++++++--- .../crowdstrike/crowdstrike_actions_client.ts | 16 ++++ .../endpoint/endpoint_actions_client.test.ts | 6 +- .../lib/base_response_actions_client.ts | 12 +++ .../services/actions/clients/lib/types.ts | 15 ++++ .../services/actions/clients/mocks.ts | 15 ++++ .../services/feature_usage/feature_keys.ts | 2 +- 15 files changed, 282 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/index.ts create mode 100644 x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts create mode 100644 x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/index.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/index.ts new file mode 100644 index 0000000000000..0d09b1656ba87 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/index.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts new file mode 100644 index 0000000000000..dfa88941b34e0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts @@ -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; diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml b/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml new file mode 100644 index 0000000000000..228070e1d0277 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml @@ -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. diff --git a/x-pack/plugins/security_solution/common/api/endpoint/index.ts b/x-pack/plugins/security_solution/common/api/endpoint/index.ts index 5917101be93a1..af2df1d1ce09a 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/index.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/index.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 061182d5075ac..131a8d0c6df5c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -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, @@ -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 = @@ -233,7 +254,8 @@ export type EndpointActionResponseDataOutput = | GetProcessesActionOutputContent | SuspendProcessActionOutputContent | KillProcessActionOutputContent - | ResponseActionScanOutputContent; + | ResponseActionScanOutputContent + | ResponseActionRunScriptOutputContent; /** * The data stored with each Response Action under `EndpointActions.data` property @@ -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 = K extends keyof T + ? Required> & Partial> + : never; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index 6851111f99dd6..8c99186f69d93 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -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), diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts index da407b589a84d..a539fec35b194 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts @@ -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' } diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts index d3cec917610fe..7575b1ba7d1b5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts @@ -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', }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts index 0fc90c7589b99..3c3f891af3c8a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts @@ -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 { @@ -42,6 +46,7 @@ import { ISOLATE_HOST_ROUTE, ISOLATE_HOST_ROUTE_V2, KILL_PROCESS_ROUTE, + RUN_SCRIPT_ROUTE, SCAN_ROUTE, SUSPEND_PROCESS_ROUTE, UNISOLATE_HOST_ROUTE, @@ -49,23 +54,25 @@ import { 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, @@ -363,6 +370,33 @@ export function registerResponseActionRoutes( responseActionRequestHandler(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( + endpointContext, + 'runscript' + ) + ) + ); } function responseActionRequestHandler( @@ -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}]`, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts index f4ad7f981ab9d..0c505b12c129d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts @@ -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 { @@ -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 + > { + // TODO: just a placeholder for now + return Promise.resolve({ output: 'runscript', code: 200 }) as never as ActionDetails< + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters + >; + } + private async completeCrowdstrikeAction( actionResponse: ActionTypeExecutorResult | undefined, doc: LogsEndpointAction diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts index a406397c2be19..3dde5e798c666 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts @@ -347,7 +347,8 @@ describe('EndpointActionsClient', () => { type ResponseActionsMethodsOnly = keyof Omit< ResponseActionsClient, - 'processPendingActions' | 'getFileDownload' | 'getFileInfo' + // TODO: not yet implemented + 'processPendingActions' | 'getFileDownload' | 'getFileInfo' | 'runscript' >; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -375,6 +376,9 @@ describe('EndpointActionsClient', () => { upload: responseActionsClientMock.createUploadOptions(getCommonResponseActionOptions()), scan: responseActionsClientMock.createScanOptions(getCommonResponseActionOptions()), + + // TODO: not yet implemented + // runscript: responseActionsClientMock.createRunScriptOptions(getCommonResponseActionOptions()), }; it.each(Object.keys(responseActionMethods) as ResponseActionsMethodsOnly[])( diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts index 7a8f14b6e9a8e..3e4c21d403bf7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts @@ -71,6 +71,8 @@ import type { SuspendProcessActionOutputContent, UploadedFileInfo, WithAllKeys, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, } from '../../../../../../common/endpoint/types'; import type { ExecuteActionRequestBody, @@ -79,6 +81,7 @@ import type { KillProcessRequestBody, ResponseActionGetFileRequestBody, ResponseActionsRequestBody, + RunScriptActionRequestBody, ScanActionRequestBody, SuspendProcessRequestBody, UnisolationRouteRequestBody, @@ -841,6 +844,15 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient throw new ResponseActionsNotSupportedError('scan'); } + public async runscript( + actionRequest: RunScriptActionRequestBody, + options?: CommonResponseActionMethodOptions + ): Promise< + ActionDetails + > { + throw new ResponseActionsNotSupportedError('runscript'); + } + public async processPendingActions(_: ProcessPendingActionsMethodOptions): Promise { this.log.debug(`#processPendingActions() method is not implemented for ${this.agentType}!`); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts index e3407b5ba959a..a703e3c16cdd6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts @@ -23,6 +23,8 @@ import type { UploadedFileInfo, ResponseActionScanOutputContent, ResponseActionScanParameters, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, } from '../../../../../../common/endpoint/types'; import type { IsolationRouteRequestBody, @@ -35,6 +37,7 @@ import type { ScanActionRequestBody, KillProcessRequestBody, SuspendProcessRequestBody, + RunScriptActionRequestBody, } from '../../../../../../common/api/endpoint'; type OmitUnsupportedAttributes = Omit< @@ -155,4 +158,16 @@ export interface ResponseActionsClient { actionRequest: OmitUnsupportedAttributes, options?: CommonResponseActionMethodOptions ) => Promise>; + + /** + * Run a script + * @param actionRequest + * @param options + */ + runscript: ( + actionRequest: OmitUnsupportedAttributes, + options?: CommonResponseActionMethodOptions + ) => Promise< + ActionDetails + >; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts index 69901033eaafd..6360ceba71cef 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts @@ -49,6 +49,7 @@ import type { ResponseActionGetFileRequestBody, UploadActionApiRequestBody, ScanActionRequestBody, + RunScriptActionRequestBody, } from '../../../../../common/api/endpoint'; export interface ResponseActionsClientOptionsMock extends ResponseActionsClientOptions { @@ -70,6 +71,7 @@ const createResponseActionClientMock = (): jest.Mocked => getFileInfo: jest.fn().mockReturnValue(Promise.resolve()), getFileDownload: jest.fn().mockReturnValue(Promise.resolve()), scan: jest.fn().mockReturnValue(Promise.resolve()), + runscript: jest.fn().mockReturnValue(Promise.resolve()), }; }; @@ -240,6 +242,18 @@ const createScanOptionsMock = ( return merge(options, overrides); }; +const createRunScriptOptionsMock = ( + overrides: Partial = {} +): RunScriptActionRequestBody => { + const options: RunScriptActionRequestBody = { + ...createNoParamsResponseActionOptionsMock(), + parameters: { + Raw: 'ls', + }, + }; + return merge(options, overrides); +}; + const createConnectorMock = ( overrides: DeepPartial = {} ): ConnectorWithExtraFindData => { @@ -316,6 +330,7 @@ export const responseActionsClientMock = Object.freeze({ createExecuteOptions: createExecuteOptionsMock, createUploadOptions: createUploadOptionsMock, createScanOptions: createScanOptionsMock, + createRunScriptOptions: createRunScriptOptionsMock, createIndexedResponse: createEsIndexTransportResponseMock, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts index d29e9b70606e0..65db7d72cc48e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts @@ -24,9 +24,9 @@ export const FEATURE_KEYS = { UPLOAD: 'Upload file', EXECUTE: 'Execute command', SCAN: 'Scan files', + RUN_SCRIPT: 'Run script', ALERTS_BY_PROCESS_ANCESTRY: 'Get related alerts by process ancestry', ENDPOINT_EXCEPTIONS: 'Endpoint exceptions', - RUN_SCRIPT: 'Run script', } as const; export type FeatureKeys = keyof typeof FEATURE_KEYS;