From 5958878a53e7f6c9e56e9696ce2a5fdd8860ea83 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Thu, 16 Jun 2022 08:33:46 -0500 Subject: [PATCH] [Security Solution] add suspend-process response action API (#134486) --- .../common/endpoint/schema/actions.test.ts | 14 +++---- .../common/endpoint/schema/actions.ts | 4 +- .../endpoint/service/authz/authz.test.ts | 11 +++++ .../common/endpoint/service/authz/authz.ts | 2 + .../common/endpoint/types/actions.ts | 16 +++++--- .../common/endpoint/types/authz.ts | 2 + .../routes/actions/response_actions.test.ts | 41 +++++++++++++++++++ .../routes/actions/response_actions.ts | 29 +++++++++++-- .../services/feature_usage/service.ts | 1 + 9 files changed, 101 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts index 47e78cdab4a53..60cab431a5444 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; import { EndpointActionListRequestSchema, HostIsolationRequestSchema, - KillProcessRequestSchema, + KillOrSuspendProcessRequestSchema, } from './actions'; describe('actions schemas', () => { @@ -190,7 +190,7 @@ describe('actions schemas', () => { }); }); - describe('KillProcessRequestSchema', () => { + describe('KillOrSuspendProcessRequestSchema', () => { it('should require at least 1 Endpoint ID', () => { expect(() => { HostIsolationRequestSchema.body.validate({}); @@ -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, @@ -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, @@ -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, @@ -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: {}, @@ -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: { diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index 944b21b9b910d..c4dfa7a5b434c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -22,7 +22,7 @@ export const HostIsolationRequestSchema = { body: schema.object({ ...BaseActionRequestSchema }), }; -export const KillProcessRequestSchema = { +export const KillOrSuspendProcessRequestSchema = { body: schema.object({ ...BaseActionRequestSchema, parameters: schema.oneOf([ @@ -34,7 +34,7 @@ export const KillProcessRequestSchema = { export const ResponseActionBodySchema = schema.oneOf([ HostIsolationRequestSchema.body, - KillProcessRequestSchema.body, + KillOrSuspendProcessRequestSchema.body, ]); export const EndpointActionLogRequestSchema = { diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts index b9fe201a1cddf..0389ac8e216ae 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -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 @@ -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); @@ -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 @@ -97,6 +107,7 @@ describe('Endpoint Authz service', () => { canUnIsolateHost: true, canCreateArtifactsByPolicy: false, canKillProcess: false, + canSuspendProcess: false, }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts index 7c515cf1a3595..5acf3e5df1975 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -33,6 +33,7 @@ export const calculateEndpointAuthz = ( canIsolateHost: isPlatinumPlusLicense && hasAllAccessToFleet, canUnIsolateHost: hasAllAccessToFleet, canKillProcess: hasAllAccessToFleet && isPlatinumPlusLicense, + canSuspendProcess: hasAllAccessToFleet && isPlatinumPlusLicense, }; }; @@ -44,5 +45,6 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => { canIsolateHost: false, canUnIsolateHost: true, canKillProcess: false, + canSuspendProcess: false, }; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 38d5bdca028b8..2dd82d7609de4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -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, @@ -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 { command: ResponseActions; @@ -194,7 +198,7 @@ export interface HostIsolationResponse { } export interface ResponseActionApiResponse { - action?: string; + action?: string; // only if command is isolate or release data: ActionDetails; } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts index 3b07bc5e9b162..3f7a50537177f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts @@ -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; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts index cc04517d87bbd..3a91ee35269be 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts @@ -39,6 +39,7 @@ import { metadataTransformPrefix, ENDPOINT_ACTIONS_INDEX, KILL_PROCESS_ROUTE, + SUSPEND_PROCESS_ROUTE, } from '../../../../common/endpoint/constants'; import { ActionDetails, @@ -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 + ).body!; + expect(actionDoc.data.command).toEqual('suspend-process'); + }); + describe('With endpoint data streams', () => { it('handles unisolation', async () => { const ctx = await callRoute( @@ -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, + indexDoc.mock.calls[1][0] as estypes.IndexRequest, + ]; + + 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( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts index 91bec0ad66101..5f7ad42127f7c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts @@ -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'; @@ -27,6 +27,7 @@ import { ENDPOINT_ACTION_RESPONSES_DS, failedFleetActionErrorCode, KILL_PROCESS_ROUTE, + SUSPEND_PROCESS_ROUTE, } from '../../../../common/endpoint/constants'; import type { EndpointAction, @@ -36,7 +37,7 @@ import type { LogsEndpointAction, LogsEndpointActionResponse, ResponseActions, - KillProcessParameters, + ResponseActionParametersWithPidOrEntityId, } from '../../../../common/endpoint/types'; import type { SecuritySolutionPluginRouter, @@ -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(endpointContext, 'kill-process') + responseActionRequestHandler( + endpointContext, + 'kill-process' + ) + ) + ); + + router.post( + { + path: SUSPEND_PROCESS_ROUTE, + validate: KillOrSuspendProcessRequestSchema, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }, + withEndpointAuthz( + { all: ['canSuspendProcess'] }, + logger, + responseActionRequestHandler( + endpointContext, + 'suspend-process' + ) ) ); } @@ -98,6 +118,7 @@ const commandToFeatureKeyMap = new Map([ ['isolate', 'HOST_ISOLATION'], ['unisolate', 'HOST_ISOLATION'], ['kill-process', 'KILL_PROCESS'], + ['suspend-process', 'SUSPEND_PROCESS'], ]); const returnActionIdCommands: ResponseActions[] = ['isolate', 'unisolate']; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts index fa03aaf039e50..4171bb803fe65 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts @@ -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;