From 9b27804a9b4f90a48f68c1f543270373d7bab6db Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Tue, 10 Dec 2024 15:02:12 +0100 Subject: [PATCH] [EDR Workflows] Add RunScript CS Command - UI (#202012) --- .../common/endpoint/constants.ts | 1 + .../endpoint_action_generator.ts | 11 +++ .../service/response_actions/constants.ts | 8 ++ .../is_response_action_supported.ts | 12 +++ .../common/experimental_features.ts | 6 ++ .../public/management/common/translations.ts | 82 +++++++++++++++++++ .../console/components/command_usage.tsx | 11 ++- .../management/components/console/types.ts | 14 +++- .../lib/console_commands_definition.ts | 68 ++++++++++++++- .../console_commands_definition.test.tsx | 8 +- .../components/actions_log_filter.tsx | 3 + .../components/actions_log_filters.tsx | 4 + .../components/hooks.tsx | 4 + .../response_actions_log.test.tsx | 7 +- .../response_actions_log.tsx | 3 + .../serverless/feature_access/complete.cy.ts | 4 +- .../feature_access/essentials.cy.ts | 4 +- .../essentials_with_endpoint.cy.ts | 4 +- .../roles/complete_with_endpoint_roles.cy.ts | 6 +- .../management/cypress/screens/responder.ts | 3 +- .../cypress/tasks/response_actions.ts | 6 ++ .../server/endpoint/routes/actions/utils.ts | 5 ++ .../services/actions/action_list.test.ts | 2 +- .../services/feature_usage/feature_keys.ts | 2 + 24 files changed, 259 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 6aae156e9ac11..c08dc5b811f84 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -96,6 +96,7 @@ export const GET_FILE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/get_file`; export const EXECUTE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/execute`; export const UPLOAD_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/upload`; export const SCAN_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/scan`; +export const RUN_SCRIPT_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/run_script`; /** Endpoint Actions Routes */ export const ENDPOINT_ACTION_LOG_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_log/{agent_id}`; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index 91de8579426ea..e221bad5fcb28 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts @@ -147,6 +147,17 @@ export class EndpointActionGenerator extends BaseDataGenerator { } } + if (command === 'runscript') { + if (!output) { + output = { + type: 'json', + content: { + code: '200', + }, + }; + } + } + if (command === 'execute') { if (!output) { output = this.generateExecuteActionResponseOutput(); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts index 3f06fbd4e4ffc..d60002d5a060c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts @@ -28,6 +28,7 @@ export const RESPONSE_ACTION_API_COMMANDS_NAMES = [ 'execute', 'upload', 'scan', + 'runscript', ] as const; export type ResponseActionsApiCommandNames = (typeof RESPONSE_ACTION_API_COMMANDS_NAMES)[number]; @@ -54,6 +55,7 @@ export const ENDPOINT_CAPABILITIES = [ 'execute', 'upload_file', 'scan', + 'runscript', ] as const; export type EndpointCapabilities = (typeof ENDPOINT_CAPABILITIES)[number]; @@ -72,6 +74,7 @@ export const CONSOLE_RESPONSE_ACTION_COMMANDS = [ 'execute', 'upload', 'scan', + 'runscript', ] as const; export type ConsoleResponseActionCommands = (typeof CONSOLE_RESPONSE_ACTION_COMMANDS)[number]; @@ -100,6 +103,7 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL: Record< execute: 'writeExecuteOperations', upload: 'writeFileOperations', scan: 'writeScanOperations', + runscript: 'writeExecuteOperations', }); export const RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP = Object.freeze< @@ -114,6 +118,7 @@ export const RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP = Object.freeze< 'suspend-process': 'suspend-process', upload: 'upload', scan: 'scan', + runscript: 'runscript', }); export const RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP = Object.freeze< @@ -128,6 +133,7 @@ export const RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP = Object.freeze< 'suspend-process': 'suspend-process', upload: 'upload', scan: 'scan', + runscript: 'runscript', }); export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY = Object.freeze< @@ -142,6 +148,7 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY = Object.fr 'suspend-process': 'suspend_process', upload: 'upload_file', scan: 'scan', + runscript: 'runscript', }); /** @@ -159,6 +166,7 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ = Object.freeze< 'kill-process': 'canKillProcess', 'suspend-process': 'canSuspendProcess', scan: 'canWriteScanOperations', + runscript: 'canWriteExecuteOperations', }); // 4 hrs in seconds diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts index 928327fc0e28d..0e1fc072b2604 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts @@ -126,6 +126,18 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = { crowdstrike: false, }, }, + runscript: { + automated: { + endpoint: false, + sentinel_one: false, + crowdstrike: false, + }, + manual: { + endpoint: false, + sentinel_one: false, + crowdstrike: true, + }, + }, }; /** diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index def958c07bd2d..03412c7ff4b26 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -257,6 +257,12 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the Defend Insights feature */ defendInsights: false, + + /** + * Enables CrowdStrike's RunScript RTR command + */ + + crowdstrikeRunScriptEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index c07c78deb4c70..6929452e5d9e9 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -215,6 +215,88 @@ export const CONSOLE_COMMANDS = { }, }; +export const CROWDSTRIKE_CONSOLE_COMMANDS = { + runscript: { + args: { + raw: { + about: i18n.translate( + 'xpack.securitySolution.crowdStrikeConsoleCommands.runscript.args.raw.about', + { + defaultMessage: 'Raw script content', + } + ), + }, + cloudFile: { + about: i18n.translate( + 'xpack.securitySolution.crowdStrikeConsoleCommands.runscript.args.cloudFile.about', + { + defaultMessage: 'Script name in cloud storage', + } + ), + }, + commandLine: { + about: i18n.translate( + 'xpack.securitySolution.crowdStrikeConsoleCommands.runscript.args.commandLine.about', + { + defaultMessage: 'Command line arguments', + } + ), + }, + hostPath: { + about: i18n.translate( + 'xpack.securitySolution.crowdStrikeConsoleCommands.runscript.args.hostPath.about', + { + defaultMessage: 'Absolute or relative path of script on host machine', + } + ), + }, + timeout: { + about: i18n.translate( + 'xpack.securitySolution.crowdStrikeConsoleCommands.runscript.args.timeout.about', + { + defaultMessage: 'Timeout in seconds', + } + ), + }, + }, + title: i18n.translate('xpack.securitySolution.crowdStrikeConsoleCommands.runscript.title', { + defaultMessage: 'Isolate', + }), + about: i18n.translate('xpack.securitySolution.crowdStrikeConsoleCommands.runscript.about', { + defaultMessage: 'Run a script on the host', + }), + helpUsage: i18n.translate('xpack.securitySolution.crowdStrikeConsoleCommands.runscript.about', { + defaultMessage: ` +Command Examples for Running Scripts: + +1. Executes a script saved in the CrowdStrike cloud with the specified command-line arguments. + + runscript --CloudFile="CloudScript1.ps1" --CommandLine="-Verbose true" + +2. Executes a script saved in the CrowdStrike cloud with the specified command-line arguments and a 180-second timeout. + + runscript --CloudFile="CloudScript1.ps1" --CommandLine="-Verbose true" -Timeout=180 + +3. Executes a raw script provided entirely within the "--Raw" flag. + + runscript --Raw="Get-ChildItem." + +4. Executes a script located on the remote host at the specified path with the provided command-line arguments. + + runscript --HostPath="C:\\temp\\LocalScript.ps1" --CommandLine="-Verbose true" + +`, + }), + privileges: i18n.translate( + 'xpack.securitySolution.crowdStrikeConsoleCommands.runscript.privileges', + { + defaultMessage: + 'Insufficient privileges to run script. Contact your Kibana administrator if you think you should have this permission.', + } + ), + }, +}; + export const CONFIRM_WARNING_MODAL_LABELS = (entryType: string) => { return { title: i18n.translate('xpack.securitySolution.artifacts.confirmWarningModal.title', { diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx index 7d1895ba7d1b7..ef88832c9d2e5 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx @@ -33,6 +33,13 @@ export const CommandInputUsage = memo>(({ }); }, [commandDef]); + const helpExample = useMemo(() => { + if (commandDef.helpUsage) { + return commandDef.helpUsage; + } + return commandDef.exampleUsage; + }, [commandDef]); + return ( <> >(({ titleProps={additionalProps} /> - {commandDef.exampleUsage && ( + {helpExample && ( >(({ })} ), - description: {commandDef.exampleUsage}, + description: {helpExample}, }, ]} descriptionProps={additionalProps} diff --git a/x-pack/plugins/security_solution/public/management/components/console/types.ts b/x-pack/plugins/security_solution/public/management/components/console/types.ts index fb4e836f26c42..68cf5c62ab929 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/types.ts @@ -49,7 +49,12 @@ export interface CommandArgDefinition { * - `truthy`: The argument must have a value and the values must be "truthy" (evaluate to `Boolean` true) */ mustHaveValue?: boolean | 'non-empty-string' | 'number' | 'number-greater-than-zero' | 'truthy'; + + /** + * Specifies that one or more arguments might be required, but only one of them can be used at a time. + */ exclusiveOr?: boolean; + /** * Validate the individual values given to this argument. * Should return `true` if valid or a string with the error message @@ -124,10 +129,17 @@ export interface CommandDefinition { /** * Displayed in the input hint area when the user types the command as well as in the output of * this command's `--help`. This value will override the command usage generated by the console - * from the Command Definition. + * from the Command Definition. It's value displayed in `--help` would overriden by `helpUsage` if defined. */ exampleUsage?: string; + /** + * Displayed in the output of this command's `--help`. + * This value will override the command usage generated by the console + * from the Command Definition. + */ + helpUsage?: string; + /** * Validate the command entered by the user. This is called only after the Console has ran * through all of its builtin validations (based on `CommandDefinition`). 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 efc52fd59c326..6851111f99dd6 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 @@ -42,7 +42,7 @@ import { import { getCommandAboutInfo } from './get_command_about_info'; import { validateUnitOfTime } from './utils'; -import { CONSOLE_COMMANDS } from '../../../common/translations'; +import { CONSOLE_COMMANDS, CROWDSTRIKE_CONSOLE_COMMANDS } from '../../../common/translations'; import { ScanActionResult } from '../command_render_components/scan_action'; const emptyArgumentValidator = (argData: ParsedArgData): true | string => { @@ -167,6 +167,7 @@ export const getEndpointConsoleCommands = ({ const featureFlags = ExperimentalFeaturesService.get(); const isUploadEnabled = featureFlags.responseActionUploadEnabled; + const crowdstrikeRunScriptEnabled = featureFlags.crowdstrikeRunScriptEnabled; const doesEndpointSupportCommand = (commandName: ConsoleResponseActionCommands) => { // Agent capabilities is only validated for Endpoint agent types @@ -523,6 +524,71 @@ export const getEndpointConsoleCommands = ({ privileges: endpointPrivileges, }), }); + if (crowdstrikeRunScriptEnabled) { + consoleCommands.push({ + name: 'runscript', + about: getCommandAboutInfo({ + aboutInfo: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about, + isSupported: doesEndpointSupportCommand('runscript'), + }), + RenderComponent: () => null, + meta: { + agentType, + endpointId: endpointAgentId, + capabilities: endpointCapabilities, + privileges: endpointPrivileges, + }, + exampleUsage: `runscript --Raw=\`\`\`Get-ChildItem .\`\`\` -CommandLine=""`, + helpUsage: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.helpUsage, + exampleInstruction: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about, + validate: capabilitiesAndPrivilegesValidator(agentType), + mustHaveArgs: true, + args: { + Raw: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.raw.about, + mustHaveValue: 'non-empty-string', + exclusiveOr: true, + }, + CloudFile: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.cloudFile.about, + mustHaveValue: 'non-empty-string', + exclusiveOr: true, + }, + CommandLine: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.commandLine.about, + mustHaveValue: 'non-empty-string', + }, + HostPath: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.hostPath.about, + mustHaveValue: 'non-empty-string', + exclusiveOr: true, + }, + Timeout: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.timeout.about, + mustHaveValue: 'number-greater-than-zero', + }, + ...commandCommentArgument(), + }, + helpGroupLabel: HELP_GROUPS.responseActions.label, + helpGroupPosition: HELP_GROUPS.responseActions.position, + helpCommandPosition: 9, + helpDisabled: !doesEndpointSupportCommand('runscript'), + helpHidden: !getRbacControl({ + commandName: 'runscript', + privileges: endpointPrivileges, + }), + }); + } switch (agentType) { case 'sentinel_one': diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx index caf33de458f83..863215f4c427c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/integration_tests/console_commands_definition.test.tsx @@ -73,7 +73,10 @@ describe('When displaying Endpoint Response Actions', () => { HELP_GROUPS.responseActions.label ); - const expectedCommands: string[] = [...CONSOLE_RESPONSE_ACTION_COMMANDS]; + const endpointCommands = CONSOLE_RESPONSE_ACTION_COMMANDS.filter( + (command) => command !== 'runscript' + ); + const expectedCommands: string[] = [...endpointCommands]; // add status to the list of expected commands in that order expectedCommands.splice(2, 0, 'status'); @@ -149,6 +152,7 @@ describe('When displaying Endpoint Response Actions', () => { beforeEach(() => { (ExperimentalFeaturesService.get as jest.Mock).mockReturnValue({ responseActionsCrowdstrikeManualHostIsolationEnabled: true, + crowdstrikeRunScriptEnabled: true, }); commands = getEndpointConsoleCommands({ agentType: 'crowdstrike', @@ -176,7 +180,7 @@ describe('When displaying Endpoint Response Actions', () => { HELP_GROUPS.responseActions.label ); - expect(commandsInPanel).toEqual(['isolate', 'release']); + expect(commandsInPanel).toEqual(['isolate', 'release', 'runscript --Raw']); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx index a7bf44db48f3b..f54062a8bf5b2 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx @@ -35,12 +35,14 @@ export const ActionsLogFilter = memo( isFlyout, onChangeFilterOptions, 'data-test-subj': dataTestSubj, + 'data-test-height': dataTestHeight, }: { filterName: ActionsLogPopupFilters; typesFilters?: TypesFilters; isFlyout: boolean; onChangeFilterOptions?: (selectedOptions: string[]) => void; 'data-test-subj'?: string; + 'data-test-height'?: number; }) => { const getTestId = useTestIdGenerator(dataTestSubj); @@ -263,6 +265,7 @@ export const ActionsLogFilter = memo( data-test-subj={dataTestSubj} > ['refetch']; showHostsFilter: boolean; 'data-test-subj'?: string; + 'data-test-height'?: number; }) => { const getTestId = useTestIdGenerator(dataTestSubj); @@ -76,6 +78,7 @@ export const ActionsLogFilters = memo( isFlyout={isFlyout} onChangeFilterOptions={onChangeCommandsFilter} data-test-subj={dataTestSubj} + data-test-height={dataTestHeight} /> ({ key: commandName, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index 98cfe87336adc..8a967da55c732 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -1518,6 +1518,7 @@ describe('Response actions history', () => { beforeEach(() => { featureFlags = { responseActionUploadEnabled: true, + crowdstrikeRunScriptEnabled: true, }; mockedContext.setExperimentalFlag(featureFlags); @@ -1537,8 +1538,9 @@ describe('Response actions history', () => { ); }); - it('should show a list of actions (with `scan`) when opened', async () => { - render(); + it('should show a list of actions (with `runscript`) when opened', async () => { + // Note: when we enable new commands, it might be needed to increase the height + render({ 'data-test-height': 350 }); const { getByTestId, getAllByTestId } = renderResult; await user.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); @@ -1557,6 +1559,7 @@ describe('Response actions history', () => { 'execute. To check this option, press Enter.', 'upload. To check this option, press Enter.', 'scan. To check this option, press Enter.', + 'runscript. To check this option, press Enter.', ]); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx index 66efadb3c212b..a8725a68c89e3 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx @@ -38,6 +38,7 @@ export const ResponseActionsLog = memo< isFlyout?: boolean; setIsDataInResponse?: (isData: boolean) => void; 'data-test-subj'?: string; + 'data-test-height'?: number; } >( ({ @@ -46,6 +47,7 @@ export const ResponseActionsLog = memo< isFlyout = true, setIsDataInResponse, 'data-test-subj': dataTestSubj = 'response-actions-list', + 'data-test-height': dataTestHeight, }) => { const { pagination: paginationFromUrlParams, setPagination: setPaginationOnUrlParams } = useUrlPagination(); @@ -295,6 +297,7 @@ export const ResponseActionsLog = memo< onTimeChange={onTimeChange} showHostsFilter={showHostNames} data-test-subj={dataTestSubj} + data-test-height={dataTestHeight} /> {isFetched && !totalItemCount ? ( diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts index 3ff347b320fe7..db3daf1bf466e 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts @@ -63,7 +63,7 @@ describe( // No access to response actions (except `unisolate`) for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' + (apiName) => apiName !== 'unisolate' && apiName !== 'runscript' )) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); @@ -88,7 +88,7 @@ describe( // No access to response actions (except `unisolate`) for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' + (apiName) => apiName !== 'unisolate' && apiName !== 'runscript' )) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts index ad3c2a3a1bb61..f1089f3ded978 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts @@ -63,7 +63,7 @@ describe( // No access to response actions (except `unisolate`) for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' + (apiName) => apiName !== 'unisolate' && apiName !== 'runscript' )) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); @@ -88,7 +88,7 @@ describe( // No access to response actions (except `unisolate`) for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' + (apiName) => apiName !== 'unisolate' && apiName !== 'runscript' )) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts index c43acf317bf21..b700df4880a7d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts @@ -70,7 +70,7 @@ describe( } for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' + (apiName) => apiName !== 'unisolate' && apiName !== 'runscript' )) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); @@ -99,7 +99,7 @@ describe( }); for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' + (apiName) => apiName !== 'unisolate' && apiName !== 'runscript' )) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts index 17a378cb95fa8..ca3d0b9f6c1a1 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts @@ -43,9 +43,7 @@ describe( // This is not needed for this test, but it's a good example of // how to enable experimental features in the Cypress tests. // kbnServerArgs: [ - // `--xpack.securitySolution.enableExperimental=${JSON.stringify([ - // 'featureFlagName', - // ])}`, + // `--xpack.securitySolution.enableExperimental=${JSON.stringify(['featureFlagName'])}`, // ], }, }, @@ -127,6 +125,8 @@ describe( 'get-file', 'upload', 'scan' + // TODO: currently not implemented for Endpoint + // 'runscript' ); const deniedResponseActions = pick(consoleHelpPanelResponseActionsTestSubj, 'execute'); 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 19a86eb153bc2..d3cec917610fe 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 @@ -15,7 +15,8 @@ const TEST_SUBJ = Object.freeze({ }); export const getConsoleHelpPanelResponseActionTestSubj = (): Record< - ConsoleResponseActionCommands, + // TODO: currently runscript is not supported in Endpoint + Exclude, string > => { return { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index 51530fd0d7a7a..3fd44e988b4ad 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -16,6 +16,7 @@ import { GET_PROCESSES_ROUTE, ISOLATE_HOST_ROUTE_V2, KILL_PROCESS_ROUTE, + RUN_SCRIPT_ROUTE, SCAN_ROUTE, SUSPEND_PROCESS_ROUTE, UNISOLATE_HOST_ROUTE_V2, @@ -274,6 +275,11 @@ export const ensureResponseActionAuthzAccess = ( Object.assign(apiPayload, { parameters: { path: 'scan/two' } }); break; + case 'runscript': + url = RUN_SCRIPT_ROUTE; + Object.assign(apiPayload, { parameters: { Raw: 'ls' } }); + break; + default: throw new Error(`Response action [${responseAction}] has no API payload defined`); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/utils.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/utils.ts index 92033801e71b6..a8261727d32f4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/utils.ts @@ -70,6 +70,11 @@ const COMMANDS_WITH_ACCESS_TO_FILES: CommandsWithFileAccess = deepFreeze { outputs: { 'agent-a': { content: { - code: 'ra_scan_success_done', + code: '200', }, type: 'json', }, 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 52f6cd3671123..d29e9b70606e0 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 @@ -26,6 +26,7 @@ export const FEATURE_KEYS = { SCAN: 'Scan files', 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; @@ -41,6 +42,7 @@ const RESPONSE_ACTIONS_FEATURE_KEY: Readonly