diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 46b4191056f35..58abfc3e1f7f9 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -15392,6 +15392,39 @@ paths: tags: - Security Timeline API - access:securitySolution + /api/risk_score/engine/dangerously_delete_data: + delete: + description: >- + Cleaning up the the Risk Engine by removing the indices, mapping and + transforms + operationId: CleanUpRiskEngine + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + cleanup_successful: + type: boolean + description: Successful response + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_TaskManagerUnavailableResponse + description: Task manager is unavailable + default: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse + description: Unexpected error + summary: Cleanup the Risk Engine + tags: + - Security Entity Analytics API /api/risk_score/engine/schedule_now: post: description: >- @@ -29750,6 +29783,27 @@ components: required: - id_value - id_field + Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse: + type: object + properties: + cleanup_successful: + example: false + type: boolean + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + required: + - cleanup_successful + - errors Security_Entity_Analytics_API_CreateAssetCriticalityRecord: allOf: - $ref: >- diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index ea8c34440c3b2..59a22166f3498 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -18822,6 +18822,39 @@ paths: tags: - Security Timeline API - access:securitySolution + /api/risk_score/engine/dangerously_delete_data: + delete: + description: >- + Cleaning up the the Risk Engine by removing the indices, mapping and + transforms + operationId: CleanUpRiskEngine + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + cleanup_successful: + type: boolean + description: Successful response + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_TaskManagerUnavailableResponse + description: Task manager is unavailable + default: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse + description: Unexpected error + summary: Cleanup the Risk Engine + tags: + - Security Entity Analytics API /api/risk_score/engine/schedule_now: post: description: >- @@ -37759,6 +37792,27 @@ components: required: - id_value - id_field + Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse: + type: object + properties: + cleanup_successful: + example: false + type: boolean + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + required: + - cleanup_successful + - errors Security_Entity_Analytics_API_CreateAssetCriticalityRecord: allOf: - $ref: >- diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_cleanup_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_cleanup_route.gen.ts new file mode 100644 index 0000000000000..13194051244cb --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_cleanup_route.gen.ts @@ -0,0 +1,33 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Risk Scoring API + * version: 1 + */ + +import { z } from '@kbn/zod'; + +export type CleanUpRiskEngineErrorResponse = z.infer; +export const CleanUpRiskEngineErrorResponse = z.object({ + cleanup_successful: z.boolean(), + errors: z.array( + z.object({ + seq: z.number().int(), + error: z.string(), + }) + ), +}); + +export type CleanUpRiskEngineResponse = z.infer; +export const CleanUpRiskEngineResponse = z.object({ + cleanup_successful: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_cleanup_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_cleanup_route.schema.yaml new file mode 100644 index 0000000000000..2dffe3879961e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_cleanup_route.schema.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.0 +info: + version: '1' + title: Risk Scoring API + description: These APIs allow the consumer to manage Entity Risk Scores within Entity Analytics. +paths: + /api/risk_score/engine/dangerously_delete_data: + delete: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: CleanUpRiskEngine + summary: Cleanup the Risk Engine + description: Cleaning up the the Risk Engine by removing the indices, mapping and transforms + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + cleanup_successful: + type: boolean + '400': + description: Task manager is unavailable + content: + application/json: + schema: + $ref: '../common/common.schema.yaml#/components/schemas/TaskManagerUnavailableResponse' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/CleanUpRiskEngineErrorResponse' + +components: + schemas: + CleanUpRiskEngineErrorResponse: + type: object + required: + - cleanup_successful + - errors + properties: + cleanup_successful: + type: boolean + example: false + errors: + type: array + items: + type: object + required: + - seq + - error + properties: + seq: + type: integer + error: + type: string + diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts index 94d587cd2bfc7..21dc89544c8d8 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts @@ -15,3 +15,4 @@ export * from './calculation_route.gen'; export * from './preview_route.gen'; export * from './entity_calculation_route.gen'; export * from './get_risk_engine_privileges.gen'; +export * from './engine_cleanup_route.gen'; diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 1af4e60124ef1..9b057bb19d7e2 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -274,6 +274,7 @@ import type { ListEntitiesRequestQueryInput, ListEntitiesResponse, } from './entity_analytics/entity_store/entities/list_entities.gen'; +import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen'; import type { DisableRiskEngineResponse } from './entity_analytics/risk_engine/engine_disable_route.gen'; import type { EnableRiskEngineResponse } from './entity_analytics/risk_engine/engine_enable_route.gen'; import type { InitRiskEngineResponse } from './entity_analytics/risk_engine/engine_init_route.gen'; @@ -540,6 +541,21 @@ If asset criticality records already exist for the specified entities, those rec }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Cleaning up the the Risk Engine by removing the indices, mapping and transforms + */ + async cleanUpRiskEngine() { + this.log.info(`${new Date().toISOString()} Calling API CleanUpRiskEngine`); + return this.kbnClient + .request({ + path: '/api/risk_score/engine/dangerously_delete_data', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'DELETE', + }) + .catch(catchAxiosErrorFormatAndThrow); + } async createAlertsIndex() { this.log.info(`${new Date().toISOString()} Calling API CreateAlertsIndex`); return this.kbnClient diff --git a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts index 17cfcf1da8e84..0eda694aed24b 100644 --- a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts +++ b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts @@ -16,6 +16,7 @@ export const RISK_ENGINE_SETTINGS_URL = `${RISK_ENGINE_URL}/settings` as const; // Public Risk Score routes export const PUBLIC_RISK_ENGINE_URL = `${PUBLIC_RISK_SCORE_URL}/engine` as const; export const RISK_ENGINE_SCHEDULE_NOW_URL = `${RISK_ENGINE_URL}/schedule_now` as const; +export const RISK_ENGINE_CLEANUP_URL = `${PUBLIC_RISK_ENGINE_URL}/dangerously_delete_data` as const; type ClusterPrivilege = 'manage_index_templates' | 'manage_transform'; export const RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES = [ diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_1.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_1.bundled.schema.yaml new file mode 100644 index 0000000000000..9d6d57abd382a --- /dev/null +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_1.bundled.schema.yaml @@ -0,0 +1,88 @@ +openapi: 3.0.3 +info: + description: '' + title: Security Entity Analytics API (Elastic Cloud and self-hosted) + version: '1' +servers: + - url: http://{kibana_host}:{port} + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/risk_score/engine/dangerously_delete_data: + delete: + description: >- + Cleaning up the the Risk Engine by removing the indices, mapping and + transforms + operationId: CleanUpRiskEngine + responses: + '200': + content: + application/json: + schema: + type: object + properties: + cleanup_successful: + type: boolean + description: Successful response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/TaskManagerUnavailableResponse' + description: Task manager is unavailable + default: + content: + application/json: + schema: + $ref: '#/components/schemas/CleanUpRiskEngineErrorResponse' + description: Unexpected error + summary: Cleanup the Risk Engine + tags: + - Security Entity Analytics API +components: + schemas: + CleanUpRiskEngineErrorResponse: + type: object + properties: + cleanup_successful: + example: false + type: boolean + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + required: + - cleanup_successful + - errors + TaskManagerUnavailableResponse: + description: Task manager is unavailable + type: object + properties: + message: + type: string + status_code: + minimum: 400 + type: integer + required: + - status_code + - message + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: + - description: '' + name: Security Entity Analytics API diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_1.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_1.bundled.schema.yaml new file mode 100644 index 0000000000000..835d8f79b1fea --- /dev/null +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_1.bundled.schema.yaml @@ -0,0 +1,88 @@ +openapi: 3.0.3 +info: + description: '' + title: Security Entity Analytics API (Elastic Cloud Serverless) + version: '1' +servers: + - url: http://{kibana_host}:{port} + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/risk_score/engine/dangerously_delete_data: + delete: + description: >- + Cleaning up the the Risk Engine by removing the indices, mapping and + transforms + operationId: CleanUpRiskEngine + responses: + '200': + content: + application/json: + schema: + type: object + properties: + cleanup_successful: + type: boolean + description: Successful response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/TaskManagerUnavailableResponse' + description: Task manager is unavailable + default: + content: + application/json: + schema: + $ref: '#/components/schemas/CleanUpRiskEngineErrorResponse' + description: Unexpected error + summary: Cleanup the Risk Engine + tags: + - Security Entity Analytics API +components: + schemas: + CleanUpRiskEngineErrorResponse: + type: object + properties: + cleanup_successful: + example: false + type: boolean + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + required: + - cleanup_successful + - errors + TaskManagerUnavailableResponse: + description: Task manager is unavailable + type: object + properties: + message: + type: string + status_code: + minimum: 400 + type: integer + required: + - status_code + - message + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: + - description: '' + name: Security Entity Analytics API diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index f958d20d7c96b..18cb9ef570bd5 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -7,12 +7,12 @@ import { useMemo } from 'react'; import { LIST_ENTITIES_URL } from '../../../common/entity_analytics/entity_store/constants'; -import type { RiskEngineScheduleNowResponse } from '../../../common/api/entity_analytics/risk_engine/engine_schedule_now_route.gen'; -import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; import type { UploadAssetCriticalityRecordsResponse } from '../../../common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; +import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; import type { RiskEngineStatusResponse } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; import type { InitRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; import type { EnableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen'; +import type { RiskEngineScheduleNowResponse } from '../../../common/api/entity_analytics/risk_engine/engine_schedule_now_route.gen'; import type { RiskScoresPreviewRequest, RiskScoresPreviewResponse, @@ -40,6 +40,7 @@ import { ASSET_CRITICALITY_PUBLIC_CSV_UPLOAD_URL, RISK_SCORE_ENTITY_CALCULATION_URL, API_VERSIONS, + RISK_ENGINE_CLEANUP_URL, RISK_ENGINE_SCHEDULE_NOW_URL, } from '../../../common/constants'; import type { SnakeToCamelCase } from '../common/utils'; @@ -191,12 +192,18 @@ export const useEntityAnalyticsRoutes = () => { }); const deleteAssetCriticality = async ( - params: Pick & { refresh?: 'wait_for' } + params: Pick & { + refresh?: 'wait_for'; + } ): Promise<{ deleted: true }> => { await http.fetch(ASSET_CRITICALITY_PUBLIC_URL, { version: API_VERSIONS.public.v1, method: 'DELETE', - query: { id_value: params.idValue, id_field: params.idField, refresh: params.refresh }, + query: { + id_value: params.idValue, + id_field: params.idField, + refresh: params.refresh, + }, }); // spoof a response to allow us to better distnguish a delete from a create in use_asset_criticality.ts @@ -220,7 +227,9 @@ export const useEntityAnalyticsRoutes = () => { fileContent: string, fileName: string ): Promise => { - const file = new File([new Blob([fileContent])], fileName, { type: 'text/csv' }); + const file = new File([new Blob([fileContent])], fileName, { + type: 'text/csv', + }); const body = new FormData(); body.append('file', file); @@ -267,6 +276,16 @@ export const useEntityAnalyticsRoutes = () => { method: 'GET', }); + /** + * Deletes Risk engine installation and associated data + */ + + const cleanUpRiskEngine = () => + http.fetch(RISK_ENGINE_CLEANUP_URL, { + version: '1', + method: 'DELETE', + }); + return { fetchRiskScorePreview, fetchRiskEngineStatus, @@ -283,6 +302,7 @@ export const useEntityAnalyticsRoutes = () => { getRiskScoreIndexStatus, fetchRiskEngineSettings, calculateEntityRiskScore, + cleanUpRiskEngine, fetchEntitiesList, }; }, [http]); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.mock.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.mock.ts index a8d7b7a9c763b..e9819c5b290d3 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.mock.ts @@ -15,6 +15,9 @@ const createRiskEngineDataClientMock = () => getConfiguration: jest.fn(), getStatus: jest.fn(), init: jest.fn(), + tearDown: jest.fn(), } as unknown as jest.Mocked); -export const riskEngineDataClientMock = { create: createRiskEngineDataClientMock }; +export const riskEngineDataClientMock = { + create: createRiskEngineDataClientMock, +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts new file mode 100644 index 0000000000000..5c66b70c75c13 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts @@ -0,0 +1,186 @@ +/* + * 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 { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { RISK_ENGINE_CLEANUP_URL } from '../../../../../common/constants'; +import { + serverMock, + requestContextMock, + requestMock, +} from '../../../detection_engine/routes/__mocks__'; +import { riskEnginePrivilegesMock } from './risk_engine_privileges.mock'; +import { riskEngineDataClientMock } from '../risk_engine_data_client.mock'; +import { riskEngineCleanupRoute } from './delete'; + +describe('risk engine cleanup route', () => { + let server: ReturnType; + let context: ReturnType; + let mockTaskManagerStart: ReturnType; + let mockRiskEngineDataClient: ReturnType; + let getStartServicesMock: jest.Mock; + + beforeEach(() => { + jest.resetAllMocks(); + + server = serverMock.create(); + const { clients } = requestContextMock.createTools(); + mockRiskEngineDataClient = riskEngineDataClientMock.create(); + context = requestContextMock.convertContext( + requestContextMock.create({ + ...clients, + riskEngineDataClient: mockRiskEngineDataClient, + }) + ); + mockTaskManagerStart = taskManagerMock.createStart(); + }); + + const buildRequest = () => { + return requestMock.create({ + method: 'delete', + path: RISK_ENGINE_CLEANUP_URL, + body: {}, + }); + }; + describe('invokes the risk engine cleanup route', () => { + beforeEach(() => { + getStartServicesMock = jest.fn().mockResolvedValue([ + {}, + { + taskManager: mockTaskManagerStart, + security: riskEnginePrivilegesMock.createMockSecurityStartWithFullRiskEngineAccess(), + }, + ]); + riskEngineCleanupRoute(server.router, getStartServicesMock); + }); + + it('should call the router with the correct route and handler', async () => { + const request = buildRequest(); + await server.inject(request, context); + expect(mockRiskEngineDataClient.tearDown).toHaveBeenCalled(); + }); + + it('returns a 200 when cleanup is successful', async () => { + const request = buildRequest(); + const response = await server.inject(request, context); + expect(response.status).toBe(200); + expect(response.body).toEqual({ cleanup_successful: true }); + }); + + it('returns a 400 when cleanup endpoint is called multiple times', async () => { + mockRiskEngineDataClient.tearDown.mockImplementation(async () => { + return [Error('Risk engine is disabled or deleted already.')]; + }); + const request = buildRequest(); + const response = await server.inject(request, context); + expect(response.status).toBe(400); + expect(response.body).toEqual({ + cleanup_successful: false, + errors: [ + { + seq: 1, + error: 'Error: Risk engine is disabled or deleted already.', + }, + ], + status_code: 400, + }); + }); + + it('returns a 500 when cleanup is unsuccessful', async () => { + mockRiskEngineDataClient.tearDown.mockImplementation(() => { + throw new Error('Error tearing down'); + }); + const request = buildRequest(); + const response = await server.inject(request, context); + expect(response.status).toBe(500); + expect(response.body).toEqual({ + errors: { + error: '{}', + seq: 1, + }, + cleanup_successful: false, + status_code: 500, + }); + }); + + it('returns a 500 when cleanup is unsuccessful with multiple errors', async () => { + mockRiskEngineDataClient.tearDown.mockImplementation(async () => { + return [ + Error('Error while removing risk scoring task'), + Error('Error while deleting saved objects'), + Error('Error while removing risk score index'), + ]; + }); + const request = buildRequest(); + const response = await server.inject(request, context); + expect(response.status).toBe(500); + expect(response.body).toEqual({ + errors: [ + { + seq: 1, + error: 'Error: Error while removing risk scoring task', + }, + { + seq: 2, + error: 'Error: Error while deleting saved objects', + }, + { + seq: 3, + error: 'Error: Error while removing risk score index', + }, + ], + cleanup_successful: false, + status_code: 500, + }); + }); + }); + describe('when task manager is unavailable', () => { + beforeEach(() => { + getStartServicesMock = jest.fn().mockResolvedValue([ + {}, + { + security: riskEnginePrivilegesMock.createMockSecurityStartWithFullRiskEngineAccess(), + }, + ]); + riskEngineCleanupRoute(server.router, getStartServicesMock); + }); + + it('returns a 400 when task manager is unavailable', async () => { + const request = buildRequest(); + const response = await server.inject(request, context); + expect(response.status).toBe(400); + expect(response.body).toEqual({ + message: + 'Task Manager is unavailable, but is required by the risk engine. Please enable the taskManager plugin and try again.', + status_code: 400, + }); + }); + }); + + describe('when user does not have the required privileges', () => { + beforeEach(() => { + getStartServicesMock = jest.fn().mockResolvedValue([ + {}, + { + taskManager: mockTaskManagerStart, + security: riskEnginePrivilegesMock.createMockSecurityStartWithNoRiskEngineAccess(), + }, + ]); + riskEngineCleanupRoute(server.router, getStartServicesMock); + }); + + it('returns a 403 when user does not have the required privileges', async () => { + const request = buildRequest(); + const response = await server.inject(request, context); + expect(response.status).toBe(403); + expect(response.body).toEqual({ + message: + 'User is missing risk engine privileges. Missing cluster privileges: manage_index_templates, manage_transform.', + status_code: 403, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.ts new file mode 100644 index 0000000000000..1776ddcca69b1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.ts @@ -0,0 +1,103 @@ +/* + * 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 { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import type { IKibanaResponse } from '@kbn/core-http-server'; +import { withRiskEnginePrivilegeCheck } from '../risk_engine_privileges'; +import { RISK_ENGINE_CLEANUP_URL, APP_ID, API_VERSIONS } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; +import { RiskEngineAuditActions } from '../audit'; +import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; +import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; +import type { CleanUpRiskEngineResponse } from '../../../../../common/api/entity_analytics'; + +export const riskEngineCleanupRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + getStartServices: EntityAnalyticsRoutesDeps['getStartServices'] +) => { + router.versioned + .delete({ + access: 'public', + path: RISK_ENGINE_CLEANUP_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { version: API_VERSIONS.public.v1, validate: {} }, + withRiskEnginePrivilegeCheck( + getStartServices, + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + const securitySolution = await context.securitySolution; + const [_, { taskManager }] = await getStartServices(); + const riskEngineClient = securitySolution.getRiskEngineDataClient(); + const riskScoreDataClient = securitySolution.getRiskScoreDataClient(); + + if (!taskManager) { + securitySolution.getAuditLogger()?.log({ + message: + 'User attempted to perform a cleanup of risk engine, but the Kibana Task Manager was unavailable', + event: { + action: RiskEngineAuditActions.RISK_ENGINE_REMOVE_TASK, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.DELETION, + outcome: AUDIT_OUTCOME.FAILURE, + }, + error: { + message: + 'User attempted to perform a cleanup of risk engine, but the Kibana Task Manager was unavailable', + }, + }); + + return siemResponse.error({ + statusCode: 400, + body: TASK_MANAGER_UNAVAILABLE_ERROR, + }); + } + + try { + const errors = await riskEngineClient.tearDown({ + taskManager, + riskScoreDataClient, + }); + if (errors && errors.length > 0) { + return siemResponse.error({ + statusCode: errors.some((error) => + error.message.includes('Risk engine is disabled or deleted already.') + ) + ? 400 + : 500, + body: { + cleanup_successful: false, + errors: errors.map((error, seq) => ({ + seq: seq + 1, + error: error.toString(), + })), + }, + bypassErrorFormat: true, + }); + } else { + return response.ok({ body: { cleanup_successful: true } }); + } + } catch (error) { + return siemResponse.error({ + statusCode: 500, + body: { + cleanup_successful: false, + errors: { + seq: 1, + error: JSON.stringify(error), + }, + }, + bypassErrorFormat: true, + }); + } + } + ) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/register_risk_engine_routes.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/register_risk_engine_routes.ts index 99b0bbe5a5e87..f4edb7d798188 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/register_risk_engine_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/register_risk_engine_routes.ts @@ -12,6 +12,7 @@ import { riskEnginePrivilegesRoute } from './privileges'; import { riskEngineSettingsRoute } from './settings'; import type { EntityAnalyticsRoutesDeps } from '../../types'; import { riskEngineScheduleNowRoute } from './schedule_now'; +import { riskEngineCleanupRoute } from './delete'; export const registerRiskEngineRoutes = ({ router, @@ -24,4 +25,5 @@ export const registerRiskEngineRoutes = ({ riskEngineScheduleNowRoute(router, getStartServices); riskEngineSettingsRoute(router); riskEnginePrivilegesRoute(router, getStartServices); + riskEngineCleanupRoute(router, getStartServices); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges.mock.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges.mock.ts index 10c772cfcf05e..189e72624c15c 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges.mock.ts @@ -29,6 +29,27 @@ const createMockSecurityStartWithFullRiskEngineAccess = () => { return mockSecurityStart; }; +const createMockSecurityStartWithNoRiskEngineAccess = () => { + const mockSecurityStart = securityMock.createStart(); + + const mockCheckPrivileges = jest.fn().mockResolvedValue({ + hasAllRequested: false, + privileges: { + elasticsearch: { + cluster: [], + index: [], + }, + }, + }); + + mockSecurityStart.authz.checkPrivilegesDynamicallyWithRequest = jest + .fn() + .mockReturnValue(mockCheckPrivileges); + + return mockSecurityStart; +}; + export const riskEnginePrivilegesMock = { createMockSecurityStartWithFullRiskEngineAccess, + createMockSecurityStartWithNoRiskEngineAccess, }; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index bb229ddcd693f..74f14c2c5f6ad 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -256,6 +256,16 @@ If asset criticality records already exist for the specified entities, those rec .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + /** + * Cleaning up the the Risk Engine by removing the indices, mapping and transforms + */ + cleanUpRiskEngine() { + return supertest + .delete('/api/risk_score/engine/dangerously_delete_data') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, createAlertsIndex() { return supertest .post('/api/detection_engine/index') diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/index.ts index 4879cce14f3a6..2aa04a898a449 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Entity Analytics - Risk Engine', function () { loadTestFile(require.resolve('./init_and_status_apis')); + loadTestFile(require.resolve('./risk_engine_cleanup_api')); loadTestFile(require.resolve('./risk_score_preview')); loadTestFile(require.resolve('./risk_scoring_task/task_execution')); loadTestFile(require.resolve('./risk_scoring_task/task_execution_nondefault_spaces')); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_cleanup_api.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_cleanup_api.ts new file mode 100644 index 0000000000000..48344403093b3 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_cleanup_api.ts @@ -0,0 +1,67 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { + buildDocument, + riskEngineRouteHelpersFactory, + waitForRiskScoresToBePresent, + createAndSyncRuleAndAlertsFactory, +} from '../../utils'; +import { dataGeneratorFactory } from '../../../detections_response/utils'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest); + const es = getService('es'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('@ess @ serverless @serverless QA risk_engine_cleanup_api', () => { + const createAndSyncRuleAndAlerts = createAndSyncRuleAndAlertsFactory({ supertest, log }); + const { indexListOfDocuments } = dataGeneratorFactory({ + es, + index: 'ecs_compliant', + log, + }); + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ecs_compliant'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/ecs_compliant'); + }); + + it('should return response with success status', async () => { + const status1 = await riskEngineRoutes.getStatus(); + expect(status1.body.risk_engine_status).to.be('NOT_INSTALLED'); + expect(status1.body.legacy_risk_engine_status).to.be('NOT_INSTALLED'); + + const firstDocumentId = uuidv4(); + await indexListOfDocuments([buildDocument({ host: { name: 'host-1' } }, firstDocumentId)]); + await createAndSyncRuleAndAlerts({ query: `id: ${firstDocumentId}` }); + + await riskEngineRoutes.init(); + await waitForRiskScoresToBePresent({ es, log, scoreCount: 1 }); + + const status2 = await riskEngineRoutes.getStatus(); + expect(status2.body.risk_engine_status).to.be('ENABLED'); + expect(status2.body.legacy_risk_engine_status).to.be('NOT_INSTALLED'); + + const response = await riskEngineRoutes.delete(); + expect(response.body).to.eql({ + cleanup_successful: true, + }); + + const status3 = await riskEngineRoutes.getStatus(); + expect(status3.body.risk_engine_status).to.be('NOT_INSTALLED'); + expect(status3.body.legacy_risk_engine_status).to.be('NOT_INSTALLED'); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts index 3b96bc61cc7ba..977ab1b3675f9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts @@ -22,6 +22,7 @@ import { RISK_ENGINE_ENABLE_URL, RISK_ENGINE_STATUS_URL, RISK_ENGINE_PRIVILEGES_URL, + RISK_ENGINE_CLEANUP_URL, RISK_ENGINE_SCHEDULE_NOW_URL, } from '@kbn/security-solution-plugin/common/constants'; import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; @@ -574,6 +575,14 @@ export const riskEngineRouteHelpersFactory = (supertest: SuperTest.Agent, namesp .send() .expect(expectStatusCode), + delete: async (expectStatusCode: number = 200) => + await supertest + .delete(routeWithNamespace(RISK_ENGINE_CLEANUP_URL, namespace)) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send() + .expect(expectStatusCode), + scheduleNow: async (expectStatusCode: number = 200) => await supertest .post(routeWithNamespace(RISK_ENGINE_SCHEDULE_NOW_URL, namespace))