Skip to content

Commit b0c3176

Browse files
authored
[Security Solution][Endpoint] Add runscript command to the Response Console for SentinelOne hosts (#230310)
## Summary The following changes are in support of `runscript` response action for SentinelOne hosts - currently behind feature flag `responseActionsSentinelOneRunScriptEnabled`: - Adds `runscript` command for SentinelOne hosts to the Response Console - Accepts `--script` argument, which triggers a popup that displays the available scripts from SentinelOne, as well as `--inputParams` - For now, the `runscript` response action will remain `pending`. A follow up PR will add logic to check and complete these actions - Added support to the custom scripts internal API for retrieving SentinelOne list of scripts - Returned data was also enhanced so that each script can optionally defined a `meta` property with additional information about the script that may exists for 3rd party EDRs - a new optional API parameter was added to allow for filtering by OS type (`osType: string`) - Console component was enhanced with: - `exampleUsage` property of a command's definition can now be defined as a callback that returns a `string`. This allows for specific usage information to be displayed for SentinelOne when a script is selected. The callback is provided (if available) the information about what has been entered in the console (`Command`).
1 parent 4eaaed5 commit b0c3176

File tree

35 files changed

+1050
-334
lines changed

35 files changed

+1050
-334
lines changed

x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_custom_scripts_route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { schema, type TypeOf } from '@kbn/config-schema';
9+
import type { DeepMutable } from '../../../endpoint/types';
910
import { AgentTypeSchemaLiteral, HostOsTypeSchemaLiteral } from '..';
1011

1112
export const CustomScriptsRequestSchema = {
@@ -30,4 +31,6 @@ export const CustomScriptsRequestSchema = {
3031
}),
3132
};
3233

33-
export type CustomScriptsRequestQueryParams = TypeOf<typeof CustomScriptsRequestSchema.query>;
34+
export type CustomScriptsRequestQueryParams = DeepMutable<
35+
TypeOf<typeof CustomScriptsRequestSchema.query>
36+
>;

x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import type { TypeOf } from '@kbn/config-schema';
99
import type { EcsError } from '@elastic/ecs';
1010
import type { BaseFileMetadata, FileCompression, FileJSON } from '@kbn/files-plugin/common';
1111
import type {
12-
UploadActionApiRequestBody,
1312
ActionStatusRequestSchema,
1413
KillProcessRequestBody,
15-
SuspendProcessRequestBody,
1614
RunScriptActionRequestBody,
15+
SuspendProcessRequestBody,
16+
UploadActionApiRequestBody,
1717
} from '../../api/endpoint';
1818

1919
import type {
@@ -612,3 +612,34 @@ export interface ResponseActionUploadOutputContent {
612612
/** The free space available (after saving the file) of the drive where the file was saved to, In Bytes */
613613
disk_free_space: number;
614614
}
615+
616+
/**
617+
* A Response Action script
618+
*/
619+
export interface ResponseActionScript<TMeta extends {} = {}> {
620+
/**
621+
* Unique identifier for the script
622+
*/
623+
id: string;
624+
/**
625+
* Display name of the script
626+
*/
627+
name: string;
628+
/**
629+
* Description of what the script does
630+
*/
631+
description: string;
632+
633+
/**
634+
* Additional meta info. about the script. Can be used to store EDR specific
635+
* information about the script for use in the UI
636+
*/
637+
meta?: TMeta;
638+
}
639+
640+
/**
641+
* API response with list of Response Actions scripts available on the system
642+
*/
643+
export interface ResponseActionScriptsApiResponse<TMeta extends {} = {}> {
644+
data: ResponseActionScript<TMeta>[];
645+
}

x-pack/solutions/security/plugins/security_solution/common/endpoint/types/sentinel_one.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* 2.0.
66
*/
77

8+
import type { SentinelOneGetRemoteScriptsResponse } from '@kbn/stack-connectors-plugin/common/sentinelone/types';
9+
810
/**
911
* The `activity` document ingested from SentinelOne via the integration
1012
*
@@ -175,3 +177,17 @@ export interface SentinelOneRunScriptResponseMeta {
175177
/** The SentinelOne task ID associated with the completion of the run script action */
176178
taskId: string;
177179
}
180+
181+
/**
182+
* A subset of properties from the SentinelOne Script API response
183+
*/
184+
export type SentinelOneScript = Pick<
185+
SentinelOneGetRemoteScriptsResponse['data'][number],
186+
| 'id'
187+
| 'scriptDescription'
188+
| 'osTypes'
189+
| 'inputInstructions'
190+
| 'inputExample'
191+
| 'inputRequired'
192+
| 'shortFileName'
193+
>;

x-pack/solutions/security/plugins/security_solution/public/common/lib/endpoint/utils/is_agent_type_and_action_supported.test.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('isAgentTypeAndActionSupported() util', () => {
2424
responseActionsSentinelOneGetFileEnabled: true,
2525
responseActionsCrowdstrikeManualHostIsolationEnabled: true,
2626
responseActionsMSDefenderEndpointEnabled: true,
27+
responseActionsSentinelOneRunScriptEnabled: true,
2728
...overrides,
2829
});
2930
};
@@ -37,6 +38,9 @@ describe('isAgentTypeAndActionSupported() util', () => {
3738
const disableMicrosoftIsolationFeature = () => {
3839
enableFeatures({ responseActionsMSDefenderEndpointEnabled: false });
3940
};
41+
const disableS1RunScript = () => {
42+
enableFeatures({ responseActionsSentinelOneRunScriptEnabled: false });
43+
};
4044

4145
const resetFeatures = (): void => {
4246
(ExperimentalFeaturesService.get as jest.Mock).mockReturnValue({
@@ -53,21 +57,24 @@ describe('isAgentTypeAndActionSupported() util', () => {
5357
});
5458

5559
it.each`
56-
agentType | actionName | actionType | expectedValue | runSetup
57-
${'endpoint'} | ${undefined} | ${undefined} | ${true} | ${undefined}
58-
${'endpoint'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
59-
${'endpoint'} | ${'isolate'} | ${'automated'} | ${true} | ${undefined}
60-
${'sentinel_one'} | ${undefined} | ${undefined} | ${true} | ${undefined}
61-
${'sentinel_one'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
62-
${'sentinel_one'} | ${'get-file'} | ${'manual'} | ${true} | ${undefined}
63-
${'sentinel_one'} | ${'get-file'} | ${undefined} | ${false} | ${disableS1GetFileFeature}
64-
${'crowdstrike'} | ${undefined} | ${undefined} | ${true} | ${undefined}
65-
${'crowdstrike'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
66-
${'crowdstrike'} | ${'isolate'} | ${undefined} | ${false} | ${disableCSIsolateFeature}
67-
${'microsoft_defender_endpoint'} | ${undefined} | ${undefined} | ${true} | ${undefined}
68-
${'microsoft_defender_endpoint'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
69-
${'microsoft_defender_endpoint'} | ${'isolate'} | ${'automated'} | ${false} | ${undefined}
70-
${'microsoft_defender_endpoint'} | ${'isolate'} | ${undefined} | ${false} | ${disableMicrosoftIsolationFeature}
60+
agentType | actionName | actionType | expectedValue | runSetup
61+
${'endpoint'} | ${undefined} | ${undefined} | ${true} | ${undefined}
62+
${'endpoint'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
63+
${'endpoint'} | ${'isolate'} | ${'automated'} | ${true} | ${undefined}
64+
${'sentinel_one'} | ${undefined} | ${undefined} | ${true} | ${undefined}
65+
${'sentinel_one'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
66+
${'sentinel_one'} | ${'get-file'} | ${'manual'} | ${true} | ${undefined}
67+
${'sentinel_one'} | ${'get-file'} | ${undefined} | ${false} | ${disableS1GetFileFeature}
68+
${'sentinel_one'} | ${'runscript'} | ${'manual'} | ${true} | ${undefined}
69+
${'sentinel_one'} | ${'runscript'} | ${'automated'} | ${false} | ${undefined}
70+
${'sentinel_one'} | ${'runscript'} | ${undefined} | ${false} | ${disableS1RunScript}
71+
${'crowdstrike'} | ${undefined} | ${undefined} | ${true} | ${undefined}
72+
${'crowdstrike'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
73+
${'crowdstrike'} | ${'isolate'} | ${undefined} | ${false} | ${disableCSIsolateFeature}
74+
${'microsoft_defender_endpoint'} | ${undefined} | ${undefined} | ${true} | ${undefined}
75+
${'microsoft_defender_endpoint'} | ${'isolate'} | ${'manual'} | ${true} | ${undefined}
76+
${'microsoft_defender_endpoint'} | ${'isolate'} | ${'automated'} | ${false} | ${undefined}
77+
${'microsoft_defender_endpoint'} | ${'isolate'} | ${undefined} | ${false} | ${disableMicrosoftIsolationFeature}
7178
`(
7279
'should return `$expectedValue` for $agentType $actionName ($actionType)',
7380
({

x-pack/solutions/security/plugins/security_solution/public/common/lib/endpoint/utils/is_agent_type_and_action_supported.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const isAgentTypeAndActionSupported = (
2525
const features = ExperimentalFeaturesService.get();
2626
const isSentinelOneV1Enabled = features.responseActionsSentinelOneV1Enabled;
2727
const isSentinelOneGetFileEnabled = features.responseActionsSentinelOneGetFileEnabled;
28+
const isSentinelOneRunScriptEnabled = features.responseActionsSentinelOneRunScriptEnabled;
2829
const isCrowdstrikeHostIsolationEnabled =
2930
features.responseActionsCrowdstrikeManualHostIsolationEnabled;
3031
const isMicrosoftDefenderEndpointEnabled = features.responseActionsMSDefenderEndpointEnabled;
@@ -48,6 +49,10 @@ export const isAgentTypeAndActionSupported = (
4849
isActionNameSupported = false;
4950
}
5051
break;
52+
case 'runscript':
53+
if (!isSentinelOneRunScriptEnabled) {
54+
isActionNameSupported = false;
55+
}
5156
}
5257

5358
break;

x-pack/solutions/security/plugins/security_solution/public/management/components/console/components/command_input/command_input.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,10 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
153153

154154
const handleInputCapture = useCallback<InputCaptureProps['onCapture']>(
155155
({ value, selection, eventDetails }) => {
156-
const keyCode = eventDetails.keyCode;
156+
const key = eventDetails.code;
157157

158158
// UP arrow key
159-
if (keyCode === 38) {
159+
if (key === 'ArrowUp') {
160160
dispatch({ type: 'removeFocusFromKeyCapture' });
161161
dispatch({ type: 'updateInputPopoverState', payload: { show: 'input-history' } });
162162

@@ -193,19 +193,19 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
193193

194194
inputText.addValue(processedValue ?? '', selection);
195195

196-
switch (keyCode) {
196+
switch (key) {
197197
// BACKSPACE
198-
case 8:
198+
case 'Backspace':
199199
inputText.backspaceChar(selection);
200200
break;
201201

202202
// DELETE
203-
case 46:
203+
case 'Delete':
204204
inputText.deleteChar(selection);
205205
break;
206206

207-
// ENTER = Execute command and blank out the input area
208-
case 13:
207+
// ENTER = Execute command and blank out the input area
208+
case 'Enter':
209209
setCommandToExecute({
210210
input: inputText.getFullText(true),
211211
enteredCommand: prevEnteredCommand as ConsoleDataState['input']['enteredCommand'],
@@ -215,22 +215,22 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
215215
break;
216216

217217
// ARROW LEFT
218-
case 37:
218+
case 'ArrowLeft':
219219
inputText.moveCursorTo('left');
220220
break;
221221

222222
// ARROW RIGHT
223-
case 39:
223+
case 'ArrowRight':
224224
inputText.moveCursorTo('right');
225225
break;
226226

227227
// HOME
228-
case 36:
228+
case 'Home':
229229
inputText.moveCursorTo('home');
230230
break;
231231

232232
// END
233-
case 35:
233+
case 'End':
234234
inputText.moveCursorTo('end');
235235
break;
236236
}

x-pack/solutions/security/plugins/security_solution/public/management/components/console/components/command_input/components/argument_selector_wrapper.tsx

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,13 @@
55
* 2.0.
66
*/
77

8-
import React, { memo, useCallback, useMemo } from 'react';
8+
import React, { memo, useCallback } from 'react';
99
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
1010
import styled from 'styled-components';
11+
import { useInputCommand } from '../../../hooks/state_selectors/use_input_command';
1112
import { useConsoleStateDispatch } from '../../../hooks/state_selectors/use_console_state_dispatch';
1213
import { useWithCommandArgumentState } from '../../../hooks/state_selectors/use_with_command_argument_state';
13-
import { useConsoleStore } from '../../console_state/console_state';
14-
import type {
15-
CommandArgDefinition,
16-
CommandArgumentValueSelectorProps,
17-
Command,
18-
} from '../../../types';
14+
import type { CommandArgDefinition, CommandArgumentValueSelectorProps } from '../../../types';
1915

2016
const ArgumentSelectorWrapperContainer = styled.span`
2117
border: ${({ theme: { eui } }) => eui.euiBorderThin};
@@ -64,24 +60,13 @@ export interface ArgumentSelectorWrapperProps {
6460
export const ArgumentSelectorWrapper = memo<ArgumentSelectorWrapperProps>(
6561
({ argName, argIndex, argDefinition: { SelectorComponent } }) => {
6662
const dispatch = useConsoleStateDispatch();
63+
const command = useInputCommand();
6764
const { valueText, value, store } = useWithCommandArgumentState(argName, argIndex);
68-
const { state: consoleState } = useConsoleStore();
6965

70-
// Construct the Command object from console state
71-
const command = useMemo<Command>(() => {
72-
const { enteredCommand } = consoleState.input;
73-
if (!enteredCommand) {
74-
throw new Error('ArgumentSelectorWrapper should only be used when a command is entered');
75-
}
76-
77-
// Construct the full Command object needed by selectors
78-
return {
79-
input: consoleState.input.leftOfCursorText + consoleState.input.rightOfCursorText,
80-
inputDisplay: consoleState.input.leftOfCursorText + consoleState.input.rightOfCursorText,
81-
args: consoleState.input.parsedInput,
82-
commandDefinition: enteredCommand.commandDefinition,
83-
};
84-
}, [consoleState.input]);
66+
if (!command) {
67+
// FIXME: PT we should not throw here as that would likely crash the UI.
68+
throw new Error('ArgumentSelectorWrapper should only be used when a command is entered');
69+
}
8570

8671
// Create requestFocus callback that uses proper console dispatch instead of direct state manipulation
8772
const requestFocus = useCallback(() => {

x-pack/solutions/security/plugins/security_solution/public/management/components/console/components/command_input/components/input_capture.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export type InputCaptureProps = PropsWithChildren<{
8282
/** Keyboard control keys from the keyboard event */
8383
eventDetails: Pick<
8484
KeyboardEvent,
85-
'key' | 'altKey' | 'ctrlKey' | 'keyCode' | 'metaKey' | 'repeat' | 'shiftKey'
85+
'key' | 'altKey' | 'ctrlKey' | 'keyCode' | 'metaKey' | 'repeat' | 'shiftKey' | 'code'
8686
>;
8787
}) => void;
8888
/** Sets an interface that allows interactions with this component's focus/blur states */
@@ -164,6 +164,7 @@ export const InputCapture = memo<InputCaptureProps>(
164164
'metaKey',
165165
'repeat',
166166
'shiftKey',
167+
'code',
167168
]);
168169

169170
onCapture({
@@ -198,6 +199,7 @@ export const InputCapture = memo<InputCaptureProps>(
198199
metaKey: true,
199200
repeat: false,
200201
shiftKey: false,
202+
code: 'MetaLeft',
201203
};
202204

203205
onCapture({

x-pack/solutions/security/plugins/security_solution/public/management/components/console/components/command_input/hooks/use_input_hints.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { useEffect, useMemo } from 'react';
99
import { i18n } from '@kbn/i18n';
10+
import { useInputCommand } from '../../../hooks/state_selectors/use_input_command';
1011
import { useWithInputTextEntered } from '../../../hooks/state_selectors/use_with_input_text_entered';
1112
import { getArgumentsForCommand } from '../../../service/parsed_command_input';
1213
import type { CommandDefinition } from '../../..';
@@ -36,23 +37,27 @@ export const UP_ARROW_ACCESS_HISTORY_HINT = i18n.translate(
3637
export const useInputHints = () => {
3738
const dispatch = useConsoleStateDispatch();
3839
const isInputPopoverOpen = Boolean(useWithInputShowPopover());
39-
const commandEntered = useWithInputCommandEntered();
40+
const commandNameEntered = useWithInputCommandEntered();
4041
const commandList = useWithCommandList();
42+
const inputCommand = useInputCommand();
4143
const { leftOfCursorText } = useWithInputTextEntered();
4244

4345
const commandEnteredDefinition = useMemo<CommandDefinition | undefined>(() => {
44-
if (commandEntered) {
45-
return commandList.find((commandDef) => commandDef.name === commandEntered);
46+
if (commandNameEntered) {
47+
return commandList.find((commandDef) => commandDef.name === commandNameEntered);
4648
}
47-
}, [commandEntered, commandList]);
49+
}, [commandNameEntered, commandList]);
4850

4951
useEffect(() => {
5052
// If we know the command name and the input popover is not opened, then show hints (if any)
51-
if (commandEntered && !isInputPopoverOpen) {
53+
if (commandNameEntered && !isInputPopoverOpen) {
5254
// Is valid command name? ==> show usage
5355
if (commandEnteredDefinition && commandEnteredDefinition.helpHidden !== true) {
5456
const exampleInstruction = commandEnteredDefinition?.exampleInstruction ?? '';
55-
const exampleUsage = commandEnteredDefinition?.exampleUsage ?? '';
57+
const exampleUsage =
58+
typeof commandEnteredDefinition?.exampleUsage === 'function'
59+
? commandEnteredDefinition?.exampleUsage(inputCommand)
60+
: commandEnteredDefinition?.exampleUsage ?? '';
5661

5762
let hint = exampleInstruction ?? '';
5863

@@ -95,7 +100,7 @@ export const useInputHints = () => {
95100
dispatch({
96101
type: 'updateFooterContent',
97102
payload: {
98-
value: UNKNOWN_COMMAND_HINT(commandEntered),
103+
value: UNKNOWN_COMMAND_HINT(commandNameEntered),
99104
},
100105
});
101106

@@ -110,5 +115,12 @@ export const useInputHints = () => {
110115
});
111116
dispatch({ type: 'setInputState', payload: { value: undefined } });
112117
}
113-
}, [commandEntered, commandEnteredDefinition, dispatch, isInputPopoverOpen, leftOfCursorText]);
118+
}, [
119+
commandNameEntered,
120+
commandEnteredDefinition,
121+
dispatch,
122+
isInputPopoverOpen,
123+
leftOfCursorText,
124+
inputCommand,
125+
]);
114126
};

0 commit comments

Comments
 (0)