diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index bf3c74d906cef..f39fba3534fbb 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -33483,6 +33483,58 @@ paths: tags: - Security Entity Analytics API x-beta: true + /api/risk_score/engine/saved_object/configure: + patch: + description: Configuring the Risk Engine Saved Object + operationId: ConfigureRiskEngineSavedObject + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + exclude_alert_statuses: + items: + type: string + type: array + exclude_alert_tags: + items: + type: string + type: array + range: + type: object + properties: + end: + type: string + start: + type: string + required: true + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + risk_engine_saved_object_configured: + 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_ConfigureRiskEngineSavedObjectErrorResponse' + description: Unexpected error + summary: Configure the Risk Engine Saved Object + tags: + - Security Entity Analytics API + x-beta: true /api/risk_score/engine/schedule_now: post: description: Schedule the risk scoring engine to run as soon as possible. You can use this to recalculate entity risk scores after updating their asset criticality. @@ -46925,6 +46977,27 @@ components: required: - cleanup_successful - errors + Security_Entity_Analytics_API_ConfigureRiskEngineSavedObjectErrorResponse: + type: object + properties: + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + risk_engine_saved_object_configured: + example: false + type: boolean + required: + - risk_engine_saved_object_configured + - errors Security_Entity_Analytics_API_CreateAssetCriticalityRecord: allOf: - $ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityRecordIdParts' diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index aea9bacebd061..8d40f48e99e32 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -36204,6 +36204,57 @@ paths: summary: Cleanup the Risk Engine tags: - Security Entity Analytics API + /api/risk_score/engine/saved_object/configure: + patch: + description: Configuring the Risk Engine Saved Object + operationId: ConfigureRiskEngineSavedObject + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + exclude_alert_statuses: + items: + type: string + type: array + exclude_alert_tags: + items: + type: string + type: array + range: + type: object + properties: + end: + type: string + start: + type: string + required: true + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + risk_engine_saved_object_configured: + 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_ConfigureRiskEngineSavedObjectErrorResponse' + description: Unexpected error + summary: Configure the Risk Engine Saved Object + tags: + - Security Entity Analytics API /api/risk_score/engine/schedule_now: post: description: Schedule the risk scoring engine to run as soon as possible. You can use this to recalculate entity risk scores after updating their asset criticality. @@ -54606,6 +54657,27 @@ components: required: - cleanup_successful - errors + Security_Entity_Analytics_API_ConfigureRiskEngineSavedObjectErrorResponse: + type: object + properties: + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + risk_engine_saved_object_configured: + example: false + type: boolean + required: + - risk_engine_saved_object_configured + - errors Security_Entity_Analytics_API_CreateAssetCriticalityRecord: allOf: - $ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityRecordIdParts' diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen.ts new file mode 100644 index 0000000000000..19e64986b822f --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen.ts @@ -0,0 +1,54 @@ +/* + * 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: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +export type ConfigureRiskEngineSavedObjectErrorResponse = z.infer< + typeof ConfigureRiskEngineSavedObjectErrorResponse +>; +export const ConfigureRiskEngineSavedObjectErrorResponse = z.object({ + risk_engine_saved_object_configured: z.boolean(), + errors: z.array( + z.object({ + seq: z.number().int(), + error: z.string(), + }) + ), +}); + +export type ConfigureRiskEngineSavedObjectRequestBody = z.infer< + typeof ConfigureRiskEngineSavedObjectRequestBody +>; +export const ConfigureRiskEngineSavedObjectRequestBody = z.object({ + exclude_alert_statuses: z.array(z.string()).optional(), + range: z + .object({ + start: z.string().optional(), + end: z.string().optional(), + }) + .optional(), + exclude_alert_tags: z.array(z.string()).optional(), +}); +export type ConfigureRiskEngineSavedObjectRequestBodyInput = z.input< + typeof ConfigureRiskEngineSavedObjectRequestBody +>; + +export type ConfigureRiskEngineSavedObjectResponse = z.infer< + typeof ConfigureRiskEngineSavedObjectResponse +>; +export const ConfigureRiskEngineSavedObjectResponse = z.object({ + risk_engine_saved_object_configured: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.schema.yaml new file mode 100644 index 0000000000000..4c9a1cf1f3693 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.schema.yaml @@ -0,0 +1,81 @@ +openapi: 3.0.0 +info: + version: '2023-10-31' + title: Risk Scoring API + description: These APIs allow the consumer to configure the Risk Engine Saved Object. +paths: + /api/risk_score/engine/saved_object/configure: + patch: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: ConfigureRiskEngineSavedObject + summary: Configure the Risk Engine Saved Object + description: Configuring the Risk Engine Saved Object + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + exclude_alert_statuses: + type: array + items: + type: string + range: + type: object + properties: + start: + type: string + end: + type: string + exclude_alert_tags: + type: array + items: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + risk_engine_saved_object_configured: + 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/ConfigureRiskEngineSavedObjectErrorResponse' + +components: + schemas: + ConfigureRiskEngineSavedObjectErrorResponse: + type: object + required: + - risk_engine_saved_object_configured + - errors + properties: + risk_engine_saved_object_configured: + 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 21dc89544c8d8..98d62fd1b5a9e 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 @@ -16,3 +16,4 @@ export * from './preview_route.gen'; export * from './entity_calculation_route.gen'; export * from './get_risk_engine_privileges.gen'; export * from './engine_cleanup_route.gen'; +export * from './engine_configure_saved_object_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 b5d72fc1ef207..2e31db6246e74 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 @@ -276,6 +276,10 @@ import type { GetEntityStoreStatusResponse, } from './entity_analytics/entity_store/status.gen'; import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen'; +import type { + ConfigureRiskEngineSavedObjectRequestBodyInput, + ConfigureRiskEngineSavedObjectResponse, +} from './entity_analytics/risk_engine/engine_configure_saved_object_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'; @@ -600,6 +604,22 @@ If asset criticality records already exist for the specified entities, those rec }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Configuring the Risk Engine Saved Object + */ + async configureRiskEngineSavedObject(props: ConfigureRiskEngineSavedObjectProps) { + this.log.info(`${new Date().toISOString()} Calling API ConfigureRiskEngineSavedObject`); + return this.kbnClient + .request({ + path: '/api/risk_score/engine/saved_object/configure', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'PATCH', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Copies and returns a timeline or timeline template. @@ -2275,6 +2295,9 @@ export interface BulkUpsertAssetCriticalityRecordsProps { export interface CleanDraftTimelinesProps { body: CleanDraftTimelinesRequestBodyInput; } +export interface ConfigureRiskEngineSavedObjectProps { + body: ConfigureRiskEngineSavedObjectRequestBodyInput; +} export interface CopyTimelineProps { body: CopyTimelineRequestBodyInput; } 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 0eda694aed24b..9d71e984021f8 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 @@ -17,6 +17,8 @@ export const RISK_ENGINE_SETTINGS_URL = `${RISK_ENGINE_URL}/settings` as const; 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; +export const RISK_ENGINE_CONFIGURE_SO_URL = + `${PUBLIC_RISK_ENGINE_URL}/saved_object/configure` 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_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index b1b85b8222786..a242136f9f237 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -646,6 +646,58 @@ paths: summary: Cleanup the Risk Engine tags: - Security Entity Analytics API + /api/risk_score/engine/saved_object/configure: + patch: + description: Configuring the Risk Engine Saved Object + operationId: ConfigureRiskEngineSavedObject + requestBody: + content: + application/json: + schema: + type: object + properties: + exclude_alert_statuses: + items: + type: string + type: array + exclude_alert_tags: + items: + type: string + type: array + range: + type: object + properties: + end: + type: string + start: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + type: object + properties: + risk_engine_saved_object_configured: + 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/ConfigureRiskEngineSavedObjectErrorResponse + description: Unexpected error + summary: Configure the Risk Engine Saved Object + tags: + - Security Entity Analytics API /api/risk_score/engine/schedule_now: post: description: >- @@ -798,6 +850,27 @@ components: required: - cleanup_successful - errors + ConfigureRiskEngineSavedObjectErrorResponse: + type: object + properties: + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + risk_engine_saved_object_configured: + example: false + type: boolean + required: + - risk_engine_saved_object_configured + - errors CreateAssetCriticalityRecord: allOf: - $ref: '#/components/schemas/AssetCriticalityRecordIdParts' diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 4a3b3495467e9..3426bd3c7c721 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -646,6 +646,58 @@ paths: summary: Cleanup the Risk Engine tags: - Security Entity Analytics API + /api/risk_score/engine/saved_object/configure: + patch: + description: Configuring the Risk Engine Saved Object + operationId: ConfigureRiskEngineSavedObject + requestBody: + content: + application/json: + schema: + type: object + properties: + exclude_alert_statuses: + items: + type: string + type: array + exclude_alert_tags: + items: + type: string + type: array + range: + type: object + properties: + end: + type: string + start: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + type: object + properties: + risk_engine_saved_object_configured: + 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/ConfigureRiskEngineSavedObjectErrorResponse + description: Unexpected error + summary: Configure the Risk Engine Saved Object + tags: + - Security Entity Analytics API /api/risk_score/engine/schedule_now: post: description: >- @@ -798,6 +850,27 @@ components: required: - cleanup_successful - errors + ConfigureRiskEngineSavedObjectErrorResponse: + type: object + properties: + errors: + items: + type: object + properties: + error: + type: string + seq: + type: integer + required: + - seq + - error + type: array + risk_engine_saved_object_configured: + example: false + type: boolean + required: + - risk_engine_saved_object_configured + - errors CreateAssetCriticalityRecord: allOf: - $ref: '#/components/schemas/AssetCriticalityRecordIdParts' diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/audit.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/audit.ts index 9ade355d54bf3..3fc2f6d48923c 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/audit.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/audit.ts @@ -17,4 +17,5 @@ export enum RiskEngineAuditActions { RISK_ENGINE_DISABLE_LEGACY_ENGINE = 'risk_engine_disable_legacy_engine', RISK_ENGINE_REMOVE_TASK = 'risk_engine_remove_task', RISK_ENGINE_SCHEDULE_NOW = 'risk_engine_schedule_now', + RISK_ENGINE_CONFIGURE_SAVED_OBJECT = 'risk_engine_configure_saved_object', } diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts index 241523f62e12c..c76df85f992f4 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts @@ -319,4 +319,11 @@ export class RiskEngineDataClient { return RiskEngineStatusEnum.ENABLED; } + + public async update_risk_engine_saved_object(attributes: {}) { + return updateSavedObjectAttribute({ + savedObjectsClient: this.options.soClient, + attributes, + }); + } } diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.test.ts new file mode 100644 index 0000000000000..0da0ca13a2506 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { + serverMock, + requestContextMock, + requestMock, +} from '../../../detection_engine/routes/__mocks__'; +import { riskEnginePrivilegesMock } from './risk_engine_privileges.mock'; +import { riskEngineDataClientMock } from '../risk_engine_data_client.mock'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { RISK_ENGINE_CONFIGURE_SO_URL } from '../../../../../common/constants'; +import { riskEngineConfigureSavedObjectRoute } from './configure_saved_object'; + +describe('riskEnginConfigureSavedObjectRoute', () => { + 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(); + mockRiskEngineDataClient.update_risk_engine_saved_object = jest.fn(); + context = requestContextMock.convertContext( + requestContextMock.create({ + ...clients, + riskEngineDataClient: mockRiskEngineDataClient, + }) + ); + mockTaskManagerStart = taskManagerMock.createStart(); + getStartServicesMock = jest.fn().mockResolvedValue([ + {}, + { + taskManager: mockTaskManagerStart, + security: riskEnginePrivilegesMock.createMockSecurityStartWithFullRiskEngineAccess(), + }, + ]); + riskEngineConfigureSavedObjectRoute(server.router, getStartServicesMock); + }); + + const buildRequest = (body: {}) => { + return requestMock.create({ + method: 'put', + path: RISK_ENGINE_CONFIGURE_SO_URL, + body, + }); + }; + + it('should call the router with the correct route and handler', async () => { + const request = buildRequest({}); + await server.inject(request, context); + expect(mockRiskEngineDataClient.update_risk_engine_saved_object).toHaveBeenCalled(); + }); + + it('returns a 200 when the saved object is updated successfully', async () => { + const request = buildRequest({ + exclude_alert_statuses: ['open'], + range: { start: 'now-30d', end: 'now' }, + exclude_alert_tags: ['tag1'], + }); + const response = await server.inject(request, context); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ risk_engine_saved_object_configured: true }); + expect(mockRiskEngineDataClient.update_risk_engine_saved_object).toHaveBeenCalledWith({ + excludeAlertStatuses: ['open'], + range: { start: 'now-30d', end: 'now' }, + excludeAlertTags: ['tag1'], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.ts new file mode 100644 index 0000000000000..fdca11e5027d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.ts @@ -0,0 +1,106 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import type { IKibanaResponse } from '@kbn/core-http-server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { ConfigureRiskEngineSavedObjectResponse } from '../../../../../common/api/entity_analytics'; +import { ConfigureRiskEngineSavedObjectRequestBody } from '../../../../../common/api/entity_analytics'; +import { RISK_ENGINE_CONFIGURE_SO_URL, APP_ID } from '../../../../../common/constants'; +import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; +import { withRiskEnginePrivilegeCheck } from '../risk_engine_privileges'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; +import { RiskEngineAuditActions } from '../audit'; +import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; + +export const riskEngineConfigureSavedObjectRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + getStartServices: EntityAnalyticsRoutesDeps['getStartServices'] +) => { + router.versioned + .put({ + access: 'public', + path: RISK_ENGINE_CONFIGURE_SO_URL, + security: { + authz: { + requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], + }, + }, + }) + .addVersion( + { + version: '2023-10-31', + validate: { + request: { body: buildRouteValidationWithZod(ConfigureRiskEngineSavedObjectRequestBody) }, + }, + }, + withRiskEnginePrivilegeCheck( + getStartServices, + async ( + context, + request, + response + ): Promise> => { + const securitySolution = await context.securitySolution; + + securitySolution.getAuditLogger()?.log({ + message: 'User attempted to configure the saved object of the risk engine', + event: { + action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURE_SAVED_OBJECT, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.CHANGE, + outcome: AUDIT_OUTCOME.UNKNOWN, + }, + }); + + const siemResponse = buildSiemResponse(response); + const [_, { taskManager }] = await getStartServices(); + const riskEngineClient = securitySolution.getRiskEngineDataClient(); + + if (!taskManager) { + securitySolution.getAuditLogger()?.log({ + message: + 'User attempted to configure the saved object of the risk engine, but the Kibana Task Manager was unavailable', + event: { + action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURE_SAVED_OBJECT, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.CHANGE, + outcome: AUDIT_OUTCOME.FAILURE, + }, + error: { + message: + 'User attempted to configure the saved object of the risk engine, but the Kibana Task Manager was unavailable', + }, + }); + + return siemResponse.error({ + statusCode: 400, + body: TASK_MANAGER_UNAVAILABLE_ERROR, + }); + } + + try { + await riskEngineClient.update_risk_engine_saved_object({ + excludeAlertStatuses: request.body.exclude_alert_statuses, + range: request.body.range, + excludeAlertTags: request.body.exclude_alert_tags, + }); + return response.ok({ body: { risk_engine_saved_object_configured: true } }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + 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 f4edb7d798188..a82ca38f7e1fd 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 @@ -13,6 +13,7 @@ import { riskEngineSettingsRoute } from './settings'; import type { EntityAnalyticsRoutesDeps } from '../../types'; import { riskEngineScheduleNowRoute } from './schedule_now'; import { riskEngineCleanupRoute } from './delete'; +import { riskEngineConfigureSavedObjectRoute } from './configure_saved_object'; export const registerRiskEngineRoutes = ({ router, @@ -26,4 +27,5 @@ export const registerRiskEngineRoutes = ({ riskEngineSettingsRoute(router); riskEnginePrivilegesRoute(router, getStartServices); riskEngineCleanupRoute(router, getStartServices); + riskEngineConfigureSavedObjectRoute(router, getStartServices); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts index 4282e0a793f47..a72835a92c1f4 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts @@ -42,7 +42,10 @@ export const updateSavedObjectAttribute = async ({ attributes, }: SavedObjectsClientArg & { attributes: { - enabled: boolean; + enabled?: boolean; + excludeAlertIds?: string[]; + range?: { start: string; end: string }; + excludeAlertTags?: string[]; }; }) => { const savedObjectConfiguration = await getConfigurationSavedObject({ 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 a6d0ac86a810c..a7622f0549d84 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 @@ -28,6 +28,7 @@ import { BulkPatchRulesRequestBodyInput } from '@kbn/security-solution-plugin/co import { BulkUpdateRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.gen'; import { BulkUpsertAssetCriticalityRecordsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen'; import { CleanDraftTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route.gen'; +import { ConfigureRiskEngineSavedObjectRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen'; import { CopyTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/copy_timeline/copy_timeline_route.gen'; import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; @@ -313,6 +314,20 @@ If asset criticality records already exist for the specified entities, those rec .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Configuring the Risk Engine Saved Object + */ + configureRiskEngineSavedObject( + props: ConfigureRiskEngineSavedObjectProps, + kibanaSpace: string = 'default' + ) { + return supertest + .patch(routeWithNamespace('/api/risk_score/engine/saved_object/configure', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, /** * Copies and returns a timeline or timeline template. @@ -1612,6 +1627,9 @@ export interface BulkUpsertAssetCriticalityRecordsProps { export interface CleanDraftTimelinesProps { body: CleanDraftTimelinesRequestBodyInput; } +export interface ConfigureRiskEngineSavedObjectProps { + body: ConfigureRiskEngineSavedObjectRequestBodyInput; +} export interface CopyTimelineProps { body: CopyTimelineRequestBodyInput; } 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 2aa04a898a449..3aee9687843bf 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 @@ -21,5 +21,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./asset_criticality_csv_upload')); loadTestFile(require.resolve('./risk_score_entity_calculation')); loadTestFile(require.resolve('./risk_engine_schedule_now')); + loadTestFile(require.resolve('./risk_engine_so_config')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_so_config.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_so_config.ts new file mode 100644 index 0000000000000..8b780d0540dca --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_so_config.ts @@ -0,0 +1,143 @@ +/* + * 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 expect from '@kbn/expect'; +import { riskEngineConfigurationTypeName } from '@kbn/security-solution-plugin/server/lib/entity_analytics/risk_engine/saved_object'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { + riskEngineRouteHelpersFactory, + getRiskEngineConfigSO, + waitForRiskEngineRun, + waitForRiskEngineTaskToBeGone, +} from '../../utils'; + +export default ({ getService }: FtrProviderContext) => { + const spaceName = 'space1'; + const supertest = getService('supertest'); + const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest); + const riskEngineRoutesForNamespace = riskEngineRouteHelpersFactory(supertest, spaceName); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + describe('@ess @ serverless @serverless QA risk_engine_so_update_config', () => { + before(async () => { + const soId = await kibanaServer.savedObjects.find({ + type: riskEngineConfigurationTypeName, + space: spaceName, + }); + if (soId.saved_objects.length !== 0) { + await kibanaServer.savedObjects.delete({ + type: riskEngineConfigurationTypeName, + space: spaceName, + id: soId.saved_objects[0].id, + }); + } + const soId2 = await kibanaServer.savedObjects.find({ + type: riskEngineConfigurationTypeName, + }); + if (soId2.saved_objects.length !== 0) { + await kibanaServer.savedObjects.delete({ + type: riskEngineConfigurationTypeName, + id: soId2.saved_objects[0].id, + }); + } + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ecs_compliant'); + }); + + after(async () => { + const soId = await kibanaServer.savedObjects.find({ + type: riskEngineConfigurationTypeName, + space: spaceName, + }); + if (soId.saved_objects.length !== 0) { + await kibanaServer.savedObjects.delete({ + type: riskEngineConfigurationTypeName, + space: spaceName, + id: soId.saved_objects[0].id, + }); + } + const soId2 = await kibanaServer.savedObjects.find({ + type: riskEngineConfigurationTypeName, + }); + if (soId2.saved_objects.length !== 0) { + await kibanaServer.savedObjects.delete({ + type: riskEngineConfigurationTypeName, + id: soId2.saved_objects[0].id, + }); + } + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/ecs_compliant'); + }); + + it('should include the right keys as per the update', async () => { + await riskEngineRoutes.init(); + await waitForRiskEngineRun; + + const currentSoConfig = await getRiskEngineConfigSO({ kibanaServer }); + + expect(currentSoConfig.attributes).to.not.have.property('excludeAlertTags'); + expect(currentSoConfig.attributes).to.not.have.property('excludeAlertStatuses'); + + const updatedSoBody = { + exclude_alert_tags: ['False Positive'], + exclude_alert_statuses: ['open'], + }; + + await riskEngineRoutes.soConfig(updatedSoBody, 200); + const currentSoConfig2 = await getRiskEngineConfigSO({ kibanaServer }); + + expect(currentSoConfig2.attributes).to.have.property('excludeAlertTags'); + expect(currentSoConfig2.attributes).to.have.property('excludeAlertStatuses'); + + await riskEngineRoutes.disable(); + await waitForRiskEngineTaskToBeGone; + + updatedSoBody.exclude_alert_statuses = []; + + await riskEngineRoutes.soConfig(updatedSoBody, 200); + + await riskEngineRoutes.enable(); + await waitForRiskEngineRun; + + const currentSoConfig3 = await getRiskEngineConfigSO({ kibanaServer }); + expect(JSON.stringify(currentSoConfig3.attributes.excludeAlertStatuses)).to.equal( + JSON.stringify(updatedSoBody.exclude_alert_statuses) + ); + }); + + it('should succeed while updating the saved object', async () => { + await riskEngineRoutes.init(); + await waitForRiskEngineRun; + + const updatedSoBody = { + exclude_alert_tags: ['False Positive'], + exclude_alert_statuses: ['open'], + }; + const response = await riskEngineRoutes.soConfig(updatedSoBody); + expect(response.status).to.equal(200); + }); + + it('should update the config in the right space', async () => { + await riskEngineRoutesForNamespace.init(); + await riskEngineRoutes.init(); + await waitForRiskEngineRun; + + const updatedSoBody = { + exclude_alert_tags: ['False Positive'], + exclude_alert_statuses: ['open', 'closed'], + }; + + await riskEngineRoutesForNamespace.soConfig(updatedSoBody, 200); + const currentSoConfig = await getRiskEngineConfigSO({ kibanaServer, space: 'space1' }); + + expect(currentSoConfig.namespaces).to.eql(['space1']); + expect(currentSoConfig.attributes.excludeAlertTags).to.eql(updatedSoBody.exclude_alert_tags); + expect(currentSoConfig.attributes.excludeAlertStatuses).to.eql( + updatedSoBody.exclude_alert_statuses + ); + }); + }); +}; 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 0a88e9fbe2518..b90ef13a735f7 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 @@ -24,6 +24,7 @@ import { RISK_ENGINE_PRIVILEGES_URL, RISK_ENGINE_CLEANUP_URL, RISK_ENGINE_SCHEDULE_NOW_URL, + RISK_ENGINE_CONFIGURE_SO_URL, } from '@kbn/security-solution-plugin/common/constants'; import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { removeLegacyTransforms } from '@kbn/security-solution-plugin/server/lib/entity_analytics/utils/transforms'; @@ -365,9 +366,16 @@ export const waitForRiskScoresToBeGone = async ({ ); }; -export const getRiskEngineConfigSO = async ({ kibanaServer }: { kibanaServer: KbnClient }) => { +export const getRiskEngineConfigSO = async ({ + kibanaServer, + space, +}: { + kibanaServer: KbnClient; + space?: string; +}) => { const soResponse = await kibanaServer.savedObjects.find({ type: riskEngineConfigurationTypeName, + space, }); return soResponse?.saved_objects?.[0]; @@ -580,6 +588,17 @@ export const riskEngineRouteHelpersFactory = (supertest: SuperTest.Agent, namesp assertStatusCode(expectStatusCode, response); return response; }, + + soConfig: async (configParams: {}, expectStatusCode: number = 200) => { + const response = await supertest + .put(routeWithNamespace(RISK_ENGINE_CONFIGURE_SO_URL, namespace)) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(configParams); + assertStatusCode(expectStatusCode, response); + return response; + }, }; };