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..379b0656f1f28 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 './so_configure_route.gen'; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/so_configure_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/so_configure_route.gen.ts new file mode 100644 index 0000000000000..0c95db5537714 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/so_configure_route.gen.ts @@ -0,0 +1,58 @@ +/* + * 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 Engine API + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +export type ConfigureRiskEngineRequest = z.infer; +export const ConfigureRiskEngineRequest = z.object({ + dataViewId: z.string().optional(), + enabled: z.boolean().optional(), + filter: z.object({}).strict().optional(), + identifierType: z.string().optional(), + interval: z.string().optional(), + pageSize: z.number().int().optional(), + alertSampleSizePerShard: z.number().int().optional(), + range: z + .object({ + start: z.string().optional(), + end: z.string().optional(), + }) + .optional(), + excludeAlertStatuses: z + .array(z.enum(['open', 'closed', 'in-progress', 'acknowledged'])) + .optional(), + excludeAlertTags: z + .array(z.enum(['Duplicate', 'False Positive', 'Futher investigation required'])) + .optional(), +}); + +export type ConfigureRiskEngineResponse = z.infer; +export const ConfigureRiskEngineResponse = z.object({ + configuration_successful: z.boolean().optional(), +}); + +export type ConfigureRiskEngineSavedObjectRequestBody = z.infer< + typeof ConfigureRiskEngineSavedObjectRequestBody +>; +export const ConfigureRiskEngineSavedObjectRequestBody = ConfigureRiskEngineRequest; +export type ConfigureRiskEngineSavedObjectRequestBodyInput = z.input< + typeof ConfigureRiskEngineSavedObjectRequestBody +>; + +export type ConfigureRiskEngineSavedObjectResponse = z.infer< + typeof ConfigureRiskEngineSavedObjectResponse +>; +export const ConfigureRiskEngineSavedObjectResponse = ConfigureRiskEngineResponse; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/so_configure_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/so_configure_route.schema.yaml new file mode 100644 index 0000000000000..09360d0f01522 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/so_configure_route.schema.yaml @@ -0,0 +1,83 @@ +openapi: 3.0.0 +info: + version: '2023-10-31' + title: Risk Engine API + description: These APIs allow the consumer to configure the Risk Engine saved object. +paths: + /api/risk_engine/saved_object/configure: + post: + x-labels: [ess, serverless] + x-internal: false + x-codegen-enabled: true + operationId: ConfigureRiskEngineSavedObject + summary: Configure the Risk Engine as per user requirements + requestBody: + description: User defined configuration the risk engine + content: + application/json: + schema: + $ref: '#/components/schemas/ConfigureRiskEngineRequest' + required: true + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ConfigureRiskEngineResponse' + '400': + description: Invalid request + +components: + schemas: + ConfigureRiskEngineRequest: + type: object + properties: + dataViewId: + type: string + enabled: + type: boolean + filter: + type: object + additionalProperties: false + properties: {} + identifierType: + type: string + interval: + type: string + pageSize: + type: integer + alertSampleSizePerShard: + type: integer + range: + type: object + properties: + start: + type: string + end: + type: string + excludeAlertStatuses: + type: array + items: + type: string + enum: + - open + - closed + - in-progress + - acknowledged + excludeAlertTags: + type: array + items: + type: string + enum: + - 'Duplicate' + - 'False Positive' + - 'Futher investigation required' + + ConfigureRiskEngineResponse: + type: object + properties: + configuration_successful: + type: boolean + + 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 288a08fdb8afb..ffa2be2b9ee3a 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 @@ -292,6 +292,10 @@ import type { PreviewRiskScoreRequestBodyInput, PreviewRiskScoreResponse, } from './entity_analytics/risk_engine/preview_route.gen'; +import type { + ConfigureRiskEngineSavedObjectRequestBodyInput, + ConfigureRiskEngineSavedObjectResponse, +} from './entity_analytics/risk_engine/so_configure_route.gen'; import type { CleanDraftTimelinesRequestBodyInput, CleanDraftTimelinesResponse, @@ -560,6 +564,19 @@ If asset criticality records already exist for the specified entities, those rec }) .catch(catchAxiosErrorFormatAndThrow); } + async configureRiskEngineSavedObject(props: ConfigureRiskEngineSavedObjectProps) { + this.log.info(`${new Date().toISOString()} Calling API ConfigureRiskEngineSavedObject`); + return this.kbnClient + .request({ + path: '/api/risk_engine/saved_object/configure', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'POST', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Copies and returns a timeline or timeline template. @@ -2014,6 +2031,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..bc02ff3459663 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_SO_CONFIGURATION_URL = + `${PUBLIC_RISK_ENGINE_URL}/saved_object/config` 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/server/lib/entity_analytics/risk_engine/audit.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/audit.ts index 9ade355d54bf3..c317cfe8442f2 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_CONFIGURATION_UPDATE = 'risk_engine_configuration_update', } diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.test.ts index 0f807ebe22265..8891003ac25e3 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.test.ts @@ -402,5 +402,56 @@ describe('RiskEngineDataClient', () => { expect(errors).toEqual([error]); }); }); + + describe('updateSavedObjectConfiguration', () => { + it('should update the risk engine saved object configuration in the respective namespace', async () => { + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + const namespaces = { + default: 'default', + custom: 'space_2', + }; + const options = { + logger, + kibanaVersion: '8.9.0', + esClient, + soClient: mockSavedObjectClient, + namespace: namespaces.default, + auditLogger: undefined, + }; + riskEngineDataClient = new RiskEngineDataClient(options); + const attributes = { + enabled: true, + excludeAlertStatuses: ['closed'], + excludeAlertTags: ['Duplicate'], + }; + mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration()); + + mockSavedObjectClient.update.mockResolvedValueOnce({ + attributes, + } as unknown as SavedObject); + + const result = await riskEngineDataClient.updateSavedObjectConfiguration({ attributes }); + expect(result.attributes).toEqual(attributes); + + // Check for the saved object configuration in the non-default space + + options.namespace = namespaces.custom; + const riskEngineDataClient2 = new RiskEngineDataClient(options); + const attributes2 = { + enabled: true, + excludeAlertStatuses: ['open', 'closed'], + excludeAlertTags: ['False Positive', 'Duplicate'], + }; + mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration()); + mockSavedObjectClient.update.mockResolvedValueOnce({ + attributes, + } as unknown as SavedObject); + + const result2 = await riskEngineDataClient2.updateSavedObjectConfiguration({ + attributes: attributes2, + }); + expect(result2.attributes).toEqual(attributes2); + }); + }); }); }); 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..436ed7feeacb2 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,21 @@ export class RiskEngineDataClient { return RiskEngineStatusEnum.ENABLED; } + + public async updateSavedObjectConfiguration({ attributes }: { attributes: {} }) { + this.options.auditLogger?.log({ + message: 'User updates the Risk Engine savedObject', + event: { + action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURATION_UPDATE, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.CHANGE, + outcome: AUDIT_OUTCOME.SUCCESS, + }, + }); + + return updateSavedObjectAttribute({ + savedObjectsClient: this.options.soClient, + attributes, + }); + } } 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..3d131be32c67f 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 { riskEngineSOConfigurationRoute } from '../saved_object/routes/configure'; export const registerRiskEngineRoutes = ({ router, @@ -26,4 +27,5 @@ export const registerRiskEngineRoutes = ({ riskEngineSettingsRoute(router); riskEnginePrivilegesRoute(router, getStartServices); riskEngineCleanupRoute(router, getStartServices); + riskEngineSOConfigurationRoute(router); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/constants.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/constants.ts new file mode 100644 index 0000000000000..86512de437e06 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/constants.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * Public Risk Engine Saved Object Configuration routes + */ + +export const APP_ID = 'securitySolution' as const; +export const PUBLIC_RISK_ENGINE_SO_URL = '/api/risk_score/engine/saved_object' as const; +export const RISK_ENGINE_SAVED_OBJECT_CONFIG_URL = `${PUBLIC_RISK_ENGINE_SO_URL}/config` as const; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/index.ts index da4681008403e..44a17b803ad5f 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/index.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/index.ts @@ -6,3 +6,4 @@ */ export * from './risk_engine_configuration_type'; +export * from './routes/configure'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/routes/configure.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/routes/configure.ts new file mode 100644 index 0000000000000..268c4d3a9220b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/saved_object/routes/configure.ts @@ -0,0 +1,65 @@ +/* + * 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 { ConfigureRiskEngineResponse } from '../../../../../../common/api/entity_analytics/risk_engine'; +import { ConfigureRiskEngineSavedObjectRequestBody } from '../../../../../../common/api/entity_analytics/risk_engine'; +import { RISK_ENGINE_SAVED_OBJECT_CONFIG_URL, APP_ID } from '../constants'; +import type { EntityAnalyticsRoutesDeps } from '../../../types'; +import { API_VERSIONS } from '../../../../../../common/constants'; + +export const riskEngineSOConfigurationRoute = (router: EntityAnalyticsRoutesDeps['router']) => { + router.versioned + .post({ + access: 'public', + path: RISK_ENGINE_SAVED_OBJECT_CONFIG_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + body: buildRouteValidationWithZod(ConfigureRiskEngineSavedObjectRequestBody), + }, + }, + }, + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + const attributes = request.body; + + const securitySolution = await context.securitySolution; + const riskEngineClient = securitySolution.getRiskEngineDataClient(); + + try { + const result = await riskEngineClient.updateSavedObjectConfiguration({ attributes }); + if (!result) { + throw new Error('Unable to update risk engine configuration'); + } + return response.ok({ + body: { + configuration_successful: 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/utils/saved_object_configuration.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts index 4282e0a793f47..ad107888934db 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,19 @@ export const updateSavedObjectAttribute = async ({ attributes, }: SavedObjectsClientArg & { attributes: { - enabled: boolean; + enabled?: boolean; + dataViewId?: string; + filter?: object; + identifierType?: string; + interval?: string; + pageSize?: number; + alertSampleSizePerShard?: number; + range?: { + start?: string; + end?: string; + }; + excludeAlertStatuses?: Array<'open' | 'closed' | 'in-progress' | 'acknowledged'>; + excludeAlertTags?: Array<'Duplicate' | 'False Positive' | 'Futher investigation required'>; }; }) => { const savedObjectConfiguration = await getConfigurationSavedObject({ diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts index af683db517716..6a43ed7349501 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts @@ -72,6 +72,8 @@ export interface RiskEngineConfiguration { pageSize: number; range: Range; alertSampleSizePerShard?: number; + excludeAlertStatuses?: string[]; + excludeAlertTags?: string[]; } export interface CalculateScoresParams { 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 c110ce8676edb..db41fb3c143be 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/so_configure_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'; @@ -273,6 +274,17 @@ 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'); }, + configureRiskEngineSavedObject( + props: ConfigureRiskEngineSavedObjectProps, + kibanaSpace: string = 'default' + ) { + return supertest + .post(routeWithNamespace('/api/risk_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. @@ -1363,6 +1375,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..71c7da24b794b --- /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 = { + excludeAlertTags: ['False Positive'], + excludeAlertStatuses: ['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.excludeAlertStatuses = []; + + 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.excludeAlertStatuses) + ); + }); + + it('should fail if the values of the keys are not correct', async () => { + await riskEngineRoutes.init(); + await waitForRiskEngineRun; + + const updatedSoBody = { + excludeAlertTags: ['AnyTag'], + excludeAlertStatuses: ['AnyStatus'], + }; + const response = await riskEngineRoutes.soConfig(updatedSoBody, 400); + expect(response.status).to.equal(400); + }); + + it('should update the config in the right space', async () => { + await riskEngineRoutesForNamespace.init(); + await riskEngineRoutes.init(); + await waitForRiskEngineRun; + + const updatedSoBody = { + excludeAlertTags: ['False Positive'], + excludeAlertStatuses: ['open', 'closed'], + }; + + await riskEngineRoutesForNamespace.soConfig(updatedSoBody, 200); + const currentSoConfig = await getRiskEngineConfigSO({ kibanaServer, space: 'space1' }); + const SoConfig = await getRiskEngineConfigSO({ kibanaServer }); + + expect(JSON.stringify(currentSoConfig.attributes.excludeAlertStatuses)).to.equal( + JSON.stringify(updatedSoBody.excludeAlertStatuses) + ); + expect(JSON.stringify(SoConfig.attributes.excludeAlertStatuses)).to.equal(JSON.stringify([])); + }); + }); +}; 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 23a6c11b0b5cc..1d2162184aa1d 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_SO_CONFIGURATION_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'; @@ -331,9 +332,16 @@ export const waitForRiskEngineTaskToBeGone = 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]; @@ -546,6 +554,16 @@ export const riskEngineRouteHelpersFactory = (supertest: SuperTest.Agent, namesp assertStatusCode(expectStatusCode, response); return response; }, + + soConfig: async (requestBody: {}, expectStatusCode: number) => { + const response = await supertest + .post(routeWithNamespace(RISK_ENGINE_SO_CONFIGURATION_URL, namespace)) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send(requestBody); + assertStatusCode(expectStatusCode, response); + return response; + }, }; };