mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Endpoint] Add processes
API and Response Console command support for SentinelOne (#188151)
## Summary - Adds UI support to the Response Console for `processes` against SentinelOne hosts - Adds API support for triggering a `running_processes` response action against SentinelOne Hosts - Functionality is behind a new feature flag (disabled by default): `responseActionsSentinelOneProcessesEnabled` > [!NOTE] > The `processes` response action for SentinelOne will remain in `pending` and will not complete. A subsequent PR will introduce the necessary logic to check and complete this response action.
This commit is contained in:
parent
92634f40a5
commit
7e67c48adb
10 changed files with 417 additions and 46 deletions
|
@ -110,7 +110,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
sentinel_one: true,
|
||||
crowdstrike: false,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -9,10 +9,10 @@ import type { TypeOf } from '@kbn/config-schema';
|
|||
import type { EcsError } from '@elastic/ecs';
|
||||
import type { BaseFileMetadata, FileCompression, FileJSON } from '@kbn/files-plugin/common';
|
||||
import type {
|
||||
ResponseActionBodySchema,
|
||||
UploadActionApiRequestBody,
|
||||
KillProcessRouteRequestSchema,
|
||||
ResponseActionBodySchema,
|
||||
SuspendProcessRouteRequestSchema,
|
||||
UploadActionApiRequestBody,
|
||||
} from '../../api/endpoint';
|
||||
import type { ActionStatusRequestSchema } from '../../api/endpoint/actions/action_status_route';
|
||||
import type { NoParametersRequestSchema } from '../../api/endpoint/actions/common/base';
|
||||
|
|
|
@ -116,6 +116,14 @@ export interface SentinelOneGetFileResponseMeta {
|
|||
filename: string;
|
||||
}
|
||||
|
||||
export interface SentinelOneProcessesRequestMeta extends SentinelOneGetFileRequestMeta {
|
||||
/**
|
||||
* The Parent Task Is that is executing the kill process action in SentinelOne.
|
||||
* Used to check on the status of that action
|
||||
*/
|
||||
parentTaskId: string;
|
||||
}
|
||||
|
||||
export interface SentinelOneKillProcessRequestMeta extends SentinelOneIsolationRequestMeta {
|
||||
/**
|
||||
* The Parent Task Is that is executing the kill process action in SentinelOne.
|
||||
|
|
|
@ -84,6 +84,9 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
/** Enables the `kill-process` response action for SentinelOne */
|
||||
responseActionsSentinelOneKillProcessEnabled: false,
|
||||
|
||||
/** Enable the `processes` response actions for SentinelOne */
|
||||
responseActionsSentinelOneProcessesEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables the ability to send Response actions to Crowdstrike and persist the results
|
||||
* in ES.
|
||||
|
|
|
@ -45,17 +45,19 @@ const StyledEuiBasicTable = styled(EuiBasicTable)`
|
|||
|
||||
export const GetProcessesActionResult = memo<ActionRequestComponentProps>(
|
||||
({ command, setStore, store, status, setStatus, ResultComponent }) => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const { endpointId, agentType } = command.commandDefinition?.meta ?? {};
|
||||
const comment = command.args.args?.comment?.[0];
|
||||
const actionCreator = useSendGetEndpointProcessesRequest();
|
||||
|
||||
const actionRequestBody = useMemo(() => {
|
||||
return endpointId
|
||||
? {
|
||||
endpoint_ids: [endpointId],
|
||||
comment: command.args.args?.comment?.[0],
|
||||
comment,
|
||||
agent_type: agentType,
|
||||
}
|
||||
: undefined;
|
||||
}, [endpointId, command.args.args?.comment]);
|
||||
}, [endpointId, comment, agentType]);
|
||||
|
||||
const { result, actionDetails: completedActionDetails } = useConsoleActionSubmitter<
|
||||
ProcessesRequestBody,
|
||||
|
|
|
@ -14,49 +14,59 @@ import {
|
|||
import React from 'react';
|
||||
import { getEndpointConsoleCommands } from '../../lib/console_commands_definition';
|
||||
import { responseActionsHttpMocks } from '../../../../mocks/response_actions_http_mocks';
|
||||
import { enterConsoleCommand } from '../../../console/mocks';
|
||||
import { enterConsoleCommand, getConsoleSelectorsAndActionMock } from '../../../console/mocks';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { getEndpointAuthzInitialState } from '../../../../../../common/endpoint/service/authz';
|
||||
import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants';
|
||||
import type {
|
||||
EndpointCapabilities,
|
||||
ResponseActionAgentType,
|
||||
} from '../../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { ENDPOINT_CAPABILITIES } from '../../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { UPGRADE_AGENT_FOR_RESPONDER } from '../../../../../common/translations';
|
||||
|
||||
jest.mock('../../../../../common/experimental_features_service');
|
||||
import type { CommandDefinition } from '../../../console';
|
||||
|
||||
describe('When using processes action from response actions console', () => {
|
||||
let render: (
|
||||
capabilities?: EndpointCapabilities[]
|
||||
) => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
|
||||
let consoleManagerMockAccess: ReturnType<
|
||||
typeof getConsoleManagerMockRenderResultQueriesAndActions
|
||||
>;
|
||||
let consoleSelectors: ReturnType<typeof getConsoleSelectorsAndActionMock>;
|
||||
let consoleCommands: CommandDefinition[];
|
||||
|
||||
const setConsoleCommands = (
|
||||
capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES],
|
||||
agentType: ResponseActionAgentType = 'endpoint'
|
||||
): void => {
|
||||
consoleCommands = getEndpointConsoleCommands({
|
||||
agentType,
|
||||
endpointAgentId: 'a.b.c',
|
||||
endpointCapabilities: capabilities,
|
||||
endpointPrivileges: {
|
||||
...getEndpointAuthzInitialState(),
|
||||
loading: false,
|
||||
canKillProcess: true,
|
||||
canSuspendProcess: true,
|
||||
canGetRunningProcesses: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
|
||||
setConsoleCommands();
|
||||
|
||||
render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => {
|
||||
render = async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<ConsoleManagerTestComponent
|
||||
registerConsoleProps={() => {
|
||||
return {
|
||||
consoleProps: {
|
||||
'data-test-subj': 'test',
|
||||
commands: getEndpointConsoleCommands({
|
||||
agentType: 'endpoint',
|
||||
endpointAgentId: 'a.b.c',
|
||||
endpointCapabilities: [...capabilities],
|
||||
endpointPrivileges: {
|
||||
...getEndpointAuthzInitialState(),
|
||||
loading: false,
|
||||
canKillProcess: true,
|
||||
canSuspendProcess: true,
|
||||
canGetRunningProcesses: true,
|
||||
},
|
||||
}),
|
||||
commands: consoleCommands,
|
||||
},
|
||||
};
|
||||
}}
|
||||
|
@ -67,13 +77,15 @@ describe('When using processes action from response actions console', () => {
|
|||
|
||||
await consoleManagerMockAccess.clickOnRegisterNewConsole();
|
||||
await consoleManagerMockAccess.openRunningConsole();
|
||||
consoleSelectors = getConsoleSelectorsAndActionMock(renderResult);
|
||||
|
||||
return renderResult;
|
||||
};
|
||||
});
|
||||
|
||||
it('should show an error if the `running_processes` capability is not present in the endpoint', async () => {
|
||||
await render([]);
|
||||
setConsoleCommands([]);
|
||||
await render();
|
||||
enterConsoleCommand(renderResult, 'processes');
|
||||
|
||||
expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual(
|
||||
|
@ -228,4 +240,80 @@ describe('When using processes action from response actions console', () => {
|
|||
expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and when agent type is SentinelOne', () => {
|
||||
beforeEach(() => {
|
||||
mockedContext.setExperimentalFlag({ responseActionsSentinelOneProcessesEnabled: true });
|
||||
setConsoleCommands([], 'sentinel_one');
|
||||
});
|
||||
|
||||
it('should display processes command --help', async () => {
|
||||
await render();
|
||||
enterConsoleCommand(renderResult, 'processes --help');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.getByTestId('test-helpOutput').textContent).toEqual(
|
||||
'About' +
|
||||
'Show all running processes' +
|
||||
'Usage' +
|
||||
'processes [--comment]' +
|
||||
'Example' +
|
||||
'processes --comment "get the processes"' +
|
||||
'Optional parameters' +
|
||||
'--comment - A comment to go along with the action'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display correct entry in help panel', async () => {
|
||||
await render();
|
||||
consoleSelectors.openHelpPanel();
|
||||
|
||||
expect(
|
||||
renderResult.getByTestId('test-commandList-Responseactions-processes')
|
||||
).toHaveTextContent('processesShow all running processes');
|
||||
});
|
||||
|
||||
it('should call the api with agentType of SentinelOne', async () => {
|
||||
await render();
|
||||
enterConsoleCommand(renderResult, 'processes');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.processes).toHaveBeenCalledWith({
|
||||
body: '{"endpoint_ids":["a.b.c"],"agent_type":"sentinel_one"}',
|
||||
path: '/api/endpoint/action/running_procs',
|
||||
version: '2023-10-31',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and `responseActionsSentinelOneProcessesEnabled` feature flag is disabled', () => {
|
||||
beforeEach(() => {
|
||||
mockedContext.setExperimentalFlag({ responseActionsSentinelOneProcessesEnabled: false });
|
||||
setConsoleCommands([], 'sentinel_one');
|
||||
});
|
||||
|
||||
it('should not display `processes` command in console help', async () => {
|
||||
await render();
|
||||
consoleSelectors.openHelpPanel();
|
||||
|
||||
expect(renderResult.queryByTestId('test-commandList-Responseactions-processes')).toBeNull();
|
||||
});
|
||||
|
||||
it('should error if user enters `process` command', async () => {
|
||||
await render();
|
||||
enterConsoleCommand(renderResult, 'processes');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.getByTestId('test-validationError')).toHaveTextContent(
|
||||
'Unsupported actionSupport for processes is not currently available for SentinelOne.'
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.processes).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -551,6 +551,7 @@ const adjustCommandsForSentinelOne = ({
|
|||
}): CommandDefinition[] => {
|
||||
const featureFlags = ExperimentalFeaturesService.get();
|
||||
const isKillProcessEnabled = featureFlags.responseActionsSentinelOneKillProcessEnabled;
|
||||
const isProcessesEnabled = featureFlags.responseActionsSentinelOneProcessesEnabled;
|
||||
|
||||
return commandList.map((command) => {
|
||||
// Kill-Process: adjust command to accept only `processName`
|
||||
|
@ -574,6 +575,7 @@ const adjustCommandsForSentinelOne = ({
|
|||
if (
|
||||
command.name === 'status' ||
|
||||
(command.name === 'kill-process' && !isKillProcessEnabled) ||
|
||||
(command.name === 'processes' && !isProcessesEnabled) ||
|
||||
!isAgentTypeAndActionSupported(
|
||||
'sentinel_one',
|
||||
RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP[command.name as ConsoleResponseActionCommands],
|
||||
|
|
|
@ -37,7 +37,10 @@ import type {
|
|||
KillOrSuspendProcessRequestBody,
|
||||
} from '../../../../../../common/endpoint/types';
|
||||
import type { SearchHit, SearchResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ResponseActionGetFileRequestBody } from '../../../../../../common/api/endpoint';
|
||||
import type {
|
||||
ResponseActionGetFileRequestBody,
|
||||
GetProcessesRequestBody,
|
||||
} from '../../../../../../common/api/endpoint';
|
||||
import { SUB_ACTION } from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
|
||||
import { ACTIONS_SEARCH_PAGE_SIZE } from '../../constants';
|
||||
import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
|
@ -75,14 +78,15 @@ describe('SentinelOneActionsClient class', () => {
|
|||
s1ActionsClient = new SentinelOneActionsClient(classConstructorOptions);
|
||||
});
|
||||
|
||||
it.each(['suspendProcess', 'runningProcesses', 'execute', 'upload', 'scan'] as Array<
|
||||
keyof ResponseActionsClient
|
||||
>)('should throw an un-supported error for %s', async (methodName) => {
|
||||
// @ts-expect-error Purposely passing in empty object for options
|
||||
await expect(s1ActionsClient[methodName]({})).rejects.toBeInstanceOf(
|
||||
ResponseActionsNotSupportedError
|
||||
);
|
||||
});
|
||||
it.each(['suspendProcess', 'execute', 'upload', 'scan'] as Array<keyof ResponseActionsClient>)(
|
||||
'should throw an un-supported error for %s',
|
||||
async (methodName) => {
|
||||
// @ts-expect-error Purposely passing in empty object for options
|
||||
await expect(s1ActionsClient[methodName]({})).rejects.toBeInstanceOf(
|
||||
ResponseActionsNotSupportedError
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('should error if multiple agent ids are received', async () => {
|
||||
const payload = createS1IsolationOptions();
|
||||
|
@ -1294,4 +1298,163 @@ describe('SentinelOneActionsClient class', () => {
|
|||
expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#runningProcesses()', () => {
|
||||
let processesActionRequest: GetProcessesRequestBody;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error readonly prop assignment
|
||||
classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneProcessesEnabled =
|
||||
true;
|
||||
|
||||
processesActionRequest = responseActionsClientMock.createRunningProcessesOptions();
|
||||
});
|
||||
|
||||
it('should error if feature flag is disabled', async () => {
|
||||
// @ts-expect-error readonly prop assignment
|
||||
classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneProcessesEnabled =
|
||||
false;
|
||||
|
||||
await expect(s1ActionsClient.runningProcesses(processesActionRequest)).rejects.toThrow(
|
||||
`processes not supported for sentinel_one agent type. Feature disabled`
|
||||
);
|
||||
});
|
||||
|
||||
it('should error if host is running Windows', async () => {
|
||||
connectorActionsMock.execute.mockResolvedValue(
|
||||
responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: sentinelOneMock.createGetAgentsResponse([
|
||||
sentinelOneMock.createSentinelOneAgentDetails({ osType: 'windows' }),
|
||||
]),
|
||||
})
|
||||
);
|
||||
|
||||
await expect(s1ActionsClient.runningProcesses(processesActionRequest)).rejects.toThrow(
|
||||
'Retrieval of running processes for Windows host is not supported by SentinelOne'
|
||||
);
|
||||
});
|
||||
|
||||
it('should retrieve script execution information from S1 using host OS', async () => {
|
||||
await s1ActionsClient.runningProcesses(processesActionRequest);
|
||||
|
||||
expect(connectorActionsMock.execute).toHaveBeenCalledWith({
|
||||
params: {
|
||||
subAction: 'getRemoteScripts',
|
||||
subActionParams: {
|
||||
osTypes: 'linux',
|
||||
query: 'process list',
|
||||
scriptType: 'dataCollection',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if unable to get S1 script information', async () => {
|
||||
const executeMockImplementation = connectorActionsMock.execute.getMockImplementation()!;
|
||||
connectorActionsMock.execute.mockImplementation(async (options) => {
|
||||
if (options.params.subAction === SUB_ACTION.GET_REMOTE_SCRIPTS) {
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: { data: [] },
|
||||
});
|
||||
}
|
||||
return executeMockImplementation.call(connectorActionsMock, options);
|
||||
});
|
||||
|
||||
await expect(s1ActionsClient.runningProcesses(processesActionRequest)).rejects.toThrow(
|
||||
'Unable to find a script from SentinelOne to handle [running-processes] response action for host running [linux])'
|
||||
);
|
||||
});
|
||||
|
||||
it('should send execute script request to S1 for process list', async () => {
|
||||
await s1ActionsClient.runningProcesses(processesActionRequest);
|
||||
|
||||
expect(connectorActionsMock.execute).toHaveBeenCalledWith({
|
||||
params: {
|
||||
subAction: 'executeScript',
|
||||
subActionParams: {
|
||||
filter: { uuids: '1-2-3' },
|
||||
script: {
|
||||
inputParams: '',
|
||||
outputDestination: 'SentinelCloud',
|
||||
requiresApproval: false,
|
||||
scriptId: '1466645476786791838',
|
||||
taskDescription: expect.stringContaining(
|
||||
'Action triggered from Elastic Security by user [foo] for action [running-processes'
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return action details on success', async () => {
|
||||
await s1ActionsClient.runningProcesses(processesActionRequest);
|
||||
|
||||
expect(getActionDetailsByIdMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create action request doc with expected meta info', async () => {
|
||||
await s1ActionsClient.runningProcesses(processesActionRequest);
|
||||
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith(
|
||||
{
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
data: {
|
||||
command: 'running-processes',
|
||||
comment: 'test comment',
|
||||
hosts: { '1-2-3': { name: 'sentinelone-1460' } },
|
||||
parameters: undefined,
|
||||
},
|
||||
expiration: expect.any(String),
|
||||
input_type: 'sentinel_one',
|
||||
type: 'INPUT_ACTION',
|
||||
},
|
||||
agent: { id: ['1-2-3'] },
|
||||
meta: {
|
||||
agentId: '1845174760470303882',
|
||||
agentUUID: '1-2-3',
|
||||
hostName: 'sentinelone-1460',
|
||||
parentTaskId: 'task-789',
|
||||
},
|
||||
user: { id: 'foo' },
|
||||
},
|
||||
index: '.logs-endpoint.actions-default',
|
||||
refresh: 'wait_for',
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should update cases', async () => {
|
||||
processesActionRequest = {
|
||||
...processesActionRequest,
|
||||
case_ids: ['case-1'],
|
||||
};
|
||||
await s1ActionsClient.runningProcesses(processesActionRequest);
|
||||
|
||||
expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should still create action request when running in automated mode', async () => {
|
||||
classConstructorOptions.isAutomated = true;
|
||||
classConstructorOptions.connectorActions =
|
||||
responseActionsClientMock.createNormalizedExternalConnectorClient(
|
||||
sentinelOneMock.createConnectorActionsClient()
|
||||
);
|
||||
s1ActionsClient = new SentinelOneActionsClient(classConstructorOptions);
|
||||
await s1ActionsClient.runningProcesses(processesActionRequest);
|
||||
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
document: expect.objectContaining({
|
||||
error: { message: 'Action [running-processes] not supported' },
|
||||
}),
|
||||
}),
|
||||
{ meta: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,7 +27,11 @@ import type {
|
|||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { Readable } from 'stream';
|
||||
import type { Mutable } from 'utility-types';
|
||||
import type { SentinelOneKillProcessScriptArgs, SentinelOneScriptArgs } from './types';
|
||||
import type {
|
||||
SentinelOneKillProcessScriptArgs,
|
||||
SentinelOneScriptArgs,
|
||||
SentinelOneProcessListScriptArgs,
|
||||
} from './types';
|
||||
import { ACTIONS_SEARCH_PAGE_SIZE } from '../../constants';
|
||||
import type { NormalizedExternalConnectorClient } from '../lib/normalized_external_connector_client';
|
||||
import { SENTINEL_ONE_ACTIVITY_INDEX_PATTERN } from '../../../../../../common';
|
||||
|
@ -65,8 +69,11 @@ import type {
|
|||
SentinelOneKillProcessRequestMeta,
|
||||
UploadedFileInfo,
|
||||
ResponseActionParametersWithProcessName,
|
||||
GetProcessesActionOutputContent,
|
||||
SentinelOneProcessesRequestMeta,
|
||||
} from '../../../../../../common/endpoint/types';
|
||||
import type {
|
||||
GetProcessesRequestBody,
|
||||
IsolationRouteRequestBody,
|
||||
ResponseActionGetFileRequestBody,
|
||||
} from '../../../../../../common/api/endpoint';
|
||||
|
@ -663,6 +670,80 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
return actionDetails;
|
||||
}
|
||||
|
||||
public async runningProcesses(
|
||||
actionRequest: GetProcessesRequestBody,
|
||||
options?: CommonResponseActionMethodOptions
|
||||
): Promise<ActionDetails<GetProcessesActionOutputContent>> {
|
||||
if (
|
||||
!this.options.endpointService.experimentalFeatures.responseActionsSentinelOneProcessesEnabled
|
||||
) {
|
||||
throw new ResponseActionsClientError(
|
||||
`processes not supported for ${this.agentType} agent type. Feature disabled`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
|
||||
undefined,
|
||||
GetProcessesActionOutputContent,
|
||||
Partial<SentinelOneProcessesRequestMeta>
|
||||
> = {
|
||||
...actionRequest,
|
||||
...this.getMethodOptions(options),
|
||||
command: 'running-processes',
|
||||
meta: { parentTaskId: '' },
|
||||
};
|
||||
|
||||
if (!reqIndexOptions.error) {
|
||||
let error = (await this.validateRequest(reqIndexOptions)).error;
|
||||
|
||||
if (!error) {
|
||||
const s1AgentDetails = await this.getAgentDetails(reqIndexOptions.endpoint_ids[0]);
|
||||
const processesScriptInfo = await this.fetchScriptInfo<SentinelOneProcessListScriptArgs>(
|
||||
'running-processes',
|
||||
s1AgentDetails.osType
|
||||
);
|
||||
|
||||
try {
|
||||
const s1Response = await this.sendAction<SentinelOneExecuteScriptResponse>(
|
||||
SUB_ACTION.EXECUTE_SCRIPT,
|
||||
{
|
||||
filter: {
|
||||
uuids: actionRequest.endpoint_ids[0],
|
||||
},
|
||||
script: {
|
||||
scriptId: processesScriptInfo.scriptId,
|
||||
taskDescription: this.buildExternalComment(reqIndexOptions),
|
||||
requiresApproval: false,
|
||||
outputDestination: 'SentinelCloud',
|
||||
inputParams: processesScriptInfo.buildScriptArgs({}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
reqIndexOptions.meta = {
|
||||
parentTaskId: s1Response.data?.data?.parentTaskId ?? '',
|
||||
};
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
|
||||
reqIndexOptions.error = error?.message;
|
||||
|
||||
if (!this.options.isAutomated && error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const { actionDetails } = await this.handleResponseActionCreation<
|
||||
undefined,
|
||||
GetProcessesActionOutputContent
|
||||
>(reqIndexOptions);
|
||||
|
||||
return actionDetails;
|
||||
}
|
||||
|
||||
async processPendingActions({
|
||||
abortSignal,
|
||||
addToQueue,
|
||||
|
@ -670,7 +751,6 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
if (abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
for await (const pendingActions of this.fetchAllPendingActions()) {
|
||||
if (abortSignal.aborted) {
|
||||
return;
|
||||
|
@ -729,14 +809,15 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
private async fetchScriptInfo<
|
||||
TScriptOptions extends SentinelOneScriptArgs = SentinelOneScriptArgs
|
||||
>(
|
||||
scriptType: Extract<ResponseActionsApiCommandNames, 'kill-process'>,
|
||||
scriptType: Extract<ResponseActionsApiCommandNames, 'kill-process' | 'running-processes'>,
|
||||
osType: string | 'linux' | 'macos' | 'windows'
|
||||
): Promise<FetchScriptInfoResponse<TScriptOptions>> {
|
||||
const searchQueryParams: Mutable<Partial<SentinelOneGetRemoteScriptsParams>> = {
|
||||
query: '',
|
||||
osTypes: osType,
|
||||
};
|
||||
let buildScriptArgs = NOOP_THROW as FetchScriptInfoResponse<TScriptOptions>['buildScriptArgs'];
|
||||
let buildScriptArgs: FetchScriptInfoResponse<TScriptOptions>['buildScriptArgs'] =
|
||||
NOOP_THROW as FetchScriptInfoResponse<TScriptOptions>['buildScriptArgs'];
|
||||
let isDesiredScript: (
|
||||
scriptInfo: SentinelOneGetRemoteScriptsResponse['data'][number]
|
||||
) => boolean = () => false;
|
||||
|
@ -746,7 +827,6 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
case 'kill-process':
|
||||
searchQueryParams.query = 'terminate';
|
||||
searchQueryParams.scriptType = 'action';
|
||||
|
||||
isDesiredScript = (scriptInfo) => {
|
||||
return (
|
||||
scriptInfo.creator === 'SentinelOne' &&
|
||||
|
@ -758,6 +838,22 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
};
|
||||
break;
|
||||
|
||||
case 'running-processes':
|
||||
if (osType === 'windows') {
|
||||
throw new ResponseActionsClientError(
|
||||
`Retrieval of running processes for Windows host is not supported by SentinelOne`,
|
||||
405
|
||||
);
|
||||
}
|
||||
|
||||
searchQueryParams.query = 'process list';
|
||||
searchQueryParams.scriptType = 'dataCollection';
|
||||
isDesiredScript = (scriptInfo) => {
|
||||
return scriptInfo.creator === 'SentinelOne' && scriptInfo.creatorId === '-1';
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ResponseActionsClientError(
|
||||
`Unable to fetch SentinelOne script for OS [${osType}]. Unknown script type [${scriptType}]`
|
||||
|
@ -780,9 +876,10 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
);
|
||||
}
|
||||
|
||||
// Define the `buildScriptArgs` callback for the Script type
|
||||
switch (scriptType) {
|
||||
case 'kill-process':
|
||||
buildScriptArgs = (args: SentinelOneKillProcessScriptArgs) => {
|
||||
buildScriptArgs = ((args: SentinelOneKillProcessScriptArgs) => {
|
||||
if (!args.processName) {
|
||||
throw new ResponseActionsClientError(
|
||||
`'processName' missing while building script args for [${s1Script.scriptName} (id: ${s1Script.id})] script`
|
||||
|
@ -795,9 +892,13 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
|
||||
// Linux + Macos
|
||||
return `--terminate --processes "${args.processName}" --force`;
|
||||
};
|
||||
}) as FetchScriptInfoResponse<TScriptOptions>['buildScriptArgs'];
|
||||
|
||||
break;
|
||||
|
||||
case 'running-processes':
|
||||
buildScriptArgs = () => '';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -9,7 +9,11 @@ export interface SentinelOneKillProcessScriptArgs {
|
|||
processName: string;
|
||||
}
|
||||
|
||||
export type SentinelOneProcessListScriptArgs = Record<string, never>;
|
||||
|
||||
/**
|
||||
* All the possible set of arguments running SentinelOne scripts that we support for response actions
|
||||
*/
|
||||
export type SentinelOneScriptArgs = SentinelOneKillProcessScriptArgs;
|
||||
export type SentinelOneScriptArgs =
|
||||
| SentinelOneKillProcessScriptArgs
|
||||
| SentinelOneProcessListScriptArgs;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue