diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 6ec1b18657c5b..2a942bc85c3bc 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -33545,6 +33545,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. @@ -46987,6 +47039,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 64111b953ca7b..9945bce1322a3 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -36270,6 +36270,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. @@ -54672,6 +54723,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/entity_analytics/risk_engine/preview_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts index c58d2c02d562f..1b410a143683a 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts @@ -58,9 +58,11 @@ export const RiskScoresPreviewRequest = z.object({ /** * A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included. */ - excludeAlertStatuses: z - .array(z.enum(['open', 'closed', 'in-progress', 'acknowledged'])) - .optional(), + exclude_alert_statuses: z.array(z.string()).optional(), + /** + * A list of alert tags to exclude from the risk score calculation. If unspecified, all alert tags are included. + */ + exclude_alert_tags: z.array(z.string()).optional(), }); export type RiskScoresPreviewResponse = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml index a634a1a75975c..45d40a057f22c 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml @@ -58,16 +58,16 @@ components: description: Defines the time period over which scores will be evaluated. If unspecified, a range of `[now, now-30d]` will be used. weights: $ref: '../common/common.schema.yaml#/components/schemas/RiskScoreWeights' - excludeAlertStatuses: + exclude_alert_statuses: description: A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included. type: array items: type: string - enum: - - open - - closed - - in-progress - - acknowledged + exclude_alert_tags: + description: A list of alert tags to exclude from the risk score calculation. If unspecified, all alert tags are included. + type: array + items: + type: string RiskScoresPreviewResponse: 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 3487fdf81c0c9..fa315e3c421aa 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'; @@ -602,6 +606,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. @@ -2295,6 +2315,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 9a0c456e5efe3..1db469620f680 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 356a63567c401..184eb912ad7d1 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/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index fa33d8fa575be..f30b841277267 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 @@ -45,6 +45,7 @@ import { API_VERSIONS, RISK_ENGINE_CLEANUP_URL, RISK_ENGINE_SCHEDULE_NOW_URL, + RISK_ENGINE_CONFIGURE_SO_URL, } from '../../../common/constants'; import type { SnakeToCamelCase } from '../common/utils'; import { useKibana } from '../../common/lib/kibana/kibana_react'; @@ -298,6 +299,14 @@ export const useEntityAnalyticsRoutes = () => { method: 'DELETE', }); + const updateSavedObjectConfiguration = (params: {}) => { + http.fetch(RISK_ENGINE_CONFIGURE_SO_URL, { + version: API_VERSIONS.public.v1, + method: 'PUT', + body: JSON.stringify(params), + }); + }; + return { fetchRiskScorePreview, fetchRiskEngineStatus, @@ -317,6 +326,7 @@ export const useEntityAnalyticsRoutes = () => { calculateEntityRiskScore, cleanUpRiskEngine, fetchEntitiesList, + updateSavedObjectConfiguration, }; }, [http]); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_configure_risk_engine_saved_object.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_configure_risk_engine_saved_object.ts new file mode 100644 index 0000000000000..a45cc84075972 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_configure_risk_engine_saved_object.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { TaskManagerUnavailableResponse } from '../../../../common/api/entity_analytics/common'; +import { useEntityAnalyticsRoutes } from '../api'; +import type { ConfigureRiskEngineSavedObjectResponse } from '../../../../common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen'; + +interface ConfigureRiskEngineParams { + includeClosedAlerts: boolean; + range: { start: string; end: string }; +} + +export const useConfigureSORiskEngineMutation = ( + options?: UseMutationOptions< + ConfigureRiskEngineSavedObjectResponse, + { body: ConfigureRiskEngineSavedObjectResponse | TaskManagerUnavailableResponse }, + ConfigureRiskEngineParams + > +) => { + const { updateSavedObjectConfiguration } = useEntityAnalyticsRoutes(); + + return useMutation< + ConfigureRiskEngineSavedObjectResponse, + { body: ConfigureRiskEngineSavedObjectResponse | TaskManagerUnavailableResponse }, + ConfigureRiskEngineParams + >(async (params: ConfigureRiskEngineParams) => { + await updateSavedObjectConfiguration({ + exclude_alert_statuses: params.includeClosedAlerts ? [] : ['closed'], + range: params.range, + }); + return { risk_engine_saved_object_configured: true }; + }, options); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_preview_risk_scores.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_preview_risk_scores.ts index 96a4453815125..bc488786cff65 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_preview_risk_scores.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_preview_risk_scores.ts @@ -17,11 +17,12 @@ export const useRiskScorePreview = ({ data_view_id: dataViewId, range, filter, + exclude_alert_statuses: excludeAlertStatuses, }: UseRiskScorePreviewParams) => { const { fetchRiskScorePreview } = useEntityAnalyticsRoutes(); return useQuery( - ['POST', 'FETCH_PREVIEW_RISK_SCORE', range, filter], + ['POST', 'FETCH_PREVIEW_RISK_SCORE', range, filter, excludeAlertStatuses], async ({ signal }) => { if (!dataViewId) { return; @@ -49,6 +50,10 @@ export const useRiskScorePreview = ({ params.filter = filter; } + if (excludeAlertStatuses) { + params.exclude_alert_statuses = excludeAlertStatuses; + } + const response = await fetchRiskScorePreview({ signal, params }); return response; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/__snapshots__/risk_score_configuration_section.test.tsx.snap b/x-pack/plugins/security_solution/public/entity_analytics/components/__snapshots__/risk_score_configuration_section.test.tsx.snap new file mode 100644 index 0000000000000..9115b2f118516 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/__snapshots__/risk_score_configuration_section.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RiskScoreConfigurationSection renders correctly 1`] = ` + + +
+ +
+ +
+ +
+
+ + +

+ Enable this option to factor both open and closed alerts into the risk engine + calculations. Including closed alerts helps provide a more comprehensive risk assessment + based on past incidents, leading to more accurate scoring and insights. +

+
+
+`; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.test.tsx new file mode 100644 index 0000000000000..93a7e0776d925 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.test.tsx @@ -0,0 +1,120 @@ +/* + * 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 React from 'react'; +import { RiskScoreConfigurationSection } from './risk_score_configuration_section'; +import { shallow, mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiSuperDatePicker, EuiSwitch } from '@elastic/eui'; +import * as i18n from '../translations'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { useConfigureSORiskEngineMutation } from '../api/hooks/use_configure_risk_engine_saved_object'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/hooks/use_app_toasts'); +jest.mock('../api/hooks/use_configure_risk_engine_saved_object'); + +describe('RiskScoreConfigurationSection', () => { + const mockConfigureSO = useConfigureSORiskEngineMutation as jest.Mock; + const defaultProps = { + includeClosedAlerts: false, + setIncludeClosedAlerts: jest.fn(), + from: 'now-30m', + to: 'now', + onDateChange: jest.fn(), + }; + + const mockAddSuccess = jest.fn(); + const mockMutate = jest.fn(); + + beforeEach(() => { + (useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess }); + mockConfigureSO.mockReturnValue({ mutate: mockMutate }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('toggles includeClosedAlerts', () => { + const wrapper = mount( + + ); + wrapper.find(EuiSwitch).simulate('click'); + expect(defaultProps.setIncludeClosedAlerts).toHaveBeenCalledWith(true); + }); + + it('calls onDateChange on date change', () => { + const wrapper = mount(); + wrapper.find(EuiSuperDatePicker).props().onTimeChange({ start: 'now-30m', end: 'now' }); + expect(defaultProps.onDateChange).toHaveBeenCalledWith({ start: 'now-30m', end: 'now' }); + }); + + it('shows bottom bar when changes are made', async () => { + const wrapper = mount( + + ); + wrapper.find(EuiSwitch).simulate('click'); + wrapper.find(EuiSuperDatePicker).props().onTimeChange({ start: 'now-14m', end: 'now' }); + wrapper.update(); + await new Promise((resolve) => setTimeout(resolve, 0)); // wait for the component to update + expect(wrapper.find('EuiBottomBar').exists()).toBe(true); + }); + + it('saves changes', () => { + const wrapper = mount( + + ); + + // Simulate clicking the switch + const closedAlertsToggle = wrapper.find('button[data-test-subj="includeClosedAlertsSwitch"]'); + expect(closedAlertsToggle.exists()).toBe(true); + closedAlertsToggle.simulate('click'); + + wrapper.update(); + + const saveChangesButton = wrapper.find('button[data-test-subj="riskScoreSaveButton"]'); + expect(saveChangesButton.exists()).toBe(true); + saveChangesButton.simulate('click'); + const callArgs = mockMutate.mock.calls[0][0]; + expect(callArgs).toEqual({ + includeClosedAlerts: true, + range: { start: 'now-30m', end: 'now' }, + }); + }); + + it('shows success toast on save', () => { + const wrapper = mount( + + ); + + act(() => { + wrapper.find('button[data-test-subj="includeClosedAlertsSwitch"]').simulate('click'); + }); + wrapper.update(); + + act(() => { + wrapper.find('button[data-test-subj="riskScoreSaveButton"]').simulate('click'); + }); + + act(() => { + mockMutate.mock.calls[0][1].onSuccess(); + }); + + expect(mockAddSuccess).toHaveBeenCalledWith( + i18n.RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS, + { + toastLifeTimeMs: 5000, + } + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.tsx new file mode 100644 index 0000000000000..fa0f33e5b6040 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_configuration_section.tsx @@ -0,0 +1,199 @@ +/* + * 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 React, { useState, useEffect, useRef } from 'react'; +import { + EuiSuperDatePicker, + EuiButton, + EuiText, + EuiFlexGroup, + EuiSwitch, + EuiFlexItem, + EuiBottomBar, + EuiButtonEmpty, + EuiSpacer, + useEuiTheme, +} from '@elastic/eui'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import * as i18n from '../translations'; +import { useConfigureSORiskEngineMutation } from '../api/hooks/use_configure_risk_engine_saved_object'; +import { getEntityAnalyticsRiskScorePageStyles } from './risk_score_page_styles'; + +export const RiskScoreConfigurationSection = ({ + includeClosedAlerts, + setIncludeClosedAlerts, + from, + to, + onDateChange, +}: { + includeClosedAlerts: boolean; + setIncludeClosedAlerts: (value: boolean) => void; + from: string; + to: string; + onDateChange: ({ start, end }: { start: string; end: string }) => void; +}) => { + const { euiTheme } = useEuiTheme(); + const styles = getEntityAnalyticsRiskScorePageStyles(euiTheme); + const [start, setFrom] = useState(from); + const [end, setTo] = useState(to); + const [isLoading, setIsLoading] = useState(false); + const [showBar, setShowBar] = useState(false); + const { addSuccess } = useAppToasts(); + const initialIncludeClosedAlerts = useRef(includeClosedAlerts); + const initialStart = useRef(from); + const initialEnd = useRef(to); + + const [savedIncludeClosedAlerts, setSavedIncludeClosedAlerts] = useLocalStorage( + 'includeClosedAlerts', + includeClosedAlerts ?? false + ); + const [savedStart, setSavedStart] = useLocalStorage( + 'entityAnalytics:riskScoreConfiguration:fromDate', + from + ); + const [savedEnd, setSavedEnd] = useLocalStorage( + 'entityAnalytics:riskScoreConfiguration:toDate', + to + ); + + useEffect(() => { + if (savedIncludeClosedAlerts !== null && savedIncludeClosedAlerts !== undefined) { + initialIncludeClosedAlerts.current = savedIncludeClosedAlerts; + setIncludeClosedAlerts(savedIncludeClosedAlerts); + } + if (savedStart && savedEnd) { + initialStart.current = savedStart; + initialEnd.current = savedEnd; + setFrom(savedStart); + setTo(savedEnd); + } + }, [savedIncludeClosedAlerts, savedStart, savedEnd, setIncludeClosedAlerts]); + + const onRefresh = ({ start: newStart, end: newEnd }: { start: string; end: string }) => { + setFrom(newStart); + setTo(newEnd); + onDateChange({ start: newStart, end: newEnd }); + checkForChanges(newStart, newEnd, includeClosedAlerts); + }; + + const handleToggle = () => { + const newValue = !includeClosedAlerts; + setIncludeClosedAlerts(newValue); + checkForChanges(start, end, newValue); + }; + + const checkForChanges = (newStart: string, newEnd: string, newIncludeClosedAlerts: boolean) => { + if ( + newStart !== initialStart.current || + newEnd !== initialEnd.current || + newIncludeClosedAlerts !== initialIncludeClosedAlerts.current + ) { + setShowBar(true); + } else { + setShowBar(false); + } + }; + + const { mutate } = useConfigureSORiskEngineMutation(); + + const handleSave = () => { + setIsLoading(true); + mutate( + { + includeClosedAlerts, + range: { start, end }, + }, + { + onSuccess: () => { + setShowBar(false); + addSuccess(i18n.RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS, { + toastLifeTimeMs: 5000, + }); + setIsLoading(false); + + initialStart.current = start; + initialEnd.current = end; + initialIncludeClosedAlerts.current = includeClosedAlerts; + + setSavedIncludeClosedAlerts(includeClosedAlerts); + setSavedStart(start); + setSavedEnd(end); + }, + onError: () => { + setIsLoading(false); + }, + } + ); + }; + + return ( + <> + +
+ +
+ +
+ +
+
+ + +

{i18n.RISK_ENGINE_INCLUDE_CLOSED_ALERTS_DESCRIPTION}

+
+ {showBar && ( + + + + + { + setShowBar(false); + setFrom(initialStart.current); + setTo(initialEnd.current); + setIncludeClosedAlerts(initialIncludeClosedAlerts.current); + }} + > + {i18n.DISCARD_CHANGES} + + + + + {i18n.SAVE_CHANGES} + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx index 63ff39ebca7dc..547ee0235e773 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx @@ -10,11 +10,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiHealth, - EuiHorizontalRule, - EuiLink, EuiSpacer, EuiSwitch, - EuiTitle, EuiLoadingSpinner, EuiBadge, EuiButtonEmpty, @@ -28,8 +25,6 @@ import { EuiCallOut, EuiAccordion, } from '@elastic/eui'; -import { LinkAnchor } from '@kbn/security-solution-navigation/links'; -import { SecurityPageName } from '@kbn/security-solution-navigation'; import type { RiskEngineStatus } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; import { RiskEngineStatusEnum } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; import * as i18n from '../translations'; @@ -38,8 +33,6 @@ import { useInitRiskEngineMutation } from '../api/hooks/use_init_risk_engine_mut import { useEnableRiskEngineMutation } from '../api/hooks/use_enable_risk_engine_mutation'; import { useDisableRiskEngineMutation } from '../api/hooks/use_disable_risk_engine_mutation'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; -import { RiskInformationFlyout } from './risk_information'; -import { useOnOpenCloseHandler } from '../../helper_hooks'; import type { RiskEngineMissingPrivilegesResponse } from '../hooks/use_missing_risk_engine_privileges'; const MIN_WIDTH_TO_PREVENT_LABEL_FROM_MOVING = '50px'; @@ -144,12 +137,12 @@ const RiskEngineHealth: React.FC<{ currentRiskEngineStatus?: RiskEngineStatus | currentRiskEngineStatus, }) => { if (!currentRiskEngineStatus) { - return {'-'}; + return {'-'}; } if (currentRiskEngineStatus === RiskEngineStatusEnum.ENABLED) { return {i18n.RISK_SCORE_MODULE_STATUS_ON}; } - return {i18n.RISK_SCORE_MODULE_STATUS_OFF}; + return {i18n.RISK_SCORE_MODULE_STATUS_OFF}; }; const RiskEngineStatusRow: React.FC<{ @@ -181,7 +174,6 @@ const RiskEngineStatusRow: React.FC<{ data-test-subj="risk-score-switch" checked={currentRiskEngineStatus === RiskEngineStatusEnum.ENABLED} onChange={onSwitchClick} - compressed disabled={btnIsDisabled} aria-describedby={'switchRiskModule'} /> @@ -221,8 +213,6 @@ export const RiskScoreEnableSection: React.FC<{ const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); - const [isFlyoutVisible, handleOnOpen, handleOnClose] = useOnOpenCloseHandler(); - const isLoading = initRiskEngineMutation.isLoading || enableRiskEngineMutation.isLoading || @@ -254,9 +244,6 @@ export const RiskScoreEnableSection: React.FC<{ return ( <> <> - -

{i18n.RISK_SCORE_MODULE_STATUS}

-
{initRiskEngineMutation.isError && } {disableRiskEngineMutation.isError && ( @@ -273,12 +260,10 @@ export const RiskScoreEnableSection: React.FC<{ isLoading={initRiskEngineMutation.isLoading} closeModal={closeModal} /> - - {i18n.ENTITY_RISK_SCORING} {isUpdateAvailable && {i18n.UPDATE_AVAILABLE}} @@ -310,29 +295,9 @@ export const RiskScoreEnableSection: React.FC<{ )} - - <> - -

{i18n.USEFUL_LINKS}

-
- -
    -
  • - {i18n.EA_DASHBOARD_LINK} - -
  • -
  • - - {i18n.EA_DOCS_ENTITY_RISK_SCORE} - - {isFlyoutVisible && } - -
  • -
- ); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_page_styles.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_page_styles.tsx new file mode 100644 index 0000000000000..ea3bae7e87187 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_page_styles.tsx @@ -0,0 +1,24 @@ +/* + * 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 styled from '@emotion/styled'; +import type { EuiThemeComputed } from '@elastic/eui'; +interface EntityAnalyticsRiskScorePageStyles { + VerticalSeparator: ReturnType; +} + +export const getEntityAnalyticsRiskScorePageStyles = ( + euiTheme: EuiThemeComputed +): EntityAnalyticsRiskScorePageStyles => ({ + VerticalSeparator: styled.div` + :before { + content: ''; + height: ${euiTheme.size.l}; + border-left: ${euiTheme.border.width.thin} solid ${euiTheme.colors.lightShade}; + } + `, +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx index 9693bf13589ad..f818eaf429cd2 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import React, { useState, useCallback, useMemo, useEffect } from 'react'; -import type { DataView } from '@kbn/data-views-plugin/public'; +import React, { useState, useMemo } from 'react'; import { EuiAccordion, - EuiFormRow, EuiPanel, EuiSpacer, EuiTitle, @@ -22,8 +20,7 @@ import { EuiFlexItem, EuiCode, } from '@elastic/eui'; -import type { BoolQuery, TimeRange, Query } from '@kbn/es-query'; -import { buildEsQuery } from '@kbn/es-query'; +import type { BoolQuery } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import type { EntityRiskScoreRecord } from '../../../common/api/entity_analytics/common'; import { @@ -33,10 +30,8 @@ import { import { RiskScorePreviewTable } from './risk_score_preview_table'; import * as i18n from '../translations'; import { useRiskScorePreview } from '../api/hooks/use_preview_risk_scores'; -import { useKibana } from '../../common/lib/kibana'; import { SourcererScopeName } from '../../sourcerer/store/model'; import { useSourcererDataView } from '../../sourcerer/containers'; -import { useAppToasts } from '../../common/hooks/use_app_toasts'; import type { RiskEngineMissingPrivilegesResponse } from '../hooks/use_missing_risk_engine_privileges'; import { userHasRiskEngineReadPermissions } from '../common'; interface IRiskScorePreviewPanel { @@ -55,7 +50,10 @@ const getRiskiestScores = (scores: EntityRiskScoreRecord[] = [], field: string) export const RiskScorePreviewSection: React.FC<{ privileges: RiskEngineMissingPrivilegesResponse; -}> = ({ privileges }) => { + includeClosedAlerts: boolean; + from: string; + to: string; +}> = ({ privileges, includeClosedAlerts, from, to }) => { const sectionBody = useMemo(() => { if (privileges.isLoading) { return ( @@ -67,11 +65,11 @@ export const RiskScorePreviewSection: React.FC<{ ); } if (userHasRiskEngineReadPermissions(privileges)) { - return ; + return ; } return ; - }, [privileges]); + }, [privileges, includeClosedAlerts, from, to]); return ( <> @@ -138,65 +136,30 @@ const RiskScorePreviewPanel = ({ ); }; -const RiskEnginePreview = () => { - const [dateRange, setDateRange] = useState<{ from: string; to: string }>({ - from: 'now-24h', - to: 'now', - }); - - const [filters, setFilters] = useState<{ bool: BoolQuery }>({ +const RiskEnginePreview: React.FC<{ includeClosedAlerts: boolean; from: string; to: string }> = ({ + includeClosedAlerts, + from, + to, +}) => { + const [filters] = useState<{ bool: BoolQuery }>({ bool: { must: [], filter: [], should: [], must_not: [] }, }); - const [dataViewsArray, setDataViewsArray] = useState([]); - - const { - unifiedSearch: { - ui: { SearchBar }, - }, - dataViews, - } = useKibana().services; - - const { addError } = useAppToasts(); - const { sourcererDataView } = useSourcererDataView(SourcererScopeName.detections); const { data, isLoading, refetch, isError } = useRiskScorePreview({ data_view_id: sourcererDataView.title, filter: filters, range: { - start: dateRange.from, - end: dateRange.to, + start: from, + end: to, }, + exclude_alert_statuses: includeClosedAlerts ? [] : ['closed'], }); const hosts = getRiskiestScores(data?.scores.host, 'host.name'); const users = getRiskiestScores(data?.scores.user, 'user.name'); - const onQuerySubmit = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - setDateRange({ - from: payload.dateRange.from, - to: payload.dateRange.to, - }); - try { - const newFilters = buildEsQuery( - undefined, - payload.query ?? { query: '', language: 'kuery' }, - [] - ); - setFilters(newFilters); - } catch (e) { - addError(e, { title: i18n.PREVIEW_QUERY_ERROR_TITLE }); - } - }, - [addError, setDateRange, setFilters] - ); - - useEffect(() => { - dataViews.create(sourcererDataView).then((dataView) => setDataViewsArray([dataView])); - }, [dataViews, sourcererDataView]); - if (isError) { return ( { return ( <> {i18n.PREVIEW_DESCRIPTION} - - - - + { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + const handleOnOpen = () => setIsFlyoutVisible(true); + const handleOnClose = () => setIsFlyoutVisible(false); + + return ( + <> + +

{i18n.USEFUL_LINKS}

+
+ + +
  • + {i18n.EA_DASHBOARD_LINK} + +
  • +
  • + + {i18n.EA_DOCS_ENTITY_RISK_SCORE} + + {isFlyoutVisible && } + +
  • +
    + + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx index ac9dfd9eb8ab8..84ce908d94ee5 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_management_page.tsx @@ -5,38 +5,164 @@ * 2.0. */ -import React from 'react'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiPageHeader, EuiSpacer } from '@elastic/eui'; - +import React, { useState, useCallback } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPageHeader, + EuiHorizontalRule, + EuiButton, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import moment from 'moment'; import { RiskScorePreviewSection } from '../components/risk_score_preview_section'; import { RiskScoreEnableSection } from '../components/risk_score_enable_section'; import { ENTITY_ANALYTICS_RISK_SCORE } from '../../app/translations'; -import { BETA } from '../../common/translations'; import { RiskEnginePrivilegesCallOut } from '../components/risk_engine_privileges_callout'; import { useMissingRiskEnginePrivileges } from '../hooks/use_missing_risk_engine_privileges'; +import { RiskScoreUsefulLinksSection } from '../components/risk_score_useful_links_section'; +import { RiskScoreConfigurationSection } from '../components/risk_score_configuration_section'; +import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status'; +import { useScheduleNowRiskEngineMutation } from '../api/hooks/use_schedule_now_risk_engine_mutation'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import * as i18n from '../translations'; +import { getEntityAnalyticsRiskScorePageStyles } from '../components/risk_score_page_styles'; + +const TEN_SECONDS = 10000; export const EntityAnalyticsManagementPage = () => { + const { euiTheme } = useEuiTheme(); + const styles = getEntityAnalyticsRiskScorePageStyles(euiTheme); const privileges = useMissingRiskEnginePrivileges(); + const [includeClosedAlerts, setIncludeClosedAlerts] = useState(false); + const [from, setFrom] = useState(localStorage.getItem('dateStart') || 'now-30m'); + const [to, setTo] = useState(localStorage.getItem('dateEnd') || 'now'); + const { data: riskEngineStatus } = useRiskEngineStatus({ + refetchInterval: TEN_SECONDS, + structuralSharing: false, // Force the component to rerender after every Risk Engine Status API call + }); + const currentRiskEngineStatus = riskEngineStatus?.risk_engine_status; + const runEngineEnabled = currentRiskEngineStatus === 'ENABLED'; + const [isLoading, setIsLoading] = useState(false); + const { mutate: scheduleNowRiskEngine } = useScheduleNowRiskEngineMutation(); + const { addSuccess, addError } = useAppToasts(); + + const handleRunEngineClick = async () => { + setIsLoading(true); + try { + scheduleNowRiskEngine(); + + if (!isLoading) { + addSuccess(i18n.RISK_SCORE_ENGINE_RUN_SUCCESS, { toastLifeTimeMs: 5000 }); + } + } catch (error) { + addError(error, { + title: i18n.RISK_SCORE_ENGINE_RUN_FAILURE, + }); + } finally { + setIsLoading(false); + } + }; + + const handleIncludeClosedAlertsToggle = useCallback( + (value: boolean) => { + setIncludeClosedAlerts(value); + }, + [setIncludeClosedAlerts] + ); + + const handleDateChange = ({ start, end }: { start: string; end: string }) => { + setFrom(start); + setTo(end); + localStorage.setItem('dateStart', start); + localStorage.setItem('dateEnd', end); + }; + + const { status, runAt } = riskEngineStatus?.risk_engine_task_status || {}; + + const isRunning = status === 'running' || (!!runAt && new Date(runAt) < new Date()); + + const formatTimeFromNow = (time: string | undefined): string => { + if (!time) { + return ''; + } + return i18n.RISK_ENGINE_NEXT_RUN_TIME(moment(time).fromNow(true)); + }; + + const countDownText = isRunning + ? 'Now running' + : formatTimeFromNow(riskEngineStatus?.risk_engine_task_status?.runAt); + return ( <> + + {/* Page Title */} {ENTITY_ANALYTICS_RISK_SCORE} - + + {/* Controls Section */} + + + {/* Run Engine Section */} + {runEngineEnabled && ( + <> + {/* Run Engine Button */} + + {i18n.RUN_RISK_SCORE_ENGINE} + + + {/* Vertical Line */} + + + {/* Countdown Text */} +
    + + {countDownText} + +
    + + )} + + {/* Risk Score Enable Section */} +
    + +
    +
    +
    } /> - - + + + - + + + - + diff --git a/x-pack/plugins/security_solution/public/entity_analytics/translations.ts b/x-pack/plugins/security_solution/public/entity_analytics/translations.ts index 2e06ec9ad1eb9..5d75b8f795d55 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/translations.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/translations.ts @@ -302,3 +302,64 @@ export const RISK_SCORE_MODULE_TURNED_OFF = i18n.translate( defaultMessage: 'Entity risk score has been turned off', } ); + +export const RISK_SCORE_ENGINE_RUN_SUCCESS = i18n.translate( + 'xpack.securitySolution.riskScore.engineRunSuccess', + { + defaultMessage: 'Entity risk score engine started successfully', + } +); + +export const RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.riskScore.savedObject.configurationSuccess', + { + defaultMessage: 'Risk engine Saved Object configuration updated successfully', + } +); + +export const INCLUDE_CLOSED_ALERTS_LABEL = i18n.translate( + 'xpack.securitySolution.riskScore.includeClosedAlertsLabel', + { + defaultMessage: 'Include closed alerts for risk scoring', + } +); + +export const RISK_ENGINE_INCLUDE_CLOSED_ALERTS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.riskScore.includeClosedAlertsDescription', + { + defaultMessage: `Enable this option to factor both open and closed alerts into the risk engine + calculations. Including closed alerts helps provide a more comprehensive risk assessment + based on past incidents, leading to more accurate scoring and insights.`, + } +); + +export const RISK_ENGINE_NEXT_RUN_TIME = (timeInMinutes: string) => + i18n.translate('xpack.securitySolution.riskScore.engineNextRunTime', { + defaultMessage: `Next engine run in {timeInMinutes}`, + values: { timeInMinutes }, + }); + +export const RUN_RISK_SCORE_ENGINE = i18n.translate('xpack.securitySolution.riskScore.runEngine', { + defaultMessage: 'Run Engine', +}); + +export const SAVE_CHANGES = i18n.translate( + 'xpack.securitySolution.riskScore.engineSavedObjectsaveChanges', + { + defaultMessage: 'Save', + } +); + +export const DISCARD_CHANGES = i18n.translate( + 'xpack.securitySolution.riskScore.engineSavedObject.discardChanges', + { + defaultMessage: 'Discard', + } +); + +export const RISK_SCORE_ENGINE_RUN_FAILURE = i18n.translate( + 'xpack.securitySolution.riskScore.engineRunSuccess', + { + defaultMessage: 'Entity risk score engine failed to start', + } +); 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..09ef77d3df6ac 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 @@ -82,10 +82,13 @@ export class RiskEngineDataClient { } try { - await initSavedObjects({ + const soResult = await initSavedObjects({ savedObjectsClient: this.options.soClient, namespace, }); + this.options.logger.info( + `Risk engine savedObject configuration: ${JSON.stringify(soResult, null, 2)}` + ); result.riskEngineConfigurationCreated = true; } catch (e) { result.errors.push(e.message); @@ -319,4 +322,25 @@ export class RiskEngineDataClient { return RiskEngineStatusEnum.ENABLED; } + + public async updateRiskEngineSavedObject(attributes: {}) { + try { + const configuration = await this.getConfiguration(); + if (!configuration) { + await initSavedObjects({ + savedObjectsClient: this.options.soClient, + namespace: this.options.namespace, + }); + } + return await updateSavedObjectAttribute({ + savedObjectsClient: this.options.soClient, + attributes, + }); + } catch (e) { + this.options.logger.error( + `Error updating risk score engine saved object attributes: ${e.message}` + ); + throw e; + } + } } 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..c70af5c70c7af --- /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.updateRiskEngineSavedObject = 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.updateRiskEngineSavedObject).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.updateRiskEngineSavedObject).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..5a1e847637522 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.ts @@ -0,0 +1,110 @@ +/* + * 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, + API_VERSIONS, +} 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: API_VERSIONS.public.v1, + 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.updateRiskEngineSavedObject({ + 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/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts index ff1062393c935..3981781ca3fac 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts @@ -14,6 +14,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ALERT_RISK_SCORE, ALERT_WORKFLOW_STATUS, + ALERT_WORKFLOW_TAGS, } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; import type { AssetCriticalityRecord, @@ -219,6 +220,7 @@ export const calculateRiskScores = async ({ weights, alertSampleSizePerShard = 10_000, excludeAlertStatuses = [], + excludeAlertTags = [], }: { assetCriticalityService: AssetCriticalityService; esClient: ElasticsearchClient; @@ -236,6 +238,11 @@ export const calculateRiskScores = async ({ if (!isEmpty(userFilter)) { filter.push(userFilter as QueryDslQueryContainer); } + if (excludeAlertTags.length > 0) { + filter.push({ + bool: { must_not: { terms: { [ALERT_WORKFLOW_TAGS]: excludeAlertTags } } }, + }); + } const identifierTypes: IdentifierType[] = identifierType ? [identifierType] : ['host', 'user']; const request = { size: 0, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts index b5ff9c3487a07..6aa2339b999d6 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts @@ -250,5 +250,35 @@ describe('POST risk_engine/preview route', () => { expect(result.ok).toHaveBeenCalledWith(expect.objectContaining({ after_keys: {} })); }); }); + + describe('exclude_alert_statuses', () => { + it('respects the provided exclude_alert_statuses', async () => { + const request = buildRequest({ + exclude_alert_statuses: ['open'], + }); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.calculateScores).toHaveBeenCalledWith( + expect.objectContaining({ excludeAlertStatuses: ['open'] }) + ); + }); + }); + + describe('exclude_alert_tags', () => { + it('respects the provided exclude_alert_tags', async () => { + const request = buildRequest({ + exclude_alert_tags: ['tag1'], + }); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.calculateScores).toHaveBeenCalledWith( + expect.objectContaining({ excludeAlertTags: ['tag1'] }) + ); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts index 5ab6791a300c3..eb4cd433b74c2 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts @@ -65,7 +65,8 @@ export const riskScorePreviewRoute = ( filter, range: userRange, weights, - excludeAlertStatuses, + exclude_alert_statuses: excludedStatuses, + exclude_alert_tags: excludedTags, } = request.body; const entityAnalyticsConfig = await riskScoreService.getConfigurationWithDefaults( @@ -84,6 +85,8 @@ export const riskScorePreviewRoute = ( const afterKeys = userAfterKeys ?? {}; const range = userRange ?? { start: 'now-15d', end: 'now' }; const pageSize = userPageSize ?? DEFAULT_RISK_SCORE_PAGE_SIZE; + const excludeAlertStatuses = excludedStatuses || ['closed']; + const excludeAlertTags = excludedTags || []; const result = await riskScoreService.calculateScores({ afterKeys, @@ -97,6 +100,7 @@ export const riskScorePreviewRoute = ( weights, alertSampleSizePerShard, excludeAlertStatuses, + excludeAlertTags, }); securityContext.getAuditLogger()?.log({ diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts index 4230b8fa05e2d..06ec89061e7e5 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts @@ -257,6 +257,7 @@ export const runTask = async ({ const configuration = await riskScoreService.getConfigurationWithDefaults( entityAnalyticsConfig ); + log(`Risk engine running with configuration : ${JSON.stringify(configuration, null, 2)}`); if (configuration == null) { log( 'Risk engine configuration not found; exiting task. Please reinitialize the risk engine and try again' 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..48ddc9b4a7698 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 @@ -86,6 +86,7 @@ export interface CalculateScoresParams { weights?: RiskScoreWeights; alertSampleSizePerShard?: number; excludeAlertStatuses?: string[]; + excludeAlertTags?: string[]; } export interface CalculateAndPersistScoresParams { 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 30903c2f572b2..9ffdc1c43a2a1 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'; @@ -314,6 +315,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. @@ -1634,6 +1649,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; + }, }; }; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page.cy.ts index b8d222471c87e..5059eab2cfb41 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page.cy.ts @@ -7,15 +7,9 @@ import { PAGE_TITLE, - HOST_RISK_PREVIEW_TABLE, - HOST_RISK_PREVIEW_TABLE_ROWS, - USER_RISK_PREVIEW_TABLE, - USER_RISK_PREVIEW_TABLE_ROWS, RISK_PREVIEW_ERROR, - LOCAL_QUERY_BAR_SELECTOR, RISK_SCORE_ERROR_PANEL, RISK_SCORE_STATUS, - LOCAL_QUERY_BAR_SEARCH_INPUT_SELECTOR, } from '../../screens/entity_analytics_management'; import { deleteRiskScore, installRiskScoreModule } from '../../tasks/api_calls/risk_scores'; @@ -31,8 +25,6 @@ import { interceptRiskPreviewSuccess, interceptRiskInitError, } from '../../tasks/api_calls/risk_engine'; -import { updateDateRangeInLocalDatePickers } from '../../tasks/date_picker'; -import { submitLocalSearch } from '../../tasks/search_bar'; import { riskEngineStatusChange, upgradeRiskEngine, @@ -65,31 +57,6 @@ describe( }); describe('Risk preview', () => { - it('risk scores reacts on change in datepicker', () => { - const START_DATE = 'Jan 18, 2019 @ 20:33:29.186'; - const END_DATE = 'Jan 19, 2019 @ 20:33:29.186'; - - cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5); - cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5); - - updateDateRangeInLocalDatePickers(LOCAL_QUERY_BAR_SELECTOR, START_DATE, END_DATE); - - cy.get(HOST_RISK_PREVIEW_TABLE).contains('No items found'); - cy.get(USER_RISK_PREVIEW_TABLE).contains('No items found'); - }); - - it('risk scores reacts on change in search bar query', () => { - cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5); - cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5); - cy.get(LOCAL_QUERY_BAR_SEARCH_INPUT_SELECTOR).type('host.name: "test-host1"'); - submitLocalSearch(LOCAL_QUERY_BAR_SELECTOR); - - cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1); - cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).contains('test-host1'); - cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1); - cy.get(USER_RISK_PREVIEW_TABLE_ROWS).contains('test1'); - }); - it('show error panel if API returns error and then try to refetch data', () => { interceptRiskPreviewError();