Skip to content

Commit

Permalink
[Security Solution][Endpoint] Add processes API and Response Consol…
Browse files Browse the repository at this point in the history
…e 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.
  • Loading branch information
paul-tavares authored Jul 17, 2024
1 parent 92634f4 commit 7e67c48
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
},
manual: {
endpoint: true,
sentinel_one: false,
sentinel_one: true,
crowdstrike: false,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
}}
Expand All @@ -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(
Expand Down Expand Up @@ -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();
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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],
Expand Down
Loading

0 comments on commit 7e67c48

Please sign in to comment.