diff --git a/src/plugins/files/server/mocks.ts b/src/plugins/files/server/mocks.ts index 4da690b99240e..2edf314923464 100644 --- a/src/plugins/files/server/mocks.ts +++ b/src/plugins/files/server/mocks.ts @@ -9,6 +9,7 @@ import { KibanaRequest } from '@kbn/core/server'; import { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import * as stream from 'stream'; +import { clone } from 'lodash'; import { File } from '../common'; import { FileClient, FileServiceFactory, FileServiceStart, FilesSetup } from '.'; @@ -56,7 +57,9 @@ export const createFileMock = (): DeeplyMockedKeys => { share: jest.fn(), listShares: jest.fn(), unshare: jest.fn(), - toJSON: jest.fn(), + toJSON: jest.fn(() => { + return clone(fileMock.data); + }), }; fileMock.update.mockResolvedValue(fileMock); diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 97510abec6ba8..542c4fa248790 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -82,6 +82,7 @@ export const KILL_PROCESS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/kill_process`; export const SUSPEND_PROCESS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/suspend_process`; 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`; /** 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/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index fdf75da0a134e..ff37ada6d5577 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 @@ -24,6 +24,8 @@ import type { ResponseActionGetFileParameters, ResponseActionsExecuteParameters, ResponseActionExecuteOutputContent, + ResponseActionUploadOutputContent, + ResponseActionUploadParameters, } from '../types'; import { ActivityLogItemTypes } from '../types'; import { @@ -239,6 +241,24 @@ export class EndpointActionGenerator extends BaseDataGenerator { } } + if (command === 'upload') { + if (!details.parameters) { + ( + details as ActionDetails< + ResponseActionUploadOutputContent, + ResponseActionUploadParameters + > + ).parameters = { + file: { + file_id: 'file-x-y-z', + file_name: 'foo.txt', + size: 1234, + sha256: 'file-hash-sha-256', + }, + }; + } + } + return merge(details, overrides as ActionDetails) as unknown as ActionDetails< TOutputType, TParameters diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts index bc429feb208d7..7d1b6f9086bcd 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -129,6 +129,11 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { capabilities.push('execute'); } + // v8.9 introduced `upload` capability + if (gte(agentVersion, '8.9.0')) { + capabilities.push('upload_file'); + } + const hostMetadataDoc: HostMetadataInterface = { '@timestamp': ts, event: { 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 c22dd615a5d89..757bd1ab3f084 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 @@ -13,7 +13,10 @@ import { NoParametersRequestSchema, KillOrSuspendProcessRequestSchema, ExecuteActionRequestSchema, + UploadActionRequestSchema, } from './actions'; +import { createHapiReadableStreamMock } from '../../../server/endpoint/services/actions/mocks'; +import type { HapiReadableStream } from '../../../server/types'; describe('actions schemas', () => { describe('Endpoint action list API Schema', () => { @@ -639,4 +642,56 @@ describe('actions schemas', () => { }).not.toThrow(); }); }); + + describe(`UploadActionRequestSchema`, () => { + let fileStream: HapiReadableStream; + + beforeEach(() => { + fileStream = createHapiReadableStreamMock(); + }); + + it('should not error if `override` parameter is not defined', () => { + expect(() => { + UploadActionRequestSchema.body.validate({ + endpoint_ids: ['endpoint_id'], + file: fileStream, + }); + }).not.toThrow(); + }); + + it('should allow `override` parameter', () => { + expect(() => { + UploadActionRequestSchema.body.validate({ + endpoint_ids: ['endpoint_id'], + parameters: { + overwrite: true, + }, + file: fileStream, + }); + }).not.toThrow(); + }); + + it('should error if `file` is not defined', () => { + expect(() => { + UploadActionRequestSchema.body.validate({ + endpoint_ids: ['endpoint_id'], + parameters: { + override: true, + }, + }); + }).toThrow(); + }); + + it('should error if `file` is not a Stream', () => { + expect(() => { + UploadActionRequestSchema.body.validate({ + endpoint_ids: ['endpoint_id'], + parameters: { + overwrite: true, + }, + file: {}, + }); + }).toThrow(); + }); + }); }); 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 a4ab7609d75c8..c55132d3ff083 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -239,3 +239,17 @@ export const ResponseActionBodySchema = schema.oneOf([ EndpointActionGetFileSchema.body, ExecuteActionRequestSchema.body, ]); + +export const UploadActionRequestSchema = { + body: schema.object({ + ...BaseActionRequestSchema, + + parameters: schema.object({ + overwrite: schema.maybe(schema.boolean()), + }), + + file: schema.stream(), + }), +}; + +export type UploadActionRequestBody = TypeOf; 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 ea7ee057dc273..c80b6146b7d69 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 @@ -18,6 +18,7 @@ export const RESPONSE_ACTION_API_COMMANDS_NAMES = [ 'running-processes', 'get-file', 'execute', + 'upload', ] as const; export type ResponseActionsApiCommandNames = typeof RESPONSE_ACTION_API_COMMANDS_NAMES[number]; @@ -36,6 +37,7 @@ export const ENDPOINT_CAPABILITIES = [ 'running_processes', 'get_file', 'execute', + 'upload_file', ] as const; export type EndpointCapabilities = typeof ENDPOINT_CAPABILITIES[number]; @@ -52,6 +54,7 @@ export const CONSOLE_RESPONSE_ACTION_COMMANDS = [ 'processes', 'get-file', 'execute', + 'upload', ] as const; export type ConsoleResponseActionCommands = typeof CONSOLE_RESPONSE_ACTION_COMMANDS[number]; @@ -74,6 +77,7 @@ export const commandToRBACMap: Record { + const featureFlags = ExperimentalFeaturesService.get(); + // `get-file` is currently behind FF - if ( - commandName === 'get-file' && - !ExperimentalFeaturesService.get().responseActionGetFileEnabled - ) { + if (commandName === 'get-file' && !featureFlags.responseActionGetFileEnabled) { return false; } // TODO: remove this when `execute` is no longer behind FF // planned for 8.8 - if ( - commandName === 'execute' && - !ExperimentalFeaturesService.get().responseActionExecuteEnabled - ) { + if (commandName === 'execute' && !featureFlags.responseActionExecuteEnabled) { + return false; + } + + // upload - v8.9 + if (commandName === 'upload' && !featureFlags.responseActionUploadEnabled) { return false; } 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 6fc62dd6cf851..088809509cd50 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 @@ -118,8 +118,6 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { jest.mock('../../../hooks/endpoint/use_get_endpoints_list'); -jest.mock('../../../../common/experimental_features_service'); - jest.mock('../../../../common/components/user_privileges'); const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; @@ -917,6 +915,7 @@ describe('Response actions history', () => { }); it('should show a list of actions when opened', () => { + mockedContext.setExperimentalFlag({ responseActionUploadEnabled: true }); render(); const { getByTestId, getAllByTestId } = renderResult; @@ -934,6 +933,7 @@ describe('Response actions history', () => { 'processes', 'get-file', 'execute', + 'upload', ]); }); diff --git a/x-pack/plugins/security_solution/server/config.mock.ts b/x-pack/plugins/security_solution/server/config.mock.ts index 6fcaa94629643..b0d06b40a2b84 100644 --- a/x-pack/plugins/security_solution/server/config.mock.ts +++ b/x-pack/plugins/security_solution/server/config.mock.ts @@ -16,6 +16,7 @@ export const createMockConfig = (): ConfigType => { 'responseActionGetFileEnabled', // remove property below once `execute` FF is enabled or removed 'responseActionExecuteEnabled', + 'responseActionUploadEnabled', ]; return { @@ -29,6 +30,7 @@ export const createMockConfig = (): ConfigType => { prebuiltRulesPackageVersion: '', alertMergeStrategy: 'missingFields', alertIgnoreFields: [], + maxUploadResponseActionFileBytes: 26214400, experimentalFeatures: parseExperimentalConfigValue(enableExperimental), enabled: true, diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index a0283858590cb..44ff0e4d1aa73 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -123,6 +123,14 @@ export const configSchema = schema.object({ */ prebuiltRulesPackageVersion: schema.maybe(schema.string()), enabled: schema.boolean({ defaultValue: true }), + + /** + * The Max number of Bytes allowed for the `upload` endpoint response action + */ + maxUploadResponseActionFileBytes: schema.number({ + defaultValue: 26214400, // 25MB, + max: 104857600, // 100MB, + }), }); export type ConfigSchema = TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 46ffabfb5de17..54ebe064ca313 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -40,6 +40,8 @@ import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks'; import type { RequestFixtureOptions } from '@kbn/core-http-router-server-mocks'; import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { casesPluginMock } from '@kbn/cases-plugin/server/mocks'; +import { createCasesClientMock } from '@kbn/cases-plugin/server/client/mocks'; +import { createActionCreateServiceMock } from './services/actions/mocks'; import { getEndpointAuthzInitialStateMock } from '../../common/endpoint/service/authz/mocks'; import { xpackMocks } from '../fixtures'; import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__'; @@ -69,11 +71,14 @@ import { createFeatureUsageServiceMock } from './services/feature_usage/mocks'; export const createMockEndpointAppContext = ( mockManifestManager?: ManifestManager ): EndpointAppContext => { + const config = createMockConfig(); + return { logFactory: loggingSystemMock.create(), - config: () => Promise.resolve(createMockConfig()), + config: () => Promise.resolve(config), + serverConfig: config, service: createMockEndpointAppContextService(mockManifestManager), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + experimentalFeatures: parseExperimentalConfigValue(config.enableExperimental), }; }; @@ -84,6 +89,8 @@ export const createMockEndpointAppContextService = ( mockManifestManager?: ManifestManager ): jest.Mocked => { const mockEndpointMetadataContext = createEndpointMetadataServiceTestContextMock(); + const casesClientMock = createCasesClientMock(); + return { start: jest.fn(), stop: jest.fn(), @@ -92,6 +99,8 @@ export const createMockEndpointAppContextService = ( getEndpointMetadataService: jest.fn(() => mockEndpointMetadataContext.endpointMetadataService), getInternalFleetServices: jest.fn(() => mockEndpointMetadataContext.fleetServices), getEndpointAuthz: jest.fn(getEndpointAuthzInitialStateMock), + getCasesClient: jest.fn().mockReturnValue(casesClientMock), + getActionCreateService: jest.fn().mockReturnValue(createActionCreateServiceMock()), } as unknown as jest.Mocked; }; @@ -220,6 +229,11 @@ export interface HttpApiTestSetupMock

{ method: keyof Pick, path: string ) => RequestHandler; + /** Retrieves the route handler configuration that was registered with the router */ + getRegisteredRouteConfig: ( + method: keyof Pick, + path: string + ) => RouteConfig; } /** @@ -243,7 +257,7 @@ export const createHttpApiTestSetupMock =

(): HttpApi path ): RequestHandler => { const methodCalls = routerMock[method].mock.calls as Array< - [route: RouteConfig, handler: RequestHandler] + [route: RouteConfig, handler: RequestHandler] >; const handler = methodCalls.find(([routeConfig]) => routeConfig.path.startsWith(path)); @@ -253,6 +267,21 @@ export const createHttpApiTestSetupMock =

(): HttpApi return handler[1]; }; + const getRegisteredRouteConfig: HttpApiTestSetupMock['getRegisteredRouteConfig'] = ( + method, + path + ): RouteConfig => { + const methodCalls = routerMock[method].mock.calls as Array< + [route: RouteConfig, handler: RequestHandler] + >; + const handler = methodCalls.find(([routeConfig]) => routeConfig.path.startsWith(path)); + + if (!handler) { + throw new Error(`Handler for [${method}][${path}] not found`); + } + + return handler[0]; + }; return { routerMock, @@ -277,5 +306,6 @@ export const createHttpApiTestSetupMock =

(): HttpApi }, getRegisteredRouteHandler, + getRegisteredRouteConfig, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index 120f1753fc18e..5ef461a26f318 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -13,7 +13,6 @@ import { elasticsearchServiceMock, httpServerMock, httpServiceMock, - loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; import type { @@ -22,10 +21,9 @@ import type { } from '../../../../common/endpoint/schema/actions'; import { EndpointActionLogRequestSchema } from '../../../../common/endpoint/schema/actions'; import { ENDPOINT_ACTION_LOG_ROUTE } from '../../../../common/endpoint/constants'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -136,12 +134,7 @@ describe('Action Log API', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); - registerActionAuditLogRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); + registerActionAuditLogRoutes(routerMock, createMockEndpointAppContext()); getActivityLog = async ( params: { agent_id: string }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_upload_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_upload_handler.test.ts new file mode 100644 index 0000000000000..f92c66898dca4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_upload_handler.test.ts @@ -0,0 +1,219 @@ +/* + * 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 { HttpApiTestSetupMock } from '../../mocks'; +import { createHttpApiTestSetupMock } from '../../mocks'; +import type { UploadActionRequestBody } from '../../../../common/endpoint/schema/actions'; +import type { getActionFileUploadHandler } from './file_upload_handler'; +import { registerActionFileUploadRoute } from './file_upload_handler'; +import { UPLOAD_ROUTE } from '../../../../common/endpoint/constants'; +import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; +import { EndpointAuthorizationError } from '../../errors'; +import type { HapiReadableStream } from '../../../types'; +import { createHapiReadableStreamMock } from '../../services/actions/mocks'; +import { + createFile as _createFile, + deleteFile as _deleteFile, + setFileActionId as _setFileActionId, +} from '../../services'; +import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; +import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; +import type { ActionDetails } from '../../../../common/endpoint/types'; +import { omit } from 'lodash'; + +jest.mock('../../services'); +const createFileMock = _createFile as jest.Mock; +const deleteFileMock = _deleteFile as jest.Mock; +const setFileActionIdMock = _setFileActionId as jest.Mock; + +describe('Upload response action create API handler', () => { + type UploadHttpApiTestSetupMock = HttpApiTestSetupMock; + + let testSetup: UploadHttpApiTestSetupMock; + let httpRequestMock: ReturnType; + let httpHandlerContextMock: UploadHttpApiTestSetupMock['httpHandlerContextMock']; + let httpResponseMock: UploadHttpApiTestSetupMock['httpResponseMock']; + + beforeEach(() => { + testSetup = createHttpApiTestSetupMock(); + + ({ httpHandlerContextMock, httpResponseMock } = testSetup); + httpRequestMock = testSetup.createRequestMock(); + }); + + describe('registerActionFileUploadRoute()', () => { + it('should register the route', () => { + registerActionFileUploadRoute(testSetup.routerMock, testSetup.endpointAppContextMock); + + expect(testSetup.getRegisteredRouteHandler('post', UPLOAD_ROUTE)).toBeDefined(); + }); + + it('should NOT register route if feature flag is false', () => { + // @ts-expect-error + testSetup.endpointAppContextMock.experimentalFeatures.responseActionUploadEnabled = false; + registerActionFileUploadRoute(testSetup.routerMock, testSetup.endpointAppContextMock); + + expect(() => testSetup.getRegisteredRouteHandler('post', UPLOAD_ROUTE)).toThrow( + 'Handler for [post][/api/endpoint/action/upload] not found' + ); + }); + + it('should use maxUploadResponseActionFileBytes config value', () => { + // @ts-expect-error + testSetup.endpointAppContextMock.serverConfig.maxUploadResponseActionFileBytes = 999; + registerActionFileUploadRoute(testSetup.routerMock, testSetup.endpointAppContextMock); + + expect(testSetup.getRegisteredRouteConfig('post', UPLOAD_ROUTE)?.options?.body).toEqual({ + accepts: ['multipart/form-data'], + maxBytes: 999, + output: 'stream', + }); + }); + + it('should error if user has no authz to api', async () => { + ( + (await httpHandlerContextMock.securitySolution).getEndpointAuthz as jest.Mock + ).mockResolvedValue(getEndpointAuthzInitialStateMock({ canWriteFileOperations: false })); + registerActionFileUploadRoute(testSetup.routerMock, testSetup.endpointAppContextMock); + await testSetup.getRegisteredRouteHandler('post', UPLOAD_ROUTE)( + httpHandlerContextMock, + httpRequestMock, + httpResponseMock + ); + + expect(httpResponseMock.forbidden).toHaveBeenCalledWith({ + body: expect.any(EndpointAuthorizationError), + }); + }); + }); + + describe('route request handler', () => { + let callHandler: () => ReturnType>; + let fileContent: HapiReadableStream; + let createdUploadAction: ActionDetails; + + beforeEach(() => { + fileContent = createHapiReadableStreamMock(); + + createFileMock.mockResolvedValue({ + file: { + created: '2022-10-10T14:57:30.682Z', + updated: '2022-10-19T14:43:20.112Z', + extension: '.txt', + hash: { + sha256: 'abc', + }, + id: '123', + meta: {}, + mimeType: 'text/plain', + name: 'test.txt', + size: 1234, + status: 'READY', + }, + }); + + const reqBody: UploadActionRequestBody = { + file: fileContent, + endpoint_ids: ['123-456'], + parameters: { + overwrite: true, + }, + }; + + httpRequestMock = testSetup.createRequestMock({ body: reqBody }); + registerActionFileUploadRoute(testSetup.routerMock, testSetup.endpointAppContextMock); + + createdUploadAction = new EndpointActionGenerator('seed').generateActionDetails({ + command: 'upload', + }); + + ( + testSetup.endpointAppContextMock.service.getActionCreateService().createAction as jest.Mock + ).mockResolvedValue(createdUploadAction); + + const handler: ReturnType = + testSetup.getRegisteredRouteHandler('post', UPLOAD_ROUTE); + + callHandler = () => handler(httpHandlerContextMock, httpRequestMock, httpResponseMock); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should create a file', async () => { + await callHandler(); + + expect(createFileMock).toHaveBeenCalledWith({ + esClient: expect.anything(), + logger: expect.anything(), + fileStream: fileContent, + agents: ['123-456'], + maxFileBytes: + testSetup.endpointAppContextMock.serverConfig.maxUploadResponseActionFileBytes, + }); + }); + + it('should create the action using parameters with stored file info', async () => { + await callHandler(); + const casesClientMock = + testSetup.endpointAppContextMock.service.getCasesClient(httpRequestMock); + const createActionMock = testSetup.endpointAppContextMock.service.getActionCreateService() + .createAction as jest.Mock; + + expect(createActionMock).toHaveBeenCalledWith( + { + command: 'upload', + endpoint_ids: ['123-456'], + parameters: { + file: { + file_id: '123', + file_name: 'test.txt', + sha256: 'abc', + size: 1234, + }, + overwrite: true, + }, + user: undefined, + }, + { casesClient: casesClientMock } + ); + }); + + it('should delete file if creation of Action fails', async () => { + const createActionMock = testSetup.endpointAppContextMock.service.getActionCreateService() + .createAction as jest.Mock; + createActionMock.mockImplementation(async () => { + throw new CustomHttpRequestError('oh oh'); + }); + await callHandler(); + + expect(deleteFileMock).toHaveBeenCalledWith(expect.anything(), expect.anything(), '123'); + }); + + it('should update file with action id', async () => { + await callHandler(); + + expect(setFileActionIdMock).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + createdUploadAction + ); + }); + + it('should return expected response on success', async () => { + await callHandler(); + + expect(httpResponseMock.ok).toHaveBeenCalledWith({ + body: { + action: createdUploadAction.action, + data: omit(createdUploadAction, 'action'), + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_upload_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_upload_handler.ts new file mode 100644 index 0000000000000..2c10c12214912 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_upload_handler.ts @@ -0,0 +1,136 @@ +/* + * 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 { RequestHandler } from '@kbn/core/server'; +import { createFile, deleteFile, setFileActionId } from '../../services'; +import type { + ResponseActionUploadParameters, + ResponseActionUploadOutputContent, +} from '../../../../common/endpoint/types'; +import { UPLOAD_ROUTE } from '../../../../common/endpoint/constants'; +import { + type UploadActionRequestBody, + UploadActionRequestSchema, +} from '../../../../common/endpoint/schema/actions'; +import { withEndpointAuthz } from '../with_endpoint_authz'; +import type { + SecuritySolutionPluginRouter, + SecuritySolutionRequestHandlerContext, + HapiReadableStream, +} from '../../../types'; +import type { EndpointAppContext } from '../../types'; +import { errorHandler } from '../error_handler'; + +export const registerActionFileUploadRoute = ( + router: SecuritySolutionPluginRouter, + endpointContext: EndpointAppContext +) => { + if (!endpointContext.experimentalFeatures.responseActionUploadEnabled) { + return; + } + + const logger = endpointContext.logFactory.get('uploadAction'); + + router.post( + { + path: UPLOAD_ROUTE, + validate: UploadActionRequestSchema, + options: { + authRequired: true, + tags: ['access:securitySolution'], + body: { + accepts: ['multipart/form-data'], + output: 'stream', + maxBytes: endpointContext.serverConfig.maxUploadResponseActionFileBytes, + }, + }, + }, + withEndpointAuthz( + { all: ['canWriteFileOperations'] }, + logger, + getActionFileUploadHandler(endpointContext) + ) + ); +}; + +export const getActionFileUploadHandler = ( + endpointContext: EndpointAppContext +): RequestHandler => { + const logger = endpointContext.logFactory.get('uploadAction'); + const maxFileBytes = endpointContext.serverConfig.maxUploadResponseActionFileBytes; + + return async (context, req, res) => { + const user = endpointContext.service.security?.authc.getCurrentUser(req); + const esClient = (await context.core).elasticsearch.client.asInternalUser; + const fileStream = req.body.file as HapiReadableStream; + const { file: _, parameters: userParams, ...actionPayload } = req.body; + const uploadParameters: ResponseActionUploadParameters = { + ...userParams, + file: { + file_id: '', + file_name: '', + sha256: '', + size: 0, + }, + }; + + try { + const createdFile = await createFile({ + esClient, + logger, + fileStream, + agents: actionPayload.endpoint_ids, + maxFileBytes, + }); + + uploadParameters.file.file_id = createdFile.file.id; + uploadParameters.file.file_name = createdFile.file.name; + uploadParameters.file.sha256 = createdFile.file.hash?.sha256; + uploadParameters.file.size = createdFile.file.size; + } catch (err) { + return errorHandler(logger, res, err); + } + + try { + const casesClient = await endpointContext.service.getCasesClient(req); + const { action: actionId, ...data } = await endpointContext.service + .getActionCreateService() + .createAction( + { + ...actionPayload, + parameters: uploadParameters, + command: 'upload', + user, + }, + { casesClient } + ); + + await setFileActionId(esClient, logger, data); + + return res.ok({ + body: { + action: actionId, + data, + }, + }); + } catch (err) { + if (uploadParameters.file.file_id) { + // Try to delete the created file since creating the action threw an error + try { + await deleteFile(esClient, logger, uploadParameters.file.file_id); + } catch (e) { + logger.error( + `Attempt to clean up file (after action creation was unsuccessful) failed; ${e.message}`, + e + ); + } + } + + return errorHandler(logger, res, err); + } + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts index 5829428db2eab..a9e4ec70214b4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts @@ -11,6 +11,7 @@ import type { EndpointActionListRequestQuery } from '../../../../common/endpoint import type { EndpointAuthz } from '../../../../common/endpoint/types/authz'; import type { License } from '@kbn/licensing-plugin/common/license'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -19,15 +20,12 @@ import { elasticsearchServiceMock, httpServerMock, httpServiceMock, - loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; import type { KibanaResponseFactory, RequestHandler, RouteConfig } from '@kbn/core/server'; import { BASE_ENDPOINT_ACTION_ROUTE } from '../../../../common/endpoint/constants'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { LicenseService } from '../../../../common/license'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { Subject } from 'rxjs'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; @@ -83,12 +81,7 @@ describe('Action List Route', () => { licenseService, }); - registerActionListRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); + registerActionListRoutes(routerMock, createMockEndpointAppContext()); callApiRoute = async ( routePrefix: string, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts index 2854f0cd6ef8d..560e059952fe2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts @@ -13,15 +13,13 @@ import { elasticsearchServiceMock, httpServerMock, httpServiceMock, - loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; import type { EndpointActionListRequestQuery } from '../../../../common/endpoint/schema/actions'; import { BASE_ENDPOINT_ACTION_ROUTE } from '../../../../common/endpoint/constants'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -54,12 +52,7 @@ describe('Action List Handler', () => { endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); mockDoesLogsEndpointActionsIndexExist.mockResolvedValue(true); - registerActionListRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); + registerActionListRoutes(routerMock, createMockEndpointAppContext()); actionListHandler = async ( query?: EndpointActionListRequestQuery 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 b2bc7ea008b10..11078e0134330 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 @@ -23,13 +23,11 @@ import { elasticsearchServiceMock, httpServerMock, httpServiceMock, - loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; import { AGENT_ACTIONS_INDEX } from '@kbn/fleet-plugin/common'; import type { CasesClientMock } from '@kbn/cases-plugin/server/client/mocks'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { LicenseService } from '../../../../common/license'; import { ISOLATE_HOST_ROUTE_V2, @@ -54,10 +52,10 @@ import type { } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import type { EndpointAuthz } from '../../../../common/endpoint/types/authz'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import type { SecuritySolutionRequestHandlerContextMock } from '../../../lib/detection_engine/routes/__mocks__/request_context'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -123,10 +121,8 @@ describe('Response actions', () => { licenseService.start(licenseEmitter); const endpointContext = { - logFactory: loggingSystemMock.create(), + ...createMockEndpointAppContext(), service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), }; endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); 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 0924f8ade419c..0348227561677 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 @@ -38,6 +38,7 @@ import type { } from '../../../types'; import type { EndpointAppContext } from '../../types'; import { withEndpointAuthz } from '../with_endpoint_authz'; +import { registerActionFileUploadRoute } from './file_upload_handler'; export function registerResponseActionRoutes( router: SecuritySolutionPluginRouter, @@ -175,6 +176,8 @@ export function registerResponseActionRoutes( ) ); } + + registerActionFileUploadRoute(router, endpointContext); } function responseActionRequestHandler( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.test.ts index 9612eab2c2821..d6930f7fcf44c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.test.ts @@ -6,22 +6,18 @@ */ import type { ScopedClusterClientMock } from '@kbn/core/server/mocks'; -import { loggingSystemMock, httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; +import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; import type { KibanaResponseFactory, SavedObjectsClientContract } from '@kbn/core/server'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; import type { EndpointAuthz } from '../../../../common/endpoint/types/authz'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; - import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; -import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; - import { registerActionStateRoutes } from './state'; import type { RouterMock } from '@kbn/core-http-router-server-mocks'; import { ACTION_STATE_ROUTE } from '../../../../common/endpoint/constants'; @@ -77,19 +73,7 @@ describe('when calling the Action state route handler', () => { 'when can encrypt is set to %s it returns proper value', async (canEncrypt) => { const routerMock: RouterMock = httpServiceMock.createRouter(); - const endpointAppContextService = new EndpointAppContextService(); - registerActionStateRoutes( - routerMock, - { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue( - createMockConfig().enableExperimental - ), - }, - canEncrypt - ); + registerActionStateRoutes(routerMock, createMockEndpointAppContext(), canEncrypt); await callRoute(routerMock, ACTION_STATE_ROUTE, { authz: { canIsolateHost: true }, @@ -102,17 +86,7 @@ describe('when calling the Action state route handler', () => { describe('without having right privileges', () => { it('it returns unauthorized error', async () => { const routerMock: RouterMock = httpServiceMock.createRouter(); - const endpointAppContextService = new EndpointAppContextService(); - registerActionStateRoutes( - routerMock, - { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - true - ); + registerActionStateRoutes(routerMock, createMockEndpointAppContext(), true); await callRoute(routerMock, ACTION_STATE_ROUTE, { authz: { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts index 52269d32e3663..c04d9e34a7c4d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts @@ -12,7 +12,6 @@ import { elasticsearchServiceMock, httpServerMock, httpServiceMock, - loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; import { ActionStatusRequestSchema } from '../../../../common/endpoint/schema/actions'; @@ -21,10 +20,9 @@ import { ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, ENDPOINT_ACTIONS_INDEX, } from '../../../../common/endpoint/constants'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -66,12 +64,13 @@ describe('Endpoint Pending Action Summary API', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); + const endpointContextMock = createMockEndpointAppContext(); + registerActionStatusRoutes(routerMock, { - logFactory: loggingSystemMock.create(), + ...endpointContextMock, service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), experimentalFeatures: { - ...parseExperimentalConfigValue(createMockConfig().enableExperimental), + ...endpointContextMock.experimentalFeatures, pendingActionResponsesWithAck, }, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 6a454dbcb759e..021af284f8e35 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -15,21 +15,19 @@ import { elasticsearchServiceMock, httpServerMock, httpServiceMock, - loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; import type { HostInfo, MetadataListResponse } from '../../../../common/endpoint/types'; import { HostStatus } from '../../../../common/endpoint/types'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { registerEndpointRoutes } from '.'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; import type { EndpointAppContextServiceStartContract } from '../../endpoint_app_context_services'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import type { Agent } from '@kbn/fleet-plugin/common/types/models'; import { @@ -111,10 +109,8 @@ describe('test endpoint routes', () => { .agentPolicy as jest.Mocked; registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), + ...createMockEndpointAppContext(), service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index c8ea7bbbedb48..2adbb0638912a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -7,6 +7,7 @@ import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContext, createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -16,17 +17,12 @@ import type { KibanaResponseFactory, SavedObjectsClientContract } from '@kbn/cor import { elasticsearchServiceMock, httpServerMock, - loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { - createMockConfig, - requestContextMock, -} from '../../../lib/detection_engine/routes/__mocks__'; +import { requestContextMock } from '../../../lib/detection_engine/routes/__mocks__'; import type { Agent } from '@kbn/fleet-plugin/common/types/models'; import type { AgentClient } from '@kbn/fleet-plugin/server/services'; import { get } from 'lodash'; @@ -172,14 +168,12 @@ describe('test policy response handler', () => { it('should return the summary of all the agent with the given policy name', async () => { mockAgentClient.listAgents - .mockImplementationOnce(() => Promise.resolve(agentListResult)) - .mockImplementationOnce(() => Promise.resolve(emptyAgentListResult)); + .mockImplementation(() => Promise.resolve(emptyAgentListResult)) + .mockImplementationOnce(() => Promise.resolve(agentListResult)); const policySummarysHandler = getAgentPolicySummaryHandler({ - logFactory: loggingSystemMock.create(), + ...createMockEndpointAppContext(), service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), }); const mockRequest = httpServerMock.createKibanaRequest({ @@ -209,10 +203,8 @@ describe('test policy response handler', () => { .mockImplementationOnce(() => Promise.resolve(emptyAgentListResult)); const agentPolicySummaryHandler = getAgentPolicySummaryHandler({ - logFactory: loggingSystemMock.create(), + ...createMockEndpointAppContext(), service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), }); const mockRequest = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts index f8dd0e7a848cf..9923dc18ea48a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts @@ -8,7 +8,6 @@ import type { TypeOf } from '@kbn/config-schema'; import type { ScopedClusterClientMock } from '@kbn/core/server/mocks'; import { - loggingSystemMock, elasticsearchServiceMock, savedObjectsClientMock, httpServerMock, @@ -26,10 +25,7 @@ import { } from '../../mocks'; import type { EndpointAuthz } from '../../../../common/endpoint/types/authz'; import { applyActionsEsSearchMock } from '../../services/actions/mocks'; -import { - createMockConfig, - requestContextMock, -} from '../../../lib/detection_engine/routes/__mocks__'; +import { requestContextMock } from '../../../lib/detection_engine/routes/__mocks__'; import type { EndpointSuggestionsSchema } from '../../../../common/endpoint/schema/suggestions'; import { getEndpointSuggestionsRequestHandler, @@ -40,7 +36,6 @@ import { EndpointActionGenerator } from '../../../../common/endpoint/data_genera import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; import { eventsIndexPattern, SUGGESTIONS_ROUTE } from '../../../../common/endpoint/constants'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { EXCEPTIONABLE_ENDPOINT_EVENT_FIELDS } from '../../../../common/endpoint/exceptions/exceptionable_endpoint_event_fields'; jest.mock('@kbn/unified-search-plugin/server/autocomplete/terms_enum', () => { @@ -187,10 +182,8 @@ describe('when calling the Suggestions route handler', () => { const endpointAppContextService = new EndpointAppContextService(); // add the suggestions route handlers to routerMock registerEndpointSuggestionsRoutes(routerMock, config$, { - logFactory: loggingSystemMock.create(), + ...createMockEndpointAppContext(), service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), }); // define a convenience function to execute an API call for a given route 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 4ff9afee51d74..452bf46316d60 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 @@ -80,12 +80,30 @@ describe('When using `getActionDetailsById()', () => { status: 'successful', createdBy: doc?.user.id, parameters: doc?.EndpointActions.data.parameters, - outputs: {}, + 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, + }, + type: 'json', + }, + }, agentState: { 'agent-a': { completedAt: '2022-04-30T16:08:47.449Z', isCompleted: true, wasSuccessful: true, + errors: undefined, }, }, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.test.ts index ae3638c50446b..629be3be03b2a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.test.ts @@ -8,9 +8,18 @@ import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import type { Logger } from '@kbn/core/server'; -import { createEsFileClient as _createEsFileClient } from '@kbn/files-plugin/server'; -import { createFileClientMock } from '@kbn/files-plugin/server/mocks'; -import { getFileDownloadStream, getFileInfo } from './action_files'; +import { + createEsFileClient as _createEsFileClient, + createFileHashTransform as _createFileHashTransform, +} from '@kbn/files-plugin/server'; +import { createFileClientMock, createFileMock } from '@kbn/files-plugin/server/mocks'; +import { + createFile, + deleteFile, + getFileDownloadStream, + getFileInfo, + setFileActionId, +} from './action_files'; import type { DiagnosticResult } from '@elastic/elasticsearch'; import { errors } from '@elastic/elasticsearch'; import { NotFoundError } from '../../errors'; @@ -20,10 +29,18 @@ import { } from '../../../../common/endpoint/constants'; import { BaseDataGenerator } from '../../../../common/endpoint/data_generators/base_data_generator'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { generateFileMetadataDocumentMock } from './mocks'; +import { createHapiReadableStreamMock, generateFileMetadataDocumentMock } from './mocks'; +import type { HapiReadableStream } from '../../../types'; +import type { + ActionDetails, + ResponseActionUploadOutputContent, + ResponseActionUploadParameters, +} from '../../../../common/endpoint/types'; +import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; jest.mock('@kbn/files-plugin/server'); const createEsFileClient = _createEsFileClient as jest.Mock; +const createFileHashTransformMock = _createFileHashTransform as jest.Mock; describe('Action Files service', () => { let loggerMock: Logger; @@ -128,4 +145,117 @@ describe('Action Files service', () => { ); }); }); + + describe('#createFile()', () => { + let fileContent: HapiReadableStream; + let fileMock: ReturnType; + let createFileOptions: Parameters[0]; + let fileHashTransform: ReturnType; + + beforeEach(() => { + fileContent = createHapiReadableStreamMock(); + fileMock = createFileMock(); + fileClientMock.create.mockResolvedValue(fileMock); + + fileMock.data.hash = { sha256: 'abc' }; + + fileHashTransform = jest.requireActual('@kbn/files-plugin/server').createFileHashTransform(); + createFileHashTransformMock.mockReturnValue(fileHashTransform); + + createFileOptions = { + esClient: esClientMock, + logger: loggerMock, + fileStream: fileContent, + maxFileBytes: Infinity, + agents: ['123'], + }; + }); + + it('should create a new file metadata and set expected data', async () => { + await createFile(createFileOptions); + + expect(fileClientMock.create).toHaveBeenCalledWith({ + id: expect.any(String), + metadata: { + meta: { + action_id: '', + target_agents: ['123'], + }, + mime: 'application/text', + name: 'foo.txt', + }, + }); + }); + + it('should use File Hash transform when uploading file', async () => { + await createFile(createFileOptions); + + expect(fileMock.uploadContent).toHaveBeenCalledWith(fileContent, undefined, { + transforms: [fileHashTransform], + }); + }); + + it('should return expected response', async () => { + await expect(createFile(createFileOptions)).resolves.toEqual({ + file: { + created: '2022-10-10T14:57:30.682Z', + updated: '2022-10-19T14:43:20.112Z', + extension: '.txt', + hash: { + sha256: 'abc', + }, + id: '123', + meta: {}, + mimeType: 'text/plain', + name: 'test.txt', + size: 1234, + status: 'READY', + }, + }); + }); + }); + + describe('#deleteFile()', () => { + it('Delete a file using id', async () => { + await deleteFile(esClientMock, loggerMock, 'abc'); + + expect(fileClientMock.delete).toHaveBeenCalledWith({ + id: 'abc', + hasContent: true, + }); + }); + }); + + describe('#setFileActionId()', () => { + let action: ActionDetails; + let fileMock: ReturnType; + + beforeEach(() => { + action = new EndpointActionGenerator('seed').generateActionDetails< + ResponseActionUploadOutputContent, + ResponseActionUploadParameters + >({ command: 'upload' }); + + fileMock = createFileMock(); + fileClientMock.get.mockResolvedValue(fileMock); + }); + + it('should update file meta with action id', async () => { + await setFileActionId(esClientMock, loggerMock, action); + + expect(fileMock.update).toHaveBeenCalledWith({ + meta: { + action_id: '123', + }, + }); + }); + + it('should throw an error no `action.parameters.file.file_id` defined', async () => { + action.parameters!.file.file_id = ''; + + await expect(setFileActionId(esClientMock, loggerMock, action)).rejects.toThrow( + "Action [123] has no 'parameters.file.file_id' defined. Unable to set action id on file record" + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.ts index 61315d19abcbe..b00b3f62ae5c8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_files.ts @@ -8,11 +8,22 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import type { Readable } from 'stream'; import type { FileClient } from '@kbn/files-plugin/server'; -import { createEsFileClient } from '@kbn/files-plugin/server'; +import { createEsFileClient, createFileHashTransform } from '@kbn/files-plugin/server'; import { errors } from '@elastic/elasticsearch'; import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { v4 as uuidV4 } from 'uuid'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import assert from 'assert'; +import type { File } from '@kbn/files-plugin/common'; +import type { HapiReadableStream } from '../../../types'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; -import type { FileUploadMetadata, UploadedFileInfo } from '../../../../common/endpoint/types'; +import type { + ActionDetails, + FileUploadMetadata, + UploadedFileInfo, + ResponseActionUploadParameters, + ResponseActionUploadOutputContent, +} from '../../../../common/endpoint/types'; import { NotFoundError } from '../../errors'; import { FILE_STORAGE_DATA_INDEX, @@ -20,13 +31,18 @@ import { } from '../../../../common/endpoint/constants'; import { EndpointError } from '../../../../common/endpoint/errors'; -const getFileClient = (esClient: ElasticsearchClient, logger: Logger): FileClient => { +const getFileClient = ( + esClient: ElasticsearchClient, + logger: Logger, + maxSizeBytes?: number +): FileClient => { return createEsFileClient({ metadataIndex: FILE_STORAGE_METADATA_INDEX, blobStorageIndex: FILE_STORAGE_DATA_INDEX, elasticsearchClient: esClient, logger, indexIsAlias: true, + maxSizeBytes, }); }; @@ -183,3 +199,119 @@ export const validateActionFileId = async ( throw new CustomHttpRequestError(`Invalid file id [${fileId}] for action [${actionId}]`, 400); } }; + +interface UploadFileInternalStorageMeta { + target_agents: string[]; + action_id: string; +} + +interface UploadedFile { + file: Pick< + Required>, + 'id' | 'created' | 'updated' | 'name' | 'mimeType' | 'extension' | 'meta' | 'status' + > & { size: number; hash: { sha256: string } }; +} + +export const createFile = async ({ + esClient, + logger, + fileStream, + maxFileBytes, + agents, +}: { + esClient: ElasticsearchClient; + logger: Logger; + fileStream: HapiReadableStream; + maxFileBytes: number; + agents: string[]; +}): Promise => { + const fileClient = getFileClient(esClient, logger, maxFileBytes); + + const uploadedFile = await fileClient.create({ + id: uuidV4(), + metadata: { + name: fileStream.hapi.filename ?? 'unknown_file_name', + mime: fileStream.hapi.headers['content-type'] ?? 'application/octet-stream', + meta: { + target_agents: agents, + action_id: '', // Populated later once we have the action is created + }, + }, + }); + + await uploadedFile.uploadContent(fileStream, undefined, { + transforms: [createFileHashTransform()], + }); + + assert(uploadedFile.data.hash && uploadedFile.data.hash.sha256, 'File hash was not generated!'); + + return toUploadedFileInterface(uploadedFile); +}; + +const toUploadedFileInterface = (file: File): UploadedFile => { + const { name, created, meta, id, mimeType, size, status, extension, hash, updated } = + file.toJSON(); + + return { + file: { + name, + created, + updated, + meta, + id, + mimeType, + status, + extension, + size: size ?? 0, + hash: { sha256: hash?.sha256 ?? '' }, + }, + }; +}; + +/** + * Deletes a file by ID + * @param esClient + * @param logger + * @param fileId + */ +export const deleteFile = async ( + esClient: ElasticsearchClient, + logger: Logger, + fileId: string +): Promise => { + const fileClient = getFileClient(esClient, logger); + + await fileClient.delete({ id: fileId, hasContent: true }); +}; + +/** + * Sets the `meta.action_id` on the file associated with the `upload` action + * @param esClient + * @param logger + * @param action + */ +export const setFileActionId = async ( + esClient: ElasticsearchClient, + logger: Logger, + action: ActionDetails +): Promise => { + assert( + action.parameters?.file.file_id, + `Action [${action.id}] has no 'parameters.file.file_id' defined. Unable to set action id on file record` + ); + + const fileClient = getFileClient(esClient, logger); + const file = await fileClient.get({ + id: action.parameters?.file.file_id, + }); + + await file.update({ + meta: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...file.data.meta!, + action_id: action.id, + }, + }); + + return toUploadedFileInterface(file); +}; 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 042a0dd3cd5f4..ebf88e6effd2e 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 @@ -163,7 +163,24 @@ describe('When using `getActionList()', () => { isExpired: false, startedAt: '2022-04-27T16:08:47.449Z', status: 'successful', - outputs: {}, + 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, + }, + type: 'json', + }, + }, comment: doc?.EndpointActions.data.comment, createdBy: doc?.user.id, parameters: doc?.EndpointActions.data.parameters, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts index 8ab1ec7994381..90945f49a8edf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts @@ -38,6 +38,7 @@ import type { LogsEndpointAction, LogsEndpointActionResponse, ResponseActionsExecuteParameters, + EndpointActionDataParameterTypes, } from '../../../../../common/endpoint/types'; import type { EndpointAppContext } from '../../../types'; import type { FeatureKeys } from '../../feature_usage'; @@ -70,10 +71,13 @@ interface CreateActionMetadata { export interface ActionCreateService { createActionFromAlert: (payload: CreateActionPayload) => Promise; - createAction: ( + createAction: < + TOutputContent extends object = object, + TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes + >( payload: CreateActionPayload, metadata: CreateActionMetadata - ) => Promise; + ) => Promise>; } export const actionCreateService = ( @@ -84,10 +88,13 @@ export const actionCreateService = ( return createAction({ ...payload }, { minimumLicenseRequired: 'enterprise' }); }; - const createAction = async ( + const createAction = async < + TOutputContent extends object = object, + TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes + >( payload: CreateActionPayload, { casesClient, minimumLicenseRequired = 'basic' }: CreateActionMetadata - ): Promise => { + ): Promise> => { const featureKey = commandToFeatureKeyMap.get(payload.command) as FeatureKeys; if (featureKey) { endpointContext.service.getFeatureUsageService().notifyUsage(featureKey); @@ -311,7 +318,7 @@ export const actionCreateService = ( return { ...actionId, ...data, - }; + } as ActionDetails; }; return { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts index 3b16c3fb86ad4..d1818b9b494e7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts @@ -8,6 +8,8 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common'; +import { Readable } from 'stream'; +import type { HapiReadableStream } from '../../../types'; import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; import { FleetActionGenerator } from '../../../../common/endpoint/data_generators/fleet_action_generator'; import type { @@ -20,6 +22,7 @@ import { ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, ENDPOINT_ACTIONS_INDEX, } from '../../../../common/endpoint/constants'; +import type { actionCreateService } from '..'; export const createActionRequestsEsSearchResultsMock = ( agentIds?: string[], @@ -215,3 +218,26 @@ export const generateFileMetadataDocumentMock = ( ...overrides, }; }; + +export const createHapiReadableStreamMock = (): HapiReadableStream => { + const readable = Readable.from(['test']) as HapiReadableStream; + readable.hapi = { + filename: 'foo.txt', + headers: { + 'content-type': 'application/text', + }, + }; + + return readable; +}; + +export const createActionCreateServiceMock = (): jest.Mocked< + ReturnType +> => { + const createdActionMock = new EndpointActionGenerator('seed').generateActionDetails(); + + return { + createAction: jest.fn().mockResolvedValue(createdActionMock), + createActionFromAlert: jest.fn().mockResolvedValue(createdActionMock), + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts index 3750d70f6e020..8be12acae17c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts @@ -260,7 +260,7 @@ describe('When using Actions service utilities', () => { errors: ['Endpoint action response error: endpoint failed to apply'], isCompleted: true, wasSuccessful: false, - outputs: {}, + outputs: expect.anything(), agentState: { '123': { completedAt: endpointResponseAtError.item.data['@timestamp'], @@ -301,7 +301,7 @@ describe('When using Actions service utilities', () => { ], isCompleted: true, wasSuccessful: false, - outputs: {}, + outputs: expect.anything(), agentState: { '123': { completedAt: endpointResponseAtError.item.data['@timestamp'], diff --git a/x-pack/plugins/security_solution/server/endpoint/types.ts b/x-pack/plugins/security_solution/server/endpoint/types.ts index c93276aaec90a..f8f293931478d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/types.ts @@ -7,6 +7,7 @@ import type { LoggerFactory } from '@kbn/core/server'; +import type { DeepReadonly } from 'utility-types'; import type { ConfigType } from '../config'; import type { EndpointAppContextService } from './endpoint_app_context_services'; import type { HostMetadata } from '../../common/endpoint/types'; @@ -17,7 +18,14 @@ import type { ExperimentalFeatures } from '../../common/experimental_features'; */ export interface EndpointAppContext { logFactory: LoggerFactory; + + /** + * @deprecated use `EndpointAppContext.serverConfig` property instead + */ config(): Promise; + + serverConfig: DeepReadonly; + experimentalFeatures: ExperimentalFeatures; /** diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index d8ac197423f8e..c53017acf0a53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -43,11 +43,11 @@ import { getSignalsMigrationStatusSchemaMock } from '../../../../../common/detec // eslint-disable-next-line no-restricted-imports import type { LegacyRuleNotificationAlertType } from '../../rule_actions_legacy'; -import type { HapiReadableStream } from '../../rule_management/logic/import/hapi_readable_stream'; import type { RuleAlertType, RuleParams } from '../../rule_schema'; import { getQueryRuleParams } from '../../rule_schema/mocks'; import { requestMock } from './request'; +import type { HapiReadableStream } from '../../../../types'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 848a0933da542..4d30d9add5d5e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -10,7 +10,7 @@ import { Readable } from 'stream'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import type { RuleResponse } from '../../../../../common/detection_engine/rule_schema'; -import type { HapiReadableStream } from '../../rule_management/logic/import/hapi_readable_stream'; +import type { HapiReadableStream } from '../../../../types'; /** * Given a string, builds a hapi stream as our @@ -19,11 +19,11 @@ import type { HapiReadableStream } from '../../rule_management/logic/import/hapi * @param filename String to declare file extension */ export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiReadableStream => { - const HapiStream = class extends Readable { - public readonly hapi: { filename: string }; + const HapiStream = class extends Readable implements HapiReadableStream { + public readonly hapi; constructor(fileName: string) { super(); - this.hapi = { filename: fileName }; + this.hapi = { filename: fileName, headers: {} }; } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts index c4490e418b859..1ff192a9c1ca7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts @@ -18,7 +18,7 @@ import { importQuerySchema } from '@kbn/securitysolution-io-ts-types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../../../common/constants'; import { ImportRulesResponse } from '../../../../../../../common/detection_engine/rule_management'; -import type { SecuritySolutionPluginRouter } from '../../../../../../types'; +import type { HapiReadableStream, SecuritySolutionPluginRouter } from '../../../../../../types'; import type { ConfigType } from '../../../../../../config'; import type { SetupPlugins } from '../../../../../../plugin'; import { buildMlAuthz } from '../../../../../machine_learning/authz'; @@ -35,7 +35,6 @@ import type { RuleExceptionsPromiseFromStreams } from '../../../logic/import/imp import { importRules as importRulesHelper } from '../../../logic/import/import_rules_utils'; import { getReferencedExceptionLists } from '../../../logic/import/gather_referenced_exceptions'; import { importRuleExceptions } from '../../../logic/import/import_rule_exceptions'; -import type { HapiReadableStream } from '../../../logic/import/hapi_readable_stream'; import { importRuleActionConnectors } from '../../../logic/import/action_connectors/import_rule_action_connectors'; const CHUNK_PARSED_OBJECT_SIZE = 50; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/hapi_readable_stream.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/hapi_readable_stream.ts deleted file mode 100644 index 420d264a617ac..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/hapi_readable_stream.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 { Readable } from 'stream'; - -export interface HapiReadableStream extends Readable { - hapi: { - filename: string; - }; -} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index b25b58c0045c2..f4869b211b1ea 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -130,8 +130,10 @@ export class Plugin implements ISecuritySolutionPlugin { private endpointContext: EndpointAppContext; constructor(context: PluginInitializerContext) { + const serverConfig = createConfig(context); + this.pluginContext = context; - this.config = createConfig(context); + this.config = serverConfig; this.logger = context.logger.get(); this.appClientFactory = new AppClientFactory(); @@ -145,6 +147,9 @@ export class Plugin implements ISecuritySolutionPlugin { logFactory: this.pluginContext.logger, service: this.endpointAppContextService, config: (): Promise => Promise.resolve(this.config), + get serverConfig() { + return serverConfig; + }, experimentalFeatures: this.config.experimentalFeatures, }; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts index c266dcf6ee603..7b5c07c16d34f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts @@ -8,15 +8,13 @@ import type { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import type { IEsSearchResponse } from '@kbn/data-plugin/common'; -import { allowedExperimentalValues } from '../../../../../../../common/experimental_features'; import type { HostAggEsItem, HostsRequestOptions, } from '../../../../../../../common/search_strategy'; import { Direction, HostsFields, HostsQueries } from '../../../../../../../common/search_strategy'; -import type { EndpointAppContextService } from '../../../../../../endpoint/endpoint_app_context_services'; -import type { EndpointAppContext } from '../../../../../../endpoint/types'; +import { createMockEndpointAppContext } from '../../../../../../endpoint/mocks'; export const mockOptions: HostsRequestOptions = { defaultIndex: [ @@ -713,15 +711,6 @@ export const expectedDsl = { export const mockDeps = { esClient: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: {} as SavedObjectsClientContract, - endpointContext: { - logFactory: { - get: jest.fn(), - }, - config: jest.fn().mockResolvedValue({}), - experimentalFeatures: { - ...allowedExperimentalValues, - }, - service: {} as EndpointAppContextService, - } as EndpointAppContext, + endpointContext: createMockEndpointAppContext(), request: {} as KibanaRequest, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index fd718ceec69aa..2347d13de42e5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -17,25 +17,12 @@ import type { KibanaRequest, SavedObjectsClientContract, } from '@kbn/core/server'; -import type { EndpointAppContext } from '../../../../../endpoint/types'; -import type { EndpointAppContextService } from '../../../../../endpoint/endpoint_app_context_services'; -import { allowedExperimentalValues } from '../../../../../../common/experimental_features'; +import { createMockEndpointAppContext } from '../../../../../endpoint/mocks'; const mockDeps = { esClient: {} as IScopedClusterClient, savedObjectsClient: {} as SavedObjectsClientContract, - endpointContext: { - logFactory: { - get: jest.fn().mockReturnValue({ - warn: jest.fn(), - }), - }, - config: jest.fn().mockResolvedValue({}), - experimentalFeatures: { - ...allowedExperimentalValues, - }, - service: {} as EndpointAppContextService, - } as EndpointAppContext, + endpointContext: createMockEndpointAppContext(), request: {} as KibanaRequest, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts index 4eb72798264d5..227744fcc88f1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts @@ -10,18 +10,16 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { riskScore } from '.'; import type { IEsSearchResponse } from '@kbn/data-plugin/public'; -import { allowedExperimentalValues } from '../../../../../../common/experimental_features'; import type { HostRiskScore, RiskScoreRequestOptions, } from '../../../../../../common/search_strategy'; import { RiskScoreEntity, RiskSeverity } from '../../../../../../common/search_strategy'; -import type { EndpointAppContextService } from '../../../../../endpoint/endpoint_app_context_services'; -import type { EndpointAppContext } from '../../../../../endpoint/types'; import * as buildQuery from './query.risk_score.dsl'; import { get } from 'lodash/fp'; import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; +import { createMockEndpointAppContext } from '../../../../../endpoint/mocks'; export const mockSearchStrategyResponse: IEsSearchResponse = { rawResponse: { @@ -73,18 +71,7 @@ const mockDeps = { })), }, savedObjectsClient: {} as SavedObjectsClientContract, - endpointContext: { - logFactory: { - get: jest.fn().mockReturnValue({ - warn: jest.fn(), - }), - }, - config: jest.fn().mockResolvedValue({}), - experimentalFeatures: { - ...allowedExperimentalValues, - }, - service: {} as EndpointAppContextService, - } as EndpointAppContext, + endpointContext: createMockEndpointAppContext(), request: {} as KibanaRequest, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/all/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/all/__mocks__/index.ts index f808f0fdf7a5f..850a305f081bc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/all/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/all/__mocks__/index.ts @@ -7,15 +7,13 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { IEsSearchResponse } from '@kbn/data-plugin/common'; -import { allowedExperimentalValues } from '../../../../../../../common/experimental_features'; import { Direction } from '../../../../../../../common/search_strategy'; import { UsersQueries } from '../../../../../../../common/search_strategy/security_solution/users'; import type { UsersRequestOptions } from '../../../../../../../common/search_strategy/security_solution/users/all'; import { UsersFields } from '../../../../../../../common/search_strategy/security_solution/users/common'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; -import type { EndpointAppContextService } from '../../../../../../endpoint/endpoint_app_context_services'; -import type { EndpointAppContext } from '../../../../../../endpoint/types'; +import { createMockEndpointAppContext } from '../../../../../../endpoint/mocks'; export const mockOptions: UsersRequestOptions = { defaultIndex: ['test_indices*'], @@ -136,16 +134,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { export const mockDeps = () => ({ esClient: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: {} as SavedObjectsClientContract, - endpointContext: { - logFactory: { - get: jest.fn(), - }, - config: jest.fn().mockResolvedValue({}), - experimentalFeatures: { - ...allowedExperimentalValues, - }, - service: {} as EndpointAppContextService, - } as EndpointAppContext, + endpointContext: createMockEndpointAppContext(), request: {} as KibanaRequest, spaceId: 'test-space', }); diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 993d031dec440..a29193250ea28 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -6,18 +6,19 @@ */ import type { - IRouter, - CustomRequestHandlerContext, CoreRequestHandlerContext, + CustomRequestHandlerContext, + IRouter, KibanaRequest, } from '@kbn/core/server'; import type { ActionsApiRequestHandlerContext } from '@kbn/actions-plugin/server'; import type { AlertingApiRequestHandlerContext } from '@kbn/alerting-plugin/server'; import type { FleetRequestHandlerContext } from '@kbn/fleet-plugin/server'; import type { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; -import type { ListsApiRequestHandlerContext, ExceptionListClient } from '@kbn/lists-plugin/server'; -import type { IRuleDataService, AlertsClient } from '@kbn/rule-registry-plugin/server'; +import type { ExceptionListClient, ListsApiRequestHandlerContext } from '@kbn/lists-plugin/server'; +import type { AlertsClient, IRuleDataService } from '@kbn/rule-registry-plugin/server'; +import type { Readable } from 'stream'; import type { Immutable } from '../common/endpoint/types'; import { AppClient } from './client'; import type { ConfigType } from './config'; @@ -52,3 +53,13 @@ export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext< }>; export type SecuritySolutionPluginRouter = IRouter; + +/** + * Readable returned by Hapi when `stream` is used to defined a property and/or route payload + */ +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + headers: Record; + }; +} diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 5ab440f404d80..76500c1be2b6b 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -156,6 +156,7 @@ "@kbn/core-lifecycle-browser", "@kbn/ecs", "@kbn/url-state", - "@kbn/ml-anomaly-utils" + "@kbn/ml-anomaly-utils", + "@kbn/shared-ux-file-types" ] }