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" ] }