From 5ec58aabc53202ac960b8841a140da13c917a4d1 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 14 Oct 2024 11:37:56 +0100 Subject: [PATCH] [ML] Adds ML tasks to the kibana audit log (#195120) Adds a new `MlAuditLogger` service for logging calls to elasticsearch in kibana's audit log. Not all calls are logged, only ones which make changes to ML jobs or trained models, e.g. creating, deleting, starting, stopping etc. Calls to the es client are wrapped in a logging function so successes and failures can be caught and logged. the audit log can be enabed by adding this to the kibana yml or dev.yml file `xpack.security.audit.enabled: true` An example log entry (NDJSON formatted to make it readable): ``` { "event": { "action": "ml_start_ad_datafeed", "type": [ "change" ], "category": [ "database" ], "outcome": "success" }, "labels": { "application": "elastic/ml" }, "user": { "id": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", "name": "elastic", "roles": [ "superuser" ] }, "kibana": { "space_id": "default", "session_id": "U6HQCDkk+fAEUCXs7i4qM2/MZITPxE02pp8o7h09P68=" }, "trace": { "id": "4f1b616b-8535-43e1-8516-32ea9fe76d19" }, "client": { "ip": "127.0.0.1" }, "http": { "request": { "headers": { "x-forwarded-for": "127.0.0.1" } } }, "service": { "node": { "roles": [ "background_tasks", "ui" ] } }, "ecs": { "version": "8.11.0" }, "@timestamp": "2024-10-11T09:07:47.933+01:00", "message": "Starting anomaly detection datafeed datafeed-11aaaa", "log": { "level": "INFO", "logger": "plugins.security.audit.ecs" }, "process": { "pid": 58305, "uptime": 100.982390291 }, "transaction": { "id": "77c14aadc6901324" } } ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 923c450c1b044a12dd938c0c5ea380a895eeaf88) --- docs/user/security/audit-logging.asciidoc | 158 +++++++- .../plugins/ml/server/lib/ml_client/index.ts | 1 + .../server/lib/ml_client/ml_audit_logger.ts | 383 ++++++++++++++++++ .../ml/server/lib/ml_client/ml_client.ts | 225 ++++++---- x-pack/plugins/ml/server/lib/route_guard.ts | 8 +- x-pack/plugins/ml/server/plugin.ts | 4 + .../server/shared_services/shared_services.ts | 21 +- x-pack/plugins/ml/tsconfig.json | 91 +++-- 8 files changed, 743 insertions(+), 148 deletions(-) create mode 100644 x-pack/plugins/ml/server/lib/ml_client/ml_audit_logger.ts diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index 0ddc830e4ed49..1ac40bcc7764a 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -17,20 +17,20 @@ by cluster-wide privileges. For more information on enabling audit logging in Audit logs are **disabled** by default. To enable this functionality, you must set `xpack.security.audit.enabled` to `true` in `kibana.yml`. -You can optionally configure audit logs location, file/rolling file appenders and +You can optionally configure audit logs location, file/rolling file appenders and ignore filters using <>. ============================================================================ [[xpack-security-ecs-audit-logging]] ==== Audit events -Refer to the table of events that can be logged for auditing purposes. +Refer to the table of events that can be logged for auditing purposes. Each event is broken down into <>, <>, <> and <> fields to make it easy to filter, query and aggregate the resulting logs. The <> field can be used to correlate multiple events that originate from the same request. -Refer to <> for a table of fields that get logged with audit event. +Refer to <> for a table of fields that get logged with audit event. [NOTE] ============================================================================ @@ -116,6 +116,38 @@ Refer to the corresponding {es} logs for potential write errors. .1+| `case_user_action_create_case` | `success` | User has created a case. +.2+| `ml_put_ad_job` +| `success` | Creating anomaly detection job. +| `failure` | Failed to create anomaly detection job. + +.2+| `ml_put_ad_datafeed` +| `success` | Creating anomaly detection datafeed. +| `failure` | Failed to create anomaly detection datafeed. + +.2+| `ml_put_calendar` +| `success` | Creating calendar. +| `failure` | Failed to create calendar. + +.2+| `ml_post_calendar_events` +| `success` | Adding events to calendar. +| `failure` | Failed to add events to calendar. + +.2+| `ml_forecast` +| `success` | Creating anomaly detection forecast. +| `failure` | Failed to create anomaly detection forecast. + +.2+| `ml_put_filter` +| `success` | Creating filter. +| `failure` | Failed to create filter. + +.2+| `ml_put_dfa_job` +| `success` | Creating data frame analytics job. +| `failure` | Failed to create data frame analytics job. + +.2+| `ml_put_trained_model` +| `success` | Creating trained model. +| `failure` | Failed to create trained model. + 3+a| ====== Type: change @@ -234,6 +266,74 @@ Refer to the corresponding {es} logs for potential write errors. .1+| `case_user_action_update_case_title` | `success` | User has updated the case title. +.2+| `ml_open_ad_job` +| `success` | Opening anomaly detection job. +| `failure` | Failed to open anomaly detection job. + +.2+| `ml_close_ad_job` +| `success` | Closing anomaly detection job. +| `failure` | Failed to close anomaly detection job. + +.2+| `ml_start_ad_datafeed` +| `success` | Starting anomaly detection datafeed. +| `failure` | Failed to start anomaly detection datafeed. + +.2+| `ml_stop_ad_datafeed` +| `success` | Stopping anomaly detection datafeed. +| `failure` | Failed to stop anomaly detection datafeed. + +.2+| `ml_update_ad_job` +| `success` | Updating anomaly detection job. +| `failure` | Failed to update anomaly detection job. + +.2+| `ml_reset_ad_job` +| `success` | Resetting anomaly detection job. +| `failure` | Failed to reset anomaly detection job. + +.2+| `ml_revert_ad_snapshot` +| `success` | Reverting anomaly detection snapshot. +| `failure` | Failed to revert anomaly detection snapshot. + +.2+| `ml_update_ad_datafeed` +| `success` | Updating anomaly detection datafeed. +| `failure` | Failed to update anomaly detection datafeed. + +.2+| `ml_put_calendar_job` +| `success` | Adding job to calendar. +| `failure` | Failed to add job to calendar. + +.2+| `ml_delete_calendar_job` +| `success` | Removing job from calendar. +| `failure` | Failed to remove job from calendar. + +.2+| `ml_update_filter` +| `success` | Updating filter. +| `failure` | Failed to update filter. + +.2+| `ml_start_dfa_job` +| `success` | Starting data frame analytics job. +| `failure` | Failed to start data frame analytics job. + +.2+| `ml_stop_dfa_job` +| `success` | Stopping data frame analytics job. +| `failure` | Failed to stop data frame analytics job. + +.2+| `ml_update_dfa_job` +| `success` | Updating data frame analytics job. +| `failure` | Failed to update data frame analytics job. + +.2+| `ml_start_trained_model_deployment` +| `success` | Starting trained model deployment. +| `failure` | Failed to start trained model deployment. + +.2+| `ml_stop_trained_model_deployment` +| `success` | Stopping trained model deployment. +| `failure` | Failed to stop trained model deployment. + +.2+| `ml_update_trained_model_deployment` +| `success` | Updating trained model deployment. +| `failure` | Failed to update trained model deployment. + 3+a| ====== Type: deletion @@ -289,6 +389,42 @@ Refer to the corresponding {es} logs for potential write errors. .1+| `case_user_action_delete_case_tags` | `success` | User has removed tags from a case. +.2+| `ml_delete_ad_job` +| `success` | Deleting anomaly detection job. +| `failure` | Failed to delete anomaly detection job. + +.2+| `ml_delete_model_snapshot` +| `success` | Deleting model snapshot. +| `failure` | Failed to delete model snapshot. + +.2+| `ml_delete_ad_datafeed` +| `success` | Deleting anomaly detection datafeed. +| `failure` | Failed to delete anomaly detection datafeed. + +.2+| `ml_delete_calendar` +| `success` | Deleting calendar. +| `failure` | Failed to delete calendar. + +.2+| `ml_delete_calendar_event` +| `success` | Deleting calendar event. +| `failure` | Failed to delete calendar event. + +.2+| `ml_delete_filter` +| `success` | Deleting filter. +| `failure` | Failed to delete filter. + +.2+| `ml_delete_forecast` +| `success` | Deleting forecast. +| `failure` | Failed to delete forecast. + +.2+| `ml_delete_dfa_job` +| `success` | Deleting data frame analytics job. +| `failure` | Failed to delete data frame analytics job. + +.2+| `ml_delete_trained_model` +| `success` | Deleting trained model. +| `failure` | Failed to delete trained model. + 3+a| ====== Type: access @@ -448,6 +584,10 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | User has accessed the connectors of a case. | `failure` | User is not authorized to access the connectors of a case. +.2+| `ml_infer_trained_model` +| `success` | Inferring using trained model. +| `failure` | Failed to infer using trained model. + 3+a| ===== Category: web @@ -474,12 +614,12 @@ Audit logs are written in JSON using https://www.elastic.co/guide/en/ecs/1.6/ind | *Description* | `@timestamp` -| Time when the event was generated. +| Time when the event was generated. Example: `2016-05-23T08:05:34.853Z` | `message` -| Human readable description of the event. +| Human readable description of the event. 2+a| ===== Event Fields @@ -489,7 +629,7 @@ Example: `2016-05-23T08:05:34.853Z` | [[field-event-action]] `event.action` | The action captured by the event. -Refer to <> for a table of possible actions. +Refer to <> for a table of possible actions. | [[field-event-category]] `event.category` | High level category associated with the event. @@ -513,7 +653,7 @@ Possible values: `deletion` | [[field-event-outcome]] `event.outcome` -a| Denotes whether the event represents a success or failure: +a| Denotes whether the event represents a success or failure: * Any actions that the user is not authorized to perform are logged with outcome: `failure` * Authorized read operations are only logged after successfully fetching the data from {es} with outcome: `success` @@ -553,7 +693,7 @@ Example: `[kibana_admin, reporting_user]` Example: `default` | `kibana.session_id` -| ID of the user session associated with the event. +| ID of the user session associated with the event. Each login attempt results in a unique session id. @@ -604,7 +744,7 @@ Example: `[marketing]` | Error code describing the error. | `error.message` -| Error message. +| Error message. 2+a| ===== HTTP and URL Fields diff --git a/x-pack/plugins/ml/server/lib/ml_client/index.ts b/x-pack/plugins/ml/server/lib/ml_client/index.ts index 03fc5e6244b31..92ef7b822e80a 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/index.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/index.ts @@ -8,3 +8,4 @@ export { getMlClient } from './ml_client'; export { MLJobNotFound, MLModelNotFound } from './errors'; export type { MlClient } from './types'; +export { MlAuditLogger } from './ml_audit_logger'; diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_audit_logger.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_audit_logger.ts new file mode 100644 index 0000000000000..1d678497cc205 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_audit_logger.ts @@ -0,0 +1,383 @@ +/* + * 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 { KibanaRequest } from '@kbn/core-http-server'; +import type { AuditLogger, CoreAuditService, EcsEvent } from '@kbn/core/server'; +import type { ArrayElement } from '@kbn/utility-types'; +import type { MlClient, MlClientParams } from './types'; +import { + getADJobIdsFromRequest, + getDFAJobIdsFromRequest, + getDatafeedIdsFromRequest, + getModelIdsFromRequest, +} from './ml_client'; + +type TaskTypeAD = + | 'ml_put_ad_job' + | 'ml_delete_ad_job' + | 'ml_delete_model_snapshot' + | 'ml_open_ad_job' + | 'ml_close_ad_job' + | 'ml_update_ad_job' + | 'ml_reset_ad_job' + | 'ml_revert_ad_snapshot' + | 'ml_put_ad_datafeed' + | 'ml_delete_ad_datafeed' + | 'ml_start_ad_datafeed' + | 'ml_stop_ad_datafeed' + | 'ml_update_ad_datafeed' + | 'ml_put_calendar' + | 'ml_delete_calendar' + | 'ml_put_calendar_job' + | 'ml_delete_calendar_job' + | 'ml_post_calendar_events' + | 'ml_delete_calendar_event' + | 'ml_put_filter' + | 'ml_update_filter' + | 'ml_delete_filter' + | 'ml_forecast' + | 'ml_delete_forecast'; + +type TaskTypeDFA = + | 'ml_put_dfa_job' + | 'ml_delete_dfa_job' + | 'ml_start_dfa_job' + | 'ml_stop_dfa_job' + | 'ml_update_dfa_job'; + +type TaskTypeNLP = + | 'ml_put_trained_model' + | 'ml_delete_trained_model' + | 'ml_start_trained_model_deployment' + | 'ml_stop_trained_model_deployment' + | 'ml_update_trained_model_deployment' + | 'ml_infer_trained_model'; + +type TaskType = TaskTypeAD | TaskTypeDFA | TaskTypeNLP; + +const APPLICATION = 'elastic/ml'; +const CATEGORY = 'database'; + +const EVENT_TYPES: Record> = { + creation: 'creation', + deletion: 'deletion', + change: 'change', + access: 'access', +} as const; +type EventTypes = keyof typeof EVENT_TYPES; + +interface MlLogEntry { + event: EcsEvent; + message: string; + labels: { application: typeof APPLICATION }; +} + +export class MlAuditLogger { + private auditLogger: AuditLogger; + constructor(audit: CoreAuditService, request?: KibanaRequest) { + this.auditLogger = request ? audit.asScoped(request) : audit.withoutRequest; + } + + public async wrapTask(task: () => T, taskType: TaskType, p: P) { + try { + const resp = await task(); + this.logSuccess(taskType, p); + return resp; + } catch (error) { + this.logFailure(taskType, p); + throw error; + } + } + + public logMessage(message: string) { + this.auditLogger.log({ + message, + labels: { + application: APPLICATION, + }, + }); + } + + private logSuccess(taskType: TaskType, p: MlClientParams) { + const entry = this.createLogEntry(taskType, p, true); + if (entry) { + this.auditLogger.log(entry); + } + } + private logFailure(taskType: TaskType, p: MlClientParams) { + const entry = this.createLogEntry(taskType, p, false); + if (entry) { + this.auditLogger.log(entry); + } + } + + private createLogEntry( + taskType: TaskType, + p: MlClientParams, + success: boolean + ): MlLogEntry | undefined { + try { + const { message, type } = this.createPartialLogEntry(taskType, p); + return { + event: { + action: taskType, + type, + category: [CATEGORY], + outcome: success ? 'success' : 'failure', + }, + message, + labels: { + application: APPLICATION, + }, + }; + } catch (error) { + // if an unknown task type is passed, we won't log anything + } + } + + private createPartialLogEntry( + taskType: TaskType, + p: MlClientParams + ): { message: string; type: EventTypes[] } { + switch (taskType) { + /* Anomaly Detection */ + case 'ml_put_ad_job': { + const [jobId] = getADJobIdsFromRequest(p); + return { message: `Creating anomaly detection job ${jobId}`, type: [EVENT_TYPES.creation] }; + } + case 'ml_delete_ad_job': { + const [jobId] = getADJobIdsFromRequest(p); + return { message: `Deleting anomaly detection job ${jobId}`, type: [EVENT_TYPES.deletion] }; + } + case 'ml_delete_model_snapshot': { + const [jobId] = getADJobIdsFromRequest(p); + const [params] = p as Parameters; + const snapshotId = params.snapshot_id; + return { + message: `Deleting model snapshot ${snapshotId} from job ${jobId}`, + type: [EVENT_TYPES.deletion], + }; + } + case 'ml_open_ad_job': { + const [jobId] = getADJobIdsFromRequest(p); + return { message: `Opening anomaly detection job ${jobId}`, type: [EVENT_TYPES.change] }; + } + case 'ml_close_ad_job': { + const [jobId] = getADJobIdsFromRequest(p); + return { message: `Closing anomaly detection job ${jobId}`, type: [EVENT_TYPES.change] }; + } + case 'ml_update_ad_job': { + const [jobId] = getADJobIdsFromRequest(p); + return { message: `Updating anomaly detection job ${jobId}`, type: [EVENT_TYPES.change] }; + } + case 'ml_reset_ad_job': { + const [jobId] = getADJobIdsFromRequest(p); + return { message: `Resetting anomaly detection job ${jobId}`, type: [EVENT_TYPES.change] }; + } + case 'ml_revert_ad_snapshot': { + const [jobId] = getADJobIdsFromRequest(p); + const [params] = p as Parameters; + const snapshotId = params.snapshot_id; + return { + message: `Reverting anomaly detection snapshot ${snapshotId} in job ${jobId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_put_ad_datafeed': { + const [datafeedId] = getDatafeedIdsFromRequest(p); + const [jobId] = getADJobIdsFromRequest(p); + return { + message: `Creating anomaly detection datafeed ${datafeedId} for job ${jobId}`, + type: [EVENT_TYPES.creation], + }; + } + case 'ml_delete_ad_datafeed': { + const [datafeedId] = getDatafeedIdsFromRequest(p); + return { + message: `Deleting anomaly detection datafeed ${datafeedId}`, + type: [EVENT_TYPES.deletion], + }; + } + case 'ml_start_ad_datafeed': { + const [datafeedId] = getDatafeedIdsFromRequest(p); + return { + message: `Starting anomaly detection datafeed ${datafeedId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_stop_ad_datafeed': { + const [datafeedId] = getDatafeedIdsFromRequest(p); + return { + message: `Stopping anomaly detection datafeed ${datafeedId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_update_ad_datafeed': { + const [datafeedId] = getDatafeedIdsFromRequest(p); + return { + message: `Updating anomaly detection datafeed ${datafeedId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_put_calendar': { + const [params] = p as Parameters; + const calendarId = params.calendar_id; + // @ts-expect-error body is optional + const jobIds = (params.body ?? params).job_ids; + return { + message: `Creating calendar ${calendarId} ${jobIds ? `with job(s) ${jobIds}` : ''}`, + type: [EVENT_TYPES.creation], + }; + } + case 'ml_delete_calendar': { + const [params] = p as Parameters; + const calendarId = params.calendar_id; + return { message: `Deleting calendar ${calendarId}`, type: [EVENT_TYPES.deletion] }; + } + case 'ml_put_calendar_job': { + const [params] = p as Parameters; + const calendarId = params.calendar_id; + const jobIds = params.job_id; + return { + message: `Adding job(s) ${jobIds} to calendar ${calendarId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_delete_calendar_job': { + const [params] = p as Parameters; + const calendarId = params.calendar_id; + const jobIds = params.job_id; + return { + message: `Removing job(s) ${jobIds} from calendar ${calendarId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_post_calendar_events': { + const [params] = p as Parameters; + const calendarId = params.calendar_id; + // @ts-expect-error body is optional + const eventsCount = (params.body ?? params).events; + return { + message: `Adding ${eventsCount} event(s) to calendar ${calendarId}`, + type: [EVENT_TYPES.creation], + }; + } + case 'ml_delete_calendar_event': { + const [params] = p as Parameters; + const calendarId = params.calendar_id; + const eventId = params.event_id; + return { + message: `Removing event(s) ${eventId} from calendar ${calendarId}`, + type: [EVENT_TYPES.deletion], + }; + } + case 'ml_put_filter': { + const [params] = p as Parameters; + const filterId = params.filter_id; + return { message: `Creating filter ${filterId}`, type: [EVENT_TYPES.creation] }; + } + case 'ml_update_filter': { + const [params] = p as Parameters; + const filterId = params.filter_id; + return { message: `Updating filter ${filterId}`, type: [EVENT_TYPES.change] }; + } + case 'ml_delete_filter': { + const [params] = p as Parameters; + const filterId = params.filter_id; + return { message: `Deleting filter ${filterId}`, type: [EVENT_TYPES.deletion] }; + } + case 'ml_forecast': { + const [jobId] = getADJobIdsFromRequest(p); + return { message: `Forecasting for job ${jobId}`, type: [EVENT_TYPES.creation] }; + } + case 'ml_delete_forecast': { + const [params] = p as Parameters; + const forecastId = params.forecast_id; + const [jobId] = getADJobIdsFromRequest(p); + return { + message: `Deleting forecast ${forecastId} for job ${jobId}`, + type: [EVENT_TYPES.deletion], + }; + } + + /* Data Frame Analytics */ + case 'ml_put_dfa_job': { + const [analyticsId] = getDFAJobIdsFromRequest(p); + return { + message: `Creating data frame analytics job ${analyticsId}`, + type: [EVENT_TYPES.creation], + }; + } + case 'ml_delete_dfa_job': { + const [analyticsId] = getDFAJobIdsFromRequest(p); + return { + message: `Deleting data frame analytics job ${analyticsId}`, + type: [EVENT_TYPES.deletion], + }; + } + case 'ml_start_dfa_job': { + const [analyticsId] = getDFAJobIdsFromRequest(p); + return { + message: `Starting data frame analytics job ${analyticsId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_stop_dfa_job': { + const [analyticsId] = getDFAJobIdsFromRequest(p); + return { + message: `Stopping data frame analytics job ${analyticsId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_update_dfa_job': { + const [analyticsId] = getDFAJobIdsFromRequest(p); + return { + message: `Updating data frame analytics job ${analyticsId}`, + type: [EVENT_TYPES.change], + }; + } + + /* Trained Models */ + case 'ml_put_trained_model': { + const [modelId] = getModelIdsFromRequest(p); + return { message: `Creating trained model ${modelId}`, type: [EVENT_TYPES.creation] }; + } + case 'ml_delete_trained_model': { + const [modelId] = getModelIdsFromRequest(p); + return { message: `Deleting trained model ${modelId}`, type: [EVENT_TYPES.deletion] }; + } + case 'ml_start_trained_model_deployment': { + const [modelId] = getModelIdsFromRequest(p); + return { + message: `Starting trained model deployment for model ${modelId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_stop_trained_model_deployment': { + const [modelId] = getModelIdsFromRequest(p); + return { + message: `Stopping trained model deployment for model ${modelId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_update_trained_model_deployment': { + const [modelId] = getModelIdsFromRequest(p); + return { + message: `Updating trained model deployment for model ${modelId}`, + type: [EVENT_TYPES.change], + }; + } + case 'ml_infer_trained_model': { + const [modelId] = getModelIdsFromRequest(p); + return { message: `Inferring trained model ${modelId}`, type: [EVENT_TYPES.access] }; + } + + default: + throw new Error(`Unsupported task type: ${taskType}`); + } + } +} diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index 33f0e1462b2b2..c7bf9ca8bc5d8 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -25,10 +25,12 @@ import type { MlGetDatafeedParams, MlGetTrainedModelParams, } from './types'; +import type { MlAuditLogger } from './ml_audit_logger'; export function getMlClient( client: IScopedClusterClient, - mlSavedObjectService: MLSavedObjectService + mlSavedObjectService: MLSavedObjectService, + auditLogger: MlAuditLogger ): MlClient { const mlClient = client.asInternalUser.ml; @@ -160,28 +162,44 @@ export function getMlClient( return { async closeJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.closeJob(...p); + return auditLogger.wrapTask(() => mlClient.closeJob(...p), 'ml_close_ad_job', p); }, async deleteCalendar(...p: Parameters) { - return mlClient.deleteCalendar(...p); + return auditLogger.wrapTask(() => mlClient.deleteCalendar(...p), 'ml_delete_calendar', p); }, async deleteCalendarEvent(...p: Parameters) { - return mlClient.deleteCalendarEvent(...p); + return auditLogger.wrapTask( + () => mlClient.deleteCalendarEvent(...p), + 'ml_delete_calendar_event', + p + ); }, async deleteCalendarJob(...p: Parameters) { - return mlClient.deleteCalendarJob(...p); + return auditLogger.wrapTask( + () => mlClient.deleteCalendarJob(...p), + 'ml_delete_calendar_job', + p + ); }, async deleteDataFrameAnalytics(...p: Parameters) { await jobIdsCheck('data-frame-analytics', p); - const resp = await mlClient.deleteDataFrameAnalytics(...p); + const resp = await auditLogger.wrapTask( + () => mlClient.deleteDataFrameAnalytics(...p), + 'ml_delete_dfa_job', + p + ); // don't delete the job saved object as the real job will not be // deleted initially and could still fail. return resp; }, async deleteDatafeed(...p: Parameters) { await datafeedIdsCheck(p); - const resp = await mlClient.deleteDatafeed(...p); const [datafeedId] = getDatafeedIdsFromRequest(p); + const resp = await auditLogger.wrapTask( + () => mlClient.deleteDatafeed(...p), + 'ml_delete_ad_datafeed', + p + ); if (datafeedId !== undefined) { await mlSavedObjectService.deleteDatafeed(datafeedId); } @@ -192,26 +210,33 @@ export function getMlClient( return mlClient.deleteExpiredData(...p); }, async deleteFilter(...p: Parameters) { - return mlClient.deleteFilter(...p); + return auditLogger.wrapTask(() => mlClient.deleteFilter(...p), 'ml_delete_filter', p); }, async deleteForecast(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.deleteForecast(...p); + return auditLogger.wrapTask(() => mlClient.deleteForecast(...p), 'ml_delete_forecast', p); }, async deleteJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - const resp = await mlClient.deleteJob(...p); + return auditLogger.wrapTask(() => mlClient.deleteJob(...p), 'ml_delete_ad_job', p); // don't delete the job saved object as the real job will not be // deleted initially and could still fail. - return resp; }, async deleteModelSnapshot(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.deleteModelSnapshot(...p); + return auditLogger.wrapTask( + () => mlClient.deleteModelSnapshot(...p), + 'ml_delete_model_snapshot', + p + ); }, async deleteTrainedModel(...p: Parameters) { await modelIdsCheck(p); - return mlClient.deleteTrainedModel(...p); + return auditLogger.wrapTask( + () => mlClient.deleteTrainedModel(...p), + 'ml_delete_trained_model', + p + ); }, async estimateModelMemory(...p: Parameters) { return mlClient.estimateModelMemory(...p); @@ -229,7 +254,7 @@ export function getMlClient( }, async forecast(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.forecast(...p); + return auditLogger.wrapTask(() => mlClient.forecast(...p), 'ml_forecast', p); }, async getBuckets(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); @@ -502,16 +527,21 @@ export function getMlClient( await modelIdsCheck(p); // TODO use mlClient.startTrainedModelDeployment when esClient is updated const { model_id: modelId, adaptive_allocations: adaptiveAllocations, ...queryParams } = p[0]; - return client.asInternalUser.transport.request( - { - method: 'POST', - path: `_ml/trained_models/${modelId}/deployment/_start`, - ...(isPopulatedObject(queryParams) ? { querystring: queryParams } : {}), - ...(isPopulatedObject(adaptiveAllocations) - ? { body: { adaptive_allocations: adaptiveAllocations } } - : {}), - }, - p[1] + return auditLogger.wrapTask( + () => + client.asInternalUser.transport.request( + { + method: 'POST', + path: `_ml/trained_models/${modelId}/deployment/_start`, + ...(isPopulatedObject(queryParams) ? { querystring: queryParams } : {}), + ...(isPopulatedObject(adaptiveAllocations) + ? { body: { adaptive_allocations: adaptiveAllocations } } + : {}), + }, + p[1] + ), + 'ml_start_trained_model_deployment', + p ); }, async updateTrainedModelDeployment(...p: Parameters) { @@ -519,17 +549,26 @@ export function getMlClient( const { deployment_id: deploymentId, model_id: modelId, ...bodyParams } = p[0]; // TODO use mlClient.updateTrainedModelDeployment when esClient is updated - return client.asInternalUser.transport.request({ - method: 'POST', - path: `/_ml/trained_models/${deploymentId}/deployment/_update`, - body: bodyParams, - }); + return auditLogger.wrapTask( + () => + client.asInternalUser.transport.request({ + method: 'POST', + path: `/_ml/trained_models/${deploymentId}/deployment/_update`, + body: bodyParams, + }), + 'ml_update_trained_model_deployment', + p + ); }, async stopTrainedModelDeployment(...p: Parameters) { await modelIdsCheck(p); switchDeploymentId(p); - return mlClient.stopTrainedModelDeployment(...p); + return auditLogger.wrapTask( + () => mlClient.stopTrainedModelDeployment(...p), + 'ml_stop_trained_model_deployment', + p + ); }, async inferTrainedModel(...p: Parameters) { await modelIdsCheck(p); @@ -547,14 +586,19 @@ export function getMlClient( // @ts-expect-error body doesn't exist in the type const { model_id: id, body, query: querystring } = p[0]; - return client.asInternalUser.transport.request( - { - method: 'POST', - path: `/_ml/trained_models/${id}/_infer`, - body, - querystring, - }, - p[1] + return auditLogger.wrapTask( + () => + client.asInternalUser.transport.request( + { + method: 'POST', + path: `/_ml/trained_models/${id}/_infer`, + body, + querystring, + }, + p[1] + ), + 'ml_infer_trained_model', + p ); }, async info(...p: Parameters) { @@ -562,10 +606,14 @@ export function getMlClient( }, async openJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.openJob(...p); + return auditLogger.wrapTask(() => mlClient.openJob(...p), 'ml_open_ad_job', p); }, async postCalendarEvents(...p: Parameters) { - return mlClient.postCalendarEvents(...p); + return auditLogger.wrapTask( + () => mlClient.postCalendarEvents(...p), + 'ml_post_calendar_events', + p + ); }, async postData(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); @@ -576,22 +624,30 @@ export function getMlClient( return mlClient.previewDatafeed(...p); }, async putCalendar(...p: Parameters) { - return mlClient.putCalendar(...p); + return auditLogger.wrapTask(() => mlClient.putCalendar(...p), 'ml_put_calendar', p); }, async putCalendarJob(...p: Parameters) { - return mlClient.putCalendarJob(...p); + return auditLogger.wrapTask(() => mlClient.putCalendarJob(...p), 'ml_put_calendar_job', p); }, async putDataFrameAnalytics(...p: Parameters) { - const resp = await mlClient.putDataFrameAnalytics(...p); const [analyticsId] = getDFAJobIdsFromRequest(p); + const resp = await auditLogger.wrapTask( + () => mlClient.putDataFrameAnalytics(...p), + 'ml_put_dfa_job', + p + ); if (analyticsId !== undefined) { await mlSavedObjectService.createDataFrameAnalyticsJob(analyticsId); } return resp; }, async putDatafeed(...p: Parameters) { - const resp = await mlClient.putDatafeed(...p); const [datafeedId] = getDatafeedIdsFromRequest(p); + const resp = await auditLogger.wrapTask( + () => mlClient.putDatafeed(...p), + 'ml_put_ad_datafeed', + p + ); const jobId = getJobIdFromBody(p); if (datafeedId !== undefined && jobId !== undefined) { await mlSavedObjectService.addDatafeed(datafeedId, jobId); @@ -600,18 +656,22 @@ export function getMlClient( return resp; }, async putFilter(...p: Parameters) { - return mlClient.putFilter(...p); + return auditLogger.wrapTask(() => mlClient.putFilter(...p), 'ml_put_filter', p); }, async putJob(...p: Parameters) { - const resp = await mlClient.putJob(...p); const [jobId] = getADJobIdsFromRequest(p); + const resp = await auditLogger.wrapTask(() => mlClient.putJob(...p), 'ml_put_ad_job', p); if (jobId !== undefined) { await mlSavedObjectService.createAnomalyDetectionJob(jobId); } return resp; }, async putTrainedModel(...p: Parameters) { - const resp = await mlClient.putTrainedModel(...p); + const resp = await auditLogger.wrapTask( + () => mlClient.putTrainedModel(...p), + 'ml_put_trained_model', + p + ); const [modelId] = getModelIdsFromRequest(p); if (modelId !== undefined) { const model = (p[0] as estypes.MlPutTrainedModelRequest).body; @@ -622,70 +682,61 @@ export function getMlClient( }, async revertModelSnapshot(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.revertModelSnapshot(...p); + return auditLogger.wrapTask( + () => mlClient.revertModelSnapshot(...p), + 'ml_revert_ad_snapshot', + p + ); }, async setUpgradeMode(...p: Parameters) { return mlClient.setUpgradeMode(...p); }, async startDataFrameAnalytics(...p: Parameters) { await jobIdsCheck('data-frame-analytics', p); - return mlClient.startDataFrameAnalytics(...p); + return auditLogger.wrapTask( + () => mlClient.startDataFrameAnalytics(...p), + 'ml_start_dfa_job', + p + ); }, async startDatafeed(...p: Parameters) { await datafeedIdsCheck(p); - return mlClient.startDatafeed(...p); + return auditLogger.wrapTask(() => mlClient.startDatafeed(...p), 'ml_start_ad_datafeed', p); }, async stopDataFrameAnalytics(...p: Parameters) { await jobIdsCheck('data-frame-analytics', p); - return mlClient.stopDataFrameAnalytics(...p); + return auditLogger.wrapTask( + () => mlClient.stopDataFrameAnalytics(...p), + 'ml_stop_dfa_job', + p + ); }, async stopDatafeed(...p: Parameters) { await datafeedIdsCheck(p); - return mlClient.stopDatafeed(...p); + return auditLogger.wrapTask(() => mlClient.stopDatafeed(...p), 'ml_stop_ad_datafeed', p); }, async updateDataFrameAnalytics(...p: Parameters) { await jobIdsCheck('data-frame-analytics', p); - return mlClient.updateDataFrameAnalytics(...p); + return auditLogger.wrapTask( + () => mlClient.updateDataFrameAnalytics(...p), + 'ml_update_dfa_job', + p + ); }, async updateDatafeed(...p: Parameters) { await datafeedIdsCheck(p); - - // Temporary workaround for the incorrect updateDatafeed function in the esclient - if ( - // @ts-expect-error TS complains it's always false - p.length === 0 || - p[0] === undefined - ) { - // Temporary generic error message. This should never be triggered - // but is added for type correctness below - throw new Error('Incorrect arguments supplied'); - } - // @ts-expect-error body doesn't exist in the type - const { datafeed_id: id, body } = p[0]; - - return client.asInternalUser.transport.request( - { - method: 'POST', - path: `/_ml/datafeeds/${id}/_update`, - body, - }, - p[1] - ); - - // this should be reinstated once https://github.com/elastic/elasticsearch-js/issues/1601 - // is fixed - // return mlClient.updateDatafeed(...p); + return auditLogger.wrapTask(() => mlClient.updateDatafeed(...p), 'ml_update_ad_datafeed', p); }, async updateFilter(...p: Parameters) { - return mlClient.updateFilter(...p); + return auditLogger.wrapTask(() => mlClient.updateFilter(...p), 'ml_update_filter', p); }, async updateJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.updateJob(...p); + return auditLogger.wrapTask(() => mlClient.updateJob(...p), 'ml_update_ad_job', p); }, async resetJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); - return mlClient.resetJob(...p); + return auditLogger.wrapTask(() => mlClient.resetJob(...p), 'ml_reset_ad_job', p); }, async updateModelSnapshot(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); @@ -705,29 +756,29 @@ export function getMlClient( } as MlClient; } -function getDFAJobIdsFromRequest([params]: MlGetDFAParams): string[] { +export function getDFAJobIdsFromRequest([params]: MlGetDFAParams): string[] { const ids = params?.id?.split(','); return ids || []; } -function getModelIdsFromRequest([params]: MlGetTrainedModelParams): string[] { +export function getModelIdsFromRequest([params]: MlGetTrainedModelParams): string[] { const id = params?.model_id; const ids = Array.isArray(id) ? id : id?.split(','); return ids || []; } -function getADJobIdsFromRequest([params]: MlGetADParams): string[] { +export function getADJobIdsFromRequest([params]: MlGetADParams): string[] { const ids = typeof params?.job_id === 'string' ? params?.job_id.split(',') : params?.job_id; return ids || []; } -function getDatafeedIdsFromRequest([params]: MlGetDatafeedParams): string[] { +export function getDatafeedIdsFromRequest([params]: MlGetDatafeedParams): string[] { const ids = typeof params?.datafeed_id === 'string' ? params?.datafeed_id.split(',') : params?.datafeed_id; return ids || []; } -function getJobIdFromBody(p: any): string | undefined { +export function getJobIdFromBody(p: any): string | undefined { const [params] = p; return params?.body?.job_id; } diff --git a/x-pack/plugins/ml/server/lib/route_guard.ts b/x-pack/plugins/ml/server/lib/route_guard.ts index 733dcb66f06f2..23a324e9fd2fc 100644 --- a/x-pack/plugins/ml/server/lib/route_guard.ts +++ b/x-pack/plugins/ml/server/lib/route_guard.ts @@ -27,7 +27,7 @@ import { mlSavedObjectServiceFactory } from '../saved_objects'; import type { MlLicense } from '../../common/license'; import type { MlClient } from './ml_client'; -import { getMlClient } from './ml_client'; +import { MlAuditLogger, getMlClient } from './ml_client'; import { getDataViewsServiceFactory } from './data_views_utils'; type MLRequestHandlerContext = CustomRequestHandlerContext<{ @@ -42,6 +42,7 @@ type Handler

= (handlerParams: { mlSavedObjectService: MLSavedObjectService; mlClient: MlClient; getDataViewsService(): Promise; + auditLogger: MlAuditLogger; }) => ReturnType>; type GetMlSavedObjectClient = (request: KibanaRequest) => SavedObjectsClientContract | null; @@ -121,6 +122,8 @@ export class RouteGuard { const [coreStart] = await this._getStartServices(); const executionContext = createExecutionContext(coreStart, PLUGIN_ID, request.route.path); + const auditLogger = new MlAuditLogger(coreStart.security.audit, request); + return await coreStart.executionContext.withContext(executionContext, () => handler({ client, @@ -128,13 +131,14 @@ export class RouteGuard { response, context, mlSavedObjectService, - mlClient: getMlClient(client, mlSavedObjectService), + mlClient: getMlClient(client, mlSavedObjectService, auditLogger), getDataViewsService: getDataViewsServiceFactory( this._getDataViews, savedObjectClient, client, request ), + auditLogger, }) ); }; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 2a36e94f2f2e7..01f3ec7e5d131 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -17,6 +17,7 @@ import type { IClusterClient, SavedObjectsServiceStart, UiSettingsServiceStart, + CoreAuditService, } from '@kbn/core/server'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; @@ -94,6 +95,7 @@ export class MlServerPlugin private home: HomeServerPluginSetup | null = null; private cases: CasesServerSetup | null | undefined = null; private dataViews: DataViewsPluginStart | null = null; + private auditService: CoreAuditService | null = null; private isMlReady: Promise; private setMlReady: () => void = () => {}; private savedObjectsSyncService: SavedObjectsSyncService; @@ -218,6 +220,7 @@ export class MlServerPlugin () => this.uiSettings, () => this.fieldsFormat, getDataViews, + () => this.auditService, () => this.isMlReady, this.compatibleModuleType ); @@ -313,6 +316,7 @@ export class MlServerPlugin this.capabilities = coreStart.capabilities; this.clusterClient = coreStart.elasticsearch.client; this.savedObjectsStart = coreStart.savedObjects; + this.auditService = coreStart.security.audit; this.dataViews = plugins.dataViews; this.mlLicense.setup(plugins.licensing.license$, async (mlLicense: MlLicense) => { diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index dc65a5bc01817..f7b0d42409221 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -11,6 +11,7 @@ import type { SavedObjectsClientContract, UiSettingsServiceStart, KibanaRequest, + CoreAuditService, } from '@kbn/core/server'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import { CoreKibanaRequest } from '@kbn/core/server'; @@ -49,7 +50,7 @@ import { MLUISettingsClientUninitialized, } from './errors'; import type { MlClient } from '../lib/ml_client'; -import { getMlClient } from '../lib/ml_client'; +import { getMlClient, MlAuditLogger } from '../lib/ml_client'; import type { MLSavedObjectService } from '../saved_objects'; import { mlSavedObjectServiceFactory } from '../saved_objects'; import type { MlAlertingServiceProvider } from './providers/alerting_service'; @@ -110,6 +111,7 @@ export function createSharedServices( getUiSettings: () => UiSettingsServiceStart | null, getFieldsFormat: () => FieldFormatsStart | null, getDataViews: () => DataViewsPluginStart, + getAuditService: () => CoreAuditService | null, isMlReady: () => Promise, compatibleModuleType: CompatibleModule | null ): { @@ -137,7 +139,8 @@ export function createSharedServices( isMlReady, getUiSettings, getFieldsFormat, - getDataViews + getDataViews, + getAuditService ); const { @@ -209,7 +212,8 @@ function getRequestItemsProvider( isMlReady: () => Promise, getUiSettings: () => UiSettingsServiceStart | null, getFieldsFormat: () => FieldFormatsStart | null, - getDataViews: () => DataViewsPluginStart + getDataViews: () => DataViewsPluginStart, + getAuditService: () => CoreAuditService | null ) { return (request: KibanaRequest) => { let hasMlCapabilities: HasMlCapabilities = hasMlCapabilitiesProvider( @@ -237,6 +241,11 @@ function getRequestItemsProvider( throw new MLClusterClientUninitialized(`ML's cluster client has not been initialized`); } + const auditService = getAuditService(); + if (!auditService) { + throw new Error('Audit service not initialized'); + } + const uiSettingsClient = getUiSettings()?.asScopedToClient(savedObjectsClient); if (!uiSettingsClient) { throw new MLUISettingsClientUninitialized(`ML's UI settings client has not been initialized`); @@ -263,7 +272,8 @@ function getRequestItemsProvider( if (request instanceof CoreKibanaRequest) { scopedClient = clusterClient.asScoped(request); mlSavedObjectService = getSobSavedObjectService(scopedClient); - mlClient = getMlClient(scopedClient, mlSavedObjectService); + const auditLogger = new MlAuditLogger(auditService, request); + mlClient = getMlClient(scopedClient, mlSavedObjectService, auditLogger); } else { hasMlCapabilities = () => Promise.resolve(); const { asInternalUser } = clusterClient; @@ -273,7 +283,8 @@ function getRequestItemsProvider( asSecondaryAuthUser: asInternalUser, }; mlSavedObjectService = getSobSavedObjectService(scopedClient); - mlClient = getMlClient(scopedClient, mlSavedObjectService); + const auditLogger = new MlAuditLogger(auditService); + mlClient = getMlClient(scopedClient, mlSavedObjectService, auditLogger); } const getDataViewsService = getDataViewsServiceFactory( diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 24ed35277b93a..73ab75d5f2515 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -25,28 +25,46 @@ }, // add references to other TypeScript projects the plugin depends on "@kbn/actions-plugin", + "@kbn/aiops-change-point-detection", + "@kbn/aiops-common", "@kbn/aiops-plugin", "@kbn/alerting-plugin", + "@kbn/alerts-as-data-utils", "@kbn/analytics", "@kbn/cases-plugin", "@kbn/charts-plugin", "@kbn/cloud-plugin", + "@kbn/code-editor", "@kbn/config-schema", + "@kbn/content-management-plugin", + "@kbn/core-elasticsearch-client-server-mocks", + "@kbn/core-elasticsearch-server", + "@kbn/core-http-browser", + "@kbn/core-http-server", + "@kbn/core-lifecycle-browser", + "@kbn/core-notifications-browser-mocks", + "@kbn/core-ui-settings-browser", "@kbn/dashboard-plugin", "@kbn/data-plugin", + "@kbn/data-view-editor-plugin", "@kbn/data-views-plugin", "@kbn/data-visualizer-plugin", + "@kbn/deeplinks-management", + "@kbn/deeplinks-ml", "@kbn/embeddable-plugin", "@kbn/es-query", "@kbn/es-types", "@kbn/es-ui-shared-plugin", + "@kbn/esql-utils", "@kbn/features-plugin", "@kbn/field-formats-plugin", "@kbn/field-types", "@kbn/home-plugin", "@kbn/i18n-react", "@kbn/i18n", + "@kbn/inference_integration_flyout", "@kbn/inspector-plugin", + "@kbn/json-schemas", "@kbn/kibana-react-plugin", "@kbn/kibana-utils-plugin", "@kbn/lens-plugin", @@ -55,28 +73,55 @@ "@kbn/management-plugin", "@kbn/maps-plugin", "@kbn/ml-agg-utils", + "@kbn/ml-anomaly-utils", + "@kbn/ml-category-validator", + "@kbn/ml-creation-wizard-utils", + "@kbn/ml-data-frame-analytics-utils", + "@kbn/ml-data-grid", + "@kbn/ml-data-view-utils", "@kbn/ml-date-picker", + "@kbn/ml-date-utils", + "@kbn/ml-error-utils", + "@kbn/ml-field-stats-flyout", + "@kbn/ml-in-memory-table", "@kbn/ml-is-defined", "@kbn/ml-is-populated-object", + "@kbn/ml-kibana-theme", "@kbn/ml-local-storage", "@kbn/ml-nested-property", "@kbn/ml-number-utils", + "@kbn/ml-parse-interval", "@kbn/ml-query-utils", "@kbn/ml-route-utils", + "@kbn/ml-runtime-field-utils", "@kbn/ml-string-hash", + "@kbn/ml-time-buckets", "@kbn/ml-trained-models-utils", "@kbn/ml-trained-models-utils", + "@kbn/ml-ui-actions", "@kbn/ml-url-state", + "@kbn/ml-validators", "@kbn/monaco", + "@kbn/observability-ai-assistant-plugin", + "@kbn/presentation-containers", + "@kbn/presentation-panel-plugin", + "@kbn/presentation-publishing", + "@kbn/presentation-util-plugin", + "@kbn/react-kibana-context-render", + "@kbn/react-kibana-mount", "@kbn/rison", + "@kbn/rule-data-utils", + "@kbn/rule-registry-plugin", "@kbn/saved-objects-finder-plugin", "@kbn/saved-objects-management-plugin", "@kbn/saved-search-plugin", "@kbn/security-plugin", + "@kbn/securitysolution-ecs", "@kbn/share-plugin", "@kbn/shared-ux-link-redirect-app", "@kbn/shared-ux-page-kibana-template", "@kbn/shared-ux-router", + "@kbn/shared-ux-utility", "@kbn/spaces-plugin", "@kbn/std", "@kbn/task-manager-plugin", @@ -84,53 +129,9 @@ "@kbn/triggers-actions-ui-plugin", "@kbn/ui-actions-plugin", "@kbn/ui-theme", + "@kbn/unified-field-list", "@kbn/unified-search-plugin", "@kbn/usage-collection-plugin", "@kbn/utility-types", - "@kbn/ml-error-utils", - "@kbn/ml-anomaly-utils", - "@kbn/ml-data-frame-analytics-utils", - "@kbn/ml-data-grid", - "@kbn/ml-kibana-theme", - "@kbn/ml-runtime-field-utils", - "@kbn/ml-date-utils", - "@kbn/ml-category-validator", - "@kbn/ml-ui-actions", - "@kbn/deeplinks-ml", - "@kbn/core-notifications-browser-mocks", - "@kbn/unified-field-list", - "@kbn/core-ui-settings-browser", - "@kbn/content-management-plugin", - "@kbn/ml-in-memory-table", - "@kbn/presentation-util-plugin", - "@kbn/react-kibana-mount", - "@kbn/core-http-browser", - "@kbn/data-view-editor-plugin", - "@kbn/rule-data-utils", - "@kbn/alerts-as-data-utils", - "@kbn/rule-registry-plugin", - "@kbn/securitysolution-ecs", - "@kbn/ml-data-view-utils", - "@kbn/ml-creation-wizard-utils", - "@kbn/deeplinks-management", - "@kbn/code-editor", - "@kbn/presentation-publishing", - "@kbn/core-elasticsearch-server", - "@kbn/core-elasticsearch-client-server-mocks", - "@kbn/ml-time-buckets", - "@kbn/aiops-change-point-detection", - "@kbn/inference_integration_flyout", - "@kbn/presentation-containers", - "@kbn/presentation-panel-plugin", - "@kbn/shared-ux-utility", - "@kbn/react-kibana-context-render", - "@kbn/esql-utils", - "@kbn/core-lifecycle-browser", - "@kbn/observability-ai-assistant-plugin", - "@kbn/json-schemas", - "@kbn/ml-field-stats-flyout", - "@kbn/ml-parse-interval", - "@kbn/ml-validators", - "@kbn/aiops-common" ] }