diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts index ec394a721a97b..3472f83c153eb 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts @@ -31,6 +31,21 @@ export const InitRiskEngineErrorResponse = z.object({ full_error: z.string(), }); +export type InitRiskEngineRequestBody = z.infer; +export const InitRiskEngineRequestBody = z.object({ + /** + * If true, it will include closed alerts for risk score calculation + */ + includeClosedAlerts: z.boolean().optional().default(false), + range: z + .object({ + start: z.string().optional(), + end: z.string().optional(), + }) + .optional(), +}); +export type InitRiskEngineRequestBodyInput = z.input; + export type InitRiskEngineResponse = z.infer; export const InitRiskEngineResponse = z.object({ result: InitRiskEngineResult, diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml index 58091241d97b7..38cfa08ff820e 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml @@ -12,6 +12,24 @@ paths: operationId: InitRiskEngine summary: Initialize the Risk Engine description: Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine + requestBody: + description: Schema for the risk score engine initialization + content: + application/json: + schema: + type: object + properties: + includeClosedAlerts: + type: boolean + description: If true, it will include closed alerts for risk score calculation + default: false + range: + type: object + properties: + start: + type: string + end: + type: string responses: '200': description: Successful response 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..ab86fc764844d --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/so_configure_route.gen.ts @@ -0,0 +1,59 @@ +/* + * 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({}).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(), + includeClosedAlerts: z.boolean().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..f1b4fac4d1678 --- /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 + 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' + includeClosedAlerts: + type: boolean + + 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 25560aeffdbbe..64464f6b5b8d9 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 @@ -278,7 +278,10 @@ import type { import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen'; import type { DisableRiskEngineResponse } from './entity_analytics/risk_engine/engine_disable_route.gen'; import type { EnableRiskEngineResponse } from './entity_analytics/risk_engine/engine_enable_route.gen'; -import type { InitRiskEngineResponse } from './entity_analytics/risk_engine/engine_init_route.gen'; +import type { + InitRiskEngineRequestBodyInput, + InitRiskEngineResponse, +} from './entity_analytics/risk_engine/engine_init_route.gen'; import type { ScheduleRiskEngineNowResponse } from './entity_analytics/risk_engine/engine_schedule_now_route.gen'; import type { ReadRiskEngineSettingsResponse } from './entity_analytics/risk_engine/engine_settings_route.gen'; import type { GetRiskEngineStatusResponse } from './entity_analytics/risk_engine/engine_status_route.gen'; @@ -293,6 +296,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, @@ -578,6 +585,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. @@ -1518,7 +1538,7 @@ finalize it. /** * Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine */ - async initRiskEngine() { + async initRiskEngine(props: InitRiskEngineProps) { this.log.info(`${new Date().toISOString()} Calling API InitRiskEngine`); return this.kbnClient .request({ @@ -1527,6 +1547,7 @@ finalize it. [ELASTIC_HTTP_VERSION_HEADER]: '1', }, method: 'POST', + body: props.body, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2063,6 +2084,9 @@ export interface BulkUpsertAssetCriticalityRecordsProps { export interface CleanDraftTimelinesProps { body: CleanDraftTimelinesRequestBodyInput; } +export interface ConfigureRiskEngineSavedObjectProps { + body: ConfigureRiskEngineSavedObjectRequestBodyInput; +} export interface CopyTimelineProps { body: CopyTimelineRequestBodyInput; } @@ -2225,6 +2249,9 @@ export interface InitEntityEngineProps { params: InitEntityEngineRequestParamsInput; body: InitEntityEngineRequestBodyInput; } +export interface InitRiskEngineProps { + body: InitRiskEngineRequestBodyInput; +} export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; } 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..a0a5f8bb75c4a 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_SAVED_OBJECT_CONFIG_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/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index 18cb9ef570bd5..ecaeb7b1f517f 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 @@ -10,7 +10,10 @@ import { LIST_ENTITIES_URL } from '../../../common/entity_analytics/entity_store import type { UploadAssetCriticalityRecordsResponse } from '../../../common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; import type { RiskEngineStatusResponse } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; -import type { InitRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; +import type { + InitRiskEngineResponse, + InitRiskEngineRequestBody, +} from '../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; import type { EnableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen'; import type { RiskEngineScheduleNowResponse } from '../../../common/api/entity_analytics/risk_engine/engine_schedule_now_route.gen'; import type { @@ -42,7 +45,9 @@ import { API_VERSIONS, RISK_ENGINE_CLEANUP_URL, RISK_ENGINE_SCHEDULE_NOW_URL, + RISK_ENGINE_SAVED_OBJECT_CONFIG_URL, } from '../../../common/constants'; +// import { RISK_ENGINE_SAVED_OBJECT_CONFIG_URL } from '../../../server/lib/entity_analytics/risk_engine/saved_object/constants'; import type { SnakeToCamelCase } from '../common/utils'; import { useKibana } from '../../common/lib/kibana/kibana_react'; import type { ReadRiskEngineSettingsResponse } from '../../../common/api/entity_analytics/risk_engine'; @@ -110,10 +115,11 @@ export const useEntityAnalyticsRoutes = () => { /** * Init risk score engine */ - const initRiskEngine = () => + const initRiskEngine = (params: InitRiskEngineRequestBody) => http.fetch(RISK_ENGINE_INIT_URL, { version: '1', method: 'POST', + body: JSON.stringify(params), }); /** @@ -276,6 +282,24 @@ export const useEntityAnalyticsRoutes = () => { method: 'GET', }); + /** + * Updates the Risk Engine savedObject Configuration + */ + + const updateSavedObjectConfiguration = async (params: { + includeClosedAlerts: boolean; + range: { start: string; end: string }; + }) => { + http.fetch(RISK_ENGINE_SAVED_OBJECT_CONFIG_URL, { + version: '2023-10-31', + method: 'POST', + body: JSON.stringify({ + range: params.range, + includeClosedAlerts: params.includeClosedAlerts, + }), + }); + }; + /** * Deletes Risk engine installation and associated data */ @@ -304,6 +328,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.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_configure_risk_engine.ts new file mode 100644 index 0000000000000..c61386e2427e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_configure_risk_engine.ts @@ -0,0 +1,40 @@ +/* + * 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/so_configure_route.gen'; + +export const INIT_RISK_ENGINE_STATUS_KEY = ['POST', 'INIT_RISK_ENGINE']; +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) => { + await updateSavedObjectConfiguration({ + includeClosedAlerts: params.includeClosedAlerts, + range: params.range, + }); + return { configuration_successful: true }; + }); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts index d774853c7d026..65b8c08f65b6a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts @@ -7,25 +7,42 @@ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; import type { + InitRiskEngineRequestBody, InitRiskEngineErrorResponse, InitRiskEngineResponse, } from '../../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; import type { TaskManagerUnavailableResponse } from '../../../../common/api/entity_analytics/common'; import { useEntityAnalyticsRoutes } from '../api'; import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status'; +import * as i18n from '../../translations'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; export const INIT_RISK_ENGINE_STATUS_KEY = ['POST', 'INIT_RISK_ENGINE']; +const toastOptions = { + toastLifeTimeMs: 5000, +}; -export const useInitRiskEngineMutation = (options?: UseMutationOptions<{}>) => { +export const useInitRiskEngineMutation = ( + options?: UseMutationOptions< + InitRiskEngineResponse, + { body: InitRiskEngineErrorResponse | TaskManagerUnavailableResponse }, + InitRiskEngineRequestBody + > +) => { const invalidateRiskEngineStatusQuery = useInvalidateRiskEngineStatusQuery(); const { initRiskEngine } = useEntityAnalyticsRoutes(); + const { addSuccess } = useAppToasts(); return useMutation< InitRiskEngineResponse, - { body: InitRiskEngineErrorResponse | TaskManagerUnavailableResponse } - >(() => initRiskEngine(), { + { body: InitRiskEngineErrorResponse | TaskManagerUnavailableResponse }, + InitRiskEngineRequestBody + >((params) => initRiskEngine(params), { ...options, mutationKey: INIT_RISK_ENGINE_STATUS_KEY, + onSuccess: () => { + addSuccess(i18n.RISK_SCORE_ENGINE_RUN_SUCCESS, toastOptions); + }, onSettled: (...args) => { invalidateRiskEngineStatusQuery(); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/include_closed_alerts_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/include_closed_alerts_section.tsx new file mode 100644 index 0000000000000..a25d0bd930ad0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/include_closed_alerts_section.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiSuperDatePicker, + EuiButton, + EuiText, + EuiFlexGroup, + EuiSwitch, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { useConfigureSORiskEngineMutation } from '../api/hooks/use_configure_risk_engine'; + +export const IncludeClosedAlertsSection = ({ + includeClosedAlerts, + setIncludeClosedAlerts, + from, + to, + onDateChange, +}: { + includeClosedAlerts: boolean; + setIncludeClosedAlerts: (value: boolean) => void; + from: string; + to: string; + onDateChange: ({ start, end }: { start: string; end: string }) => void; +}) => { + const [start, setFrom] = useState(from); + const [end, setTo] = useState(to); + const [isLoading, setIsLoading] = useState(false); + + const onRefresh = ({ start: newStart, end: newEnd }: { start: string; end: string }) => { + setFrom(newStart); + setTo(newEnd); + onDateChange({ start: newStart, end: newEnd }); + }; + + const handleToggle = () => { + setIncludeClosedAlerts(!includeClosedAlerts); + }; + + const configureRiskEngineMutation = useConfigureSORiskEngineMutation({ + onMutate: () => setIsLoading(true), + onSettled: () => setIsLoading(false), + }); + + const handleSave = () => { + configureRiskEngineMutation.mutate({ + includeClosedAlerts, + range: { start, end }, + }); + }; + + return ( + <> + + + + +
+ + + onRefresh({ start: newStart, end: newEnd }) + } + width={'auto'} + compressed={false} + showUpdateButton={false} + /> + + + +

+ {`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.`} +

+
+ + + + + {'Save'} + + + + + ); +}; 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..19caa7049daad 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,7 +33,7 @@ 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'; @@ -144,12 +139,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<{ @@ -199,6 +194,7 @@ export const RiskScoreEnableSection: React.FC<{ const initRiskEngineMutation = useInitRiskEngineMutation({ onSuccess: () => { addSuccess(i18n.RISK_SCORE_MODULE_TURNED_ON, toastOptions); + // onStatusChange(true) }, onSettled: () => { setIsModalVisible(false); @@ -208,11 +204,13 @@ export const RiskScoreEnableSection: React.FC<{ const enableRiskEngineMutation = useEnableRiskEngineMutation({ onSuccess: () => { addSuccess(i18n.RISK_SCORE_MODULE_TURNED_ON, toastOptions); + // onStatusChange(true); }, }); const disableRiskEngineMutation = useDisableRiskEngineMutation({ onSuccess: () => { addSuccess(i18n.RISK_SCORE_MODULE_TURNED_OFF, toastOptions); + // onStatusChange(false); }, }); @@ -254,9 +252,6 @@ export const RiskScoreEnableSection: React.FC<{ return ( <> <> - -

{i18n.RISK_SCORE_MODULE_STATUS}

-
{initRiskEngineMutation.isError && } {disableRiskEngineMutation.isError && ( @@ -273,12 +268,10 @@ export const RiskScoreEnableSection: React.FC<{ isLoading={initRiskEngineMutation.isLoading} closeModal={closeModal} /> - - {i18n.ENTITY_RISK_SCORING} {isUpdateAvailable && {i18n.UPDATE_AVAILABLE}} @@ -310,29 +303,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_preview_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_section.tsx index 9693bf13589ad..663d2e3fbe64d 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 @@ -6,10 +6,8 @@ */ import React, { useState, useCallback, useMemo, useEffect } from 'react'; -import type { DataView } from '@kbn/data-views-plugin/public'; import { EuiAccordion, - EuiFormRow, EuiPanel, EuiSpacer, EuiTitle, @@ -33,7 +31,6 @@ 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'; @@ -55,7 +52,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 +67,11 @@ export const RiskScorePreviewSection: React.FC<{ ); } if (userHasRiskEngineReadPermissions(privileges)) { - return ; + return ; } return ; - }, [privileges]); + }, [privileges, includeClosedAlerts, from, to]); return ( <> @@ -138,24 +138,32 @@ const RiskScorePreviewPanel = ({ ); }; -const RiskEnginePreview = () => { +const RiskEnginePreview: React.FC<{ includeClosedAlerts: boolean; from: string; to: string }> = ({ + includeClosedAlerts, + from, + to, +}) => { const [dateRange, setDateRange] = useState<{ from: string; to: string }>({ - from: 'now-24h', - to: 'now', + from, + to, }); const [filters, setFilters] = useState<{ bool: BoolQuery }>({ bool: { must: [], filter: [], should: [], must_not: [] }, }); - const [dataViewsArray, setDataViewsArray] = useState([]); - - const { - unifiedSearch: { - ui: { SearchBar }, - }, - dataViews, - } = useKibana().services; + useEffect(() => { + setFilters({ + bool: { + must: [], + filter: includeClosedAlerts + ? [{ terms: { 'kibana.alert.workflow_status': ['closed', 'open'] } }] + : [], + should: [], + must_not: [], + }, + }); + }, [includeClosedAlerts]); const { addError } = useAppToasts(); @@ -192,11 +200,6 @@ const RiskEnginePreview = () => { }, [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..4e15fc036b307 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,156 @@ * 2.0. */ -import React from 'react'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import React, { useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPageHeader, + EuiHorizontalRule, + EuiButton, + useEuiTheme, +} from '@elastic/eui'; import { RiskScorePreviewSection } from '../components/risk_score_preview_section'; import { RiskScoreEnableSection } from '../components/risk_score_enable_section'; +import { IncludeClosedAlertsSection } from '../components/include_closed_alerts_section'; +import { RiskScoreUsefulLinksSection } from '../components/risk_score_useful_links_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'; +// /Users/abhishekbhatia/Work/elastic/kibana/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts +import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status'; +import { useInitRiskEngineMutation } from '../api/hooks/use_init_risk_engine_mutation'; export const EntityAnalyticsManagementPage = () => { const privileges = useMissingRiskEnginePrivileges(); + const [includeClosedAlerts, setIncludeClosedAlerts] = useState(false); + const { euiTheme } = useEuiTheme(); + const { data: riskEngineStatus } = useRiskEngineStatus(); + const currentRiskEngineStatus = riskEngineStatus?.risk_engine_status; + const runEngineEnabled = currentRiskEngineStatus === 'ENABLED'; + const [from, setFrom] = useState('now-30m'); + const [to, setTo] = useState('now'); + const { mutate: initRiskEngine } = useInitRiskEngineMutation(); + const [isLoading, setIsLoading] = useState(false); + + const handleRunEngineClick = async () => { + setIsLoading(true); + try { + await new Promise((resolve) => setTimeout(resolve, 500)); + initRiskEngine({ + includeClosedAlerts, + range: { start: from, end: to }, + }); + } catch (error) { + setIsLoading(false); + } finally { + setIsLoading(false); + } + }; + + const handleToggle = (value: boolean) => { + setIncludeClosedAlerts(value); + }; + + const handleDateChange = ({ start, end }: { start: string; end: string }) => { + setFrom(start); + setTo(end); + }; + + const calculateNextRunTime = () => { + const now = new Date(); + const nextRun = new Date(now.getTime() + (60 - now.getMinutes()) * 60000); + const minutesUntilNextRun = Math.round((nextRun.getTime() - now.getTime()) / 60000); + return `Next engine run in ${minutesUntilNextRun} minutes`; + }; + return ( <> + + {/* Left-aligned Page Title */} {ENTITY_ANALYTICS_RISK_SCORE} - + + {/* Right-aligned group with toggle, text, vertical line, and button */} + + + {/* Run Engine Button */} + {runEngineEnabled && ( + + + {'Run Engine'} + + + )} + + {/* Vertical Line */} + {runEngineEnabled && ( +
+ )} + + {/* Text: "Next engine run in 14 minutes" */} + {runEngineEnabled && ( + + + {calculateNextRunTime()} + + + )} + + {/* Toggle Button */} + + + + + } /> - - + + + + - + + + - - + + + 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..8762eb80403bc 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,24 @@ export const RISK_SCORE_MODULE_TURNED_OFF = i18n.translate( defaultMessage: 'Entity risk score has been turned off', } ); + +export const RISK_SCORE_SO_CONFIGURED_SUCCESS = i18n.translate( + 'xpack.securitySolution.riskScore.savedObjectsConfiguredSuccess', + { + defaultMessage: 'SavedObject for Entity risk score has been updated successfully', + } +); + +export const RISK_SCORE_SO_CONFIGURED_FAILURE = i18n.translate( + 'xpack.securitySolution.riskScore.savedObjectsConfiguredFailure', + { + defaultMessage: 'SavedObject for Entity risk score has not been updated', + } +); + +export const RISK_SCORE_ENGINE_RUN_SUCCESS = i18n.translate( + 'xpack.securitySolution.riskScore.engine.runSuccess', + { + defaultMessage: 'Risk engine has been run successfully', + } +); 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..3e33200810d8a 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 @@ -9,7 +9,7 @@ import type { Logger, ElasticsearchClient, SavedObjectsClientContract } from '@k import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import type { AuditLogger } from '@kbn/security-plugin-types-server'; import { RiskEngineStatusEnum } from '../../../../common/api/entity_analytics'; -import type { InitRiskEngineResult } from '../../../../common/entity_analytics/risk_engine'; +import type { InitRiskEngineResult, Range } from '../../../../common/entity_analytics/risk_engine'; import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; import { removeLegacyTransforms, getLegacyTransforms } from '../utils/transforms'; import { @@ -29,6 +29,8 @@ interface InitOpts { namespace: string; taskManager: TaskManagerStartContract; riskScoreDataClient: RiskScoreDataClient; + range: Range; + includeClosedAlerts: boolean; } interface TearDownParams { @@ -48,7 +50,13 @@ interface RiskEngineDataClientOpts { export class RiskEngineDataClient { constructor(private readonly options: RiskEngineDataClientOpts) {} - public async init({ namespace, taskManager, riskScoreDataClient }: InitOpts) { + public async init({ + namespace, + taskManager, + riskScoreDataClient, + range, + includeClosedAlerts, + }: InitOpts) { const result: InitRiskEngineResult = { legacyRiskEngineDisabled: false, riskEngineResourcesInstalled: false, @@ -85,6 +93,8 @@ export class RiskEngineDataClient { await initSavedObjects({ savedObjectsClient: this.options.soClient, namespace, + range, + includeClosedAlerts, }); result.riskEngineConfigurationCreated = true; } catch (e) { @@ -319,4 +329,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/init.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts index 4657d21cbcbe0..f4e134ab6dacd 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts @@ -8,9 +8,11 @@ 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 { InitRiskEngineRequestBody } from '../../../../../common/api/entity_analytics'; import type { - InitRiskEngineResponse, InitRiskEngineResult, + InitRiskEngineResponse, } from '../../../../../common/api/entity_analytics'; import { RISK_ENGINE_INIT_URL, APP_ID } from '../../../../../common/constants'; import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; @@ -31,7 +33,14 @@ export const riskEngineInitRoute = ( }, }) .addVersion( - { version: '1', validate: {} }, + { + version: '1', + validate: { + request: { + body: buildRouteValidationWithZod(InitRiskEngineRequestBody), + }, + }, + }, withRiskEnginePrivilegeCheck( getStartServices, async (context, request, response): Promise> => { @@ -52,6 +61,11 @@ export const riskEngineInitRoute = ( const riskEngineDataClient = securitySolution.getRiskEngineDataClient(); const riskScoreDataClient = securitySolution.getRiskScoreDataClient(); const spaceId = securitySolution.getSpaceId(); + const range = { + start: request.body.range?.start || 'now-30m', + end: request.body.range?.end || 'now', + }; + const includeClosedAlerts = request.body.includeClosedAlerts; try { if (!taskManager) { @@ -65,6 +79,8 @@ export const riskEngineInitRoute = ( taskManager, namespace: spaceId, riskScoreDataClient, + range, + includeClosedAlerts, }); const result: InitRiskEngineResult = { 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..f6fa393339774 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 @@ -9,6 +9,7 @@ import type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server'; import { getAlertsIndex } from '../../../../../common/utils/risk_score_modules'; import type { RiskEngineConfiguration } from '../../types'; import { riskEngineConfigurationTypeName } from '../saved_object'; +import type { Range } from '../../../../../common/entity_analytics/risk_engine'; export interface SavedObjectsClientArg { savedObjectsClient: SavedObjectsClientContract; @@ -16,8 +17,12 @@ export interface SavedObjectsClientArg { const getDefaultRiskEngineConfiguration = ({ namespace, + range, + includeClosedAlerts, }: { namespace: string; + range: Range; + includeClosedAlerts: boolean; }): RiskEngineConfiguration => ({ dataViewId: getAlertsIndex(namespace), enabled: false, @@ -25,7 +30,8 @@ const getDefaultRiskEngineConfiguration = ({ identifierType: undefined, interval: '1h', pageSize: 3_500, - range: { start: 'now-30d', end: 'now' }, + range, + includeClosedAlerts, }); const getConfigurationSavedObject = async ({ @@ -42,7 +48,20 @@ 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'>; + includeClosedAlerts?: boolean; }; }) => { const savedObjectConfiguration = await getConfigurationSavedObject({ @@ -70,14 +89,23 @@ export const updateSavedObjectAttribute = async ({ export const initSavedObjects = async ({ namespace, savedObjectsClient, -}: SavedObjectsClientArg & { namespace: string }) => { + range, + includeClosedAlerts, +}: SavedObjectsClientArg & { namespace: string; range: Range; includeClosedAlerts: boolean }) => { const configuration = await getConfigurationSavedObject({ savedObjectsClient }); if (configuration) { - return configuration; + const result = await updateSavedObjectAttribute({ + savedObjectsClient, + attributes: { + range, + includeClosedAlerts, + }, + }); + return result; } const result = await savedObjectsClient.create( riskEngineConfigurationTypeName, - getDefaultRiskEngineConfiguration({ namespace }), + getDefaultRiskEngineConfiguration({ namespace, range, includeClosedAlerts }), {} ); return result; 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..3b88bf5170af2 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,8 @@ export const calculateRiskScores = async ({ weights, alertSampleSizePerShard = 10_000, excludeAlertStatuses = [], + excludeAlertTags = [], + includeClosedAlerts = false, }: { assetCriticalityService: AssetCriticalityService; esClient: ElasticsearchClient; @@ -228,6 +231,10 @@ export const calculateRiskScores = async ({ const now = new Date().toISOString(); const scriptedMetricPainless = await getPainlessScripts(); const filter = [filterFromRange(range), { exists: { field: ALERT_RISK_SCORE } }]; + // Add a check if includeClosedAlerts is true then excludeAlertStatuses should not have closed status + // if (excludeAlertStatuses.includes('closed') && includeClosedAlerts) { + // excludeAlertStatuses = excludeAlertStatuses.filter((status) => status !== 'closed'); + // } if (excludeAlertStatuses.length > 0) { filter.push({ bool: { must_not: { terms: { [ALERT_WORKFLOW_STATUS]: excludeAlertStatuses } } }, @@ -236,6 +243,24 @@ export const calculateRiskScores = async ({ if (!isEmpty(userFilter)) { filter.push(userFilter as QueryDslQueryContainer); } + if (includeClosedAlerts) { + filter.push({ + bool: { + filter: [ + { + terms: { + [ALERT_WORKFLOW_STATUS]: [], // Assuming [] is the array of terms you want to match + }, + }, + ], + }, + }); + } + 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/tasks/risk_scoring_task.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/tasks/risk_scoring_task.ts index 4230b8fa05e2d..3f0f519fa69f6 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 @@ -272,6 +272,7 @@ export const runTask = async ({ range: configuredRange, pageSize, alertSampleSizePerShard, + includeClosedAlerts, } = configuration; if (!enabled) { log('risk engine is not enabled, exiting task'); @@ -307,6 +308,7 @@ export const runTask = async ({ runtimeMappings, weights: [], alertSampleSizePerShard, + includeClosedAlerts, }); const tookMs = Date.now() - now; 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..6c2d0c5ede24e 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,9 @@ export interface RiskEngineConfiguration { pageSize: number; range: Range; alertSampleSizePerShard?: number; + excludeAlertStatuses?: string[]; + excludeAlertTags?: string[]; + includeClosedAlerts?: boolean; } export interface CalculateScoresParams { @@ -86,6 +89,8 @@ export interface CalculateScoresParams { weights?: RiskScoreWeights; alertSampleSizePerShard?: number; excludeAlertStatuses?: string[]; + excludeAlertTags?: string[]; + includeClosedAlerts?: boolean; } export interface CalculateAndPersistScoresParams { @@ -101,4 +106,7 @@ export interface CalculateAndPersistScoresParams { alertSampleSizePerShard?: number; returnScores?: boolean; refresh?: 'wait_for'; + excludeAlertStatuses?: string[]; + excludeAlertTags?: string[]; + includeClosedAlerts?: boolean; } 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 3503f07fec574..b7d09c50822eb 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'; @@ -103,6 +104,7 @@ import { InitEntityEngineRequestParamsInput, InitEntityEngineRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen'; +import { InitRiskEngineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/engine_init_route.gen'; import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen'; import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen'; import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen'; @@ -281,6 +283,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. @@ -1004,12 +1017,13 @@ finalize it. /** * Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine */ - initRiskEngine(kibanaSpace: string = 'default') { + initRiskEngine(props: InitRiskEngineProps, kibanaSpace: string = 'default') { return supertest .post(routeWithNamespace('/internal/risk_score/engine/init', kibanaSpace)) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); }, /** * Install and update all Elastic prebuilt detection rules and Timelines. @@ -1392,6 +1406,9 @@ export interface BulkUpsertAssetCriticalityRecordsProps { export interface CleanDraftTimelinesProps { body: CleanDraftTimelinesRequestBodyInput; } +export interface ConfigureRiskEngineSavedObjectProps { + body: ConfigureRiskEngineSavedObjectRequestBodyInput; +} export interface CopyTimelineProps { body: CopyTimelineRequestBodyInput; } @@ -1553,6 +1570,9 @@ export interface InitEntityEngineProps { params: InitEntityEngineRequestParamsInput; body: InitEntityEngineRequestBodyInput; } +export interface InitRiskEngineProps { + body: InitRiskEngineRequestBodyInput; +} export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; } 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 0a88e9fbe2518..c5850e944a0ec 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'; @@ -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,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; + }, }; };