From 2ccc4e5da20d866cb6bb9989dcc1b1d9f4c5a9f3 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Wed, 29 May 2024 16:20:46 +0200 Subject: [PATCH 01/11] Add `scan` action request API closes elastic/security-team/issues/8991 --- .../common/api/endpoint/actions/scan.gen.ts | 28 +++ .../api/endpoint/actions/scan.schema.yaml | 41 ++++ .../common/api/endpoint/actions/scan_route.ts | 29 +++ .../common/endpoint/constants.ts | 1 + .../common/endpoint/schema/actions.test.ts | 2 + .../service/response_actions/constants.ts | 24 +- .../is_response_action_supported.ts | 12 + .../common/endpoint/types/actions.ts | 11 +- .../lib/endpoint_action_response_codes.ts | 30 +++ .../routes/actions/response_actions.test.ts | 209 ++++++++++-------- .../routes/actions/response_actions.ts | 28 +++ .../endpoint/endpoint_actions_client.test.ts | 2 + .../endpoint/endpoint_actions_client.ts | 13 ++ .../services/actions/clients/lib/types.ts | 13 ++ .../services/feature_usage/feature_keys.ts | 6 +- 15 files changed, 333 insertions(+), 116 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/api/endpoint/actions/scan.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/endpoint/actions/scan.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/endpoint/actions/scan_route.ts diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/scan.gen.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/scan.gen.ts new file mode 100644 index 00000000000000..43ab2c43548455 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/scan.gen.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from 'zod'; + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Scan Schema + * version: 2023-10-31 + */ + +import { BaseActionSchema } from '../model/schema/common.gen'; + +export type ScanActionRequestBody = z.infer; +export const ScanActionRequestBody = BaseActionSchema.merge( + z.object({ + parameters: z.object({ + path: z.string(), + }), + }) +); diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/scan.schema.yaml b/x-pack/plugins/security_solution/common/api/endpoint/actions/scan.schema.yaml new file mode 100644 index 00000000000000..be953b1330218d --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/scan.schema.yaml @@ -0,0 +1,41 @@ +openapi: 3.0.0 +info: + title: Scan Schema + version: '2023-10-31' +paths: + /api/endpoint/action/scan: + post: + summary: Scan Action + operationId: EndpointScanAction + x-codegen-enabled: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScanActionRequestBody' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../model/schema/common.schema.yaml#/components/schemas/SuccessResponse' + +components: + schemas: + ScanActionRequestBody: + allOf: + - $ref: '../model/schema/common.schema.yaml#/components/schemas/BaseActionSchema' + - type: object + required: + - parameters + properties: + parameters: + required: + - path + type: object + properties: + path: + type: string + diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/scan_route.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/scan_route.ts new file mode 100644 index 00000000000000..1c9b3c6980d909 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/scan_route.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; +import { BaseActionRequestSchema } from './common/base'; + +export const ScanActionRequestSchema = { + body: schema.object({ + ...BaseActionRequestSchema, + + parameters: schema.object({ + path: schema.string({ + minLength: 1, + validate: (value) => { + if (!value.trim().length) { + return 'path cannot be an empty string'; + } + }, + }), + }), + }), +}; + +export type ScanActionRequestBody = TypeOf; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index a4dd0b2b445187..3a10093b20a5bf 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -94,6 +94,7 @@ export const SUSPEND_PROCESS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/suspend_proc export const GET_FILE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/get_file`; export const EXECUTE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/execute`; export const UPLOAD_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/upload`; +export const SCAN_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/scan`; /** Endpoint Actions Routes */ export const ENDPOINT_ACTION_LOG_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_log/{agent_id}`; 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 bc32080fab1be6..6f399ff3fdc06f 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 @@ -759,4 +759,6 @@ describe('actions schemas', () => { }).toThrow('[file]: expected value of type [Stream] but got [Object]'); }); }); + + describe('ScanActionRequestSchema', () => {}); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts index 02716e09a882ae..32773a3fafef4f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts @@ -27,8 +27,7 @@ export const RESPONSE_ACTION_API_COMMANDS_NAMES = [ 'get-file', 'execute', 'upload', - // TODO: for API changes in a subsequent PR - // 'scan', + 'scan', ] as const; export type ResponseActionsApiCommandNames = typeof RESPONSE_ACTION_API_COMMANDS_NAMES[number]; @@ -54,8 +53,7 @@ export const ENDPOINT_CAPABILITIES = [ 'get_file', 'execute', 'upload_file', - // TODO: for API changes in a subsequent PR - // 'scan', + 'scan', ] as const; export type EndpointCapabilities = typeof ENDPOINT_CAPABILITIES[number]; @@ -73,8 +71,7 @@ export const CONSOLE_RESPONSE_ACTION_COMMANDS = [ 'get-file', 'execute', 'upload', - // TODO: for API changes in a subsequent PR - // 'scan', + 'scan', ] as const; export type ConsoleResponseActionCommands = typeof CONSOLE_RESPONSE_ACTION_COMMANDS[number]; @@ -102,8 +99,7 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL: Record< 'get-file': 'writeFileOperations', execute: 'writeExecuteOperations', upload: 'writeFileOperations', - // TODO: for API changes in a subsequent PR - // scan: 'writeScanOperations', + scan: 'writeScanOperations', }); export const RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP = Object.freeze< @@ -117,8 +113,7 @@ export const RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP = Object.freeze< 'kill-process': 'kill-process', 'suspend-process': 'suspend-process', upload: 'upload', - // TODO: for API changes in a subsequent PR - // scan: 'scan', + scan: 'scan', }); export const RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP = Object.freeze< @@ -132,8 +127,7 @@ export const RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP = Object.freeze< 'kill-process': 'kill-process', 'suspend-process': 'suspend-process', upload: 'upload', - // TODO: for API changes in a subsequent PR - // scan: 'scan', + scan: 'scan', }); export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY = Object.freeze< @@ -147,8 +141,7 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY = Object.fr 'kill-process': 'kill_process', 'suspend-process': 'suspend_process', upload: 'upload_file', - // TODO: for API changes in a subsequent PR - // scan: 'scan', + scan: 'scan', }); /** @@ -165,8 +158,7 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ = Object.freeze< processes: 'canGetRunningProcesses', 'kill-process': 'canKillProcess', 'suspend-process': 'canSuspendProcess', - // TODO: for API changes in a subsequent PR - // scan: 'canWriteScanOperations', + scan: 'canWriteScanOperations', }); // 4 hrs in seconds diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts index a2d55799c99439..0c0d0db9607094 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts @@ -114,6 +114,18 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = { crowdstrike: false, }, }, + scan: { + automated: { + endpoint: false, + sentinel_one: false, + crowdstrike: false, + }, + manual: { + endpoint: true, + sentinel_one: false, + crowdstrike: 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 c2279ec7b8be64..abf1dfa5e0fa75 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -90,6 +90,10 @@ export interface ResponseActionExecuteOutputContent { output_file_stderr_truncated: boolean; } +export interface ResponseActionScanOutputContent { + code: string; +} + export const ActivityLogItemTypes = { ACTION: 'action' as const, RESPONSE: 'response' as const, @@ -197,12 +201,17 @@ export interface ResponseActionsExecuteParameters { timeout?: number; } +export interface ResponseActionsScanParameters { + path: string; +} + export type EndpointActionDataParameterTypes = | undefined | ResponseActionParametersWithPidOrEntityId | ResponseActionsExecuteParameters | ResponseActionGetFileParameters - | ResponseActionUploadParameters; + | ResponseActionUploadParameters + | ResponseActionsScanParameters; /** Output content of the different response actions */ export type EndpointActionResponseDataOutput = diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts index a26a5d319be639..2e40fc44d5aacf 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts @@ -269,6 +269,36 @@ const CODES = Object.freeze({ 'xpack.securitySolution.endpointActionResponseCodes.upload.fileCorruption', { defaultMessage: 'Failed to save file to disk or validate its integrity' } ), + + // ----------------------------------------------------------------- + // SCAN CODES + // ----------------------------------------------------------------- + + 'ra_scan_error_scan-invalid-input': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.scan.invalidInput', + { defaultMessage: 'Scan failed. Invalid absolute file path provided.' } + ), + + // Dev: + // file path not found failure (404) + 'ra_scan_error_not-found': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.scan.notFound', + { defaultMessage: 'Scan failed. File path/folder was not found (404)' } + ), + + // Dev: + // scan quota exceeded failure + 'ra_scan_error_scan-queue-quota': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.scan.queueQuota', + { defaultMessage: 'Scan failed. Too many scans are queued.' } + ), + + // Dev: + // scan success/competed + ra_scan_success_done: i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.scan.success', + { defaultMessage: 'Success. Scan completed.' } + ), }); /** 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 d964d2bc00b706..9383fd2fdc2a38 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 @@ -24,23 +24,24 @@ import type { CasesClientMock } from '@kbn/cases-plugin/server/client/mocks'; import { LicenseService } from '../../../../common/license'; import { - ISOLATE_HOST_ROUTE_V2, - UNISOLATE_HOST_ROUTE_V2, ENDPOINT_ACTIONS_INDEX, - KILL_PROCESS_ROUTE, - SUSPEND_PROCESS_ROUTE, + EXECUTE_ROUTE, + GET_FILE_ROUTE, GET_PROCESSES_ROUTE, ISOLATE_HOST_ROUTE, + ISOLATE_HOST_ROUTE_V2, + KILL_PROCESS_ROUTE, + SCAN_ROUTE, + SUSPEND_PROCESS_ROUTE, UNISOLATE_HOST_ROUTE, - GET_FILE_ROUTE, - EXECUTE_ROUTE, + UNISOLATE_HOST_ROUTE_V2, UPLOAD_ROUTE, } from '../../../../common/endpoint/constants'; import type { ActionDetails, - ResponseActionApiResponse, HostMetadata, LogsEndpointAction, + ResponseActionApiResponse, ResponseActionRequestBody, } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; @@ -285,10 +286,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ agents: [AgentID], @@ -304,10 +303,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ user_id: testUser.username, @@ -322,10 +319,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ comment }), @@ -347,10 +342,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ action_id: expect.any(String), @@ -374,10 +367,8 @@ describe('Response actions', () => { body: { endpoint_ids: ['XYZ'] }, version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ timeout: 300, @@ -395,10 +386,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ agents: [agentId], @@ -412,10 +401,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -431,10 +418,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -450,10 +435,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -469,10 +452,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -488,10 +469,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -507,10 +486,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -526,10 +503,8 @@ describe('Response actions', () => { version: '2023-10-31', }); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -539,6 +514,24 @@ describe('Response actions', () => { ); }); + it('sends the `scan` command payload from the scan route', async () => { + await callRoute(SCAN_ROUTE, { + body: { endpoint_ids: ['XYZ'], parameters: { path: '/home/usr/' } }, + version: '2023-10-31', + }); + + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock + ).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + command: 'scan', + parameters: { path: '/home/usr/' }, + }), + }) + ); + }); + describe('With endpoint data streams', () => { it('handles unisolation', async () => { const ctx = await callRoute( @@ -550,10 +543,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -585,10 +576,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -621,10 +610,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -659,10 +646,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -696,10 +681,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -731,10 +714,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -768,10 +749,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -806,10 +785,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -836,6 +813,41 @@ describe('Response actions', () => { expect(responseBody.action).toBeUndefined(); }); + it('handles scan', async () => { + const ctx = await callRoute( + SCAN_ROUTE, + { + body: { endpoint_ids: ['XYZ'], parameters: { path: '/home/usr/' } }, + version: '2023-10-31', + }, + { endpointDsExists: true } + ); + + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock + ).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + command: 'scan', + comment: undefined, + parameters: { path: '/home/usr/' }, + }), + }) + ); + + const indexDoc = ctx.core.elasticsearch.client.asInternalUser.index; + const actionDocs: [{ index: string; document?: LogsEndpointAction }] = [ + indexDoc.mock.calls[0][0] as estypes.IndexRequest, + ]; + + expect(actionDocs[0].index).toEqual(ENDPOINT_ACTIONS_INDEX); + expect(actionDocs[0].document!.EndpointActions.data.command).toEqual('scan'); + + expect(mockResponse.ok).toBeCalled(); + const responseBody = mockResponse.ok.mock.calls[0][0]?.body as ResponseActionApiResponse; + expect(responseBody.action).toBeUndefined(); + }); + it('signs the action', async () => { await callRoute( ISOLATE_HOST_ROUTE_V2, @@ -846,10 +858,8 @@ describe('Response actions', () => { { endpointDsExists: true } ); - await expect( - ( - await endpointAppContextService.getFleetActionsClient() - ).create as jest.Mock + expect( + (await endpointAppContextService.getFleetActionsClient()).create as jest.Mock ).toHaveBeenCalledWith( expect.objectContaining({ signed: { @@ -960,6 +970,15 @@ describe('Response actions', () => { }); expect(mockResponse.forbidden).toBeCalled(); }); + + it('prohibits user from performing `scan` action if `canWriteScanOperations` is `false`', async () => { + await callRoute(SCAN_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + authz: { canWriteScanOperations: false }, + version: '2023-10-31', + }); + expect(mockResponse.forbidden).toBeCalled(); + }); }); describe('Cases', () => { 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 54af1f47166071..2b7f794d4f4c2d 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 @@ -7,6 +7,8 @@ import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; +import type { ScanActionRequestBody } from '../../../../common/api/endpoint/actions/scan_route'; +import { ScanActionRequestSchema } from '../../../../common/api/endpoint/actions/scan_route'; import { responseActionsWithLegacyActionProperty } from '../../services/actions/constants'; import { stringify } from '../../utils/stringify'; import { getResponseActionsClient, NormalizedExternalConnectorClient } from '../../services'; @@ -41,6 +43,7 @@ import { GET_FILE_ROUTE, EXECUTE_ROUTE, UPLOAD_ROUTE, + SCAN_ROUTE, } from '../../../../common/endpoint/constants'; import type { EndpointActionDataParameterTypes, @@ -48,6 +51,7 @@ import type { ResponseActionsExecuteParameters, ActionDetails, KillOrSuspendProcessRequestBody, + ResponseActionsScanParameters, } from '../../../../common/endpoint/types'; import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint/service/response_actions/constants'; import type { @@ -279,6 +283,26 @@ export function registerResponseActionRoutes( responseActionRequestHandler(endpointContext, 'upload') ) ); + + router.versioned + .post({ + access: 'public', + path: SCAN_ROUTE, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }) + .addVersion( + { + version: '2023-10-31', + validate: { + request: ScanActionRequestSchema, + }, + }, + withEndpointAuthz( + { all: ['canWriteScanOperations'] }, + logger, + responseActionRequestHandler(endpointContext, 'scan') + ) + ); } function responseActionRequestHandler( @@ -368,6 +392,10 @@ function responseActionRequestHandler { execute: responseActionsClientMock.createExecuteOptions(getCommonResponseActionOptions()), upload: responseActionsClientMock.createUploadOptions(getCommonResponseActionOptions()), + + scan: responseActionsClientMock.createUploadOptions(getCommonResponseActionOptions()), }; it.each(Object.keys(responseActionMethods) as ResponseActionsMethodsOnly[])( diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts index 96e77be833e3c5..bd25bcd2678411 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts @@ -7,6 +7,7 @@ import type { FleetActionRequest } from '@kbn/fleet-plugin/server/services/actions'; import { v4 as uuidv4 } from 'uuid'; +import type { ScanActionRequestBody } from '../../../../../../common/api/endpoint/actions/scan_route'; import { CustomHttpRequestError } from '../../../../../utils/custom_http_request_error'; import { getActionRequestExpiration } from '../../utils'; import { ResponseActionsClientError } from '../errors'; @@ -42,6 +43,8 @@ import type { LogsEndpointAction, EndpointActionDataParameterTypes, UploadedFileInfo, + ResponseActionsScanParameters, + ResponseActionScanOutputContent, } from '../../../../../../common/endpoint/types'; import type { CommonResponseActionMethodOptions, @@ -286,6 +289,16 @@ export class EndpointActionsClient extends ResponseActionsClientImpl { >('execute', actionRequestWithDefaults, options); } + async scan( + actionRequest: ScanActionRequestBody, + options: CommonResponseActionMethodOptions = {} + ): Promise> { + return this.handleResponseAction< + ScanActionRequestBody, + ActionDetails + >('scan', actionRequest, options); + } + async upload( actionRequest: UploadActionApiRequestBody, options: CommonResponseActionMethodOptions = {} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts index 9cc7f088c38403..578c82abbf65bb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts @@ -6,6 +6,7 @@ */ import type { Readable } from 'stream'; +import type { ScanActionRequestBody } from '../../../../../../common/api/endpoint/actions/scan_route'; import type { ActionDetails, KillOrSuspendProcessRequestBody, @@ -22,6 +23,8 @@ import type { EndpointActionData, LogsEndpointActionResponse, UploadedFileInfo, + ResponseActionScanOutputContent, + ResponseActionsScanParameters, } from '../../../../../../common/endpoint/types'; import type { IsolationRouteRequestBody, @@ -140,4 +143,14 @@ export interface ResponseActionsClient { * @param fileId */ getFileInfo(actionId: string, fileId: string): Promise; + + /** + * Scan a file path/folder + * @param actionRequest + * @param options + */ + scan: ( + actionRequest: OmitUnsupportedAttributes, + options?: CommonResponseActionMethodOptions + ) => Promise>; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts index c367c3d1bd91a2..52f6cd36711231 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/feature_keys.ts @@ -23,8 +23,7 @@ export const FEATURE_KEYS = { GET_FILE: 'Get file', UPLOAD: 'Upload file', EXECUTE: 'Execute command', - // TODO: for API changes in a subsequent PR - // SCAN: 'Scan files', + SCAN: 'Scan files', ALERTS_BY_PROCESS_ANCESTRY: 'Get related alerts by process ancestry', ENDPOINT_EXCEPTIONS: 'Endpoint exceptions', } as const; @@ -41,8 +40,7 @@ const RESPONSE_ACTIONS_FEATURE_KEY: Readonly Date: Thu, 30 May 2024 10:23:01 +0200 Subject: [PATCH 02/11] fix types --- .../endpoint/actions/common/response_actions.ts | 2 ++ .../common/endpoint/types/actions.ts | 3 ++- .../clients/lib/base_response_actions_client.ts | 16 +++++++++++++--- .../endpoint/services/actions/clients/mocks.ts | 6 +++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts index 269f041a25a10b..66dc4d5828ce00 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts @@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema'; import { UploadActionRequestSchema } from '../..'; import { ExecuteActionRequestSchema } from '../execute_route'; import { EndpointActionGetFileSchema } from '../get_file_route'; +import { ScanActionRequestSchema } from '../scan_route'; import { KillOrSuspendProcessRequestSchema, NoParametersRequestSchema } from './base'; export const ResponseActionBodySchema = schema.oneOf([ @@ -17,6 +18,7 @@ export const ResponseActionBodySchema = schema.oneOf([ KillOrSuspendProcessRequestSchema.body, EndpointActionGetFileSchema.body, ExecuteActionRequestSchema.body, + ScanActionRequestSchema.body, UploadActionRequestSchema.body, ]); 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 abf1dfa5e0fa75..489d75fd92afbc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -221,7 +221,8 @@ export type EndpointActionResponseDataOutput = | ResponseActionUploadOutputContent | GetProcessesActionOutputContent | SuspendProcessActionOutputContent - | KillProcessActionOutputContent; + | KillProcessActionOutputContent + | ResponseActionScanOutputContent; /** * The data stored with each Response Action under `EndpointActions.data` property diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts index 148f04a587990d..2cfa1cd674630e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts @@ -13,6 +13,7 @@ import { AttachmentType, ExternalReferenceStorageType } from '@kbn/cases-plugin/ import type { CaseAttachments } from '@kbn/cases-plugin/public/types'; import { i18n } from '@kbn/i18n'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import type { ScanActionRequestBody } from '../../../../../../common/api/endpoint/actions/scan_route'; import { validateActionId } from '../../utils/validate_action_id'; import { fetchActionResponses, @@ -35,9 +36,9 @@ import { } from '../../../../../../common/endpoint/constants'; import type { CommonResponseActionMethodOptions, + GetFileDownloadMethodResponse, ProcessPendingActionsMethodOptions, ResponseActionsClient, - GetFileDownloadMethodResponse, } from './types'; import type { ActionDetails, @@ -52,13 +53,16 @@ import type { ResponseActionGetFileOutputContent, ResponseActionGetFileParameters, ResponseActionParametersWithPidOrEntityId, + ResponseActionScanOutputContent, ResponseActionsExecuteParameters, + ResponseActionsScanParameters, ResponseActionUploadOutputContent, ResponseActionUploadParameters, SuspendProcessActionOutputContent, - WithAllKeys, UploadedFileInfo, + WithAllKeys, } from '../../../../../../common/endpoint/types'; +import { ActivityLogItemTypes } from '../../../../../../common/endpoint/types'; import type { ExecuteActionRequestBody, GetProcessesRequestBody, @@ -70,7 +74,6 @@ import type { import { stringify } from '../../../../utils/stringify'; import { CASE_ATTACHMENT_ENDPOINT_TYPE_ID } from '../../../../../../common/constants'; import { EMPTY_COMMENT } from '../../../../utils/translations'; -import { ActivityLogItemTypes } from '../../../../../../common/endpoint/types'; const ENTERPRISE_LICENSE_REQUIRED_MSG = i18n.translate( 'xpack.securitySolution.responseActionsList.error.licenseTooLow', @@ -699,6 +702,13 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient throw new ResponseActionsNotSupportedError('upload'); } + public async scan( + actionRequest: ScanActionRequestBody, + options?: CommonResponseActionMethodOptions + ): Promise> { + throw new ResponseActionsNotSupportedError('scan'); + } + public async processPendingActions(_: ProcessPendingActionsMethodOptions): Promise { this.log.debug(`#processPendingActions() method is not implemented for ${this.agentType}!`); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts index c64b107b86761e..0bc934ce488906 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts @@ -21,6 +21,7 @@ import type { AttachmentsSubClient } from '@kbn/cases-plugin/server/client/attac import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { ResponseActionsClient } from '../..'; +import { NormalizedExternalConnectorClient } from '../..'; import type { KillOrSuspendProcessRequestBody } from '../../../../../common/endpoint/types'; import { BaseDataGenerator } from '../../../../../common/endpoint/data_generators/base_data_generator'; import { @@ -43,12 +44,10 @@ import { ACTION_RESPONSE_INDICES } from '../constants'; import type { ExecuteActionRequestBody, GetProcessesRequestBody, - ResponseActionGetFileRequestBody, IsolationRouteRequestBody, + ResponseActionGetFileRequestBody, UploadActionApiRequestBody, } from '../../../../../common/api/endpoint'; -import { NormalizedExternalConnectorClient } from '../..'; -import {} from '@kbn/utility-types-jest'; export interface ResponseActionsClientOptionsMock extends ResponseActionsClientOptions { esClient: ElasticsearchClientMock; @@ -68,6 +67,7 @@ const createResponseActionClientMock = (): jest.Mocked => processPendingActions: jest.fn().mockReturnValue(Promise.resolve()), getFileInfo: jest.fn().mockReturnValue(Promise.resolve()), getFileDownload: jest.fn().mockReturnValue(Promise.resolve()), + scan: jest.fn().mockReturnValue(Promise.resolve()), }; }; From b27d2a83a78e6d5fe0cd1dd770f86eb0a7c16bf1 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Thu, 30 May 2024 11:40:52 +0200 Subject: [PATCH 03/11] fix tests --- .../endpoint_action_generator.ts | 58 ++++++++++++++++- .../components/hooks.tsx | 5 ++ .../response_actions_log.test.tsx | 29 ++++++++- .../serverless/feature_access/complete.cy.ts | 10 +-- .../complete_with_endpoint.cy.ts | 10 ++- .../feature_access/essentials.cy.ts | 10 +-- .../essentials_with_endpoint.cy.ts | 10 +-- .../utils/fetch_action_responses.test.ts | 65 ++++++++++--------- .../services/actions/utils/utils.test.ts | 28 ++++---- 9 files changed, 166 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index 83976ff215e82a..9ba617ad0052d1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts @@ -28,6 +28,8 @@ import type { ResponseActionUploadParameters, EndpointActionResponseDataOutput, WithAllKeys, + ResponseActionScanOutputContent, + ResponseActionsScanParameters, } from '../types'; import { ActivityLogItemTypes } from '../types'; import { @@ -102,9 +104,13 @@ export class EndpointActionGenerator extends BaseDataGenerator { const command = overrides?.EndpointActions?.data?.command ?? this.randomResponseActionCommand(); let output: ActionResponseOutput< - ResponseActionGetFileOutputContent | ResponseActionExecuteOutputContent + | ResponseActionGetFileOutputContent + | ResponseActionExecuteOutputContent + | ResponseActionScanOutputContent > = overrides?.EndpointActions?.data?.output as unknown as ActionResponseOutput< - ResponseActionGetFileOutputContent | ResponseActionExecuteOutputContent + | ResponseActionGetFileOutputContent + | ResponseActionExecuteOutputContent + | ResponseActionScanOutputContent >; if (command === 'get-file') { @@ -128,6 +134,17 @@ export class EndpointActionGenerator extends BaseDataGenerator { } } + if (command === 'scan') { + if (!output) { + output = { + type: 'json', + content: { + code: 'ra_scan_success_done', + }, + }; + } + } + if (command === 'execute') { if (!output) { output = this.generateExecuteActionResponseOutput(); @@ -269,6 +286,35 @@ export class EndpointActionGenerator extends BaseDataGenerator { } } + if (command === 'scan') { + if (!details.parameters) { + ( + details as unknown as ActionDetails< + ResponseActionScanOutputContent, + ResponseActionsScanParameters + > + ).parameters = { + path: '/some/file.txt', + }; + } + + if (!details.outputs || Object.keys(details.outputs).length === 0) { + ( + details as unknown as ActionDetails< + ResponseActionScanOutputContent, + ResponseActionsScanParameters + > + ).outputs = { + [details.agents[0]]: { + type: 'json', + content: { + code: 'ra_scan_success_done', + }, + }, + }; + } + } + if (command === 'execute') { if (!details.parameters) { ( @@ -347,6 +393,14 @@ export class EndpointActionGenerator extends BaseDataGenerator { ]); } + randomScanFailureCode(): string { + return this.randomChoice([ + 'ra_scan_error_scan-invalid-input', + 'ra_scan_error_not-found', + 'ra_scan_error_scan-queue-quota', + ]); + } + generateActivityLogAction( overrides: DeepPartial ): EndpointActivityLogAction { diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx index a91c87223e44c6..0fe111965f463c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx @@ -334,6 +334,11 @@ export const useActionsLogFilter = ({ return false; } + // `scan` - v8.15 + if (commandName === 'scan' && !featureFlags.responseActionScanEnabled) { + return false; + } + return true; }).map((commandName) => ({ key: commandName, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index 482edd42056e4b..c10b661124bd79 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -1112,11 +1112,37 @@ describe('Response actions history', () => { ); }); - it('should show a list of actions when opened', () => { + it('should show a list of actions (without `scan`) when opened', () => { mockedContext.setExperimentalFlag({ responseActionUploadEnabled: true }); render(); const { getByTestId, getAllByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const filterList = getByTestId(`${testPrefix}-${filterPrefix}-popoverList`); + expect(filterList).toBeTruthy(); + expect(getAllByTestId(`${filterPrefix}-option`).length).toEqual( + RESPONSE_ACTION_API_COMMANDS_NAMES.length - 1 + ); + expect(getAllByTestId(`${filterPrefix}-option`).map((option) => option.textContent)).toEqual([ + 'isolate. To check this option, press Enter.', + 'release. To check this option, press Enter.', + 'kill-process. To check this option, press Enter.', + 'suspend-process. To check this option, press Enter.', + 'processes. To check this option, press Enter.', + 'get-file. To check this option, press Enter.', + 'execute. To check this option, press Enter.', + 'upload. To check this option, press Enter.', + ]); + }); + + it('should show a list of actions (with `scan`) when opened', () => { + mockedContext.setExperimentalFlag({ + responseActionUploadEnabled: true, + responseActionScanEnabled: true, + }); + render(); + const { getByTestId, getAllByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); const filterList = getByTestId(`${testPrefix}-${filterPrefix}-popoverList`); expect(filterList).toBeTruthy(); @@ -1132,6 +1158,7 @@ describe('Response actions history', () => { 'get-file. To check this option, press Enter.', 'execute. To check this option, press Enter.', 'upload. To check this option, press Enter.', + 'scan. To check this option, press Enter.', ]); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts index 83a70c84a23074..57b2820921dd96 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts @@ -53,9 +53,10 @@ describe( } // No access to response actions (except `unisolate`) + // TODO: update tests when `scan` is included in PLIs for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' - )) { + (apiName) => apiName !== 'scan' + ).filter((apiName) => apiName !== 'unisolate')) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); }); @@ -78,9 +79,10 @@ describe( }); // No access to response actions (except `unisolate`) + // TODO: update tests when `scan` is included in PLIs for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' - )) { + (apiName) => apiName !== 'scan' + ).filter((apiName) => apiName !== 'unisolate')) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts index 13ffbfd848e881..da17beb14d760f 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts @@ -47,7 +47,10 @@ describe( }); } - for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) { + // TODO: update tests when `scan` is included in PLIs + for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( + (apiName) => apiName !== 'scan' + )) { it(`should allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('all', actionName, username, password); }); @@ -70,7 +73,10 @@ describe( }); }); - for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) { + // TODO: update tests when `scan` is included in PLIs + for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( + (apiName) => apiName !== 'scan' + )) { it(`should allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('all', actionName, username, password); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts index e7400b548debf8..e4388924f05fcb 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts @@ -55,9 +55,10 @@ describe( } // No access to response actions (except `unisolate`) + // TODO: update tests when `scan` is included in PLIs for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' - )) { + (apiName) => apiName !== 'scan' + ).filter((apiName) => apiName !== 'unisolate')) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); }); @@ -80,9 +81,10 @@ describe( }); // No access to response actions (except `unisolate`) + // TODO: update tests when `scan` is included in PLIs for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' - )) { + (apiName) => apiName !== 'scan' + ).filter((apiName) => apiName !== 'unisolate')) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts index 87d48220be6974..4a37f1089e8976 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts @@ -62,9 +62,10 @@ describe( }); } + // TODO: update tests when `scan` is included in PLIs for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' - )) { + (apiName) => apiName !== 'scan' + ).filter((apiName) => apiName !== 'unisolate')) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); }); @@ -91,9 +92,10 @@ describe( }); }); + // TODO: update tests when `scan` is included in PLIs for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter( - (apiName) => apiName !== 'unisolate' - )) { + (apiName) => apiName !== 'scan' + ).filter((apiName) => apiName !== 'unisolate')) { it(`should not allow access to Response Action: ${actionName}`, () => { ensureResponseActionAuthzAccess('none', actionName, username, password); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_responses.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_responses.test.ts index 6c366142adfb9b..096b631b187888 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_responses.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_responses.test.ts @@ -32,8 +32,9 @@ describe('fetchActionResponses()', () => { _source: { '@timestamp': '2022-04-30T16:08:47.449Z', action_data: { - command: 'get-file', + command: 'execute', comment: '', + parameter: undefined, }, action_id: '123', agent_id: 'agent-a', @@ -44,7 +45,7 @@ describe('fetchActionResponses()', () => { sort: ['abc'], }, { - _id: 'ef278144-d8b9-45c6-9c3c-484c86b57d0b', + _id: 'b2c51bc3-0cd7-4e6b-a085-84f5eb09438f', _index: '.ds-.logs-endpoint.action.responses-some_namespace-something', _score: 1, _source: { @@ -53,30 +54,32 @@ describe('fetchActionResponses()', () => { action_id: '123', completed_at: '2022-04-30T10:53:59.449Z', data: { - command: 'get-file', + command: 'execute', comment: '', + parameter: undefined, output: { content: { - code: 'ra_get-file_success_done', - contents: [ - { - file_name: 'bad_file.txt', - path: '/some/path/bad_file.txt', - sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f', - size: 1234, - type: 'file', - }, - ], - zip_size: 123, + code: 'ra_execute_success_done', + cwd: '/some/path', + output_file_id: 'some-output-file-id', + output_file_stderr_truncated: false, + output_file_stdout_truncated: true, + shell: 'bash', + shell_code: 0, + stderr_truncated: true, + stdout_truncated: true, + stderr: expect.any(String), + stdout: expect.any(String), }, type: 'json', }, }, - started_at: '2022-04-30T12:56:00.449Z', + started_at: '2022-04-30T13:56:00.449Z', }, agent: { id: 'agent-a', }, + error: undefined, }, sort: ['abc'], }, @@ -87,8 +90,9 @@ describe('fetchActionResponses()', () => { _source: { '@timestamp': '2022-04-30T16:08:47.449Z', action_data: { - command: 'get-file', + command: 'execute', comment: '', + parameter: undefined, }, action_id: '123', agent_id: 'agent-a', @@ -99,7 +103,7 @@ describe('fetchActionResponses()', () => { sort: ['abc'], }, { - _id: 'ef278144-d8b9-45c6-9c3c-484c86b57d0b', + _id: 'b2c51bc3-0cd7-4e6b-a085-84f5eb09438f', _index: '.ds-.logs-endpoint.action.responses-some_namespace-something', _score: 1, _source: { @@ -108,30 +112,31 @@ describe('fetchActionResponses()', () => { action_id: '123', completed_at: '2022-04-30T10:53:59.449Z', data: { - command: 'get-file', + command: 'execute', comment: '', output: { content: { - code: 'ra_get-file_success_done', - contents: [ - { - file_name: 'bad_file.txt', - path: '/some/path/bad_file.txt', - sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f', - size: 1234, - type: 'file', - }, - ], - zip_size: 123, + code: 'ra_execute_success_done', + cwd: '/some/path', + output_file_id: 'some-output-file-id', + output_file_stderr_truncated: false, + output_file_stdout_truncated: true, + shell: 'bash', + shell_code: 0, + stderr_truncated: true, + stdout_truncated: true, + stderr: expect.any(String), + stdout: expect.any(String), }, type: 'json', }, }, - started_at: '2022-04-30T12:56:00.449Z', + started_at: '2022-04-30T13:56:00.449Z', }, agent: { id: 'agent-a', }, + error: undefined, }, sort: ['abc'], }, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.test.ts index a2e69696b557cb..928411cd8d7dcb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.test.ts @@ -1030,20 +1030,24 @@ describe('When using Actions service utilities', () => { id: '90d62689-f72d-4a05-b5e3-500cad0dc366', agentType: 'endpoint', agents: ['6e6796b0-af39-4f12-b025-fcb06db499e5'], + alertIds: undefined, command: 'kill-process', comment: 'kill this one', completedAt: expect.any(String), startedAt: '2022-04-27T16:08:47.449Z', status: 'successful', wasSuccessful: true, - errors: undefined, createdBy: 'elastic', + errors: undefined, isCompleted: true, isExpired: false, parameters: undefined, + ruleId: undefined, + ruleName: undefined, agentState: { '6e6796b0-af39-4f12-b025-fcb06db499e5': { completedAt: expect.any(String), + errors: undefined, isCompleted: true, wasSuccessful: true, }, @@ -1056,17 +1060,17 @@ describe('When using Actions service utilities', () => { outputs: { '6e6796b0-af39-4f12-b025-fcb06db499e5': { content: { - code: 'ra_get-file_success_done', - contents: [ - { - file_name: 'bad_file.txt', - path: '/some/path/bad_file.txt', - sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f', - size: 1234, - type: 'file', - }, - ], - zip_size: 123, + code: 'ra_execute_success_done', + cwd: '/some/path', + output_file_id: 'some-output-file-id', + output_file_stderr_truncated: false, + output_file_stdout_truncated: true, + shell: 'bash', + shell_code: 0, + stderr: expect.any(String), + stderr_truncated: true, + stdout: expect.any(String), + stdout_truncated: true, }, type: 'json', }, From e5f4b8ceeb5bf06ed32c36ffc8ba90a464bb698d Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Thu, 30 May 2024 12:58:54 +0200 Subject: [PATCH 04/11] route behind feature flag --- .../routes/actions/response_actions.test.ts | 14 ++++++- .../routes/actions/response_actions.ts | 40 ++++++++++--------- 2 files changed, 35 insertions(+), 19 deletions(-) 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 9383fd2fdc2a38..8c1ba19ca626da 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 @@ -75,6 +75,8 @@ import { responseActionsClientMock } from '../../services/actions/clients/mocks' import type { ActionsApiRequestHandlerContext } from '@kbn/actions-plugin/server'; import { sentinelOneMock } from '../../services/actions/clients/sentinelone/mocks'; import { ResponseActionsClientError } from '../../services/actions/clients/errors'; +import type { EndpointAppContext } from '../../types'; +import type { ExperimentalFeatures } from '../../../../common'; jest.mock('../../services', () => { const realModule = jest.requireActual('../../services'); @@ -117,6 +119,7 @@ describe('Response actions', () => { let mockResponse: jest.Mocked; let licenseService: LicenseService; let licenseEmitter: Subject; + let endpointContext: EndpointAppContext; let callRoute: ( routePrefix: string, @@ -130,6 +133,13 @@ describe('Response actions', () => { const docGen = new EndpointDocGenerator(); + const setFeatureFlag = (ff: Partial) => { + endpointContext.experimentalFeatures = { + ...endpointContext.experimentalFeatures, + ...ff, + }; + }; + beforeEach(() => { // instantiate... everything const mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -151,7 +161,7 @@ describe('Response actions', () => { licenseService = new LicenseService(); licenseService.start(licenseEmitter); - const endpointContext = { + endpointContext = { ...createMockEndpointAppContext(), service: endpointAppContextService, }; @@ -162,6 +172,8 @@ describe('Response actions', () => { licenseService, }); + setFeatureFlag({ responseActionScanEnabled: true }); + // add the host isolation route handlers to routerMock registerResponseActionRoutes(routerMock, endpointContext); 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 2b7f794d4f4c2d..df99896872081a 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 @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; @@ -284,25 +285,28 @@ export function registerResponseActionRoutes( ) ); - router.versioned - .post({ - access: 'public', - path: SCAN_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, - }) - .addVersion( - { - version: '2023-10-31', - validate: { - request: ScanActionRequestSchema, + // 8.15 route + if (endpointContext.experimentalFeatures.responseActionScanEnabled) { + router.versioned + .post({ + access: 'public', + path: SCAN_ROUTE, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }) + .addVersion( + { + version: '2023-10-31', + validate: { + request: ScanActionRequestSchema, + }, }, - }, - withEndpointAuthz( - { all: ['canWriteScanOperations'] }, - logger, - responseActionRequestHandler(endpointContext, 'scan') - ) - ); + withEndpointAuthz( + { all: ['canWriteScanOperations'] }, + logger, + responseActionRequestHandler(endpointContext, 'scan') + ) + ); + } } function responseActionRequestHandler( From 9e62e59a0ade4ec321570ee6b40fc7cbe74fe16c Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Thu, 30 May 2024 13:11:24 +0200 Subject: [PATCH 05/11] fix type --- .../public/management/cypress/screens/responder.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts index c612c99db17b34..7e920772374c50 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts @@ -14,8 +14,9 @@ const TEST_SUBJ = Object.freeze({ actionLogFlyout: 'responderActionLogFlyout', }); +// TODO: 8.15 Include `scan` in return type when responseActionsScanEnabled when `scan` is categorized in PLIs export const getConsoleHelpPanelResponseActionTestSubj = (): Record< - ConsoleResponseActionCommands, + Exclude, string > => { return { @@ -27,6 +28,8 @@ export const getConsoleHelpPanelResponseActionTestSubj = (): Record< 'get-file': 'endpointResponseActionsConsole-commandList-Responseactions-get-file', execute: 'endpointResponseActionsConsole-commandList-Responseactions-execute', upload: 'endpointResponseActionsConsole-commandList-Responseactions-upload', + // TODO: 8.15 Include `scan` in return type when responseActionsScanEnabled when `scan` is categorized in PLIs + // scan: 'endpointResponseActionsConsole-commandList-Responseactions-scan', }; }; From ae882dfe36c34e1aa90ea74f36ad36c4ab9707ff Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Thu, 30 May 2024 13:12:16 +0200 Subject: [PATCH 06/11] fix tests --- .../actions/action_details_by_id.test.ts | 22 +++---- .../services/actions/action_list.test.ts | 62 ++++++++++++------- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts index 861a8c8870fa42..de0afe6223cd96 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts @@ -85,17 +85,17 @@ describe('When using `getActionDetailsById()', () => { outputs: { 'agent-a': { content: { - code: 'ra_get-file_success_done', - contents: [ - { - file_name: 'bad_file.txt', - path: '/some/path/bad_file.txt', - sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f', - size: 1234, - type: 'file', - }, - ], - zip_size: 123, + code: 'ra_execute_success_done', + cwd: '/some/path', + output_file_id: 'some-output-file-id', + output_file_stderr_truncated: false, + output_file_stdout_truncated: true, + shell: 'bash', + shell_code: 0, + stderr: expect.any(String), + stderr_truncated: true, + stdout: expect.any(String), + stdout_truncated: true, }, type: 'json', }, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts index 0a55f2a62e7bee..df558b97ee9d77 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts @@ -95,6 +95,7 @@ describe('When using `getActionList()', () => { agentType: 'endpoint', hosts: { 'agent-a': { name: 'Host-agent-a' } }, command: 'kill-process', + alertIds: undefined, completedAt: '2022-04-30T16:08:47.449Z', wasSuccessful: true, errors: undefined, @@ -108,6 +109,7 @@ describe('When using `getActionList()', () => { parameters: doc?.EndpointActions.data.parameters, agentState: { 'agent-a': { + errors: undefined, completedAt: '2022-04-30T16:08:47.449Z', isCompleted: true, wasSuccessful: true, @@ -116,17 +118,17 @@ describe('When using `getActionList()', () => { outputs: { 'agent-a': { content: { - code: 'ra_get-file_success_done', - contents: [ - { - file_name: 'bad_file.txt', - path: '/some/path/bad_file.txt', - sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f', - size: 1234, - type: 'file', - }, - ], - zip_size: 123, + code: 'ra_execute_success_done', + cwd: '/some/path', + output_file_id: 'some-output-file-id', + output_file_stderr_truncated: false, + output_file_stdout_truncated: true, + shell: 'bash', + shell_code: 0, + stderr: expect.any(String), + stderr_truncated: true, + stdout: expect.any(String), + stdout_truncated: true, }, type: 'json', }, @@ -166,6 +168,7 @@ describe('When using `getActionList()', () => { ).resolves.toEqual({ page: 1, pageSize: 10, + agentTypes: undefined, commands: undefined, userIds: undefined, startDate: undefined, @@ -179,8 +182,8 @@ describe('When using `getActionList()', () => { hosts: { 'agent-a': { name: 'Host-agent-a' } }, command: 'kill-process', completedAt: '2022-04-30T16:08:47.449Z', - wasSuccessful: true, errors: undefined, + wasSuccessful: true, id: '123', isCompleted: true, isExpired: false, @@ -189,17 +192,17 @@ describe('When using `getActionList()', () => { outputs: { 'agent-a': { content: { - code: 'ra_get-file_success_done', - contents: [ - { - file_name: 'bad_file.txt', - path: '/some/path/bad_file.txt', - sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f', - size: 1234, - type: 'file', - }, - ], - zip_size: 123, + code: 'ra_execute_success_done', + cwd: '/some/path', + output_file_id: 'some-output-file-id', + output_file_stderr_truncated: false, + output_file_stdout_truncated: true, + shell: 'bash', + shell_code: 0, + stderr: expect.any(String), + stderr_truncated: true, + stdout: expect.any(String), + stdout_truncated: true, }, type: 'json', }, @@ -262,6 +265,7 @@ describe('When using `getActionList()', () => { page: 1, pageSize: 10, commands: undefined, + agentTypes: undefined, userIds: undefined, startDate: undefined, elasticAgentIds: undefined, @@ -277,6 +281,7 @@ describe('When using `getActionList()', () => { 'agent-b': { name: 'Host-agent-b' }, 'agent-x': { name: '' }, }, + alertIds: undefined, command: 'kill-process', completedAt: undefined, wasSuccessful: false, @@ -289,6 +294,8 @@ describe('When using `getActionList()', () => { comment: doc?.EndpointActions.data.comment, createdBy: doc?.user.id, parameters: doc?.EndpointActions.data.parameters, + ruleId: undefined, + ruleName: undefined, agentState: { 'agent-a': { completedAt: '2022-04-30T16:08:47.449Z', @@ -309,7 +316,14 @@ describe('When using `getActionList()', () => { errors: undefined, }, }, - outputs: {}, + outputs: { + 'agent-a': { + content: { + code: 'ra_scan_success_done', + }, + type: 'json', + }, + }, }, ], total: 1, From 42c4454862410f6406d6171317ddd34328a53254 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Thu, 30 May 2024 16:23:19 +0200 Subject: [PATCH 07/11] remove redundant schema field refs changes in https://github.com/elastic/kibana/pull/165473/files#diff-b109782d19f6167e0bad2fefe051cde6ab7338fdfbf9896e31c15a080ef008dfR33 --- .../common/api/endpoint/actions/get_file.schema.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/get_file.schema.yaml b/x-pack/plugins/security_solution/common/api/endpoint/actions/get_file.schema.yaml index 87b7b834e20774..4f2fdd471bce1b 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/actions/get_file.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/get_file.schema.yaml @@ -30,7 +30,6 @@ components: - type: object required: - parameters - - file properties: parameters: required: From 8cb121aab9f07bb0fd13cccf101571fe7a5331b0 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:01:18 +0000 Subject: [PATCH 08/11] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../services/actions/clients/lib/base_response_actions_client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts index 2ed37630100341..ced25d50fc53b0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts @@ -67,7 +67,6 @@ import type { UploadedFileInfo, WithAllKeys, } from '../../../../../../common/endpoint/types'; -import { ActivityLogItemTypes } from '../../../../../../common/endpoint/types'; import type { ExecuteActionRequestBody, GetProcessesRequestBody, From 881a8bbd69c8f5792d258171949aed099b7e6584 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Mon, 3 Jun 2024 12:02:32 +0200 Subject: [PATCH 09/11] fix mock creation review changes --- .../common/api/endpoint/index.ts | 1 + .../routes/actions/response_actions.ts | 36 +++++++++---------- .../endpoint/endpoint_actions_client.test.ts | 2 +- .../endpoint/endpoint_actions_client.ts | 2 +- .../lib/base_response_actions_client.ts | 2 +- .../services/actions/clients/lib/types.ts | 2 +- .../services/actions/clients/mocks.ts | 15 ++++++++ 7 files changed, 37 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/security_solution/common/api/endpoint/index.ts b/x-pack/plugins/security_solution/common/api/endpoint/index.ts index c31a71f4494668..6dfbcd20d12fb5 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/index.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/index.ts @@ -19,6 +19,7 @@ export * from './actions/suspend_process_route'; export * from './actions/get_processes_route'; export * from './actions/get_file_route'; export * from './actions/execute_route'; +export * from './actions/scan_route'; export * from './actions/common/base'; export * from './actions/common/response_actions'; 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 df99896872081a..09512b7cbc5edc 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 @@ -8,50 +8,48 @@ import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; -import type { ScanActionRequestBody } from '../../../../common/api/endpoint/actions/scan_route'; -import { ScanActionRequestSchema } from '../../../../common/api/endpoint/actions/scan_route'; import { responseActionsWithLegacyActionProperty } from '../../services/actions/constants'; import { stringify } from '../../utils/stringify'; import { getResponseActionsClient, NormalizedExternalConnectorClient } from '../../services'; import type { ResponseActionsClient } from '../../services/actions/clients/lib/types'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; -import type { - NoParametersRequestSchema, - ResponseActionsRequestBody, - ExecuteActionRequestBody, - ResponseActionGetFileRequestBody, - UploadActionApiRequestBody, -} from '../../../../common/api/endpoint'; import { - ExecuteActionRequestSchema, EndpointActionGetFileSchema, + type ExecuteActionRequestBody, + ExecuteActionRequestSchema, + GetProcessesRouteRequestSchema, IsolateRouteRequestSchema, KillProcessRouteRequestSchema, + type NoParametersRequestSchema, + type ResponseActionGetFileRequestBody, + type ResponseActionsRequestBody, + type ScanActionRequestBody, + ScanActionRequestSchema, SuspendProcessRouteRequestSchema, UnisolateRouteRequestSchema, - GetProcessesRouteRequestSchema, + type UploadActionApiRequestBody, UploadActionRequestSchema, } from '../../../../common/api/endpoint'; import { + EXECUTE_ROUTE, + GET_FILE_ROUTE, + GET_PROCESSES_ROUTE, + ISOLATE_HOST_ROUTE, ISOLATE_HOST_ROUTE_V2, - UNISOLATE_HOST_ROUTE_V2, KILL_PROCESS_ROUTE, + SCAN_ROUTE, SUSPEND_PROCESS_ROUTE, - GET_PROCESSES_ROUTE, - ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, - GET_FILE_ROUTE, - EXECUTE_ROUTE, + UNISOLATE_HOST_ROUTE_V2, UPLOAD_ROUTE, - SCAN_ROUTE, } from '../../../../common/endpoint/constants'; import type { + ActionDetails, EndpointActionDataParameterTypes, + KillOrSuspendProcessRequestBody, ResponseActionParametersWithPidOrEntityId, ResponseActionsExecuteParameters, - ActionDetails, - KillOrSuspendProcessRequestBody, ResponseActionsScanParameters, } from '../../../../common/endpoint/types'; import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint/service/response_actions/constants'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts index 4c1e09800c4b93..858a1f53a10d6c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts @@ -262,7 +262,7 @@ describe('EndpointActionsClient', () => { upload: responseActionsClientMock.createUploadOptions(getCommonResponseActionOptions()), - scan: responseActionsClientMock.createUploadOptions(getCommonResponseActionOptions()), + scan: responseActionsClientMock.createScanOptions(getCommonResponseActionOptions()), }; it.each(Object.keys(responseActionMethods) as ResponseActionsMethodsOnly[])( diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts index bd25bcd2678411..690dd6d84730c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts @@ -7,7 +7,6 @@ import type { FleetActionRequest } from '@kbn/fleet-plugin/server/services/actions'; import { v4 as uuidv4 } from 'uuid'; -import type { ScanActionRequestBody } from '../../../../../../common/api/endpoint/actions/scan_route'; import { CustomHttpRequestError } from '../../../../../utils/custom_http_request_error'; import { getActionRequestExpiration } from '../../utils'; import { ResponseActionsClientError } from '../errors'; @@ -24,6 +23,7 @@ import type { ResponseActionGetFileRequestBody, UploadActionApiRequestBody, ResponseActionsRequestBody, + ScanActionRequestBody, } from '../../../../../../common/api/endpoint'; import { ResponseActionsClientImpl } from '../lib/base_response_actions_client'; import type { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts index ced25d50fc53b0..09180f97b72d6b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts @@ -13,7 +13,6 @@ import { AttachmentType, ExternalReferenceStorageType } from '@kbn/cases-plugin/ import type { CaseAttachments } from '@kbn/cases-plugin/public/types'; import { i18n } from '@kbn/i18n'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import type { ScanActionRequestBody } from '../../../../../../common/api/endpoint/actions/scan_route'; import { validateActionId } from '../../utils/validate_action_id'; import { fetchActionResponses, @@ -73,6 +72,7 @@ import type { IsolationRouteRequestBody, ResponseActionGetFileRequestBody, ResponseActionsRequestBody, + ScanActionRequestBody, UploadActionApiRequestBody, } from '../../../../../../common/api/endpoint'; import { stringify } from '../../../../utils/stringify'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts index 578c82abbf65bb..fa20bc9ec68953 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts @@ -6,7 +6,6 @@ */ import type { Readable } from 'stream'; -import type { ScanActionRequestBody } from '../../../../../../common/api/endpoint/actions/scan_route'; import type { ActionDetails, KillOrSuspendProcessRequestBody, @@ -33,6 +32,7 @@ import type { ExecuteActionRequestBody, UploadActionApiRequestBody, BaseActionRequestBody, + ScanActionRequestBody, } from '../../../../../../common/api/endpoint'; type OmitUnsupportedAttributes = Omit< diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts index 0bc934ce488906..e8b62fb0143069 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts @@ -20,6 +20,7 @@ import type { TransportResult } from '@elastic/elasticsearch'; import type { AttachmentsSubClient } from '@kbn/cases-plugin/server/client/attachments/client'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; + import type { ResponseActionsClient } from '../..'; import { NormalizedExternalConnectorClient } from '../..'; import type { KillOrSuspendProcessRequestBody } from '../../../../../common/endpoint/types'; @@ -47,6 +48,7 @@ import type { IsolationRouteRequestBody, ResponseActionGetFileRequestBody, UploadActionApiRequestBody, + ScanActionRequestBody, } from '../../../../../common/api/endpoint'; export interface ResponseActionsClientOptionsMock extends ResponseActionsClientOptions { @@ -224,6 +226,18 @@ const createUploadOptionsMock = ( return merge(options, overrides); }; +const createScanOptionsMock = ( + overrides: Partial = {} +): ScanActionRequestBody => { + const options: ScanActionRequestBody = { + ...createNoParamsResponseActionOptionsMock(), + parameters: { + path: '/scan/folder', + }, + }; + return merge(options, overrides); +}; + const createConnectorMock = ( overrides: DeepPartial = {} ): ConnectorWithExtraFindData => { @@ -299,6 +313,7 @@ export const responseActionsClientMock = Object.freeze({ createGetFileOptions: createGetFileOptionsMock, createExecuteOptions: createExecuteOptionsMock, createUploadOptions: createUploadOptionsMock, + createScanOptions: createScanOptionsMock, createIndexedResponse: createEsIndexTransportResponseMock, From c44b3e6884fd143679c62b90e66176ac9504b690 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Mon, 3 Jun 2024 12:14:10 +0200 Subject: [PATCH 10/11] Update actions.test.ts --- .../common/endpoint/schema/actions.test.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) 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 6f399ff3fdc06f..563633ed8413dc 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 @@ -20,6 +20,7 @@ import { NoParametersRequestSchema, } from '../../api/endpoint/actions/common/base'; import { ExecuteActionRequestSchema } from '../../api/endpoint/actions/execute_route'; +import { ScanActionRequestSchema } from '../../api/endpoint/actions/scan_route'; // NOTE: Even though schemas are kept in common/api/endpoint - we keep tests here, because common/api should import from outside describe('actions schemas', () => { @@ -760,5 +761,32 @@ describe('actions schemas', () => { }); }); - describe('ScanActionRequestSchema', () => {}); + describe('ScanActionRequestSchema', () => { + it('should not accept empty string as path', () => { + expect(() => { + ScanActionRequestSchema.body.validate({ + endpoint_ids: ['endpoint_id'], + parameters: { path: ' ' }, + }); + }).toThrowError('path cannot be an empty string'); + }); + + it('should not accept when payload does not match', () => { + expect(() => { + ScanActionRequestSchema.body.validate({ + endpoint_ids: ['endpoint_id'], + path: 'some/path', + }); + }).toThrowError('[parameters.path]: expected value of type [string] but got [undefined]'); + }); + + it('should accept path in payload if not empty', () => { + expect(() => { + ScanActionRequestSchema.body.validate({ + endpoint_ids: ['endpoint_id'], + parameters: { path: 'some/path' }, + }); + }).not.toThrow(); + }); + }); }); From 471539eec50e09f520b5b25331b4feac0a07d506 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Mon, 3 Jun 2024 15:07:37 +0200 Subject: [PATCH 11/11] update generator action errors for `scan` --- .../scripts/endpoint/common/response_actions.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts index c909b6323ad265..528078ca5d417c 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable complexity */ + import type { Client } from '@elastic/elasticsearch'; import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { basename } from 'path'; @@ -24,6 +26,7 @@ import type { ResponseActionGetFileOutputContent, ResponseActionGetFileParameters, EndpointActionResponseDataOutput, + ResponseActionScanOutputContent, } from '../../../common/endpoint/types'; import { getFileDownloadId } from '../../../common/endpoint/service/response_actions/get_file_download_id'; import { @@ -110,6 +113,16 @@ export const sendEndpointActionResponse = async ( ).code = endpointActionGenerator.randomGetFileFailureCode(); } + if ( + endpointResponse.EndpointActions.data.command === 'scan' && + endpointResponse.EndpointActions.data.output + ) { + ( + endpointResponse.EndpointActions.data.output + .content as unknown as ResponseActionScanOutputContent + ).code = endpointActionGenerator.randomScanFailureCode(); + } + if ( endpointResponse.EndpointActions.data.command === 'execute' && endpointResponse.EndpointActions.data.output