Skip to content

Commit

Permalink
[Security Solution] add suspend-process response action API (#134486)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeypoon authored Jun 16, 2022
1 parent 4da4b86 commit 5958878
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import uuid from 'uuid';
import {
EndpointActionListRequestSchema,
HostIsolationRequestSchema,
KillProcessRequestSchema,
KillOrSuspendProcessRequestSchema,
} from './actions';

describe('actions schemas', () => {
Expand Down Expand Up @@ -190,7 +190,7 @@ describe('actions schemas', () => {
});
});

describe('KillProcessRequestSchema', () => {
describe('KillOrSuspendProcessRequestSchema', () => {
it('should require at least 1 Endpoint ID', () => {
expect(() => {
HostIsolationRequestSchema.body.validate({});
Expand All @@ -199,7 +199,7 @@ describe('actions schemas', () => {

it('should accept pid', () => {
expect(() => {
KillProcessRequestSchema.body.validate({
KillOrSuspendProcessRequestSchema.body.validate({
endpoint_ids: ['ABC-XYZ-000'],
parameters: {
pid: 1234,
Expand All @@ -210,7 +210,7 @@ describe('actions schemas', () => {

it('should accept entity_id', () => {
expect(() => {
KillProcessRequestSchema.body.validate({
KillOrSuspendProcessRequestSchema.body.validate({
endpoint_ids: ['ABC-XYZ-000'],
parameters: {
entity_id: 5678,
Expand All @@ -221,7 +221,7 @@ describe('actions schemas', () => {

it('should reject pid and entity_id together', () => {
expect(() => {
KillProcessRequestSchema.body.validate({
KillOrSuspendProcessRequestSchema.body.validate({
endpoint_ids: ['ABC-XYZ-000'],
parameters: {
pid: 1234,
Expand All @@ -233,7 +233,7 @@ describe('actions schemas', () => {

it('should reject if no pid or entity_id', () => {
expect(() => {
KillProcessRequestSchema.body.validate({
KillOrSuspendProcessRequestSchema.body.validate({
endpoint_ids: ['ABC-XYZ-000'],
comment: 'a user comment',
parameters: {},
Expand All @@ -243,7 +243,7 @@ describe('actions schemas', () => {

it('should accept a comment', () => {
expect(() => {
KillProcessRequestSchema.body.validate({
KillOrSuspendProcessRequestSchema.body.validate({
endpoint_ids: ['ABC-XYZ-000'],
comment: 'a user comment',
parameters: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const HostIsolationRequestSchema = {
body: schema.object({ ...BaseActionRequestSchema }),
};

export const KillProcessRequestSchema = {
export const KillOrSuspendProcessRequestSchema = {
body: schema.object({
...BaseActionRequestSchema,
parameters: schema.oneOf([
Expand All @@ -34,7 +34,7 @@ export const KillProcessRequestSchema = {

export const ResponseActionBodySchema = schema.oneOf([
HostIsolationRequestSchema.body,
KillProcessRequestSchema.body,
KillOrSuspendProcessRequestSchema.body,
]);

export const EndpointActionLogRequestSchema = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('Endpoint Authz service', () => {
['canIsolateHost'],
['canUnIsolateHost'],
['canKillProcess'],
['canSuspendProcess'],
])('should set `%s` to `true`', (authProperty) => {
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)[authProperty]).toBe(
true
Expand All @@ -51,6 +52,14 @@ describe('Endpoint Authz service', () => {
);
});

it('should set `canSuspendProcess` to false if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);

expect(
calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canSuspendProcess
).toBe(false);
});

it('should set `canUnIsolateHost` to true even if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);

Expand All @@ -72,6 +81,7 @@ describe('Endpoint Authz service', () => {
['canIsolateHost'],
['canUnIsolateHost'],
['canKillProcess'],
['canSuspendProcess'],
])('should set `%s` to `false`', (authProperty) => {
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)[authProperty]).toBe(
false
Expand All @@ -97,6 +107,7 @@ describe('Endpoint Authz service', () => {
canUnIsolateHost: true,
canCreateArtifactsByPolicy: false,
canKillProcess: false,
canSuspendProcess: false,
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const calculateEndpointAuthz = (
canIsolateHost: isPlatinumPlusLicense && hasAllAccessToFleet,
canUnIsolateHost: hasAllAccessToFleet,
canKillProcess: hasAllAccessToFleet && isPlatinumPlusLicense,
canSuspendProcess: hasAllAccessToFleet && isPlatinumPlusLicense,
};
};

Expand All @@ -44,5 +45,6 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canIsolateHost: false,
canUnIsolateHost: true,
canKillProcess: false,
canSuspendProcess: false,
};
};
16 changes: 10 additions & 6 deletions x-pack/plugins/security_solution/common/endpoint/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {

export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';

export type ResponseActions = ISOLATION_ACTIONS | 'kill-process';
export type ResponseActions = ISOLATION_ACTIONS | 'kill-process' | 'suspend-process';

export const ActivityLogItemTypes = {
ACTION: 'action' as const,
Expand Down Expand Up @@ -76,19 +76,23 @@ export interface LogsEndpointActionResponse {
error?: EcsError;
}

interface KillProcessWithPid {
interface ResponseActionParametersWithPid {
pid: number;
entity_id?: never;
}

interface KillProcessWithEntityId {
interface ResponseActionParametersWithEntityId {
pid?: never;
entity_id: number;
}

export type KillProcessParameters = KillProcessWithPid | KillProcessWithEntityId;
export type ResponseActionParametersWithPidOrEntityId =
| ResponseActionParametersWithPid
| ResponseActionParametersWithEntityId;

export type EndpointActionDataParameterTypes = undefined | KillProcessParameters;
export type EndpointActionDataParameterTypes =
| undefined
| ResponseActionParametersWithPidOrEntityId;

export interface EndpointActionData<T extends EndpointActionDataParameterTypes = undefined> {
command: ResponseActions;
Expand Down Expand Up @@ -194,7 +198,7 @@ export interface HostIsolationResponse {
}

export interface ResponseActionApiResponse {
action?: string;
action?: string; // only if command is isolate or release
data: ActionDetails;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface EndpointAuthz {
canUnIsolateHost: boolean;
/** If user has permissions to kill process on hosts */
canKillProcess: boolean;
/** If user has permissions to suspend process on hosts */
canSuspendProcess: boolean;
}

export type EndpointAuthzKeyList = Array<keyof EndpointAuthz>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
metadataTransformPrefix,
ENDPOINT_ACTIONS_INDEX,
KILL_PROCESS_ROUTE,
SUSPEND_PROCESS_ROUTE,
} from '../../../../common/endpoint/constants';
import {
ActionDetails,
Expand Down Expand Up @@ -369,6 +370,17 @@ describe('Response actions', () => {
expect(actionDoc.data.command).toEqual('kill-process');
});

it('sends the suspend-process command payload from the suspend process route', async () => {
const ctx = await callRoute(SUSPEND_PROCESS_ROUTE, {
body: { endpoint_ids: ['XYZ'] },
});
const actionDoc: EndpointAction = (
ctx.core.elasticsearch.client.asInternalUser.index.mock
.calls[0][0] as estypes.IndexRequest<EndpointAction>
).body!;
expect(actionDoc.data.command).toEqual('suspend-process');
});

describe('With endpoint data streams', () => {
it('handles unisolation', async () => {
const ctx = await callRoute(
Expand Down Expand Up @@ -454,6 +466,35 @@ describe('Response actions', () => {
expect(responseBody.action).toBeUndefined();
});

it('handles suspend-process', async () => {
const parameters = { entity_id: 1234 };
const ctx = await callRoute(
SUSPEND_PROCESS_ROUTE,
{
body: { endpoint_ids: ['XYZ'], parameters },
},
{ endpointDsExists: true }
);
const indexDoc = ctx.core.elasticsearch.client.asInternalUser.index;
const actionDocs: [
{ index: string; body?: LogsEndpointAction },
{ index: string; body?: EndpointAction }
] = [
indexDoc.mock.calls[0][0] as estypes.IndexRequest<LogsEndpointAction>,
indexDoc.mock.calls[1][0] as estypes.IndexRequest<EndpointAction>,
];

expect(actionDocs[0].index).toEqual(ENDPOINT_ACTIONS_INDEX);
expect(actionDocs[1].index).toEqual(AGENT_ACTIONS_INDEX);
expect(actionDocs[0].body!.EndpointActions.data.command).toEqual('suspend-process');
expect(actionDocs[1].body!.data.command).toEqual('suspend-process');
expect(actionDocs[1].body!.data.parameters).toEqual(parameters);

expect(mockResponse.ok).toBeCalled();
const responseBody = mockResponse.ok.mock.calls[0][0]?.body as ResponseActionApiResponse;
expect(responseBody.action).toBeUndefined();
});

it('handles errors', async () => {
const ErrMessage = 'Uh oh!';
await callRoute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { CommentType } from '@kbn/cases-plugin/common';

import {
HostIsolationRequestSchema,
KillProcessRequestSchema,
KillOrSuspendProcessRequestSchema,
ResponseActionBodySchema,
} from '../../../../common/endpoint/schema/actions';
import { APP_ID } from '../../../../common/constants';
Expand All @@ -27,6 +27,7 @@ import {
ENDPOINT_ACTION_RESPONSES_DS,
failedFleetActionErrorCode,
KILL_PROCESS_ROUTE,
SUSPEND_PROCESS_ROUTE,
} from '../../../../common/endpoint/constants';
import type {
EndpointAction,
Expand All @@ -36,7 +37,7 @@ import type {
LogsEndpointAction,
LogsEndpointActionResponse,
ResponseActions,
KillProcessParameters,
ResponseActionParametersWithPidOrEntityId,
} from '../../../../common/endpoint/types';
import type {
SecuritySolutionPluginRouter,
Expand Down Expand Up @@ -83,13 +84,32 @@ export function registerResponseActionRoutes(
router.post(
{
path: KILL_PROCESS_ROUTE,
validate: KillProcessRequestSchema,
validate: KillOrSuspendProcessRequestSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
withEndpointAuthz(
{ all: ['canKillProcess'] },
logger,
responseActionRequestHandler<KillProcessParameters>(endpointContext, 'kill-process')
responseActionRequestHandler<ResponseActionParametersWithPidOrEntityId>(
endpointContext,
'kill-process'
)
)
);

router.post(
{
path: SUSPEND_PROCESS_ROUTE,
validate: KillOrSuspendProcessRequestSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
withEndpointAuthz(
{ all: ['canSuspendProcess'] },
logger,
responseActionRequestHandler<ResponseActionParametersWithPidOrEntityId>(
endpointContext,
'suspend-process'
)
)
);
}
Expand All @@ -98,6 +118,7 @@ const commandToFeatureKeyMap = new Map<ResponseActions, FeatureKeys>([
['isolate', 'HOST_ISOLATION'],
['unisolate', 'HOST_ISOLATION'],
['kill-process', 'KILL_PROCESS'],
['suspend-process', 'SUSPEND_PROCESS'],
]);

const returnActionIdCommands: ResponseActions[] = ['isolate', 'unisolate'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const FEATURES = {
MEMORY_THREAT_PROTECTION: 'Memory threat protection',
BEHAVIOR_PROTECTION: 'Behavior protection',
KILL_PROCESS: 'Kill process',
SUSPEND_PROCESS: 'Suspend process',
} as const;

export type FeatureKeys = keyof typeof FEATURES;
Expand Down

0 comments on commit 5958878

Please sign in to comment.