diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml
index b03abf9a15c67a..be16e5f1ee4086 100644
--- a/.buildkite/ftr_configs.yml
+++ b/.buildkite/ftr_configs.yml
@@ -123,6 +123,7 @@ enabled:
- x-pack/test/alerting_api_integration/basic/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts
+ - x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts
- x-pack/test/alerting_api_integration/spaces_only/config.ts
- x-pack/test/api_integration_basic/config.ts
diff --git a/.buildkite/scripts/steps/scalability/benchmarking.sh b/.buildkite/scripts/steps/scalability/benchmarking.sh
index 3fbf1896d18777..e47e0bc10a3c59 100755
--- a/.buildkite/scripts/steps/scalability/benchmarking.sh
+++ b/.buildkite/scripts/steps/scalability/benchmarking.sh
@@ -74,53 +74,9 @@ download_artifacts
echo "--- Clone kibana-load-testing repo and compile project"
checkout_and_compile_load_runner
-echo "--- Run Scalability Tests with Elasticsearch started only once and Kibana restart before each journey"
+echo "--- Run Scalability Tests"
cd "$KIBANA_DIR"
-node scripts/es snapshot&
-
-esPid=$!
-# Set trap on EXIT to stop Elasticsearch process
-trap "kill -9 $esPid" EXIT
-
-# unset env vars defined in other parts of CI for automatic APM collection of
-# Kibana. We manage APM config in our FTR config and performance service, and
-# APM treats config in the ENV with a very high precedence.
-unset ELASTIC_APM_ENVIRONMENT
-unset ELASTIC_APM_TRANSACTION_SAMPLE_RATE
-unset ELASTIC_APM_SERVER_URL
-unset ELASTIC_APM_SECRET_TOKEN
-unset ELASTIC_APM_ACTIVE
-unset ELASTIC_APM_CONTEXT_PROPAGATION_ONLY
-unset ELASTIC_APM_GLOBAL_LABELS
-unset ELASTIC_APM_MAX_QUEUE_SIZE
-unset ELASTIC_APM_METRICS_INTERVAL
-unset ELASTIC_APM_CAPTURE_SPAN_STACK_TRACES
-unset ELASTIC_APM_BREAKDOWN_METRICS
-
-
-export TEST_ES_DISABLE_STARTUP=true
-ES_HOST="localhost:9200"
-export TEST_ES_URL="http://elastic:changeme@${ES_HOST}"
-# Overriding Gatling default configuration
-export ES_URL="http://${ES_HOST}"
-
-# Pings the ES server every second for 2 mins until its status is green
-curl --retry 120 \
- --retry-delay 1 \
- --retry-connrefused \
- -I -XGET "${TEST_ES_URL}/_cluster/health?wait_for_nodes=>=1&wait_for_status=yellow"
-
-export ELASTIC_APM_ACTIVE=true
-
-for journey in scalability_traces/server/*; do
- export SCALABILITY_JOURNEY_PATH="$KIBANA_DIR/$journey"
- echo "--- Run scalability file: $SCALABILITY_JOURNEY_PATH"
- node scripts/functional_tests \
- --config x-pack/test/scalability/config.ts \
- --kibana-install-dir "$KIBANA_BUILD_LOCATION" \
- --logToFile \
- --debug
-done
+node scripts/run_scalability --kibana-install-dir "$KIBANA_BUILD_LOCATION" --journey-config-path "scalability_traces/server"
echo "--- Upload test results"
upload_test_results
diff --git a/docs/index-custom-title-page.html b/docs/index-custom-title-page.html
index 4c9fe7af5ba59d..7af50716913b44 100644
--- a/docs/index-custom-title-page.html
+++ b/docs/index-custom-title-page.html
@@ -63,8 +63,8 @@
Bring your data to life
- What's new
- Release notes
+ What's new
+ Release notes
How-to videos
diff --git a/package.json b/package.json
index 2f5d7a6349f14e..2a97978a24dbb0 100644
--- a/package.json
+++ b/package.json
@@ -97,7 +97,7 @@
},
"dependencies": {
"@appland/sql-parser": "^1.5.1",
- "@babel/runtime": "^7.20.1",
+ "@babel/runtime": "^7.20.6",
"@dnd-kit/core": "^3.1.1",
"@dnd-kit/sortable": "^4.0.0",
"@dnd-kit/utilities": "^2.0.0",
@@ -694,12 +694,12 @@
"devDependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@babel/cli": "^7.19.3",
- "@babel/core": "^7.20.2",
+ "@babel/core": "^7.20.5",
"@babel/eslint-parser": "^7.19.1",
"@babel/eslint-plugin": "^7.19.1",
- "@babel/generator": "^7.20.4",
+ "@babel/generator": "^7.20.5",
"@babel/helper-plugin-utils": "^7.20.2",
- "@babel/parser": "^7.20.3",
+ "@babel/parser": "^7.20.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
@@ -711,8 +711,8 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@babel/register": "^7.18.9",
- "@babel/traverse": "^7.20.1",
- "@babel/types": "^7.20.2",
+ "@babel/traverse": "^7.20.5",
+ "@babel/types": "^7.20.5",
"@bazel/ibazel": "^0.16.2",
"@bazel/typescript": "4.6.2",
"@cypress/code-coverage": "^3.10.0",
@@ -981,7 +981,7 @@
"argsplit": "^1.0.5",
"autoprefixer": "^10.4.7",
"axe-core": "^4.0.2",
- "babel-jest": "^29.2.2",
+ "babel-jest": "^29.3.1",
"babel-loader": "^8.2.5",
"babel-plugin-add-module-exports": "^1.0.4",
"babel-plugin-istanbul": "^6.1.1",
diff --git a/renovate.json b/renovate.json
index d4a31a44a40b0e..35a0f95d406af0 100644
--- a/renovate.json
+++ b/renovate.json
@@ -164,7 +164,6 @@
"@jest/console",
"@jest/reporters",
"@jest/types",
- "@types/jest",
"babel-jest",
"expect",
"jest",
diff --git a/scripts/run_scalability.js b/scripts/run_scalability.js
new file mode 100644
index 00000000000000..1524beb7e9401b
--- /dev/null
+++ b/scripts/run_scalability.js
@@ -0,0 +1,10 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+require('../src/setup_node_env');
+require('../src/dev/performance/run_scalability_cli');
diff --git a/src/dev/performance/run_scalability_cli.ts b/src/dev/performance/run_scalability_cli.ts
new file mode 100644
index 00000000000000..eec11e611f3e17
--- /dev/null
+++ b/src/dev/performance/run_scalability_cli.ts
@@ -0,0 +1,104 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createFlagError } from '@kbn/dev-cli-errors';
+import { run } from '@kbn/dev-cli-runner';
+import { REPO_ROOT } from '@kbn/utils';
+import fs from 'fs';
+import path from 'path';
+
+run(
+ async ({ log, flagsReader, procRunner }) => {
+ const kibanaInstallDir = flagsReader.path('kibana-install-dir');
+ const journeyConfigPath = flagsReader.requiredPath('journey-config-path');
+
+ if (kibanaInstallDir && !fs.existsSync(kibanaInstallDir)) {
+ throw createFlagError('--kibana-install-dir must be an existing directory');
+ }
+ if (
+ !fs.existsSync(journeyConfigPath) ||
+ (!fs.statSync(journeyConfigPath).isDirectory() && path.extname(journeyConfigPath) !== '.json')
+ ) {
+ throw createFlagError(
+ '--journey-config-path must be an existing directory or scalability json path'
+ );
+ }
+
+ const journeys = fs.statSync(journeyConfigPath).isDirectory()
+ ? fs
+ .readdirSync(journeyConfigPath)
+ .filter((fileName) => path.extname(fileName) === '.json')
+ .map((fileName) => path.resolve(journeyConfigPath, fileName))
+ : [journeyConfigPath];
+
+ log.info(`Found ${journeys.length} journeys to run:\n${JSON.stringify(journeys)}`);
+
+ const failedJourneys = [];
+
+ for (const journey of journeys) {
+ try {
+ process.stdout.write(`--- Running scalability journey: ${journey}\n`);
+ await runScalabilityJourney(journey, kibanaInstallDir);
+ } catch (e) {
+ log.error(e);
+ failedJourneys.push(journey);
+ }
+ }
+
+ if (failedJourneys.length > 0) {
+ throw new Error(`${failedJourneys.length} journeys failed: ${failedJourneys.join(',')}`);
+ }
+
+ async function runScalabilityJourney(filePath: string, kibanaDir?: string) {
+ // Pass in a clean APM environment, so that FTR can later
+ // set it's own values.
+ const cleanApmEnv = {
+ ELASTIC_APM_ACTIVE: undefined,
+ ELASTIC_APM_BREAKDOWN_METRICS: undefined,
+ ELASTIC_APM_CONTEXT_PROPAGATION_ONLY: undefined,
+ ELASTIC_APM_CAPTURE_SPAN_STACK_TRACES: undefined,
+ ELASTIC_APM_ENVIRONMENT: undefined,
+ ELASTIC_APM_GLOBAL_LABELS: undefined,
+ ELASTIC_APM_MAX_QUEUE_SIZE: undefined,
+ ELASTIC_APM_METRICS_INTERVAL: undefined,
+ ELASTIC_APM_SERVER_URL: undefined,
+ ELASTIC_APM_SECRET_TOKEN: undefined,
+ ELASTIC_APM_TRANSACTION_SAMPLE_RATE: undefined,
+ };
+
+ await procRunner.run('scalability-tests', {
+ cmd: 'node',
+ args: [
+ 'scripts/functional_tests',
+ ['--config', 'x-pack/test/scalability/config.ts'],
+ kibanaDir ? ['--kibana-install-dir', kibanaDir] : [],
+ '--debug',
+ '--logToFile',
+ '--bail',
+ ].flat(),
+ cwd: REPO_ROOT,
+ wait: true,
+ env: {
+ ...cleanApmEnv,
+ SCALABILITY_JOURNEY_PATH: filePath, // journey json file for Gatling test runner
+ KIBANA_DIR: REPO_ROOT, // Gatling test runner use it to find kbn/es archives
+ },
+ });
+ }
+ },
+ {
+ flags: {
+ string: ['kibana-install-dir', 'journey-config-path'],
+ help: `
+ --kibana-install-dir Run Kibana from existing install directory instead of from source
+ --journey-config-path Define a scalability journey config or directory with multiple
+ configs that should be executed
+ `,
+ },
+ }
+);
diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts
index ba7654694da265..e9c3c3a0cee73b 100644
--- a/x-pack/plugins/alerting/server/rules_client.mock.ts
+++ b/x-pack/plugins/alerting/server/rules_client.mock.ts
@@ -42,7 +42,6 @@ const createRulesClientMock = () => {
bulkDisableRules: jest.fn(),
snooze: jest.fn(),
unsnooze: jest.fn(),
- calculateIsSnoozedUntil: jest.fn(),
clearExpiredSnoozes: jest.fn(),
runSoon: jest.fn(),
clone: jest.fn(),
diff --git a/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts
new file mode 100644
index 00000000000000..e98fb875b74a76
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { RawRule } from '../../types';
+import { CreateAPIKeyResult } from '../types';
+
+export function apiKeyAsAlertAttributes(
+ apiKey: CreateAPIKeyResult | null,
+ username: string | null
+): Pick {
+ return apiKey && apiKey.apiKeysEnabled
+ ? {
+ apiKeyOwner: username,
+ apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'),
+ }
+ : {
+ apiKeyOwner: null,
+ apiKey: null,
+ };
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.test.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts
similarity index 95%
rename from x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.ts
rename to x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts
index 360bd0d72a5fad..e40d8e6c8c8548 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/apply_bulk_edit_operation.ts
+++ b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
import { set, get } from 'lodash';
-import type { BulkEditOperation, BulkEditFields } from '../rules_client';
+import type { BulkEditOperation, BulkEditFields } from '../types';
// defining an union type that will passed directly to generic function as a workaround for the issue similar to
// https://github.com/microsoft/TypeScript/issues/29479
diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.test.ts b/x-pack/plugins/alerting/server/rules_client/common/audit_events.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/audit_events.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/audit_events.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.ts b/x-pack/plugins/alerting/server/rules_client/common/audit_events.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/audit_events.ts
rename to x-pack/plugins/alerting/server/rules_client/common/audit_events.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.test.ts b/x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.ts b/x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/build_kuery_node_filter.ts
rename to x-pack/plugins/alerting/server/rules_client/common/build_kuery_node_filter.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/common/calculate_is_snoozed_until.ts b/x-pack/plugins/alerting/server/rules_client/common/calculate_is_snoozed_until.ts
new file mode 100644
index 00000000000000..d77a013ae3f6ba
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/calculate_is_snoozed_until.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { RuleSnooze } from '../../types';
+import { getRuleSnoozeEndTime } from '../../lib';
+
+export function calculateIsSnoozedUntil(rule: {
+ muteAll: boolean;
+ snoozeSchedule?: RuleSnooze;
+}): string | null {
+ const isSnoozedUntil = getRuleSnoozeEndTime(rule);
+ return isSnoozedUntil ? isSnoozedUntil.toISOString() : null;
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/common/constants.ts b/x-pack/plugins/alerting/server/rules_client/common/constants.ts
new file mode 100644
index 00000000000000..d72f80d41a91e4
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/constants.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 {
+ AlertingAuthorizationFilterType,
+ AlertingAuthorizationFilterOpts,
+} from '../../authorization';
+
+// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
+export const extractedSavedObjectParamReferenceNamePrefix = 'param:';
+
+// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
+export const preconfiguredConnectorActionRefPrefix = 'preconfigured:';
+
+export const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
+ type: AlertingAuthorizationFilterType.KQL,
+ fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' },
+};
+
+export const MAX_RULES_NUMBER_FOR_BULK_OPERATION = 10000;
+export const API_KEY_GENERATE_CONCURRENCY = 50;
+export const RULE_TYPE_CHECKS_CONCURRENCY = 50;
diff --git a/x-pack/plugins/alerting/server/rules_client/common/generate_api_key_name.ts b/x-pack/plugins/alerting/server/rules_client/common/generate_api_key_name.ts
new file mode 100644
index 00000000000000..bb5c8ae9bac232
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/generate_api_key_name.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { truncate, trim } from 'lodash';
+
+export function generateAPIKeyName(alertTypeId: string, alertName: string) {
+ return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 });
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/common/get_and_validate_common_bulk_options.ts b/x-pack/plugins/alerting/server/rules_client/common/get_and_validate_common_bulk_options.ts
new file mode 100644
index 00000000000000..e4497cce1e30b7
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/get_and_validate_common_bulk_options.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { BulkOptions, BulkOptionsFilter, BulkOptionsIds } from '../types';
+
+export const getAndValidateCommonBulkOptions = (options: BulkOptions) => {
+ const filter = (options as BulkOptionsFilter).filter;
+ const ids = (options as BulkOptionsIds).ids;
+
+ if (!ids && !filter) {
+ throw Boom.badRequest(
+ "Either 'ids' or 'filter' property in method's arguments should be provided"
+ );
+ }
+
+ if (ids?.length === 0) {
+ throw Boom.badRequest("'ids' property should not be an empty array");
+ }
+
+ if (ids && filter) {
+ throw Boom.badRequest(
+ "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments"
+ );
+ }
+ return { ids, filter };
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/common/include_fields_required_for_authentication.ts b/x-pack/plugins/alerting/server/rules_client/common/include_fields_required_for_authentication.ts
new file mode 100644
index 00000000000000..b89d56174c59b0
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/include_fields_required_for_authentication.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { uniq } from 'lodash';
+
+export function includeFieldsRequiredForAuthentication(fields: string[]): string[] {
+ return uniq([...fields, 'alertTypeId', 'consumer']);
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/common/index.ts b/x-pack/plugins/alerting/server/rules_client/common/index.ts
new file mode 100644
index 00000000000000..6604c417cc6393
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { mapSortField } from './map_sort_field';
+export { validateOperationOnAttributes } from './validate_attributes';
+export { retryIfBulkEditConflicts } from './retry_if_bulk_edit_conflicts';
+export { retryIfBulkDeleteConflicts } from './retry_if_bulk_delete_conflicts';
+export { retryIfBulkDisableConflicts } from './retry_if_bulk_disable_conflicts';
+export { retryIfBulkOperationConflicts } from './retry_if_bulk_operation_conflicts';
+export { applyBulkEditOperation } from './apply_bulk_edit_operation';
+export { buildKueryNodeFilter } from './build_kuery_node_filter';
+export { generateAPIKeyName } from './generate_api_key_name';
+export * from './mapped_params_utils';
+export { apiKeyAsAlertAttributes } from './api_key_as_alert_attributes';
+export { calculateIsSnoozedUntil } from './calculate_is_snoozed_until';
+export * from './inject_references';
+export { parseDate } from './parse_date';
+export { includeFieldsRequiredForAuthentication } from './include_fields_required_for_authentication';
+export { getAndValidateCommonBulkOptions } from './get_and_validate_common_bulk_options';
+export * from './snooze_utils';
diff --git a/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts b/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts
new file mode 100644
index 00000000000000..07565240ed5c47
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { omit } from 'lodash';
+import { SavedObjectReference, SavedObjectAttributes } from '@kbn/core/server';
+import { UntypedNormalizedRuleType } from '../../rule_type_registry';
+import { Rule, RawRule, RuleTypeParams } from '../../types';
+import {
+ preconfiguredConnectorActionRefPrefix,
+ extractedSavedObjectParamReferenceNamePrefix,
+} from './constants';
+
+export function injectReferencesIntoActions(
+ alertId: string,
+ actions: RawRule['actions'],
+ references: SavedObjectReference[]
+) {
+ return actions.map((action) => {
+ if (action.actionRef.startsWith(preconfiguredConnectorActionRefPrefix)) {
+ return {
+ ...omit(action, 'actionRef'),
+ id: action.actionRef.replace(preconfiguredConnectorActionRefPrefix, ''),
+ };
+ }
+
+ const reference = references.find((ref) => ref.name === action.actionRef);
+ if (!reference) {
+ throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);
+ }
+ return {
+ ...omit(action, 'actionRef'),
+ id: reference.id,
+ };
+ }) as Rule['actions'];
+}
+
+export function injectReferencesIntoParams<
+ Params extends RuleTypeParams,
+ ExtractedParams extends RuleTypeParams
+>(
+ ruleId: string,
+ ruleType: UntypedNormalizedRuleType,
+ ruleParams: SavedObjectAttributes | undefined,
+ references: SavedObjectReference[]
+): Params {
+ try {
+ const paramReferences = references
+ .filter((reference: SavedObjectReference) =>
+ reference.name.startsWith(extractedSavedObjectParamReferenceNamePrefix)
+ )
+ .map((reference: SavedObjectReference) => ({
+ ...reference,
+ name: reference.name.replace(extractedSavedObjectParamReferenceNamePrefix, ''),
+ }));
+ return ruleParams && ruleType?.useSavedObjectReferences?.injectReferences
+ ? (ruleType.useSavedObjectReferences.injectReferences(
+ ruleParams as ExtractedParams,
+ paramReferences
+ ) as Params)
+ : (ruleParams as Params);
+ } catch (err) {
+ throw Boom.badRequest(
+ `Error injecting reference into rule params for rule id ${ruleId} - ${err.message}`
+ );
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.test.ts b/x-pack/plugins/alerting/server/rules_client/common/map_sort_field.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/map_sort_field.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.ts b/x-pack/plugins/alerting/server/rules_client/common/map_sort_field.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/map_sort_field.ts
rename to x-pack/plugins/alerting/server/rules_client/common/map_sort_field.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts b/x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts b/x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts
rename to x-pack/plugins/alerting/server/rules_client/common/mapped_params_utils.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/common/parse_date.ts b/x-pack/plugins/alerting/server/rules_client/common/parse_date.ts
new file mode 100644
index 00000000000000..21c005605ea6f9
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/parse_date.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { i18n } from '@kbn/i18n';
+import { parseIsoOrRelativeDate } from '../../lib/iso_or_relative_date';
+
+export function parseDate(
+ dateString: string | undefined,
+ propertyName: string,
+ defaultValue: Date
+): Date {
+ if (dateString === undefined) {
+ return defaultValue;
+ }
+
+ const parsedDate = parseIsoOrRelativeDate(dateString);
+ if (parsedDate === undefined) {
+ throw Boom.badRequest(
+ i18n.translate('xpack.alerting.rulesClient.invalidDate', {
+ defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"',
+ values: {
+ field: propertyName,
+ dateValue: dateString,
+ },
+ })
+ );
+ }
+
+ return parsedDate;
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.test.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.ts
similarity index 98%
rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.ts
rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.ts
index 0c2bac9695c853..46a0932253276d 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_delete_conflicts.ts
+++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_delete_conflicts.ts
@@ -10,7 +10,7 @@ import { chunk } from 'lodash';
import { KueryNode } from '@kbn/es-query';
import { Logger } from '@kbn/core/server';
import { convertRuleIdsToKueryNode } from '../../lib';
-import { BulkOperationError } from '../rules_client';
+import { BulkOperationError } from '../types';
import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry';
const MAX_RULES_IDS_IN_RETRY = 1000;
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_disable_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_disable_conflicts.ts
similarity index 98%
rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_disable_conflicts.ts
rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_disable_conflicts.ts
index 90ecf57c029a0f..29fd62b9333f08 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_disable_conflicts.ts
+++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_disable_conflicts.ts
@@ -10,7 +10,7 @@ import { chunk } from 'lodash';
import { KueryNode } from '@kbn/es-query';
import { Logger, SavedObjectsBulkUpdateObject } from '@kbn/core/server';
import { convertRuleIdsToKueryNode } from '../../lib';
-import { BulkOperationError } from '../rules_client';
+import { BulkOperationError } from '../types';
import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry';
import { RawRule } from '../../types';
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.test.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts
similarity index 98%
rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.ts
rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts
index d893f2e9b5df88..984ce17d149e05 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_edit_conflicts.ts
+++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts
@@ -10,7 +10,7 @@ import { chunk } from 'lodash';
import { KueryNode } from '@kbn/es-query';
import { Logger, SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from '@kbn/core/server';
import { convertRuleIdsToKueryNode } from '../../lib';
-import { BulkOperationError } from '../rules_client';
+import { BulkOperationError } from '../types';
import { RawRule } from '../../types';
import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry';
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.test.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts
similarity index 98%
rename from x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.ts
rename to x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts
index 9b95335f3994c2..4210de12076238 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/retry_if_bulk_operation_conflicts.ts
+++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts
@@ -10,7 +10,7 @@ import { chunk } from 'lodash';
import { KueryNode } from '@kbn/es-query';
import { Logger, SavedObjectsBulkUpdateObject } from '@kbn/core/server';
import { convertRuleIdsToKueryNode } from '../../lib';
-import { BulkOperationError } from '../rules_client';
+import { BulkOperationError } from '../types';
import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry';
import { RawRule } from '../../types';
diff --git a/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts b/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts
new file mode 100644
index 00000000000000..2a6d1b3b06e7ae
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts
@@ -0,0 +1,139 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { RawRule, RuleSnoozeSchedule } from '../../types';
+import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed';
+
+export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) {
+ // If duration is -1, instead mute all
+ const { id: snoozeId, duration } = snoozeSchedule;
+
+ if (duration === -1) {
+ return {
+ muteAll: true,
+ snoozeSchedule: clearUnscheduledSnooze(attributes),
+ };
+ }
+ return {
+ snoozeSchedule: (snoozeId
+ ? clearScheduledSnoozesById(attributes, [snoozeId])
+ : clearUnscheduledSnooze(attributes)
+ ).concat(snoozeSchedule),
+ muteAll: false,
+ };
+}
+
+export function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) {
+ // If duration is -1, instead mute all
+ const { id: snoozeId, duration } = snoozeSchedule;
+
+ if (duration === -1) {
+ return {
+ muteAll: true,
+ snoozeSchedule: clearUnscheduledSnooze(attributes),
+ };
+ }
+
+ // Bulk adding snooze schedule, don't touch the existing snooze/indefinite snooze
+ if (snoozeId) {
+ const existingSnoozeSchedules = attributes.snoozeSchedule || [];
+ return {
+ muteAll: attributes.muteAll,
+ snoozeSchedule: [...existingSnoozeSchedules, snoozeSchedule],
+ };
+ }
+
+ // Bulk snoozing, don't touch the existing snooze schedules
+ return {
+ muteAll: false,
+ snoozeSchedule: [...clearUnscheduledSnooze(attributes), snoozeSchedule],
+ };
+}
+
+export function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) {
+ const snoozeSchedule = scheduleIds
+ ? clearScheduledSnoozesById(attributes, scheduleIds)
+ : clearCurrentActiveSnooze(attributes);
+
+ return {
+ snoozeSchedule,
+ ...(!scheduleIds ? { muteAll: false } : {}),
+ };
+}
+
+export function getBulkUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) {
+ // Bulk removing snooze schedules, don't touch the current snooze/indefinite snooze
+ if (scheduleIds) {
+ const newSchedules = clearScheduledSnoozesById(attributes, scheduleIds);
+ // Unscheduled snooze is also known as snooze now
+ const unscheduledSnooze =
+ attributes.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || [];
+
+ return {
+ snoozeSchedule: [...unscheduledSnooze, ...newSchedules],
+ muteAll: attributes.muteAll,
+ };
+ }
+
+ // Bulk unsnoozing, don't touch current snooze schedules that are NOT active
+ return {
+ snoozeSchedule: clearCurrentActiveSnooze(attributes),
+ muteAll: false,
+ };
+}
+
+export function clearUnscheduledSnooze(attributes: RawRule) {
+ // Clear any snoozes that have no ID property. These are "simple" snoozes created with the quick UI, e.g. snooze for 3 days starting now
+ return attributes.snoozeSchedule
+ ? attributes.snoozeSchedule.filter((s) => typeof s.id !== 'undefined')
+ : [];
+}
+
+export function clearScheduledSnoozesById(attributes: RawRule, ids: string[]) {
+ return attributes.snoozeSchedule
+ ? attributes.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id))
+ : [];
+}
+
+export function clearCurrentActiveSnooze(attributes: RawRule) {
+ // First attempt to cancel a simple (unscheduled) snooze
+ const clearedUnscheduledSnoozes = clearUnscheduledSnooze(attributes);
+ // Now clear any scheduled snoozes that are currently active and never recur
+ const activeSnoozes = getActiveScheduledSnoozes(attributes);
+ const activeSnoozeIds = activeSnoozes?.map((s) => s.id) ?? [];
+ const recurringSnoozesToSkip: string[] = [];
+ const clearedNonRecurringActiveSnoozes = clearedUnscheduledSnoozes.filter((s) => {
+ if (!activeSnoozeIds.includes(s.id!)) return true;
+ // Check if this is a recurring snooze, and return true if so
+ if (s.rRule.freq && s.rRule.count !== 1) {
+ recurringSnoozesToSkip.push(s.id!);
+ return true;
+ }
+ });
+ const clearedSnoozesAndSkippedRecurringSnoozes = clearedNonRecurringActiveSnoozes.map((s) => {
+ if (s.id && !recurringSnoozesToSkip.includes(s.id)) return s;
+ const currentRecurrence = activeSnoozes?.find((a) => a.id === s.id)?.lastOccurrence;
+ if (!currentRecurrence) return s;
+ return {
+ ...s,
+ skipRecurrences: (s.skipRecurrences ?? []).concat(currentRecurrence.toISOString()),
+ };
+ });
+ return clearedSnoozesAndSkippedRecurringSnoozes;
+}
+
+export function verifySnoozeScheduleLimit(attributes: Partial) {
+ const schedules = attributes.snoozeSchedule?.filter((snooze) => snooze.id);
+ if (schedules && schedules.length > 5) {
+ throw Error(
+ i18n.translate('xpack.alerting.rulesClient.snoozeSchedule.limitReached', {
+ defaultMessage: 'Rule cannot have more than 5 snooze schedules',
+ })
+ );
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts b/x-pack/plugins/alerting/server/rules_client/common/validate_attributes.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/validate_attributes.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts b/x-pack/plugins/alerting/server/rules_client/common/validate_attributes.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts
rename to x-pack/plugins/alerting/server/rules_client/common/validate_attributes.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.test.ts b/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.test.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.test.ts
rename to x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.test.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.ts b/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts
similarity index 100%
rename from x-pack/plugins/alerting/server/rules_client/lib/wait_before_next_retry.ts
rename to x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts
diff --git a/x-pack/plugins/alerting/server/rules_client/index.ts b/x-pack/plugins/alerting/server/rules_client/index.ts
index e5f2b18ee82d15..ed2b5a8558368f 100644
--- a/x-pack/plugins/alerting/server/rules_client/index.ts
+++ b/x-pack/plugins/alerting/server/rules_client/index.ts
@@ -6,3 +6,4 @@
*/
export * from './rules_client';
+export * from './types';
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/check_authorization_and_get_total.ts b/x-pack/plugins/alerting/server/rules_client/lib/check_authorization_and_get_total.ts
new file mode 100644
index 00000000000000..ecaa7fd172fa79
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/check_authorization_and_get_total.ts
@@ -0,0 +1,104 @@
+/*
+ * 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 pMap from 'p-map';
+import Boom from '@hapi/boom';
+import { KueryNode } from '@kbn/es-query';
+import { RawRule } from '../../types';
+import { WriteOperations, ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { BulkAction, RuleBulkOperationAggregation } from '../types';
+import {
+ MAX_RULES_NUMBER_FOR_BULK_OPERATION,
+ RULE_TYPE_CHECKS_CONCURRENCY,
+} from '../common/constants';
+import { RulesClientContext } from '../types';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+
+export const checkAuthorizationAndGetTotal = async (
+ context: RulesClientContext,
+ {
+ filter,
+ action,
+ }: {
+ filter: KueryNode | null;
+ action: BulkAction;
+ }
+) => {
+ const actionToConstantsMapping: Record<
+ BulkAction,
+ { WriteOperation: WriteOperations | ReadOperations; RuleAuditAction: RuleAuditAction }
+ > = {
+ DELETE: {
+ WriteOperation: WriteOperations.BulkDelete,
+ RuleAuditAction: RuleAuditAction.DELETE,
+ },
+ ENABLE: {
+ WriteOperation: WriteOperations.BulkEnable,
+ RuleAuditAction: RuleAuditAction.ENABLE,
+ },
+ DISABLE: {
+ WriteOperation: WriteOperations.BulkDisable,
+ RuleAuditAction: RuleAuditAction.DISABLE,
+ },
+ };
+ const { aggregations, total } = await context.unsecuredSavedObjectsClient.find<
+ RawRule,
+ RuleBulkOperationAggregation
+ >({
+ filter,
+ page: 1,
+ perPage: 0,
+ type: 'alert',
+ aggs: {
+ alertTypeId: {
+ multi_terms: {
+ terms: [
+ { field: 'alert.attributes.alertTypeId' },
+ { field: 'alert.attributes.consumer' },
+ ],
+ },
+ },
+ },
+ });
+
+ if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) {
+ throw Boom.badRequest(
+ `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk ${action.toLocaleLowerCase()}`
+ );
+ }
+
+ const buckets = aggregations?.alertTypeId.buckets;
+
+ if (buckets === undefined || buckets?.length === 0) {
+ throw Boom.badRequest(`No rules found for bulk ${action.toLocaleLowerCase()}`);
+ }
+
+ await pMap(
+ buckets,
+ async ({ key: [ruleType, consumer] }) => {
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType);
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: ruleType,
+ consumer,
+ operation: actionToConstantsMapping[action].WriteOperation,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: actionToConstantsMapping[action].RuleAuditAction,
+ error,
+ })
+ );
+ throw error;
+ }
+ },
+ { concurrency: RULE_TYPE_CHECKS_CONCURRENCY }
+ );
+ return { total };
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts
new file mode 100644
index 00000000000000..202ab4d23972d6
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { RawRule } from '../../types';
+import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common';
+import { RulesClientContext } from '../types';
+
+export async function createNewAPIKeySet(
+ context: RulesClientContext,
+ {
+ attributes,
+ username,
+ }: {
+ attributes: RawRule;
+ username: string | null;
+ }
+): Promise> {
+ let createdAPIKey = null;
+ try {
+ createdAPIKey = await context.createAPIKey(
+ generateAPIKeyName(attributes.alertTypeId, attributes.name)
+ );
+ } catch (error) {
+ throw Boom.badRequest(`Error creating API key for rule: ${error.message}`);
+ }
+
+ return apiKeyAsAlertAttributes(createdAPIKey, username);
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts
new file mode 100644
index 00000000000000..45ade4086af4a2
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SavedObjectReference, SavedObject } from '@kbn/core/server';
+import { RawRule, RuleTypeParams } from '../../types';
+import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { SavedObjectOptions } from '../types';
+import { RulesClientContext } from '../types';
+import { updateMeta } from './update_meta';
+import { scheduleTask } from './schedule_task';
+import { getAlertFromRaw } from './get_alert_from_raw';
+
+export async function createRuleSavedObject(
+ context: RulesClientContext,
+ {
+ intervalInMs,
+ rawRule,
+ references,
+ ruleId,
+ options,
+ }: {
+ intervalInMs: number;
+ rawRule: RawRule;
+ references: SavedObjectReference[];
+ ruleId: string;
+ options?: SavedObjectOptions;
+ }
+) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id: ruleId },
+ })
+ );
+
+ let createdAlert: SavedObject;
+ try {
+ createdAlert = await context.unsecuredSavedObjectsClient.create(
+ 'alert',
+ updateMeta(context, rawRule),
+ {
+ ...options,
+ references,
+ id: ruleId,
+ }
+ );
+ } catch (e) {
+ // Avoid unused API key
+ await bulkMarkApiKeysForInvalidation(
+ { apiKeys: rawRule.apiKey ? [rawRule.apiKey] : [] },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ );
+
+ throw e;
+ }
+ if (rawRule.enabled) {
+ let scheduledTask;
+ try {
+ scheduledTask = await scheduleTask(context, {
+ id: createdAlert.id,
+ consumer: rawRule.consumer,
+ ruleTypeId: rawRule.alertTypeId,
+ schedule: rawRule.schedule,
+ throwOnConflict: true,
+ });
+ } catch (e) {
+ // Cleanup data, something went wrong scheduling the task
+ try {
+ await context.unsecuredSavedObjectsClient.delete('alert', createdAlert.id);
+ } catch (err) {
+ // Skip the cleanup error and throw the task manager error to avoid confusion
+ context.logger.error(
+ `Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}`
+ );
+ }
+ throw e;
+ }
+ await context.unsecuredSavedObjectsClient.update('alert', createdAlert.id, {
+ scheduledTaskId: scheduledTask.id,
+ });
+ createdAlert.attributes.scheduledTaskId = scheduledTask.id;
+ }
+
+ // Log warning if schedule interval is less than the minimum but we're not enforcing it
+ if (
+ intervalInMs < context.minimumScheduleIntervalInMs &&
+ !context.minimumScheduleInterval.enforce
+ ) {
+ context.logger.warn(
+ `Rule schedule interval (${rawRule.schedule.interval}) for "${createdAlert.attributes.alertTypeId}" rule type with ID "${createdAlert.id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.`
+ );
+ }
+
+ return getAlertFromRaw(
+ context,
+ createdAlert.id,
+ createdAlert.attributes.alertTypeId,
+ createdAlert.attributes,
+ references,
+ false,
+ true
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts
new file mode 100644
index 00000000000000..0f7a164d2a7413
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { SavedObjectReference } from '@kbn/core/server';
+import { RawRule } from '../../types';
+import { preconfiguredConnectorActionRefPrefix } from '../common/constants';
+import { RulesClientContext } from '../types';
+import { NormalizedAlertAction } from '../types';
+
+export async function denormalizeActions(
+ context: RulesClientContext,
+ alertActions: NormalizedAlertAction[]
+): Promise<{ actions: RawRule['actions']; references: SavedObjectReference[] }> {
+ const references: SavedObjectReference[] = [];
+ const actions: RawRule['actions'] = [];
+ if (alertActions.length) {
+ const actionsClient = await context.getActionsClient();
+ const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))];
+ const actionResults = await actionsClient.getBulk(actionIds);
+ const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))];
+ actionTypeIds.forEach((id) => {
+ // Notify action type usage via "isActionTypeEnabled" function
+ actionsClient.isActionTypeEnabled(id, { notifyUsage: true });
+ });
+ alertActions.forEach(({ id, ...alertAction }, i) => {
+ const actionResultValue = actionResults.find((action) => action.id === id);
+ if (actionResultValue) {
+ if (actionsClient.isPreconfigured(id)) {
+ actions.push({
+ ...alertAction,
+ actionRef: `${preconfiguredConnectorActionRefPrefix}${id}`,
+ actionTypeId: actionResultValue.actionTypeId,
+ });
+ } else {
+ const actionRef = `action_${i}`;
+ references.push({
+ id,
+ name: actionRef,
+ type: 'action',
+ });
+ actions.push({
+ ...alertAction,
+ actionRef,
+ actionTypeId: actionResultValue.actionTypeId,
+ });
+ }
+ } else {
+ actions.push({
+ ...alertAction,
+ actionRef: '',
+ actionTypeId: '',
+ });
+ }
+ });
+ }
+ return {
+ actions,
+ references,
+ };
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts b/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts
new file mode 100644
index 00000000000000..58f6f6ab20dbcd
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { SavedObjectReference } from '@kbn/core/server';
+import { RawRule, RuleTypeParams } from '../../types';
+import { UntypedNormalizedRuleType } from '../../rule_type_registry';
+import { NormalizedAlertAction } from '../types';
+import { extractedSavedObjectParamReferenceNamePrefix } from '../common/constants';
+import { RulesClientContext } from '../types';
+import { denormalizeActions } from './denormalize_actions';
+
+export async function extractReferences<
+ Params extends RuleTypeParams,
+ ExtractedParams extends RuleTypeParams
+>(
+ context: RulesClientContext,
+ ruleType: UntypedNormalizedRuleType,
+ ruleActions: NormalizedAlertAction[],
+ ruleParams: Params
+): Promise<{
+ actions: RawRule['actions'];
+ params: ExtractedParams;
+ references: SavedObjectReference[];
+}> {
+ const { references: actionReferences, actions } = await denormalizeActions(context, ruleActions);
+
+ // Extracts any references using configured reference extractor if available
+ const extractedRefsAndParams = ruleType?.useSavedObjectReferences?.extractReferences
+ ? ruleType.useSavedObjectReferences.extractReferences(ruleParams)
+ : null;
+ const extractedReferences = extractedRefsAndParams?.references ?? [];
+ const params = (extractedRefsAndParams?.params as ExtractedParams) ?? ruleParams;
+
+ // Prefix extracted references in order to avoid clashes with framework level references
+ const paramReferences = extractedReferences.map((reference: SavedObjectReference) => ({
+ ...reference,
+ name: `${extractedSavedObjectParamReferenceNamePrefix}${reference.name}`,
+ }));
+
+ const references = [...actionReferences, ...paramReferences];
+
+ return {
+ actions,
+ params,
+ references,
+ };
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts b/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts
new file mode 100644
index 00000000000000..72cd5c0ec4b1a7
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts
@@ -0,0 +1,137 @@
+/*
+ * 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 { omit, isEmpty } from 'lodash';
+import { SavedObjectReference } from '@kbn/core/server';
+import {
+ Rule,
+ PartialRule,
+ RawRule,
+ IntervalSchedule,
+ RuleTypeParams,
+ RuleWithLegacyId,
+ PartialRuleWithLegacyId,
+} from '../../types';
+import { ruleExecutionStatusFromRaw, convertMonitoringFromRawAndVerify } from '../../lib';
+import { UntypedNormalizedRuleType } from '../../rule_type_registry';
+import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed';
+import {
+ calculateIsSnoozedUntil,
+ injectReferencesIntoActions,
+ injectReferencesIntoParams,
+} from '../common';
+import { RulesClientContext } from '../types';
+
+export function getAlertFromRaw(
+ context: RulesClientContext,
+ id: string,
+ ruleTypeId: string,
+ rawRule: RawRule,
+ references: SavedObjectReference[] | undefined,
+ includeLegacyId: boolean = false,
+ excludeFromPublicApi: boolean = false,
+ includeSnoozeData: boolean = false
+): Rule | RuleWithLegacyId {
+ const ruleType = context.ruleTypeRegistry.get(ruleTypeId);
+ // In order to support the partial update API of Saved Objects we have to support
+ // partial updates of an Alert, but when we receive an actual RawRule, it is safe
+ // to cast the result to an Alert
+ const res = getPartialRuleFromRaw(
+ context,
+ id,
+ ruleType,
+ rawRule,
+ references,
+ includeLegacyId,
+ excludeFromPublicApi,
+ includeSnoozeData
+ );
+ // include to result because it is for internal rules client usage
+ if (includeLegacyId) {
+ return res as RuleWithLegacyId;
+ }
+ // exclude from result because it is an internal variable
+ return omit(res, ['legacyId']) as Rule;
+}
+
+export function getPartialRuleFromRaw(
+ context: RulesClientContext,
+ id: string,
+ ruleType: UntypedNormalizedRuleType,
+ {
+ createdAt,
+ updatedAt,
+ meta,
+ notifyWhen,
+ legacyId,
+ scheduledTaskId,
+ params,
+ executionStatus,
+ monitoring,
+ nextRun,
+ schedule,
+ actions,
+ snoozeSchedule,
+ ...partialRawRule
+ }: Partial,
+ references: SavedObjectReference[] | undefined,
+ includeLegacyId: boolean = false,
+ excludeFromPublicApi: boolean = false,
+ includeSnoozeData: boolean = false
+): PartialRule | PartialRuleWithLegacyId {
+ const snoozeScheduleDates = snoozeSchedule?.map((s) => ({
+ ...s,
+ rRule: {
+ ...s.rRule,
+ dtstart: new Date(s.rRule.dtstart),
+ ...(s.rRule.until ? { until: new Date(s.rRule.until) } : {}),
+ },
+ }));
+ const includeSnoozeSchedule =
+ snoozeSchedule !== undefined && !isEmpty(snoozeSchedule) && !excludeFromPublicApi;
+ const isSnoozedUntil = includeSnoozeSchedule
+ ? calculateIsSnoozedUntil({
+ muteAll: partialRawRule.muteAll ?? false,
+ snoozeSchedule,
+ })
+ : null;
+ const includeMonitoring = monitoring && !excludeFromPublicApi;
+ const rule = {
+ id,
+ notifyWhen,
+ ...omit(partialRawRule, excludeFromPublicApi ? [...context.fieldsToExcludeFromPublicApi] : ''),
+ // we currently only support the Interval Schedule type
+ // Once we support additional types, this type signature will likely change
+ schedule: schedule as IntervalSchedule,
+ actions: actions ? injectReferencesIntoActions(id, actions, references || []) : [],
+ params: injectReferencesIntoParams(id, ruleType, params, references || []) as Params,
+ ...(excludeFromPublicApi ? {} : { snoozeSchedule: snoozeScheduleDates ?? [] }),
+ ...(includeSnoozeData && !excludeFromPublicApi
+ ? {
+ activeSnoozes: getActiveScheduledSnoozes({
+ snoozeSchedule,
+ muteAll: partialRawRule.muteAll ?? false,
+ })?.map((s) => s.id),
+ isSnoozedUntil,
+ }
+ : {}),
+ ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}),
+ ...(createdAt ? { createdAt: new Date(createdAt) } : {}),
+ ...(scheduledTaskId ? { scheduledTaskId } : {}),
+ ...(executionStatus
+ ? { executionStatus: ruleExecutionStatusFromRaw(context.logger, id, executionStatus) }
+ : {}),
+ ...(includeMonitoring
+ ? { monitoring: convertMonitoringFromRawAndVerify(context.logger, id, monitoring) }
+ : {}),
+ ...(nextRun ? { nextRun: new Date(nextRun) } : {}),
+ };
+
+ return includeLegacyId
+ ? ({ ...rule, legacyId } as PartialRuleWithLegacyId)
+ : (rule as PartialRule);
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts b/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts
new file mode 100644
index 00000000000000..28e42c6b12e425
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { AlertingAuthorizationEntity } from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+import { alertingAuthorizationFilterOpts } from '../common/constants';
+import { BulkAction } from '../types';
+
+export const getAuthorizationFilter = async (
+ context: RulesClientContext,
+ { action }: { action: BulkAction }
+) => {
+ try {
+ const authorizationTuple = await context.authorization.getFindAuthorizationFilter(
+ AlertingAuthorizationEntity.Rule,
+ alertingAuthorizationFilterOpts
+ );
+ return authorizationTuple.filter;
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction[action],
+ error,
+ })
+ );
+ throw error;
+ }
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/index.ts b/x-pack/plugins/alerting/server/rules_client/lib/index.ts
index f7e0620222ec67..1f9534a5c6da28 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/index.ts
+++ b/x-pack/plugins/alerting/server/rules_client/lib/index.ts
@@ -5,11 +5,13 @@
* 2.0.
*/
-export { mapSortField } from './map_sort_field';
-export { validateOperationOnAttributes } from './validate_attributes';
-export { retryIfBulkEditConflicts } from './retry_if_bulk_edit_conflicts';
-export { retryIfBulkDeleteConflicts } from './retry_if_bulk_delete_conflicts';
-export { retryIfBulkDisableConflicts } from './retry_if_bulk_disable_conflicts';
-export { retryIfBulkOperationConflicts } from './retry_if_bulk_operation_conflicts';
-export { applyBulkEditOperation } from './apply_bulk_edit_operation';
-export { buildKueryNodeFilter } from './build_kuery_node_filter';
+export { createRuleSavedObject } from './create_rule_saved_object';
+export { extractReferences } from './extract_references';
+export { validateActions } from './validate_actions';
+export { updateMeta } from './update_meta';
+export * from './get_alert_from_raw';
+export { getAuthorizationFilter } from './get_authorization_filter';
+export { checkAuthorizationAndGetTotal } from './check_authorization_and_get_total';
+export { scheduleTask } from './schedule_task';
+export { createNewAPIKeySet } from './create_new_api_key_set';
+export { recoverRuleAlerts } from './recover_rule_alerts';
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts
new file mode 100644
index 00000000000000..aaa84a8b6950bf
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { mapValues } from 'lodash';
+import { SAVED_OBJECT_REL_PRIMARY } from '@kbn/event-log-plugin/server';
+import { RawRule, SanitizedRule, RawAlertInstance as RawAlert } from '../../types';
+import { taskInstanceToAlertTaskInstance } from '../../task_runner/alert_task_instance';
+import { Alert } from '../../alert';
+import { EVENT_LOG_ACTIONS } from '../../plugin';
+import { createAlertEventLogRecordObject } from '../../lib/create_alert_event_log_record_object';
+import { RulesClientContext } from '../types';
+
+export const recoverRuleAlerts = async (
+ context: RulesClientContext,
+ id: string,
+ attributes: RawRule
+) => {
+ if (!context.eventLogger || !attributes.scheduledTaskId) return;
+ try {
+ const { state } = taskInstanceToAlertTaskInstance(
+ await context.taskManager.get(attributes.scheduledTaskId),
+ attributes as unknown as SanitizedRule
+ );
+
+ const recoveredAlerts = mapValues, Alert>(
+ state.alertInstances ?? {},
+ (rawAlertInstance, alertId) => new Alert(alertId, rawAlertInstance)
+ );
+ const recoveredAlertIds = Object.keys(recoveredAlerts);
+
+ for (const alertId of recoveredAlertIds) {
+ const { group: actionGroup } = recoveredAlerts[alertId].getLastScheduledActions() ?? {};
+ const instanceState = recoveredAlerts[alertId].getState();
+ const message = `instance '${alertId}' has recovered due to the rule was disabled`;
+
+ const event = createAlertEventLogRecordObject({
+ ruleId: id,
+ ruleName: attributes.name,
+ ruleType: context.ruleTypeRegistry.get(attributes.alertTypeId),
+ consumer: attributes.consumer,
+ instanceId: alertId,
+ action: EVENT_LOG_ACTIONS.recoveredInstance,
+ message,
+ state: instanceState,
+ group: actionGroup,
+ namespace: context.namespace,
+ spaceId: context.spaceId,
+ savedObjects: [
+ {
+ id,
+ type: 'alert',
+ typeId: attributes.alertTypeId,
+ relation: SAVED_OBJECT_REL_PRIMARY,
+ },
+ ],
+ });
+ context.eventLogger.logEvent(event);
+ }
+ } catch (error) {
+ // this should not block the rest of the disable process
+ context.logger.warn(
+ `rulesClient.disable('${id}') - Could not write recovery events - ${error.message}`
+ );
+ }
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/schedule_task.ts b/x-pack/plugins/alerting/server/rules_client/lib/schedule_task.ts
new file mode 100644
index 00000000000000..eecdcf0314d026
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/schedule_task.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { RulesClientContext } from '../types';
+import { ScheduleTaskOptions } from '../types';
+
+export async function scheduleTask(context: RulesClientContext, opts: ScheduleTaskOptions) {
+ const { id, consumer, ruleTypeId, schedule, throwOnConflict } = opts;
+ const taskInstance = {
+ id, // use the same ID for task document as the rule
+ taskType: `alerting:${ruleTypeId}`,
+ schedule,
+ params: {
+ alertId: id,
+ spaceId: context.spaceId,
+ consumer,
+ },
+ state: {
+ previousStartedAt: null,
+ alertTypeState: {},
+ alertInstances: {},
+ },
+ scope: ['alerting'],
+ enabled: true,
+ };
+ try {
+ return await context.taskManager.schedule(taskInstance);
+ } catch (err) {
+ if (err.statusCode === 409 && !throwOnConflict) {
+ return taskInstance;
+ }
+ throw err;
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/update_meta.ts b/x-pack/plugins/alerting/server/rules_client/lib/update_meta.ts
new file mode 100644
index 00000000000000..5fbe2b275f0778
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/update_meta.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { RawRule } from '../../types';
+import { RulesClientContext } from '../types';
+
+export function updateMeta>(
+ context: RulesClientContext,
+ alertAttributes: T
+): T {
+ if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) {
+ alertAttributes.meta = alertAttributes.meta ?? {};
+ alertAttributes.meta.versionApiKeyLastmodified = context.kibanaVersion;
+ }
+ return alertAttributes;
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
new file mode 100644
index 00000000000000..683b95c0663438
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { map } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { RawRule } from '../../types';
+import { UntypedNormalizedRuleType } from '../../rule_type_registry';
+import { NormalizedAlertAction } from '../types';
+import { RulesClientContext } from '../types';
+
+export async function validateActions(
+ context: RulesClientContext,
+ alertType: UntypedNormalizedRuleType,
+ data: Pick & { actions: NormalizedAlertAction[] }
+): Promise {
+ const { actions, notifyWhen, throttle } = data;
+ const hasNotifyWhen = typeof notifyWhen !== 'undefined';
+ const hasThrottle = typeof throttle !== 'undefined';
+ let usesRuleLevelFreqParams;
+ if (hasNotifyWhen && hasThrottle) usesRuleLevelFreqParams = true;
+ else if (!hasNotifyWhen && !hasThrottle) usesRuleLevelFreqParams = false;
+ else {
+ throw Boom.badRequest(
+ i18n.translate('xpack.alerting.rulesClient.usesValidGlobalFreqParams.oneUndefined', {
+ defaultMessage:
+ 'Rule-level notifyWhen and throttle must both be defined or both be undefined',
+ })
+ );
+ }
+
+ if (actions.length === 0) {
+ return;
+ }
+
+ // check for actions using connectors with missing secrets
+ const actionsClient = await context.getActionsClient();
+ const actionIds = [...new Set(actions.map((action) => action.id))];
+ const actionResults = (await actionsClient.getBulk(actionIds)) || [];
+ const actionsUsingConnectorsWithMissingSecrets = actionResults.filter(
+ (result) => result.isMissingSecrets
+ );
+
+ if (actionsUsingConnectorsWithMissingSecrets.length) {
+ throw Boom.badRequest(
+ i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', {
+ defaultMessage: 'Invalid connectors: {groups}',
+ values: {
+ groups: actionsUsingConnectorsWithMissingSecrets
+ .map((connector) => connector.name)
+ .join(', '),
+ },
+ })
+ );
+ }
+
+ // check for actions with invalid action groups
+ const { actionGroups: alertTypeActionGroups } = alertType;
+ const usedAlertActionGroups = actions.map((action) => action.group);
+ const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id'));
+ const invalidActionGroups = usedAlertActionGroups.filter(
+ (group) => !availableAlertTypeActionGroups.has(group)
+ );
+ if (invalidActionGroups.length) {
+ throw Boom.badRequest(
+ i18n.translate('xpack.alerting.rulesClient.validateActions.invalidGroups', {
+ defaultMessage: 'Invalid action groups: {groups}',
+ values: {
+ groups: invalidActionGroups.join(', '),
+ },
+ })
+ );
+ }
+
+ // check for actions using frequency params if the rule has rule-level frequency params defined
+ if (usesRuleLevelFreqParams) {
+ const actionsWithFrequency = actions.filter((action) => Boolean(action.frequency));
+ if (actionsWithFrequency.length) {
+ throw Boom.badRequest(
+ i18n.translate('xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams', {
+ defaultMessage:
+ 'Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: {groups}',
+ values: {
+ groups: actionsWithFrequency.map((a) => a.group).join(', '),
+ },
+ })
+ );
+ }
+ } else {
+ const actionsWithoutFrequency = actions.filter((action) => !action.frequency);
+ if (actionsWithoutFrequency.length) {
+ throw Boom.badRequest(
+ i18n.translate('xpack.alerting.rulesClient.validateActions.notAllActionsWithFreq', {
+ defaultMessage: 'Actions missing frequency parameters: {groups}',
+ values: {
+ groups: actionsWithoutFrequency.map((a) => a.group).join(', '),
+ },
+ })
+ );
+ }
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts
new file mode 100644
index 00000000000000..79a07b3ebad496
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts
@@ -0,0 +1,223 @@
+/*
+ * 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 { KueryNode, nodeBuilder } from '@kbn/es-query';
+import { RawRule, RuleExecutionStatusValues, RuleLastRunOutcomeValues } from '../../types';
+import { AlertingAuthorizationEntity } from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { buildKueryNodeFilter } from '../common';
+import { alertingAuthorizationFilterOpts } from '../common/constants';
+import { RulesClientContext } from '../types';
+
+export interface AggregateOptions extends IndexType {
+ search?: string;
+ defaultSearchOperator?: 'AND' | 'OR';
+ searchFields?: string[];
+ hasReference?: {
+ type: string;
+ id: string;
+ };
+ filter?: string | KueryNode;
+}
+
+interface IndexType {
+ [key: string]: unknown;
+}
+
+export interface AggregateResult {
+ alertExecutionStatus: { [status: string]: number };
+ ruleLastRunOutcome: { [status: string]: number };
+ ruleEnabledStatus?: { enabled: number; disabled: number };
+ ruleMutedStatus?: { muted: number; unmuted: number };
+ ruleSnoozedStatus?: { snoozed: number };
+ ruleTags?: string[];
+}
+
+export interface RuleAggregation {
+ status: {
+ buckets: Array<{
+ key: string;
+ doc_count: number;
+ }>;
+ };
+ outcome: {
+ buckets: Array<{
+ key: string;
+ doc_count: number;
+ }>;
+ };
+ muted: {
+ buckets: Array<{
+ key: number;
+ key_as_string: string;
+ doc_count: number;
+ }>;
+ };
+ enabled: {
+ buckets: Array<{
+ key: number;
+ key_as_string: string;
+ doc_count: number;
+ }>;
+ };
+ snoozed: {
+ count: {
+ doc_count: number;
+ };
+ };
+ tags: {
+ buckets: Array<{
+ key: string;
+ doc_count: number;
+ }>;
+ };
+}
+
+export async function aggregate(
+ context: RulesClientContext,
+ { options: { fields, filter, ...options } = {} }: { options?: AggregateOptions } = {}
+): Promise {
+ let authorizationTuple;
+ try {
+ authorizationTuple = await context.authorization.getFindAuthorizationFilter(
+ AlertingAuthorizationEntity.Rule,
+ alertingAuthorizationFilterOpts
+ );
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.AGGREGATE,
+ error,
+ })
+ );
+ throw error;
+ }
+
+ const { filter: authorizationFilter } = authorizationTuple;
+ const filterKueryNode = buildKueryNodeFilter(filter);
+
+ const resp = await context.unsecuredSavedObjectsClient.find({
+ ...options,
+ filter:
+ authorizationFilter && filterKueryNode
+ ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
+ : authorizationFilter,
+ page: 1,
+ perPage: 0,
+ type: 'alert',
+ aggs: {
+ status: {
+ terms: { field: 'alert.attributes.executionStatus.status' },
+ },
+ outcome: {
+ terms: { field: 'alert.attributes.lastRun.outcome' },
+ },
+ enabled: {
+ terms: { field: 'alert.attributes.enabled' },
+ },
+ muted: {
+ terms: { field: 'alert.attributes.muteAll' },
+ },
+ tags: {
+ terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 },
+ },
+ snoozed: {
+ nested: {
+ path: 'alert.attributes.snoozeSchedule',
+ },
+ aggs: {
+ count: {
+ filter: {
+ exists: {
+ field: 'alert.attributes.snoozeSchedule.duration',
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ if (!resp.aggregations) {
+ // Return a placeholder with all zeroes
+ const placeholder: AggregateResult = {
+ alertExecutionStatus: {},
+ ruleLastRunOutcome: {},
+ ruleEnabledStatus: {
+ enabled: 0,
+ disabled: 0,
+ },
+ ruleMutedStatus: {
+ muted: 0,
+ unmuted: 0,
+ },
+ ruleSnoozedStatus: { snoozed: 0 },
+ };
+
+ for (const key of RuleExecutionStatusValues) {
+ placeholder.alertExecutionStatus[key] = 0;
+ }
+
+ return placeholder;
+ }
+
+ const alertExecutionStatus = resp.aggregations.status.buckets.map(
+ ({ key, doc_count: docCount }) => ({
+ [key]: docCount,
+ })
+ );
+
+ const ruleLastRunOutcome = resp.aggregations.outcome.buckets.map(
+ ({ key, doc_count: docCount }) => ({
+ [key]: docCount,
+ })
+ );
+
+ const ret: AggregateResult = {
+ alertExecutionStatus: alertExecutionStatus.reduce(
+ (acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
+ {}
+ ),
+ ruleLastRunOutcome: ruleLastRunOutcome.reduce(
+ (acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
+ {}
+ ),
+ };
+
+ // Fill missing keys with zeroes
+ for (const key of RuleExecutionStatusValues) {
+ if (!ret.alertExecutionStatus.hasOwnProperty(key)) {
+ ret.alertExecutionStatus[key] = 0;
+ }
+ }
+ for (const key of RuleLastRunOutcomeValues) {
+ if (!ret.ruleLastRunOutcome.hasOwnProperty(key)) {
+ ret.ruleLastRunOutcome[key] = 0;
+ }
+ }
+
+ const enabledBuckets = resp.aggregations.enabled.buckets;
+ ret.ruleEnabledStatus = {
+ enabled: enabledBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
+ disabled: enabledBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
+ };
+
+ const mutedBuckets = resp.aggregations.muted.buckets;
+ ret.ruleMutedStatus = {
+ muted: mutedBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
+ unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
+ };
+
+ ret.ruleSnoozedStatus = {
+ snoozed: resp.aggregations.snoozed?.count?.doc_count ?? 0,
+ };
+
+ const tagsBuckets = resp.aggregations.tags?.buckets || [];
+ ret.ruleTags = tagsBuckets.map((bucket) => bucket.key);
+
+ return ret;
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts
new file mode 100644
index 00000000000000..66bbd86bf91554
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts
@@ -0,0 +1,153 @@
+/*
+ * 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 { KueryNode, nodeBuilder } from '@kbn/es-query';
+import { SavedObjectsBulkDeleteObject } from '@kbn/core/server';
+import { RawRule } from '../../types';
+import { convertRuleIdsToKueryNode } from '../../lib';
+import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { getAuthorizationFilter, checkAuthorizationAndGetTotal } from '../lib';
+import {
+ retryIfBulkDeleteConflicts,
+ buildKueryNodeFilter,
+ getAndValidateCommonBulkOptions,
+} from '../common';
+import { BulkOptions, BulkOperationError, RulesClientContext } from '../types';
+
+export const bulkDeleteRules = async (context: RulesClientContext, options: BulkOptions) => {
+ const { ids, filter } = getAndValidateCommonBulkOptions(options);
+
+ const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter);
+ const authorizationFilter = await getAuthorizationFilter(context, { action: 'DELETE' });
+
+ const kueryNodeFilterWithAuth =
+ authorizationFilter && kueryNodeFilter
+ ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode])
+ : kueryNodeFilter;
+
+ const { total } = await checkAuthorizationAndGetTotal(context, {
+ filter: kueryNodeFilterWithAuth,
+ action: 'DELETE',
+ });
+
+ const { apiKeysToInvalidate, errors, taskIdsToDelete } = await retryIfBulkDeleteConflicts(
+ context.logger,
+ (filterKueryNode: KueryNode | null) => bulkDeleteWithOCC(context, { filter: filterKueryNode }),
+ kueryNodeFilterWithAuth
+ );
+
+ const taskIdsFailedToBeDeleted: string[] = [];
+ const taskIdsSuccessfullyDeleted: string[] = [];
+ if (taskIdsToDelete.length > 0) {
+ try {
+ const resultFromDeletingTasks = await context.taskManager.bulkRemoveIfExist(taskIdsToDelete);
+ resultFromDeletingTasks?.statuses.forEach((status) => {
+ if (status.success) {
+ taskIdsSuccessfullyDeleted.push(status.id);
+ } else {
+ taskIdsFailedToBeDeleted.push(status.id);
+ }
+ });
+ if (taskIdsSuccessfullyDeleted.length) {
+ context.logger.debug(
+ `Successfully deleted schedules for underlying tasks: ${taskIdsSuccessfullyDeleted.join(
+ ', '
+ )}`
+ );
+ }
+ if (taskIdsFailedToBeDeleted.length) {
+ context.logger.error(
+ `Failure to delete schedules for underlying tasks: ${taskIdsFailedToBeDeleted.join(', ')}`
+ );
+ }
+ } catch (error) {
+ context.logger.error(
+ `Failure to delete schedules for underlying tasks: ${taskIdsToDelete.join(
+ ', '
+ )}. TaskManager bulkRemoveIfExist failed with Error: ${error.message}`
+ );
+ }
+ }
+
+ await bulkMarkApiKeysForInvalidation(
+ { apiKeys: apiKeysToInvalidate },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ );
+
+ return { errors, total, taskIdsFailedToBeDeleted };
+};
+
+const bulkDeleteWithOCC = async (
+ context: RulesClientContext,
+ { filter }: { filter: KueryNode | null }
+) => {
+ const rulesFinder =
+ await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser(
+ {
+ filter,
+ type: 'alert',
+ perPage: 100,
+ ...(context.namespace ? { namespaces: [context.namespace] } : undefined),
+ }
+ );
+
+ const rules: SavedObjectsBulkDeleteObject[] = [];
+ const apiKeysToInvalidate: string[] = [];
+ const taskIdsToDelete: string[] = [];
+ const errors: BulkOperationError[] = [];
+ const apiKeyToRuleIdMapping: Record = {};
+ const taskIdToRuleIdMapping: Record = {};
+ const ruleNameToRuleIdMapping: Record = {};
+
+ for await (const response of rulesFinder.find()) {
+ for (const rule of response.saved_objects) {
+ if (rule.attributes.apiKey) {
+ apiKeyToRuleIdMapping[rule.id] = rule.attributes.apiKey;
+ }
+ if (rule.attributes.name) {
+ ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
+ }
+ if (rule.attributes.scheduledTaskId) {
+ taskIdToRuleIdMapping[rule.id] = rule.attributes.scheduledTaskId;
+ }
+ rules.push(rule);
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.DELETE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id: rule.id },
+ })
+ );
+ }
+ }
+
+ const result = await context.unsecuredSavedObjectsClient.bulkDelete(rules);
+
+ result.statuses.forEach((status) => {
+ if (status.error === undefined) {
+ if (apiKeyToRuleIdMapping[status.id]) {
+ apiKeysToInvalidate.push(apiKeyToRuleIdMapping[status.id]);
+ }
+ if (taskIdToRuleIdMapping[status.id]) {
+ taskIdsToDelete.push(taskIdToRuleIdMapping[status.id]);
+ }
+ } else {
+ errors.push({
+ message: status.error.message ?? 'n/a',
+ status: status.error.statusCode,
+ rule: {
+ id: status.id,
+ name: ruleNameToRuleIdMapping[status.id] ?? 'n/a',
+ },
+ });
+ }
+ });
+ return { apiKeysToInvalidate, errors, taskIdsToDelete };
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts
new file mode 100644
index 00000000000000..1a3b12e618fd21
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts
@@ -0,0 +1,227 @@
+/*
+ * 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 pMap from 'p-map';
+import { KueryNode, nodeBuilder } from '@kbn/es-query';
+import { SavedObjectsBulkUpdateObject } from '@kbn/core/server';
+import { RawRule } from '../../types';
+import { convertRuleIdsToKueryNode } from '../../lib';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import {
+ retryIfBulkDisableConflicts,
+ buildKueryNodeFilter,
+ getAndValidateCommonBulkOptions,
+} from '../common';
+import {
+ getAuthorizationFilter,
+ checkAuthorizationAndGetTotal,
+ getAlertFromRaw,
+ recoverRuleAlerts,
+ updateMeta,
+} from '../lib';
+import { BulkOptions, BulkOperationError, RulesClientContext } from '../types';
+
+export const bulkDisableRules = async (context: RulesClientContext, options: BulkOptions) => {
+ const { ids, filter } = getAndValidateCommonBulkOptions(options);
+
+ const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter);
+ const authorizationFilter = await getAuthorizationFilter(context, { action: 'DISABLE' });
+
+ const kueryNodeFilterWithAuth =
+ authorizationFilter && kueryNodeFilter
+ ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode])
+ : kueryNodeFilter;
+
+ const { total } = await checkAuthorizationAndGetTotal(context, {
+ filter: kueryNodeFilterWithAuth,
+ action: 'DISABLE',
+ });
+
+ const { errors, rules, taskIdsToDisable, taskIdsToDelete } = await retryIfBulkDisableConflicts(
+ context.logger,
+ (filterKueryNode: KueryNode | null) =>
+ bulkDisableRulesWithOCC(context, { filter: filterKueryNode }),
+ kueryNodeFilterWithAuth
+ );
+
+ if (taskIdsToDisable.length > 0) {
+ try {
+ const resultFromDisablingTasks = await context.taskManager.bulkDisable(taskIdsToDisable);
+ if (resultFromDisablingTasks.tasks.length) {
+ context.logger.debug(
+ `Successfully disabled schedules for underlying tasks: ${resultFromDisablingTasks.tasks
+ .map((task) => task.id)
+ .join(', ')}`
+ );
+ }
+ if (resultFromDisablingTasks.errors.length) {
+ context.logger.error(
+ `Failure to disable schedules for underlying tasks: ${resultFromDisablingTasks.errors
+ .map((error) => error.task.id)
+ .join(', ')}`
+ );
+ }
+ } catch (error) {
+ context.logger.error(
+ `Failure to disable schedules for underlying tasks: ${taskIdsToDisable.join(
+ ', '
+ )}. TaskManager bulkDisable failed with Error: ${error.message}`
+ );
+ }
+ }
+
+ const taskIdsFailedToBeDeleted: string[] = [];
+ const taskIdsSuccessfullyDeleted: string[] = [];
+
+ if (taskIdsToDelete.length > 0) {
+ try {
+ const resultFromDeletingTasks = await context.taskManager.bulkRemoveIfExist(taskIdsToDelete);
+ resultFromDeletingTasks?.statuses.forEach((status) => {
+ if (status.success) {
+ taskIdsSuccessfullyDeleted.push(status.id);
+ } else {
+ taskIdsFailedToBeDeleted.push(status.id);
+ }
+ });
+ if (taskIdsSuccessfullyDeleted.length) {
+ context.logger.debug(
+ `Successfully deleted schedules for underlying tasks: ${taskIdsSuccessfullyDeleted.join(
+ ', '
+ )}`
+ );
+ }
+ if (taskIdsFailedToBeDeleted.length) {
+ context.logger.error(
+ `Failure to delete schedules for underlying tasks: ${taskIdsFailedToBeDeleted.join(', ')}`
+ );
+ }
+ } catch (error) {
+ context.logger.error(
+ `Failure to delete schedules for underlying tasks: ${taskIdsToDelete.join(
+ ', '
+ )}. TaskManager bulkRemoveIfExist failed with Error: ${error.message}`
+ );
+ }
+ }
+
+ const updatedRules = rules.map(({ id, attributes, references }) => {
+ return getAlertFromRaw(
+ context,
+ id,
+ attributes.alertTypeId as string,
+ attributes as RawRule,
+ references,
+ false
+ );
+ });
+
+ return { errors, rules: updatedRules, total };
+};
+
+const bulkDisableRulesWithOCC = async (
+ context: RulesClientContext,
+ { filter }: { filter: KueryNode | null }
+) => {
+ const rulesFinder =
+ await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser(
+ {
+ filter,
+ type: 'alert',
+ perPage: 100,
+ ...(context.namespace ? { namespaces: [context.namespace] } : undefined),
+ }
+ );
+
+ const rulesToDisable: Array> = [];
+ const errors: BulkOperationError[] = [];
+ const ruleNameToRuleIdMapping: Record = {};
+
+ for await (const response of rulesFinder.find()) {
+ await pMap(response.saved_objects, async (rule) => {
+ try {
+ if (rule.attributes.enabled === false) return;
+
+ recoverRuleAlerts(context, rule.id, rule.attributes);
+
+ if (rule.attributes.name) {
+ ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
+ }
+
+ const username = await context.getUserName();
+ const updatedAttributes = updateMeta(context, {
+ ...rule.attributes,
+ enabled: false,
+ scheduledTaskId:
+ rule.attributes.scheduledTaskId === rule.id ? rule.attributes.scheduledTaskId : null,
+ updatedBy: username,
+ updatedAt: new Date().toISOString(),
+ });
+
+ rulesToDisable.push({
+ ...rule,
+ attributes: {
+ ...updatedAttributes,
+ },
+ });
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.DISABLE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id: rule.id },
+ })
+ );
+ } catch (error) {
+ errors.push({
+ message: error.message,
+ rule: {
+ id: rule.id,
+ name: rule.attributes?.name,
+ },
+ });
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.DISABLE,
+ error,
+ })
+ );
+ }
+ });
+ }
+
+ const result = await context.unsecuredSavedObjectsClient.bulkCreate(rulesToDisable, {
+ overwrite: true,
+ });
+
+ const taskIdsToDisable: string[] = [];
+ const taskIdsToDelete: string[] = [];
+ const disabledRules: Array> = [];
+
+ result.saved_objects.forEach((rule) => {
+ if (rule.error === undefined) {
+ if (rule.attributes.scheduledTaskId) {
+ if (rule.attributes.scheduledTaskId !== rule.id) {
+ taskIdsToDelete.push(rule.attributes.scheduledTaskId);
+ } else {
+ taskIdsToDisable.push(rule.attributes.scheduledTaskId);
+ }
+ }
+ disabledRules.push(rule);
+ } else {
+ errors.push({
+ message: rule.error.message ?? 'n/a',
+ status: rule.error.statusCode,
+ rule: {
+ id: rule.id,
+ name: ruleNameToRuleIdMapping[rule.id] ?? 'n/a',
+ },
+ });
+ }
+ });
+
+ return { errors, rules: disabledRules, taskIdsToDisable, taskIdsToDelete };
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts
new file mode 100644
index 00000000000000..0ac7c8d24d0fea
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts
@@ -0,0 +1,544 @@
+/*
+ * 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 pMap from 'p-map';
+import Boom from '@hapi/boom';
+import { cloneDeep } from 'lodash';
+import { AlertConsumers } from '@kbn/rule-data-utils';
+import { KueryNode, nodeBuilder } from '@kbn/es-query';
+import { SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from '@kbn/core/server';
+import { RawRule, SanitizedRule, RuleTypeParams, Rule, RuleSnoozeSchedule } from '../../types';
+import {
+ validateRuleTypeParams,
+ getRuleNotifyWhenType,
+ validateMutatedRuleTypeParams,
+ convertRuleIdsToKueryNode,
+} from '../../lib';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { parseDuration } from '../../../common/parse_duration';
+import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import {
+ retryIfBulkEditConflicts,
+ applyBulkEditOperation,
+ buildKueryNodeFilter,
+ injectReferencesIntoActions,
+ generateAPIKeyName,
+ apiKeyAsAlertAttributes,
+ getBulkSnoozeAttributes,
+ getBulkUnsnoozeAttributes,
+ verifySnoozeScheduleLimit,
+} from '../common';
+import {
+ alertingAuthorizationFilterOpts,
+ MAX_RULES_NUMBER_FOR_BULK_OPERATION,
+ RULE_TYPE_CHECKS_CONCURRENCY,
+ API_KEY_GENERATE_CONCURRENCY,
+} from '../common/constants';
+import { getMappedParams } from '../common/mapped_params_utils';
+import { getAlertFromRaw, extractReferences, validateActions, updateMeta } from '../lib';
+import {
+ NormalizedAlertAction,
+ BulkOperationError,
+ RuleBulkOperationAggregation,
+ RulesClientContext,
+} from '../types';
+
+export type BulkEditFields = keyof Pick<
+ Rule,
+ 'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey'
+>;
+
+export type BulkEditOperation =
+ | {
+ operation: 'add' | 'delete' | 'set';
+ field: Extract;
+ value: string[];
+ }
+ | {
+ operation: 'add' | 'set';
+ field: Extract;
+ value: NormalizedAlertAction[];
+ }
+ | {
+ operation: 'set';
+ field: Extract;
+ value: Rule['schedule'];
+ }
+ | {
+ operation: 'set';
+ field: Extract;
+ value: Rule['throttle'];
+ }
+ | {
+ operation: 'set';
+ field: Extract;
+ value: Rule['notifyWhen'];
+ }
+ | {
+ operation: 'set';
+ field: Extract;
+ value: RuleSnoozeSchedule;
+ }
+ | {
+ operation: 'delete';
+ field: Extract;
+ value?: string[];
+ }
+ | {
+ operation: 'set';
+ field: Extract;
+ value?: undefined;
+ };
+
+type RuleParamsModifier = (params: Params) => Promise;
+
+export interface BulkEditOptionsFilter {
+ filter?: string | KueryNode;
+ operations: BulkEditOperation[];
+ paramsModifier?: RuleParamsModifier;
+}
+
+export interface BulkEditOptionsIds {
+ ids: string[];
+ operations: BulkEditOperation[];
+ paramsModifier?: RuleParamsModifier;
+}
+
+export type BulkEditOptions =
+ | BulkEditOptionsFilter
+ | BulkEditOptionsIds;
+
+export async function bulkEdit(
+ context: RulesClientContext,
+ options: BulkEditOptions
+): Promise<{
+ rules: Array>;
+ errors: BulkOperationError[];
+ total: number;
+}> {
+ const queryFilter = (options as BulkEditOptionsFilter).filter;
+ const ids = (options as BulkEditOptionsIds).ids;
+
+ if (ids && queryFilter) {
+ throw Boom.badRequest(
+ "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method arguments"
+ );
+ }
+
+ const qNodeQueryFilter = buildKueryNodeFilter(queryFilter);
+
+ const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter;
+ let authorizationTuple;
+ try {
+ authorizationTuple = await context.authorization.getFindAuthorizationFilter(
+ AlertingAuthorizationEntity.Rule,
+ alertingAuthorizationFilterOpts
+ );
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.BULK_EDIT,
+ error,
+ })
+ );
+ throw error;
+ }
+ const { filter: authorizationFilter } = authorizationTuple;
+ const qNodeFilterWithAuth =
+ authorizationFilter && qNodeFilter
+ ? nodeBuilder.and([qNodeFilter, authorizationFilter as KueryNode])
+ : qNodeFilter;
+
+ const { aggregations, total } = await context.unsecuredSavedObjectsClient.find<
+ RawRule,
+ RuleBulkOperationAggregation
+ >({
+ filter: qNodeFilterWithAuth,
+ page: 1,
+ perPage: 0,
+ type: 'alert',
+ aggs: {
+ alertTypeId: {
+ multi_terms: {
+ terms: [
+ { field: 'alert.attributes.alertTypeId' },
+ { field: 'alert.attributes.consumer' },
+ ],
+ },
+ },
+ },
+ });
+
+ if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) {
+ throw Boom.badRequest(
+ `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk edit`
+ );
+ }
+ const buckets = aggregations?.alertTypeId.buckets;
+
+ if (buckets === undefined) {
+ throw Error('No rules found for bulk edit');
+ }
+
+ await pMap(
+ buckets,
+ async ({ key: [ruleType, consumer] }) => {
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType);
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: ruleType,
+ consumer,
+ operation: WriteOperations.BulkEdit,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.BULK_EDIT,
+ error,
+ })
+ );
+ throw error;
+ }
+ },
+ { concurrency: RULE_TYPE_CHECKS_CONCURRENCY }
+ );
+
+ const { apiKeysToInvalidate, results, errors } = await retryIfBulkEditConflicts(
+ context.logger,
+ `rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${
+ options.paramsModifier ? '[Function]' : undefined
+ }')`,
+ (filterKueryNode: KueryNode | null) =>
+ bulkEditOcc(context, {
+ filter: filterKueryNode,
+ operations: options.operations,
+ paramsModifier: options.paramsModifier,
+ }),
+ qNodeFilterWithAuth
+ );
+
+ await bulkMarkApiKeysForInvalidation(
+ { apiKeys: apiKeysToInvalidate },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ );
+
+ const updatedRules = results.map(({ id, attributes, references }) => {
+ return getAlertFromRaw(
+ context,
+ id,
+ attributes.alertTypeId as string,
+ attributes as RawRule,
+ references,
+ false
+ );
+ });
+
+ // update schedules only if schedule operation is present
+ const scheduleOperation = options.operations.find(
+ (
+ operation
+ ): operation is Extract }> =>
+ operation.field === 'schedule'
+ );
+
+ if (scheduleOperation?.value) {
+ const taskIds = updatedRules.reduce((acc, rule) => {
+ if (rule.scheduledTaskId) {
+ acc.push(rule.scheduledTaskId);
+ }
+ return acc;
+ }, []);
+
+ try {
+ await context.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value);
+ context.logger.debug(
+ `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}`
+ );
+ } catch (error) {
+ context.logger.error(
+ `Failure to update schedules for underlying tasks: ${taskIds.join(
+ ', '
+ )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}`
+ );
+ }
+ }
+
+ return { rules: updatedRules, errors, total };
+}
+
+async function bulkEditOcc(
+ context: RulesClientContext,
+ {
+ filter,
+ operations,
+ paramsModifier,
+ }: {
+ filter: KueryNode | null;
+ operations: BulkEditOptions['operations'];
+ paramsModifier: BulkEditOptions['paramsModifier'];
+ }
+): Promise<{
+ apiKeysToInvalidate: string[];
+ rules: Array>;
+ resultSavedObjects: Array>;
+ errors: BulkOperationError[];
+}> {
+ const rulesFinder =
+ await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser(
+ {
+ filter,
+ type: 'alert',
+ perPage: 100,
+ ...(context.namespace ? { namespaces: [context.namespace] } : undefined),
+ }
+ );
+
+ const rules: Array> = [];
+ const errors: BulkOperationError[] = [];
+ const apiKeysToInvalidate: string[] = [];
+ const apiKeysMap = new Map();
+ const username = await context.getUserName();
+
+ for await (const response of rulesFinder.find()) {
+ await pMap(
+ response.saved_objects,
+ async (rule) => {
+ try {
+ if (rule.attributes.apiKey) {
+ apiKeysMap.set(rule.id, { oldApiKey: rule.attributes.apiKey });
+ }
+
+ const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId);
+
+ let attributes = cloneDeep(rule.attributes);
+ let ruleActions = {
+ actions: injectReferencesIntoActions(
+ rule.id,
+ rule.attributes.actions,
+ rule.references || []
+ ),
+ };
+
+ for (const operation of operations) {
+ const { field } = operation;
+ if (field === 'snoozeSchedule' || field === 'apiKey') {
+ if (rule.attributes.actions.length) {
+ try {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ } catch (error) {
+ throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`);
+ }
+ }
+ }
+ }
+
+ let hasUpdateApiKeyOperation = false;
+
+ for (const operation of operations) {
+ switch (operation.field) {
+ case 'actions':
+ await validateActions(context, ruleType, {
+ ...attributes,
+ actions: operation.value,
+ });
+ ruleActions = applyBulkEditOperation(operation, ruleActions);
+ break;
+ case 'snoozeSchedule':
+ // Silently skip adding snooze or snooze schedules on security
+ // rules until we implement snoozing of their rules
+ if (attributes.consumer === AlertConsumers.SIEM) {
+ break;
+ }
+ if (operation.operation === 'set') {
+ const snoozeAttributes = getBulkSnoozeAttributes(attributes, operation.value);
+ try {
+ verifySnoozeScheduleLimit(snoozeAttributes);
+ } catch (error) {
+ throw Error(`Error updating rule: could not add snooze - ${error.message}`);
+ }
+ attributes = {
+ ...attributes,
+ ...snoozeAttributes,
+ };
+ }
+ if (operation.operation === 'delete') {
+ const idsToDelete = operation.value && [...operation.value];
+ if (idsToDelete?.length === 0) {
+ attributes.snoozeSchedule?.forEach((schedule) => {
+ if (schedule.id) {
+ idsToDelete.push(schedule.id);
+ }
+ });
+ }
+ attributes = {
+ ...attributes,
+ ...getBulkUnsnoozeAttributes(attributes, idsToDelete),
+ };
+ }
+ break;
+ case 'apiKey': {
+ hasUpdateApiKeyOperation = true;
+ break;
+ }
+ default:
+ attributes = applyBulkEditOperation(operation, attributes);
+ }
+ }
+
+ // validate schedule interval
+ if (attributes.schedule.interval) {
+ const isIntervalInvalid =
+ parseDuration(attributes.schedule.interval as string) <
+ context.minimumScheduleIntervalInMs;
+ if (isIntervalInvalid && context.minimumScheduleInterval.enforce) {
+ throw Error(
+ `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}`
+ );
+ } else if (isIntervalInvalid && !context.minimumScheduleInterval.enforce) {
+ context.logger.warn(
+ `Rule schedule interval (${attributes.schedule.interval}) for "${ruleType.id}" rule type with ID "${attributes.id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.`
+ );
+ }
+ }
+
+ const ruleParams = paramsModifier
+ ? await paramsModifier(attributes.params as Params)
+ : attributes.params;
+
+ // validate rule params
+ const validatedAlertTypeParams = validateRuleTypeParams(
+ ruleParams,
+ ruleType.validate?.params
+ );
+ const validatedMutatedAlertTypeParams = validateMutatedRuleTypeParams(
+ validatedAlertTypeParams,
+ rule.attributes.params,
+ ruleType.validate?.params
+ );
+
+ const {
+ actions: rawAlertActions,
+ references,
+ params: updatedParams,
+ } = await extractReferences(
+ context,
+ ruleType,
+ ruleActions.actions,
+ validatedMutatedAlertTypeParams
+ );
+
+ const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation;
+
+ // create API key
+ let createdAPIKey = null;
+ try {
+ createdAPIKey = shouldUpdateApiKey
+ ? await context.createAPIKey(generateAPIKeyName(ruleType.id, attributes.name))
+ : null;
+ } catch (error) {
+ throw Error(`Error updating rule: could not create API key - ${error.message}`);
+ }
+
+ const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username);
+
+ // collect generated API keys
+ if (apiKeyAttributes.apiKey) {
+ apiKeysMap.set(rule.id, {
+ ...apiKeysMap.get(rule.id),
+ newApiKey: apiKeyAttributes.apiKey,
+ });
+ }
+
+ // get notifyWhen
+ const notifyWhen = getRuleNotifyWhenType(
+ attributes.notifyWhen ?? null,
+ attributes.throttle ?? null
+ );
+
+ const updatedAttributes = updateMeta(context, {
+ ...attributes,
+ ...apiKeyAttributes,
+ params: updatedParams as RawRule['params'],
+ actions: rawAlertActions,
+ notifyWhen,
+ updatedBy: username,
+ updatedAt: new Date().toISOString(),
+ });
+
+ // add mapped_params
+ const mappedParams = getMappedParams(updatedParams);
+
+ if (Object.keys(mappedParams).length) {
+ updatedAttributes.mapped_params = mappedParams;
+ }
+
+ rules.push({
+ ...rule,
+ references,
+ attributes: updatedAttributes,
+ });
+ } catch (error) {
+ errors.push({
+ message: error.message,
+ rule: {
+ id: rule.id,
+ name: rule.attributes?.name,
+ },
+ });
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.BULK_EDIT,
+ error,
+ })
+ );
+ }
+ },
+ { concurrency: API_KEY_GENERATE_CONCURRENCY }
+ );
+ }
+
+ let result;
+ try {
+ result = await context.unsecuredSavedObjectsClient.bulkCreate(rules, { overwrite: true });
+ } catch (e) {
+ // avoid unused newly generated API keys
+ if (apiKeysMap.size > 0) {
+ await bulkMarkApiKeysForInvalidation(
+ {
+ apiKeys: Array.from(apiKeysMap.values()).reduce((acc, value) => {
+ if (value.newApiKey) {
+ acc.push(value.newApiKey);
+ }
+ return acc;
+ }, []),
+ },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ );
+ }
+ throw e;
+ }
+
+ result.saved_objects.map(({ id, error }) => {
+ const oldApiKey = apiKeysMap.get(id)?.oldApiKey;
+ const newApiKey = apiKeysMap.get(id)?.newApiKey;
+
+ // if SO wasn't saved and has new API key it will be invalidated
+ if (error && newApiKey) {
+ apiKeysToInvalidate.push(newApiKey);
+ // if SO saved and has old Api Key it will be invalidate
+ } else if (!error && oldApiKey) {
+ apiKeysToInvalidate.push(oldApiKey);
+ }
+ });
+
+ return { apiKeysToInvalidate, resultSavedObjects: result.saved_objects, errors, rules };
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts
new file mode 100644
index 00000000000000..394f7aad0dea71
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts
@@ -0,0 +1,240 @@
+/*
+ * 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 pMap from 'p-map';
+import { KueryNode, nodeBuilder } from '@kbn/es-query';
+import { SavedObjectsBulkUpdateObject } from '@kbn/core/server';
+import { RawRule, IntervalSchedule } from '../../types';
+import { convertRuleIdsToKueryNode } from '../../lib';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import {
+ retryIfBulkOperationConflicts,
+ buildKueryNodeFilter,
+ getAndValidateCommonBulkOptions,
+} from '../common';
+import {
+ getAuthorizationFilter,
+ checkAuthorizationAndGetTotal,
+ getAlertFromRaw,
+ scheduleTask,
+ updateMeta,
+ createNewAPIKeySet,
+} from '../lib';
+import { RulesClientContext, BulkOperationError, BulkOptions } from '../types';
+
+const getShouldScheduleTask = async (
+ context: RulesClientContext,
+ scheduledTaskId: string | null | undefined
+) => {
+ if (!scheduledTaskId) return true;
+ try {
+ // make sure scheduledTaskId exist
+ await context.taskManager.get(scheduledTaskId);
+ return false;
+ } catch (err) {
+ return true;
+ }
+};
+
+export const bulkEnableRules = async (context: RulesClientContext, options: BulkOptions) => {
+ const { ids, filter } = getAndValidateCommonBulkOptions(options);
+
+ const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter);
+ const authorizationFilter = await getAuthorizationFilter(context, { action: 'ENABLE' });
+
+ const kueryNodeFilterWithAuth =
+ authorizationFilter && kueryNodeFilter
+ ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode])
+ : kueryNodeFilter;
+
+ const { total } = await checkAuthorizationAndGetTotal(context, {
+ filter: kueryNodeFilterWithAuth,
+ action: 'ENABLE',
+ });
+
+ const { errors, rules, accListSpecificForBulkOperation } = await retryIfBulkOperationConflicts({
+ action: 'ENABLE',
+ logger: context.logger,
+ bulkOperation: (filterKueryNode: KueryNode | null) =>
+ bulkEnableRulesWithOCC(context, { filter: filterKueryNode }),
+ filter: kueryNodeFilterWithAuth,
+ });
+
+ const [taskIdsToEnable] = accListSpecificForBulkOperation;
+
+ const taskIdsFailedToBeEnabled: string[] = [];
+ if (taskIdsToEnable.length > 0) {
+ try {
+ const resultFromEnablingTasks = await context.taskManager.bulkEnable(taskIdsToEnable);
+ resultFromEnablingTasks?.errors?.forEach((error) => {
+ taskIdsFailedToBeEnabled.push(error.task.id);
+ });
+ if (resultFromEnablingTasks.tasks.length) {
+ context.logger.debug(
+ `Successfully enabled schedules for underlying tasks: ${resultFromEnablingTasks.tasks
+ .map((task) => task.id)
+ .join(', ')}`
+ );
+ }
+ if (resultFromEnablingTasks.errors.length) {
+ context.logger.error(
+ `Failure to enable schedules for underlying tasks: ${resultFromEnablingTasks.errors
+ .map((error) => error.task.id)
+ .join(', ')}`
+ );
+ }
+ } catch (error) {
+ taskIdsFailedToBeEnabled.push(...taskIdsToEnable);
+ context.logger.error(
+ `Failure to enable schedules for underlying tasks: ${taskIdsToEnable.join(
+ ', '
+ )}. TaskManager bulkEnable failed with Error: ${error.message}`
+ );
+ }
+ }
+
+ const updatedRules = rules.map(({ id, attributes, references }) => {
+ return getAlertFromRaw(
+ context,
+ id,
+ attributes.alertTypeId as string,
+ attributes as RawRule,
+ references,
+ false
+ );
+ });
+
+ return { errors, rules: updatedRules, total, taskIdsFailedToBeEnabled };
+};
+
+const bulkEnableRulesWithOCC = async (
+ context: RulesClientContext,
+ { filter }: { filter: KueryNode | null }
+) => {
+ const rulesFinder =
+ await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser(
+ {
+ filter,
+ type: 'alert',
+ perPage: 100,
+ ...(context.namespace ? { namespaces: [context.namespace] } : undefined),
+ }
+ );
+
+ const rulesToEnable: Array> = [];
+ const taskIdsToEnable: string[] = [];
+ const errors: BulkOperationError[] = [];
+ const ruleNameToRuleIdMapping: Record = {};
+
+ for await (const response of rulesFinder.find()) {
+ await pMap(response.saved_objects, async (rule) => {
+ try {
+ if (rule.attributes.actions.length) {
+ try {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ } catch (error) {
+ throw Error(`Rule not authorized for bulk enable - ${error.message}`);
+ }
+ }
+ if (rule.attributes.enabled === true) return;
+ if (rule.attributes.name) {
+ ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
+ }
+
+ const username = await context.getUserName();
+
+ const updatedAttributes = updateMeta(context, {
+ ...rule.attributes,
+ ...(!rule.attributes.apiKey &&
+ (await createNewAPIKeySet(context, { attributes: rule.attributes, username }))),
+ enabled: true,
+ updatedBy: username,
+ updatedAt: new Date().toISOString(),
+ executionStatus: {
+ status: 'pending',
+ lastDuration: 0,
+ lastExecutionDate: new Date().toISOString(),
+ error: null,
+ warning: null,
+ },
+ });
+
+ const shouldScheduleTask = await getShouldScheduleTask(
+ context,
+ rule.attributes.scheduledTaskId
+ );
+
+ let scheduledTaskId;
+ if (shouldScheduleTask) {
+ const scheduledTask = await scheduleTask(context, {
+ id: rule.id,
+ consumer: rule.attributes.consumer,
+ ruleTypeId: rule.attributes.alertTypeId,
+ schedule: rule.attributes.schedule as IntervalSchedule,
+ throwOnConflict: false,
+ });
+ scheduledTaskId = scheduledTask.id;
+ }
+
+ rulesToEnable.push({
+ ...rule,
+ attributes: {
+ ...updatedAttributes,
+ ...(scheduledTaskId ? { scheduledTaskId } : undefined),
+ },
+ });
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.ENABLE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id: rule.id },
+ })
+ );
+ } catch (error) {
+ errors.push({
+ message: error.message,
+ rule: {
+ id: rule.id,
+ name: rule.attributes?.name,
+ },
+ });
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.ENABLE,
+ error,
+ })
+ );
+ }
+ });
+ }
+
+ const result = await context.unsecuredSavedObjectsClient.bulkCreate(rulesToEnable, {
+ overwrite: true,
+ });
+
+ const rules: Array> = [];
+
+ result.saved_objects.forEach((rule) => {
+ if (rule.error === undefined) {
+ if (rule.attributes.scheduledTaskId) {
+ taskIdsToEnable.push(rule.attributes.scheduledTaskId);
+ }
+ rules.push(rule);
+ } else {
+ errors.push({
+ message: rule.error.message ?? 'n/a',
+ status: rule.error.statusCode,
+ rule: {
+ id: rule.id,
+ name: ruleNameToRuleIdMapping[rule.id] ?? 'n/a',
+ },
+ });
+ }
+ });
+ return { errors, rules, accListSpecificForBulkOperation: [taskIdsToEnable] };
+};
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/clear_expired_snoozes.ts b/x-pack/plugins/alerting/server/rules_client/methods/clear_expired_snoozes.ts
new file mode 100644
index 00000000000000..284e5c89e25f53
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/clear_expired_snoozes.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { RawRule } from '../../types';
+import { partiallyUpdateAlert } from '../../saved_objects';
+import { isSnoozeExpired } from '../../lib';
+import { RulesClientContext } from '../types';
+import { updateMeta } from '../lib';
+
+export async function clearExpiredSnoozes(
+ context: RulesClientContext,
+ { id }: { id: string }
+): Promise {
+ const { attributes, version } = await context.unsecuredSavedObjectsClient.get(
+ 'alert',
+ id
+ );
+
+ const snoozeSchedule = attributes.snoozeSchedule
+ ? attributes.snoozeSchedule.filter((s) => {
+ try {
+ return !isSnoozeExpired(s);
+ } catch (e) {
+ context.logger.error(`Error checking for expiration of snooze ${s.id}: ${e}`);
+ return true;
+ }
+ })
+ : [];
+
+ if (snoozeSchedule.length === attributes.snoozeSchedule?.length) return;
+
+ const updateAttributes = updateMeta(context, {
+ snoozeSchedule,
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ });
+ const updateOptions = { version };
+
+ await partiallyUpdateAlert(
+ context.unsecuredSavedObjectsClient,
+ id,
+ updateAttributes,
+ updateOptions
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/clone.ts b/x-pack/plugins/alerting/server/rules_client/methods/clone.ts
new file mode 100644
index 00000000000000..b4ebe5891885c2
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/clone.ts
@@ -0,0 +1,131 @@
+/*
+ * 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 Semver from 'semver';
+import Boom from '@hapi/boom';
+import { AlertConsumers } from '@kbn/rule-data-utils';
+import { SavedObject, SavedObjectsUtils } from '@kbn/core/server';
+import { RawRule, SanitizedRule, RuleTypeParams } from '../../types';
+import { getDefaultMonitoring } from '../../lib';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { parseDuration } from '../../../common/parse_duration';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status';
+import { isDetectionEngineAADRuleType } from '../../saved_objects/migrations/utils';
+import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common';
+import { createRuleSavedObject } from '../lib';
+import { RulesClientContext } from '../types';
+
+export type CloneArguments = [string, { newId?: string }];
+
+export async function clone(
+ context: RulesClientContext,
+ id: string,
+ { newId }: { newId?: string }
+): Promise> {
+ let ruleSavedObject: SavedObject;
+
+ try {
+ ruleSavedObject = await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser(
+ 'alert',
+ id,
+ {
+ namespace: context.namespace,
+ }
+ );
+ } catch (e) {
+ // We'll skip invalidating the API key since we failed to load the decrypted saved object
+ context.logger.error(
+ `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
+ );
+ // Still attempt to load the object using SOC
+ ruleSavedObject = await context.unsecuredSavedObjectsClient.get('alert', id);
+ }
+
+ /*
+ * As the time of the creation of this PR, security solution already have a clone/duplicate API
+ * with some specific business logic so to avoid weird bugs, I prefer to exclude them from this
+ * functionality until we resolve our difference
+ */
+ if (
+ isDetectionEngineAADRuleType(ruleSavedObject) ||
+ ruleSavedObject.attributes.consumer === AlertConsumers.SIEM
+ ) {
+ throw Boom.badRequest(
+ 'The clone functionality is not enable for rule who belongs to security solution'
+ );
+ }
+ const ruleName =
+ ruleSavedObject.attributes.name.indexOf('[Clone]') > 0
+ ? ruleSavedObject.attributes.name
+ : `${ruleSavedObject.attributes.name} [Clone]`;
+ const ruleId = newId ?? SavedObjectsUtils.generateId();
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: ruleSavedObject.attributes.alertTypeId,
+ consumer: ruleSavedObject.attributes.consumer,
+ operation: WriteOperations.Create,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleSavedObject.attributes.alertTypeId);
+ // Throws an error if alert type isn't registered
+ const ruleType = context.ruleTypeRegistry.get(ruleSavedObject.attributes.alertTypeId);
+ const username = await context.getUserName();
+ const createTime = Date.now();
+ const lastRunTimestamp = new Date();
+ const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null;
+ let createdAPIKey = null;
+ try {
+ createdAPIKey = ruleSavedObject.attributes.enabled
+ ? await context.createAPIKey(generateAPIKeyName(ruleType.id, ruleName))
+ : null;
+ } catch (error) {
+ throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
+ }
+ const rawRule: RawRule = {
+ ...ruleSavedObject.attributes,
+ name: ruleName,
+ ...apiKeyAsAlertAttributes(createdAPIKey, username),
+ legacyId,
+ createdBy: username,
+ updatedBy: username,
+ createdAt: new Date(createTime).toISOString(),
+ updatedAt: new Date(createTime).toISOString(),
+ snoozeSchedule: [],
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
+ monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()),
+ scheduledTaskId: null,
+ };
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ return await createRuleSavedObject(context, {
+ intervalInMs: parseDuration(rawRule.schedule.interval),
+ rawRule,
+ references: ruleSavedObject.references,
+ ruleId,
+ });
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts
new file mode 100644
index 00000000000000..31707726b4e243
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts
@@ -0,0 +1,147 @@
+/*
+ * 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 Semver from 'semver';
+import Boom from '@hapi/boom';
+import { SavedObjectsUtils } from '@kbn/core/server';
+import { parseDuration } from '../../../common/parse_duration';
+import { RawRule, SanitizedRule, RuleTypeParams, RuleAction, Rule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { validateRuleTypeParams, getRuleNotifyWhenType, getDefaultMonitoring } from '../../lib';
+import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status';
+import { createRuleSavedObject, extractReferences, validateActions } from '../lib';
+import { generateAPIKeyName, getMappedParams, apiKeyAsAlertAttributes } from '../common';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+
+type NormalizedAlertAction = Omit;
+interface SavedObjectOptions {
+ id?: string;
+ migrationVersion?: Record;
+}
+
+export interface CreateOptions {
+ data: Omit<
+ Rule,
+ | 'id'
+ | 'createdBy'
+ | 'updatedBy'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'apiKey'
+ | 'apiKeyOwner'
+ | 'muteAll'
+ | 'mutedInstanceIds'
+ | 'actions'
+ | 'executionStatus'
+ | 'snoozeSchedule'
+ | 'isSnoozedUntil'
+ | 'lastRun'
+ | 'nextRun'
+ > & { actions: NormalizedAlertAction[] };
+ options?: SavedObjectOptions;
+}
+
+export async function create(
+ context: RulesClientContext,
+ { data, options }: CreateOptions
+): Promise> {
+ const id = options?.id || SavedObjectsUtils.generateId();
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: data.alertTypeId,
+ consumer: data.consumer,
+ operation: WriteOperations.Create,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId);
+
+ // Throws an error if alert type isn't registered
+ const ruleType = context.ruleTypeRegistry.get(data.alertTypeId);
+
+ const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params);
+ const username = await context.getUserName();
+
+ let createdAPIKey = null;
+ try {
+ createdAPIKey = data.enabled
+ ? await context.createAPIKey(generateAPIKeyName(ruleType.id, data.name))
+ : null;
+ } catch (error) {
+ throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
+ }
+
+ await validateActions(context, ruleType, data);
+
+ // Throw error if schedule interval is less than the minimum and we are enforcing it
+ const intervalInMs = parseDuration(data.schedule.interval);
+ if (
+ intervalInMs < context.minimumScheduleIntervalInMs &&
+ context.minimumScheduleInterval.enforce
+ ) {
+ throw Boom.badRequest(
+ `Error creating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}`
+ );
+ }
+
+ // Extract saved object references for this rule
+ const {
+ references,
+ params: updatedParams,
+ actions,
+ } = await extractReferences(context, ruleType, data.actions, validatedAlertTypeParams);
+
+ const createTime = Date.now();
+ const lastRunTimestamp = new Date();
+ const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null;
+ const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
+ const throttle = data.throttle ?? null;
+
+ const rawRule: RawRule = {
+ ...data,
+ ...apiKeyAsAlertAttributes(createdAPIKey, username),
+ legacyId,
+ actions,
+ createdBy: username,
+ updatedBy: username,
+ createdAt: new Date(createTime).toISOString(),
+ updatedAt: new Date(createTime).toISOString(),
+ snoozeSchedule: [],
+ params: updatedParams as RawRule['params'],
+ muteAll: false,
+ mutedInstanceIds: [],
+ notifyWhen,
+ throttle,
+ executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
+ monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()),
+ };
+
+ const mappedParams = getMappedParams(updatedParams);
+
+ if (Object.keys(mappedParams).length) {
+ rawRule.mapped_params = mappedParams;
+ }
+
+ return await createRuleSavedObject(context, {
+ intervalInMs,
+ rawRule,
+ references,
+ ruleId: id,
+ options,
+ });
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/delete.ts b/x-pack/plugins/alerting/server/rules_client/methods/delete.ts
new file mode 100644
index 00000000000000..406184e8f013e0
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/delete.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { RawRule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+
+export async function deleteRule(context: RulesClientContext, { id }: { id: string }) {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.delete('${id}')`,
+ async () => await deleteWithOCC(context, { id })
+ );
+}
+
+async function deleteWithOCC(context: RulesClientContext, { id }: { id: string }) {
+ let taskIdToRemove: string | undefined | null;
+ let apiKeyToInvalidate: string | null = null;
+ let attributes: RawRule;
+
+ try {
+ const decryptedAlert =
+ await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, {
+ namespace: context.namespace,
+ });
+ apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
+ taskIdToRemove = decryptedAlert.attributes.scheduledTaskId;
+ attributes = decryptedAlert.attributes;
+ } catch (e) {
+ // We'll skip invalidating the API key since we failed to load the decrypted saved object
+ context.logger.error(
+ `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
+ );
+ // Still attempt to load the scheduledTaskId using SOC
+ const alert = await context.unsecuredSavedObjectsClient.get('alert', id);
+ taskIdToRemove = alert.attributes.scheduledTaskId;
+ attributes = alert.attributes;
+ }
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.Delete,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.DELETE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.DELETE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+ const removeResult = await context.unsecuredSavedObjectsClient.delete('alert', id);
+
+ await Promise.all([
+ taskIdToRemove ? context.taskManager.removeIfExists(taskIdToRemove) : null,
+ apiKeyToInvalidate
+ ? bulkMarkApiKeysForInvalidation(
+ { apiKeys: [apiKeyToInvalidate] },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ )
+ : null,
+ ]);
+
+ return removeResult;
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts
new file mode 100644
index 00000000000000..3eae1d2df7b5d4
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 { RawRule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+import { recoverRuleAlerts, updateMeta } from '../lib';
+
+export async function disable(context: RulesClientContext, { id }: { id: string }): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.disable('${id}')`,
+ async () => await disableWithOCC(context, { id })
+ );
+}
+
+async function disableWithOCC(context: RulesClientContext, { id }: { id: string }) {
+ let attributes: RawRule;
+ let version: string | undefined;
+
+ try {
+ const decryptedAlert =
+ await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, {
+ namespace: context.namespace,
+ });
+ attributes = decryptedAlert.attributes;
+ version = decryptedAlert.version;
+ } catch (e) {
+ context.logger.error(`disable(): Failed to load API key of alert ${id}: ${e.message}`);
+ // Still attempt to load the attributes and version using SOC
+ const alert = await context.unsecuredSavedObjectsClient.get('alert', id);
+ attributes = alert.attributes;
+ version = alert.version;
+ }
+
+ recoverRuleAlerts(context, id, attributes);
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.Disable,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.DISABLE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.DISABLE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ if (attributes.enabled === true) {
+ await context.unsecuredSavedObjectsClient.update(
+ 'alert',
+ id,
+ updateMeta(context, {
+ ...attributes,
+ enabled: false,
+ scheduledTaskId: attributes.scheduledTaskId === id ? attributes.scheduledTaskId : null,
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ nextRun: null,
+ }),
+ { version }
+ );
+
+ // If the scheduledTaskId does not match the rule id, we should
+ // remove the task, otherwise mark the task as disabled
+ if (attributes.scheduledTaskId) {
+ if (attributes.scheduledTaskId !== id) {
+ await context.taskManager.removeIfExists(attributes.scheduledTaskId);
+ } else {
+ await context.taskManager.bulkDisable([attributes.scheduledTaskId]);
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts
new file mode 100644
index 00000000000000..5b26061120b0aa
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts
@@ -0,0 +1,142 @@
+/*
+ * 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 { RawRule, IntervalSchedule } from '../../types';
+import { updateMonitoring, getNextRun } from '../../lib';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+import { updateMeta, createNewAPIKeySet, scheduleTask } from '../lib';
+
+export async function enable(context: RulesClientContext, { id }: { id: string }): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.enable('${id}')`,
+ async () => await enableWithOCC(context, { id })
+ );
+}
+
+async function enableWithOCC(context: RulesClientContext, { id }: { id: string }) {
+ let existingApiKey: string | null = null;
+ let attributes: RawRule;
+ let version: string | undefined;
+
+ try {
+ const decryptedAlert =
+ await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, {
+ namespace: context.namespace,
+ });
+ existingApiKey = decryptedAlert.attributes.apiKey;
+ attributes = decryptedAlert.attributes;
+ version = decryptedAlert.version;
+ } catch (e) {
+ context.logger.error(`enable(): Failed to load API key of alert ${id}: ${e.message}`);
+ // Still attempt to load the attributes and version using SOC
+ const alert = await context.unsecuredSavedObjectsClient.get('alert', id);
+ attributes = alert.attributes;
+ version = alert.version;
+ }
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.Enable,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.ENABLE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.ENABLE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ if (attributes.enabled === false) {
+ const username = await context.getUserName();
+ const now = new Date();
+
+ const schedule = attributes.schedule as IntervalSchedule;
+
+ const updateAttributes = updateMeta(context, {
+ ...attributes,
+ ...(!existingApiKey && (await createNewAPIKeySet(context, { attributes, username }))),
+ ...(attributes.monitoring && {
+ monitoring: updateMonitoring({
+ monitoring: attributes.monitoring,
+ timestamp: now.toISOString(),
+ duration: 0,
+ }),
+ }),
+ nextRun: getNextRun({ interval: schedule.interval }),
+ enabled: true,
+ updatedBy: username,
+ updatedAt: now.toISOString(),
+ executionStatus: {
+ status: 'pending',
+ lastDuration: 0,
+ lastExecutionDate: now.toISOString(),
+ error: null,
+ warning: null,
+ },
+ });
+
+ try {
+ await context.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ let scheduledTaskIdToCreate: string | null = null;
+ if (attributes.scheduledTaskId) {
+ // If scheduledTaskId defined in rule SO, make sure it exists
+ try {
+ await context.taskManager.get(attributes.scheduledTaskId);
+ } catch (err) {
+ scheduledTaskIdToCreate = id;
+ }
+ } else {
+ // If scheduledTaskId doesn't exist in rule SO, set it to rule ID
+ scheduledTaskIdToCreate = id;
+ }
+
+ if (scheduledTaskIdToCreate) {
+ // Schedule the task if it doesn't exist
+ const scheduledTask = await scheduleTask(context, {
+ id,
+ consumer: attributes.consumer,
+ ruleTypeId: attributes.alertTypeId,
+ schedule: attributes.schedule as IntervalSchedule,
+ throwOnConflict: false,
+ });
+ await context.unsecuredSavedObjectsClient.update('alert', id, {
+ scheduledTaskId: scheduledTask.id,
+ });
+ } else {
+ // Task exists so set enabled to true
+ await context.taskManager.bulkEnable([attributes.scheduledTaskId!]);
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/find.ts b/x-pack/plugins/alerting/server/rules_client/methods/find.ts
new file mode 100644
index 00000000000000..080c72c624cb33
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/find.ts
@@ -0,0 +1,179 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { pick } from 'lodash';
+import { KueryNode, nodeBuilder } from '@kbn/es-query';
+import { RawRule, RuleTypeParams, SanitizedRule } from '../../types';
+import { AlertingAuthorizationEntity } from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import {
+ mapSortField,
+ validateOperationOnAttributes,
+ buildKueryNodeFilter,
+ includeFieldsRequiredForAuthentication,
+} from '../common';
+import {
+ getModifiedField,
+ getModifiedSearchFields,
+ getModifiedSearch,
+ modifyFilterKueryNode,
+} from '../common/mapped_params_utils';
+import { alertingAuthorizationFilterOpts } from '../common/constants';
+import { getAlertFromRaw } from '../lib/get_alert_from_raw';
+import type { IndexType, RulesClientContext } from '../types';
+
+export interface FindParams {
+ options?: FindOptions;
+ excludeFromPublicApi?: boolean;
+ includeSnoozeData?: boolean;
+}
+
+export interface FindOptions extends IndexType {
+ perPage?: number;
+ page?: number;
+ search?: string;
+ defaultSearchOperator?: 'AND' | 'OR';
+ searchFields?: string[];
+ sortField?: string;
+ sortOrder?: estypes.SortOrder;
+ hasReference?: {
+ type: string;
+ id: string;
+ };
+ fields?: string[];
+ filter?: string | KueryNode;
+}
+
+export interface FindResult {
+ page: number;
+ perPage: number;
+ total: number;
+ data: Array>;
+}
+
+export async function find(
+ context: RulesClientContext,
+ {
+ options: { fields, ...options } = {},
+ excludeFromPublicApi = false,
+ includeSnoozeData = false,
+ }: FindParams = {}
+): Promise> {
+ let authorizationTuple;
+ try {
+ authorizationTuple = await context.authorization.getFindAuthorizationFilter(
+ AlertingAuthorizationEntity.Rule,
+ alertingAuthorizationFilterOpts
+ );
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.FIND,
+ error,
+ })
+ );
+ throw error;
+ }
+
+ const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple;
+
+ const filterKueryNode = buildKueryNodeFilter(options.filter);
+ let sortField = mapSortField(options.sortField);
+ if (excludeFromPublicApi) {
+ try {
+ validateOperationOnAttributes(
+ filterKueryNode,
+ sortField,
+ options.searchFields,
+ context.fieldsToExcludeFromPublicApi
+ );
+ } catch (error) {
+ throw Boom.badRequest(`Error find rules: ${error.message}`);
+ }
+ }
+
+ sortField = mapSortField(getModifiedField(options.sortField));
+
+ // Generate new modified search and search fields, translating certain params properties
+ // to mapped_params. Thus, allowing for sort/search/filtering on params.
+ // We do the modifcation after the validate check to make sure the public API does not
+ // use the mapped_params in their queries.
+ options = {
+ ...options,
+ ...(options.searchFields && { searchFields: getModifiedSearchFields(options.searchFields) }),
+ ...(options.search && { search: getModifiedSearch(options.searchFields, options.search) }),
+ };
+
+ // Modifies kuery node AST to translate params filter and the filter value to mapped_params.
+ // This translation is done in place, and therefore is not a pure function.
+ if (filterKueryNode) {
+ modifyFilterKueryNode({ astFilter: filterKueryNode });
+ }
+
+ const {
+ page,
+ per_page: perPage,
+ total,
+ saved_objects: data,
+ } = await context.unsecuredSavedObjectsClient.find({
+ ...options,
+ sortField,
+ filter:
+ (authorizationFilter && filterKueryNode
+ ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
+ : authorizationFilter) ?? filterKueryNode,
+ fields: fields ? includeFieldsRequiredForAuthentication(fields) : fields,
+ type: 'alert',
+ });
+
+ const authorizedData = data.map(({ id, attributes, references }) => {
+ try {
+ ensureRuleTypeIsAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ AlertingAuthorizationEntity.Rule
+ );
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.FIND,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+ return getAlertFromRaw(
+ context,
+ id,
+ attributes.alertTypeId,
+ fields ? (pick(attributes, fields) as RawRule) : attributes,
+ references,
+ false,
+ excludeFromPublicApi,
+ includeSnoozeData
+ );
+ });
+
+ authorizedData.forEach(({ id }) =>
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.FIND,
+ savedObject: { type: 'alert', id },
+ })
+ )
+ );
+
+ return {
+ page,
+ perPage,
+ total,
+ data: authorizedData,
+ };
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get.ts b/x-pack/plugins/alerting/server/rules_client/methods/get.ts
new file mode 100644
index 00000000000000..932772f06d209d
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/get.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { RawRule, SanitizedRule, RuleTypeParams, SanitizedRuleWithLegacyId } from '../../types';
+import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { getAlertFromRaw } from '../lib/get_alert_from_raw';
+import { RulesClientContext } from '../types';
+
+export interface GetParams {
+ id: string;
+ includeLegacyId?: boolean;
+ includeSnoozeData?: boolean;
+ excludeFromPublicApi?: boolean;
+}
+
+export async function get(
+ context: RulesClientContext,
+ {
+ id,
+ includeLegacyId = false,
+ includeSnoozeData = false,
+ excludeFromPublicApi = false,
+ }: GetParams
+): Promise | SanitizedRuleWithLegacyId> {
+ const result = await context.unsecuredSavedObjectsClient.get('alert', id);
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: result.attributes.alertTypeId,
+ consumer: result.attributes.consumer,
+ operation: ReadOperations.Get,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET,
+ savedObject: { type: 'alert', id },
+ })
+ );
+ return getAlertFromRaw(
+ context,
+ result.id,
+ result.attributes.alertTypeId,
+ result.attributes,
+ result.references,
+ includeLegacyId,
+ excludeFromPublicApi,
+ includeSnoozeData
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_action_error_log.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_action_error_log.ts
new file mode 100644
index 00000000000000..ebd1862d6b3f65
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/get_action_error_log.ts
@@ -0,0 +1,168 @@
+/*
+ * 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 { KueryNode } from '@kbn/es-query';
+import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { SanitizedRuleWithLegacyId } from '../../types';
+import { convertEsSortToEventLogSort } from '../../lib';
+import {
+ ReadOperations,
+ AlertingAuthorizationEntity,
+ AlertingAuthorizationFilterType,
+} from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { IExecutionErrorsResult } from '../../../common';
+import { formatExecutionErrorsResult } from '../../lib/format_execution_log_errors';
+import { parseDate } from '../common';
+import { RulesClientContext } from '../types';
+import { get } from './get';
+
+const actionErrorLogDefaultFilter =
+ 'event.provider:actions AND ((event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout))';
+
+export interface GetActionErrorLogByIdParams {
+ id: string;
+ dateStart: string;
+ dateEnd?: string;
+ filter?: string;
+ page: number;
+ perPage: number;
+ sort: estypes.Sort;
+ namespace?: string;
+}
+
+export async function getActionErrorLog(
+ context: RulesClientContext,
+ { id, dateStart, dateEnd, filter, page, perPage, sort }: GetActionErrorLogByIdParams
+): Promise {
+ context.logger.debug(`getActionErrorLog(): getting action error logs for rule ${id}`);
+ const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: rule.alertTypeId,
+ consumer: rule.consumer,
+ operation: ReadOperations.GetActionErrorLog,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_ACTION_ERROR_LOG,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_ACTION_ERROR_LOG,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ // default duration of instance summary is 60 * rule interval
+ const dateNow = new Date();
+ const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
+ const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
+
+ const eventLogClient = await context.getEventLogClient();
+
+ try {
+ const errorResult = await eventLogClient.findEventsBySavedObjectIds(
+ 'alert',
+ [id],
+ {
+ start: parsedDateStart.toISOString(),
+ end: parsedDateEnd.toISOString(),
+ page,
+ per_page: perPage,
+ filter: filter
+ ? `(${actionErrorLogDefaultFilter}) AND (${filter})`
+ : actionErrorLogDefaultFilter,
+ sort: convertEsSortToEventLogSort(sort),
+ },
+ rule.legacyId !== null ? [rule.legacyId] : undefined
+ );
+ return formatExecutionErrorsResult(errorResult);
+ } catch (err) {
+ context.logger.debug(
+ `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}`
+ );
+ throw err;
+ }
+}
+
+export async function getActionErrorLogWithAuth(
+ context: RulesClientContext,
+ { id, dateStart, dateEnd, filter, page, perPage, sort, namespace }: GetActionErrorLogByIdParams
+): Promise {
+ context.logger.debug(`getActionErrorLogWithAuth(): getting action error logs for rule ${id}`);
+
+ let authorizationTuple;
+ try {
+ authorizationTuple = await context.authorization.getFindAuthorizationFilter(
+ AlertingAuthorizationEntity.Alert,
+ {
+ type: AlertingAuthorizationFilterType.KQL,
+ fieldNames: {
+ ruleTypeId: 'kibana.alert.rule.rule_type_id',
+ consumer: 'kibana.alert.rule.consumer',
+ },
+ }
+ );
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_ACTION_ERROR_LOG,
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_ACTION_ERROR_LOG,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ // default duration of instance summary is 60 * rule interval
+ const dateNow = new Date();
+ const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
+ const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
+
+ const eventLogClient = await context.getEventLogClient();
+
+ try {
+ const errorResult = await eventLogClient.findEventsWithAuthFilter(
+ 'alert',
+ [id],
+ authorizationTuple.filter as KueryNode,
+ namespace,
+ {
+ start: parsedDateStart.toISOString(),
+ end: parsedDateEnd.toISOString(),
+ page,
+ per_page: perPage,
+ filter: filter
+ ? `(${actionErrorLogDefaultFilter}) AND (${filter})`
+ : actionErrorLogDefaultFilter,
+ sort: convertEsSortToEventLogSort(sort),
+ }
+ );
+ return formatExecutionErrorsResult(errorResult);
+ } catch (err) {
+ context.logger.debug(
+ `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}`
+ );
+ throw err;
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_alert_state.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_state.ts
new file mode 100644
index 00000000000000..6497428e1c2f25
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_state.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { RuleTaskState } from '../../types';
+import { taskInstanceToAlertTaskInstance } from '../../task_runner/alert_task_instance';
+import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { RulesClientContext } from '../types';
+import { get } from './get';
+
+export interface GetAlertStateParams {
+ id: string;
+}
+export async function getAlertState(
+ context: RulesClientContext,
+ { id }: GetAlertStateParams
+): Promise {
+ const alert = await get(context, { id });
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: alert.alertTypeId,
+ consumer: alert.consumer,
+ operation: ReadOperations.GetRuleState,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ if (alert.scheduledTaskId) {
+ const { state } = taskInstanceToAlertTaskInstance(
+ await context.taskManager.get(alert.scheduledTaskId),
+ alert
+ );
+ return state;
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_alert_summary.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_summary.ts
new file mode 100644
index 00000000000000..e841423ad19499
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/get_alert_summary.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { IEvent } from '@kbn/event-log-plugin/server';
+import { AlertSummary, SanitizedRuleWithLegacyId } from '../../types';
+import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { alertSummaryFromEventLog } from '../../lib/alert_summary_from_event_log';
+import { parseDuration } from '../../../common/parse_duration';
+import { parseDate } from '../common';
+import { RulesClientContext } from '../types';
+import { get } from './get';
+
+export interface GetAlertSummaryParams {
+ id: string;
+ dateStart?: string;
+ numberOfExecutions?: number;
+}
+
+export async function getAlertSummary(
+ context: RulesClientContext,
+ { id, dateStart, numberOfExecutions }: GetAlertSummaryParams
+): Promise {
+ context.logger.debug(`getAlertSummary(): getting alert ${id}`);
+ const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
+
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: rule.alertTypeId,
+ consumer: rule.consumer,
+ operation: ReadOperations.GetAlertSummary,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ const dateNow = new Date();
+ const durationMillis = parseDuration(rule.schedule.interval) * (numberOfExecutions ?? 60);
+ const defaultDateStart = new Date(dateNow.valueOf() - durationMillis);
+ const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart);
+
+ const eventLogClient = await context.getEventLogClient();
+
+ context.logger.debug(`getAlertSummary(): search the event log for rule ${id}`);
+ let events: IEvent[];
+ let executionEvents: IEvent[];
+
+ try {
+ const [queryResults, executionResults] = await Promise.all([
+ eventLogClient.findEventsBySavedObjectIds(
+ 'alert',
+ [id],
+ {
+ page: 1,
+ per_page: 10000,
+ start: parsedDateStart.toISOString(),
+ sort: [{ sort_field: '@timestamp', sort_order: 'desc' }],
+ end: dateNow.toISOString(),
+ },
+ rule.legacyId !== null ? [rule.legacyId] : undefined
+ ),
+ eventLogClient.findEventsBySavedObjectIds(
+ 'alert',
+ [id],
+ {
+ page: 1,
+ per_page: numberOfExecutions ?? 60,
+ filter: 'event.provider: alerting AND event.action:execute',
+ sort: [{ sort_field: '@timestamp', sort_order: 'desc' }],
+ end: dateNow.toISOString(),
+ },
+ rule.legacyId !== null ? [rule.legacyId] : undefined
+ ),
+ ]);
+ events = queryResults.data;
+ executionEvents = executionResults.data;
+ } catch (err) {
+ context.logger.debug(
+ `rulesClient.getAlertSummary(): error searching event log for rule ${id}: ${err.message}`
+ );
+ events = [];
+ executionEvents = [];
+ }
+
+ return alertSummaryFromEventLog({
+ rule,
+ events,
+ executionEvents,
+ dateStart: parsedDateStart.toISOString(),
+ dateEnd: dateNow.toISOString(),
+ });
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_execution_kpi.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_kpi.ts
new file mode 100644
index 00000000000000..734df53c9cb299
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_kpi.ts
@@ -0,0 +1,158 @@
+/*
+ * 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 { KueryNode } from '@kbn/es-query';
+import { SanitizedRuleWithLegacyId } from '../../types';
+import {
+ ReadOperations,
+ AlertingAuthorizationEntity,
+ AlertingAuthorizationFilterType,
+} from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import {
+ formatExecutionKPIResult,
+ getExecutionKPIAggregation,
+} from '../../lib/get_execution_log_aggregation';
+import { RulesClientContext } from '../types';
+import { parseDate } from '../common';
+import { get } from './get';
+
+export interface GetRuleExecutionKPIParams {
+ id: string;
+ dateStart: string;
+ dateEnd?: string;
+ filter?: string;
+}
+
+export interface GetGlobalExecutionKPIParams {
+ dateStart: string;
+ dateEnd?: string;
+ filter?: string;
+ namespaces?: Array;
+}
+
+export async function getRuleExecutionKPI(
+ context: RulesClientContext,
+ { id, dateStart, dateEnd, filter }: GetRuleExecutionKPIParams
+) {
+ context.logger.debug(`getRuleExecutionKPI(): getting execution KPI for rule ${id}`);
+ const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
+
+ try {
+ // Make sure user has access to this rule
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: rule.alertTypeId,
+ consumer: rule.consumer,
+ operation: ReadOperations.GetRuleExecutionKPI,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_RULE_EXECUTION_KPI,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_RULE_EXECUTION_KPI,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ // default duration of instance summary is 60 * rule interval
+ const dateNow = new Date();
+ const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
+ const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
+
+ const eventLogClient = await context.getEventLogClient();
+
+ try {
+ const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds(
+ 'alert',
+ [id],
+ {
+ start: parsedDateStart.toISOString(),
+ end: parsedDateEnd.toISOString(),
+ aggs: getExecutionKPIAggregation(filter),
+ },
+ rule.legacyId !== null ? [rule.legacyId] : undefined
+ );
+
+ return formatExecutionKPIResult(aggResult);
+ } catch (err) {
+ context.logger.debug(
+ `rulesClient.getRuleExecutionKPI(): error searching execution KPI for rule ${id}: ${err.message}`
+ );
+ throw err;
+ }
+}
+
+export async function getGlobalExecutionKpiWithAuth(
+ context: RulesClientContext,
+ { dateStart, dateEnd, filter, namespaces }: GetGlobalExecutionKPIParams
+) {
+ context.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
+
+ let authorizationTuple;
+ try {
+ authorizationTuple = await context.authorization.getFindAuthorizationFilter(
+ AlertingAuthorizationEntity.Alert,
+ {
+ type: AlertingAuthorizationFilterType.KQL,
+ fieldNames: {
+ ruleTypeId: 'kibana.alert.rule.rule_type_id',
+ consumer: 'kibana.alert.rule.consumer',
+ },
+ }
+ );
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI,
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI,
+ })
+ );
+
+ const dateNow = new Date();
+ const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
+ const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
+
+ const eventLogClient = await context.getEventLogClient();
+
+ try {
+ const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
+ 'alert',
+ authorizationTuple.filter as KueryNode,
+ {
+ start: parsedDateStart.toISOString(),
+ end: parsedDateEnd.toISOString(),
+ aggs: getExecutionKPIAggregation(filter),
+ },
+ namespaces
+ );
+
+ return formatExecutionKPIResult(aggResult);
+ } catch (err) {
+ context.logger.debug(
+ `rulesClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}`
+ );
+ throw err;
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/get_execution_log.ts b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_log.ts
new file mode 100644
index 00000000000000..006109d71b4b50
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/get_execution_log.ts
@@ -0,0 +1,176 @@
+/*
+ * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { KueryNode } from '@kbn/es-query';
+import { SanitizedRuleWithLegacyId } from '../../types';
+import {
+ ReadOperations,
+ AlertingAuthorizationEntity,
+ AlertingAuthorizationFilterType,
+} from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import {
+ formatExecutionLogResult,
+ getExecutionLogAggregation,
+} from '../../lib/get_execution_log_aggregation';
+import { IExecutionLogResult } from '../../../common';
+import { parseDate } from '../common';
+import { RulesClientContext } from '../types';
+import { get } from './get';
+
+export interface GetExecutionLogByIdParams {
+ id: string;
+ dateStart: string;
+ dateEnd?: string;
+ filter?: string;
+ page: number;
+ perPage: number;
+ sort: estypes.Sort;
+}
+
+export interface GetGlobalExecutionLogParams {
+ dateStart: string;
+ dateEnd?: string;
+ filter?: string;
+ page: number;
+ perPage: number;
+ sort: estypes.Sort;
+ namespaces?: Array;
+}
+
+export async function getExecutionLogForRule(
+ context: RulesClientContext,
+ { id, dateStart, dateEnd, filter, page, perPage, sort }: GetExecutionLogByIdParams
+): Promise {
+ context.logger.debug(`getExecutionLogForRule(): getting execution log for rule ${id}`);
+ const rule = (await get(context, { id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
+
+ try {
+ // Make sure user has access to this rule
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: rule.alertTypeId,
+ consumer: rule.consumer,
+ operation: ReadOperations.GetExecutionLog,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_EXECUTION_LOG,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_EXECUTION_LOG,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ // default duration of instance summary is 60 * rule interval
+ const dateNow = new Date();
+ const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
+ const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
+
+ const eventLogClient = await context.getEventLogClient();
+
+ try {
+ const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds(
+ 'alert',
+ [id],
+ {
+ start: parsedDateStart.toISOString(),
+ end: parsedDateEnd.toISOString(),
+ aggs: getExecutionLogAggregation({
+ filter,
+ page,
+ perPage,
+ sort,
+ }),
+ },
+ rule.legacyId !== null ? [rule.legacyId] : undefined
+ );
+
+ return formatExecutionLogResult(aggResult);
+ } catch (err) {
+ context.logger.debug(
+ `rulesClient.getExecutionLogForRule(): error searching event log for rule ${id}: ${err.message}`
+ );
+ throw err;
+ }
+}
+
+export async function getGlobalExecutionLogWithAuth(
+ context: RulesClientContext,
+ { dateStart, dateEnd, filter, page, perPage, sort, namespaces }: GetGlobalExecutionLogParams
+): Promise {
+ context.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
+
+ let authorizationTuple;
+ try {
+ authorizationTuple = await context.authorization.getFindAuthorizationFilter(
+ AlertingAuthorizationEntity.Alert,
+ {
+ type: AlertingAuthorizationFilterType.KQL,
+ fieldNames: {
+ ruleTypeId: 'kibana.alert.rule.rule_type_id',
+ consumer: 'kibana.alert.rule.consumer',
+ },
+ }
+ );
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG,
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG,
+ })
+ );
+
+ const dateNow = new Date();
+ const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
+ const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
+
+ const eventLogClient = await context.getEventLogClient();
+
+ try {
+ const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
+ 'alert',
+ authorizationTuple.filter as KueryNode,
+ {
+ start: parsedDateStart.toISOString(),
+ end: parsedDateEnd.toISOString(),
+ aggs: getExecutionLogAggregation({
+ filter,
+ page,
+ perPage,
+ sort,
+ }),
+ },
+ namespaces
+ );
+
+ return formatExecutionLogResult(aggResult);
+ } catch (err) {
+ context.logger.debug(
+ `rulesClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}`
+ );
+ throw err;
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/list_alert_types.ts b/x-pack/plugins/alerting/server/rules_client/methods/list_alert_types.ts
new file mode 100644
index 00000000000000..eabe15834d6d63
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/list_alert_types.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { WriteOperations, ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { RulesClientContext } from '../types';
+
+export async function listAlertTypes(context: RulesClientContext) {
+ return await context.authorization.filterByRuleTypeAuthorization(
+ context.ruleTypeRegistry.list(),
+ [ReadOperations.Get, WriteOperations.Create],
+ AlertingAuthorizationEntity.Rule
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts b/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts
new file mode 100644
index 00000000000000..4ac6ad207fdc7b
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { RawRule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { partiallyUpdateAlert } from '../../saved_objects';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+import { updateMeta } from '../lib';
+import { clearUnscheduledSnooze } from '../common';
+
+export async function muteAll(context: RulesClientContext, { id }: { id: string }): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.muteAll('${id}')`,
+ async () => await muteAllWithOCC(context, { id })
+ );
+}
+
+async function muteAllWithOCC(context: RulesClientContext, { id }: { id: string }) {
+ const { attributes, version } = await context.unsecuredSavedObjectsClient.get(
+ 'alert',
+ id
+ );
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.MuteAll,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ const updateAttributes = updateMeta(context, {
+ muteAll: true,
+ mutedInstanceIds: [],
+ snoozeSchedule: clearUnscheduledSnooze(attributes),
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ });
+ const updateOptions = { version };
+
+ await partiallyUpdateAlert(
+ context.unsecuredSavedObjectsClient,
+ id,
+ updateAttributes,
+ updateOptions
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/mute_instance.ts b/x-pack/plugins/alerting/server/rules_client/methods/mute_instance.ts
new file mode 100644
index 00000000000000..67e78b9851945a
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/mute_instance.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { Rule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { MuteOptions } from '../types';
+import { RulesClientContext } from '../types';
+import { updateMeta } from '../lib';
+
+export async function muteInstance(
+ context: RulesClientContext,
+ { alertId, alertInstanceId }: MuteOptions
+): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.muteInstance('${alertId}')`,
+ async () => await muteInstanceWithOCC(context, { alertId, alertInstanceId })
+ );
+}
+
+async function muteInstanceWithOCC(
+ context: RulesClientContext,
+ { alertId, alertInstanceId }: MuteOptions
+) {
+ const { attributes, version } = await context.unsecuredSavedObjectsClient.get(
+ 'alert',
+ alertId
+ );
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.MuteAlert,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE_ALERT,
+ savedObject: { type: 'alert', id: alertId },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE_ALERT,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id: alertId },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ const mutedInstanceIds = attributes.mutedInstanceIds || [];
+ if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) {
+ mutedInstanceIds.push(alertInstanceId);
+ await context.unsecuredSavedObjectsClient.update(
+ 'alert',
+ alertId,
+ updateMeta(context, {
+ mutedInstanceIds,
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ }),
+ { version }
+ );
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/resolve.ts b/x-pack/plugins/alerting/server/rules_client/methods/resolve.ts
new file mode 100644
index 00000000000000..539e4c089d36d0
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/resolve.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 { RawRule, RuleTypeParams, ResolvedSanitizedRule } from '../../types';
+import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { getAlertFromRaw } from '../lib/get_alert_from_raw';
+import { RulesClientContext } from '../types';
+
+export interface ResolveParams {
+ id: string;
+ includeLegacyId?: boolean;
+ includeSnoozeData?: boolean;
+}
+
+export async function resolve(
+ context: RulesClientContext,
+ { id, includeLegacyId, includeSnoozeData = false }: ResolveParams
+): Promise> {
+ const { saved_object: result, ...resolveResponse } =
+ await context.unsecuredSavedObjectsClient.resolve('alert', id);
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: result.attributes.alertTypeId,
+ consumer: result.attributes.consumer,
+ operation: ReadOperations.Get,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.RESOLVE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.RESOLVE,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ const rule = getAlertFromRaw(
+ context,
+ result.id,
+ result.attributes.alertTypeId,
+ result.attributes,
+ result.references,
+ includeLegacyId,
+ false,
+ includeSnoozeData
+ );
+
+ return {
+ ...rule,
+ ...resolveResponse,
+ };
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/run_soon.ts b/x-pack/plugins/alerting/server/rules_client/methods/run_soon.ts
new file mode 100644
index 00000000000000..d683b5fbafe4f1
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/run_soon.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server';
+import { Rule } from '../../types';
+import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+
+export async function runSoon(context: RulesClientContext, { id }: { id: string }) {
+ const { attributes } = await context.unsecuredSavedObjectsClient.get('alert', id);
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: ReadOperations.RunSoon,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.RUN_SOON,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.RUN_SOON,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ // Check that the rule is enabled
+ if (!attributes.enabled) {
+ return i18n.translate('xpack.alerting.rulesClient.runSoon.disabledRuleError', {
+ defaultMessage: 'Error running rule: rule is disabled',
+ });
+ }
+
+ let taskDoc: ConcreteTaskInstance | null = null;
+ try {
+ taskDoc = attributes.scheduledTaskId
+ ? await context.taskManager.get(attributes.scheduledTaskId)
+ : null;
+ } catch (err) {
+ return i18n.translate('xpack.alerting.rulesClient.runSoon.getTaskError', {
+ defaultMessage: 'Error running rule: {errMessage}',
+ values: {
+ errMessage: err.message,
+ },
+ });
+ }
+
+ if (
+ taskDoc &&
+ (taskDoc.status === TaskStatus.Claiming || taskDoc.status === TaskStatus.Running)
+ ) {
+ return i18n.translate('xpack.alerting.rulesClient.runSoon.ruleIsRunning', {
+ defaultMessage: 'Rule is already running',
+ });
+ }
+
+ try {
+ await context.taskManager.runSoon(attributes.scheduledTaskId ? attributes.scheduledTaskId : id);
+ } catch (err) {
+ return i18n.translate('xpack.alerting.rulesClient.runSoon.runSoonError', {
+ defaultMessage: 'Error running rule: {errMessage}',
+ values: {
+ errMessage: err.message,
+ },
+ });
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts b/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts
new file mode 100644
index 00000000000000..04585bca002b0c
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { RawRule, RuleSnoozeSchedule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { partiallyUpdateAlert } from '../../saved_objects';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { validateSnoozeStartDate } from '../../lib/validate_snooze_date';
+import { RuleMutedError } from '../../lib/errors/rule_muted';
+import { RulesClientContext } from '../types';
+import { getSnoozeAttributes, verifySnoozeScheduleLimit } from '../common';
+import { updateMeta } from '../lib';
+
+export interface SnoozeParams {
+ id: string;
+ snoozeSchedule: RuleSnoozeSchedule;
+}
+
+export async function snooze(
+ context: RulesClientContext,
+ { id, snoozeSchedule }: SnoozeParams
+): Promise {
+ const snoozeDateValidationMsg = validateSnoozeStartDate(snoozeSchedule.rRule.dtstart);
+ if (snoozeDateValidationMsg) {
+ throw new RuleMutedError(snoozeDateValidationMsg);
+ }
+
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`,
+ async () => await snoozeWithOCC(context, { id, snoozeSchedule })
+ );
+}
+
+async function snoozeWithOCC(
+ context: RulesClientContext,
+ {
+ id,
+ snoozeSchedule,
+ }: {
+ id: string;
+ snoozeSchedule: RuleSnoozeSchedule;
+ }
+) {
+ const { attributes, version } = await context.unsecuredSavedObjectsClient.get(
+ 'alert',
+ id
+ );
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.Snooze,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.SNOOZE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.SNOOZE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule);
+
+ try {
+ verifySnoozeScheduleLimit(newAttrs);
+ } catch (error) {
+ throw Boom.badRequest(error.message);
+ }
+
+ const updateAttributes = updateMeta(context, {
+ ...newAttrs,
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ });
+ const updateOptions = { version };
+
+ await partiallyUpdateAlert(
+ context.unsecuredSavedObjectsClient,
+ id,
+ updateAttributes,
+ updateOptions
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts b/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts
new file mode 100644
index 00000000000000..80819de2b6cc22
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { RawRule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { partiallyUpdateAlert } from '../../saved_objects';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+import { updateMeta } from '../lib';
+import { clearUnscheduledSnooze } from '../common';
+
+export async function unmuteAll(
+ context: RulesClientContext,
+ { id }: { id: string }
+): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.unmuteAll('${id}')`,
+ async () => await unmuteAllWithOCC(context, { id })
+ );
+}
+
+async function unmuteAllWithOCC(context: RulesClientContext, { id }: { id: string }) {
+ const { attributes, version } = await context.unsecuredSavedObjectsClient.get(
+ 'alert',
+ id
+ );
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.UnmuteAll,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ const updateAttributes = updateMeta(context, {
+ muteAll: false,
+ mutedInstanceIds: [],
+ snoozeSchedule: clearUnscheduledSnooze(attributes),
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ });
+ const updateOptions = { version };
+
+ await partiallyUpdateAlert(
+ context.unsecuredSavedObjectsClient,
+ id,
+ updateAttributes,
+ updateOptions
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts b/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts
new file mode 100644
index 00000000000000..714e5c0a4f8e48
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { Rule, RawRule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { MuteOptions } from '../types';
+import { RulesClientContext } from '../types';
+import { updateMeta } from '../lib';
+
+export async function unmuteInstance(
+ context: RulesClientContext,
+ { alertId, alertInstanceId }: MuteOptions
+): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.unmuteInstance('${alertId}')`,
+ async () => await unmuteInstanceWithOCC(context, { alertId, alertInstanceId })
+ );
+}
+
+async function unmuteInstanceWithOCC(
+ context: RulesClientContext,
+ {
+ alertId,
+ alertInstanceId,
+ }: {
+ alertId: string;
+ alertInstanceId: string;
+ }
+) {
+ const { attributes, version } = await context.unsecuredSavedObjectsClient.get(
+ 'alert',
+ alertId
+ );
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.UnmuteAlert,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE_ALERT,
+ savedObject: { type: 'alert', id: alertId },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE_ALERT,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id: alertId },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ const mutedInstanceIds = attributes.mutedInstanceIds || [];
+ if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) {
+ await context.unsecuredSavedObjectsClient.update(
+ 'alert',
+ alertId,
+ updateMeta(context, {
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
+ }),
+ { version }
+ );
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unsnooze.ts b/x-pack/plugins/alerting/server/rules_client/methods/unsnooze.ts
new file mode 100644
index 00000000000000..67e8d76e649b4e
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/unsnooze.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { RawRule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { partiallyUpdateAlert } from '../../saved_objects';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { RulesClientContext } from '../types';
+import { updateMeta } from '../lib';
+import { getUnsnoozeAttributes } from '../common';
+
+export interface UnsnoozeParams {
+ id: string;
+ scheduleIds?: string[];
+}
+
+export async function unsnooze(
+ context: RulesClientContext,
+ { id, scheduleIds }: UnsnoozeParams
+): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.unsnooze('${id}')`,
+ async () => await unsnoozeWithOCC(context, { id, scheduleIds })
+ );
+}
+
+async function unsnoozeWithOCC(context: RulesClientContext, { id, scheduleIds }: UnsnoozeParams) {
+ const { attributes, version } = await context.unsecuredSavedObjectsClient.get(
+ 'alert',
+ id
+ );
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.Unsnooze,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UNSNOOZE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UNSNOOZE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+ const newAttrs = getUnsnoozeAttributes(attributes, scheduleIds);
+
+ const updateAttributes = updateMeta(context, {
+ ...newAttrs,
+ updatedBy: await context.getUserName(),
+ updatedAt: new Date().toISOString(),
+ });
+ const updateOptions = { version };
+
+ await partiallyUpdateAlert(
+ context.unsecuredSavedObjectsClient,
+ id,
+ updateAttributes,
+ updateOptions
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
new file mode 100644
index 00000000000000..289f5fe0078740
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
@@ -0,0 +1,240 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { isEqual } from 'lodash';
+import { SavedObject } from '@kbn/core/server';
+import {
+ PartialRule,
+ RawRule,
+ RuleTypeParams,
+ RuleNotifyWhenType,
+ IntervalSchedule,
+} from '../../types';
+import { validateRuleTypeParams, getRuleNotifyWhenType } from '../../lib';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { parseDuration } from '../../../common/parse_duration';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { getMappedParams } from '../common/mapped_params_utils';
+import { NormalizedAlertAction, RulesClientContext } from '../types';
+import { validateActions, extractReferences, updateMeta, getPartialRuleFromRaw } from '../lib';
+import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common';
+
+export interface UpdateOptions {
+ id: string;
+ data: {
+ name: string;
+ tags: string[];
+ schedule: IntervalSchedule;
+ actions: NormalizedAlertAction[];
+ params: Params;
+ throttle?: string | null;
+ notifyWhen?: RuleNotifyWhenType | null;
+ };
+}
+
+export async function update(
+ context: RulesClientContext,
+ { id, data }: UpdateOptions
+): Promise> {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.update('${id}')`,
+ async () => await updateWithOCC(context, { id, data })
+ );
+}
+
+async function updateWithOCC(
+ context: RulesClientContext,
+ { id, data }: UpdateOptions
+): Promise> {
+ let alertSavedObject: SavedObject;
+
+ try {
+ alertSavedObject =
+ await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, {
+ namespace: context.namespace,
+ });
+ } catch (e) {
+ // We'll skip invalidating the API key since we failed to load the decrypted saved object
+ context.logger.error(
+ `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
+ );
+ // Still attempt to load the object using SOC
+ alertSavedObject = await context.unsecuredSavedObjectsClient.get('alert', id);
+ }
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: alertSavedObject.attributes.alertTypeId,
+ consumer: alertSavedObject.attributes.consumer,
+ operation: WriteOperations.Update,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId);
+
+ const updateResult = await updateAlert(context, { id, data }, alertSavedObject);
+
+ await Promise.all([
+ alertSavedObject.attributes.apiKey
+ ? bulkMarkApiKeysForInvalidation(
+ { apiKeys: [alertSavedObject.attributes.apiKey] },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ )
+ : null,
+ (async () => {
+ if (
+ updateResult.scheduledTaskId &&
+ updateResult.schedule &&
+ !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule)
+ ) {
+ try {
+ const { tasks } = await context.taskManager.bulkUpdateSchedules(
+ [updateResult.scheduledTaskId],
+ updateResult.schedule
+ );
+
+ context.logger.debug(
+ `Rule update has rescheduled the underlying task: ${updateResult.scheduledTaskId} to run at: ${tasks?.[0]?.runAt}`
+ );
+ } catch (err) {
+ context.logger.error(
+ `Rule update failed to run its underlying task. TaskManager bulkUpdateSchedules failed with Error: ${err.message}`
+ );
+ }
+ }
+ })(),
+ ]);
+
+ return updateResult;
+}
+
+async function updateAlert(
+ context: RulesClientContext,
+ { id, data }: UpdateOptions,
+ { attributes, version }: SavedObject
+): Promise> {
+ const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId);
+
+ // Validate
+ const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params);
+ await validateActions(context, ruleType, data);
+
+ // Throw error if schedule interval is less than the minimum and we are enforcing it
+ const intervalInMs = parseDuration(data.schedule.interval);
+ if (
+ intervalInMs < context.minimumScheduleIntervalInMs &&
+ context.minimumScheduleInterval.enforce
+ ) {
+ throw Boom.badRequest(
+ `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}`
+ );
+ }
+
+ // Extract saved object references for this rule
+ const {
+ references,
+ params: updatedParams,
+ actions,
+ } = await extractReferences(context, ruleType, data.actions, validatedAlertTypeParams);
+
+ const username = await context.getUserName();
+
+ let createdAPIKey = null;
+ try {
+ createdAPIKey = attributes.enabled
+ ? await context.createAPIKey(generateAPIKeyName(ruleType.id, data.name))
+ : null;
+ } catch (error) {
+ throw Boom.badRequest(`Error updating rule: could not create API key - ${error.message}`);
+ }
+
+ const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username);
+ const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
+
+ let updatedObject: SavedObject;
+ const createAttributes = updateMeta(context, {
+ ...attributes,
+ ...data,
+ ...apiKeyAttributes,
+ params: updatedParams as RawRule['params'],
+ actions,
+ notifyWhen,
+ updatedBy: username,
+ updatedAt: new Date().toISOString(),
+ });
+
+ const mappedParams = getMappedParams(updatedParams);
+
+ if (Object.keys(mappedParams).length) {
+ createAttributes.mapped_params = mappedParams;
+ }
+
+ try {
+ updatedObject = await context.unsecuredSavedObjectsClient.create(
+ 'alert',
+ createAttributes,
+ {
+ id,
+ overwrite: true,
+ version,
+ references,
+ }
+ );
+ } catch (e) {
+ // Avoid unused API key
+ await bulkMarkApiKeysForInvalidation(
+ { apiKeys: createAttributes.apiKey ? [createAttributes.apiKey] : [] },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ );
+
+ throw e;
+ }
+
+ // Log warning if schedule interval is less than the minimum but we're not enforcing it
+ if (
+ intervalInMs < context.minimumScheduleIntervalInMs &&
+ !context.minimumScheduleInterval.enforce
+ ) {
+ context.logger.warn(
+ `Rule schedule interval (${data.schedule.interval}) for "${ruleType.id}" rule type with ID "${id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.`
+ );
+ }
+
+ return getPartialRuleFromRaw(
+ context,
+ id,
+ ruleType,
+ updatedObject.attributes,
+ updatedObject.references,
+ false,
+ true
+ );
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts b/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts
new file mode 100644
index 00000000000000..abb7d32404d570
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { RawRule } from '../../types';
+import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
+import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
+import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common';
+import { updateMeta } from '../lib';
+import { RulesClientContext } from '../types';
+
+export async function updateApiKey(
+ context: RulesClientContext,
+ { id }: { id: string }
+): Promise {
+ return await retryIfConflicts(
+ context.logger,
+ `rulesClient.updateApiKey('${id}')`,
+ async () => await updateApiKeyWithOCC(context, { id })
+ );
+}
+
+async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: string }) {
+ let apiKeyToInvalidate: string | null = null;
+ let attributes: RawRule;
+ let version: string | undefined;
+
+ try {
+ const decryptedAlert =
+ await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, {
+ namespace: context.namespace,
+ });
+ apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
+ attributes = decryptedAlert.attributes;
+ version = decryptedAlert.version;
+ } catch (e) {
+ // We'll skip invalidating the API key since we failed to load the decrypted saved object
+ context.logger.error(
+ `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
+ );
+ // Still attempt to load the attributes and version using SOC
+ const alert = await context.unsecuredSavedObjectsClient.get('alert', id);
+ attributes = alert.attributes;
+ version = alert.version;
+ }
+
+ try {
+ await context.authorization.ensureAuthorized({
+ ruleTypeId: attributes.alertTypeId,
+ consumer: attributes.consumer,
+ operation: WriteOperations.UpdateApiKey,
+ entity: AlertingAuthorizationEntity.Rule,
+ });
+ if (attributes.actions.length) {
+ await context.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE_API_KEY,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ const username = await context.getUserName();
+
+ let createdAPIKey = null;
+ try {
+ createdAPIKey = await context.createAPIKey(
+ generateAPIKeyName(attributes.alertTypeId, attributes.name)
+ );
+ } catch (error) {
+ throw Boom.badRequest(
+ `Error updating API key for rule: could not create API key - ${error.message}`
+ );
+ }
+
+ const updateAttributes = updateMeta(context, {
+ ...attributes,
+ ...apiKeyAsAlertAttributes(createdAPIKey, username),
+ updatedAt: new Date().toISOString(),
+ updatedBy: username,
+ });
+
+ context.auditLogger?.log(
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE_API_KEY,
+ outcome: 'unknown',
+ savedObject: { type: 'alert', id },
+ })
+ );
+
+ context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
+
+ try {
+ await context.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
+ } catch (e) {
+ // Avoid unused API key
+ await bulkMarkApiKeysForInvalidation(
+ { apiKeys: updateAttributes.apiKey ? [updateAttributes.apiKey] : [] },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ );
+ throw e;
+ }
+
+ if (apiKeyToInvalidate) {
+ await bulkMarkApiKeysForInvalidation(
+ { apiKeys: [apiKeyToInvalidate] },
+ context.logger,
+ context.unsecuredSavedObjectsClient
+ );
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
index 5feb6ab8c04458..d790ec3587d77d 100644
--- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts
+++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
@@ -5,4512 +5,134 @@
* 2.0.
*/
-import Semver from 'semver';
-import pMap from 'p-map';
-import Boom from '@hapi/boom';
-import {
- omit,
- isEqual,
- map,
- uniq,
- pick,
- truncate,
- trim,
- mapValues,
- cloneDeep,
- isEmpty,
-} from 'lodash';
-import { i18n } from '@kbn/i18n';
-import { AlertConsumers } from '@kbn/rule-data-utils';
-import { KueryNode, nodeBuilder } from '@kbn/es-query';
-import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import {
- Logger,
- SavedObjectsClientContract,
- SavedObjectReference,
- SavedObject,
- PluginInitializerContext,
- SavedObjectsUtils,
- SavedObjectAttributes,
- SavedObjectsBulkUpdateObject,
- SavedObjectsBulkDeleteObject,
- SavedObjectsUpdateResponse,
-} from '@kbn/core/server';
-import { ActionsClient, ActionsAuthorization } from '@kbn/actions-plugin/server';
-import {
- GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult,
- InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult,
-} from '@kbn/security-plugin/server';
-import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
-import {
- ConcreteTaskInstance,
- TaskManagerStartContract,
- TaskStatus,
-} from '@kbn/task-manager-plugin/server';
-import {
- IEvent,
- IEventLogClient,
- IEventLogger,
- SAVED_OBJECT_REL_PRIMARY,
-} from '@kbn/event-log-plugin/server';
-import { AuditLogger } from '@kbn/security-plugin/server';
-import {
- Rule,
- PartialRule,
- RawRule,
- RuleTypeRegistry,
- RuleAction,
- IntervalSchedule,
- SanitizedRule,
- RuleTaskState,
- AlertSummary,
- RuleExecutionStatusValues,
- RuleLastRunOutcomeValues,
- RuleNotifyWhenType,
- RuleTypeParams,
- ResolvedSanitizedRule,
- RuleWithLegacyId,
- SanitizedRuleWithLegacyId,
- PartialRuleWithLegacyId,
- RuleSnooze,
- RuleSnoozeSchedule,
- RawAlertInstance as RawAlert,
-} from '../types';
-import {
- validateRuleTypeParams,
- ruleExecutionStatusFromRaw,
- getRuleNotifyWhenType,
- validateMutatedRuleTypeParams,
- convertRuleIdsToKueryNode,
- getRuleSnoozeEndTime,
- convertEsSortToEventLogSort,
- getDefaultMonitoring,
- updateMonitoring,
- convertMonitoringFromRawAndVerify,
- getNextRun,
-} from '../lib';
-import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance';
-import { RegistryRuleType, UntypedNormalizedRuleType } from '../rule_type_registry';
-import {
- AlertingAuthorization,
- WriteOperations,
- ReadOperations,
- AlertingAuthorizationEntity,
- AlertingAuthorizationFilterType,
- AlertingAuthorizationFilterOpts,
-} from '../authorization';
-import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date';
-import { alertSummaryFromEventLog } from '../lib/alert_summary_from_event_log';
+import { SanitizedRule, RuleTypeParams } from '../types';
import { parseDuration } from '../../common/parse_duration';
-import { retryIfConflicts } from '../lib/retry_if_conflicts';
-import { partiallyUpdateAlert } from '../saved_objects';
-import { bulkMarkApiKeysForInvalidation } from '../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
-import { ruleAuditEvent, RuleAuditAction } from './audit_events';
+import { RulesClientContext, BulkOptions, MuteOptions } from './types';
+
+import { clone, CloneArguments } from './methods/clone';
+import { create, CreateOptions } from './methods/create';
+import { get, GetParams } from './methods/get';
+import { resolve, ResolveParams } from './methods/resolve';
+import { getAlertState, GetAlertStateParams } from './methods/get_alert_state';
+import { getAlertSummary, GetAlertSummaryParams } from './methods/get_alert_summary';
import {
- mapSortField,
- validateOperationOnAttributes,
- retryIfBulkEditConflicts,
- retryIfBulkDeleteConflicts,
- retryIfBulkDisableConflicts,
- retryIfBulkOperationConflicts,
- applyBulkEditOperation,
- buildKueryNodeFilter,
-} from './lib';
-import { getRuleExecutionStatusPending } from '../lib/rule_execution_status';
-import { Alert } from '../alert';
-import { EVENT_LOG_ACTIONS } from '../plugin';
-import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object';
+ GetExecutionLogByIdParams,
+ getExecutionLogForRule,
+ GetGlobalExecutionLogParams,
+ getGlobalExecutionLogWithAuth,
+} from './methods/get_execution_log';
import {
- getMappedParams,
- getModifiedField,
- getModifiedSearchFields,
- getModifiedSearch,
- modifyFilterKueryNode,
-} from './lib/mapped_params_utils';
-import { AlertingRulesConfig } from '../config';
+ getActionErrorLog,
+ GetActionErrorLogByIdParams,
+ getActionErrorLogWithAuth,
+} from './methods/get_action_error_log';
import {
- formatExecutionLogResult,
- formatExecutionKPIResult,
- getExecutionLogAggregation,
- getExecutionKPIAggregation,
-} from '../lib/get_execution_log_aggregation';
-import { IExecutionLogResult, IExecutionErrorsResult } from '../../common';
-import { validateSnoozeStartDate } from '../lib/validate_snooze_date';
-import { RuleMutedError } from '../lib/errors/rule_muted';
-import { formatExecutionErrorsResult } from '../lib/format_execution_log_errors';
-import { getActiveScheduledSnoozes } from '../lib/is_rule_snoozed';
-import { isSnoozeExpired } from '../lib';
-import { isDetectionEngineAADRuleType } from '../saved_objects/migrations/utils';
-
-export interface RegistryAlertTypeWithAuth extends RegistryRuleType {
- authorizedConsumers: string[];
-}
-type NormalizedAlertAction = Omit;
-export type CreateAPIKeyResult =
- | { apiKeysEnabled: false }
- | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult };
-export type InvalidateAPIKeyResult =
- | { apiKeysEnabled: false }
- | { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult };
-
-export interface RuleAggregation {
- status: {
- buckets: Array<{
- key: string;
- doc_count: number;
- }>;
- };
- outcome: {
- buckets: Array<{
- key: string;
- doc_count: number;
- }>;
- };
- muted: {
- buckets: Array<{
- key: number;
- key_as_string: string;
- doc_count: number;
- }>;
- };
- enabled: {
- buckets: Array<{
- key: number;
- key_as_string: string;
- doc_count: number;
- }>;
- };
- snoozed: {
- count: {
- doc_count: number;
- };
- };
- tags: {
- buckets: Array<{
- key: string;
- doc_count: number;
- }>;
- };
-}
-
-export interface RuleBulkOperationAggregation {
- alertTypeId: {
- buckets: Array<{
- key: string[];
- doc_count: number;
- }>;
- };
-}
-
-export interface ConstructorOptions {
- logger: Logger;
- taskManager: TaskManagerStartContract;
- unsecuredSavedObjectsClient: SavedObjectsClientContract;
- authorization: AlertingAuthorization;
- actionsAuthorization: ActionsAuthorization;
- ruleTypeRegistry: RuleTypeRegistry;
- minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
- encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
- spaceId: string;
- namespace?: string;
- getUserName: () => Promise;
- createAPIKey: (name: string) => Promise;
- getActionsClient: () => Promise;
- getEventLogClient: () => Promise;
- kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
- auditLogger?: AuditLogger;
- eventLogger?: IEventLogger;
-}
-
-export interface MuteOptions extends IndexType {
- alertId: string;
- alertInstanceId: string;
-}
-
-export interface SnoozeOptions extends IndexType {
- snoozeSchedule: RuleSnoozeSchedule;
-}
-
-export interface FindOptions extends IndexType {
- perPage?: number;
- page?: number;
- search?: string;
- defaultSearchOperator?: 'AND' | 'OR';
- searchFields?: string[];
- sortField?: string;
- sortOrder?: estypes.SortOrder;
- hasReference?: {
- type: string;
- id: string;
- };
- fields?: string[];
- filter?: string | KueryNode;
-}
-
-export type BulkEditFields = keyof Pick<
- Rule,
- 'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey'
+ GetGlobalExecutionKPIParams,
+ getGlobalExecutionKpiWithAuth,
+ getRuleExecutionKPI,
+ GetRuleExecutionKPIParams,
+} from './methods/get_execution_kpi';
+import { find, FindParams } from './methods/find';
+import { aggregate, AggregateOptions } from './methods/aggregate';
+import { deleteRule } from './methods/delete';
+import { update, UpdateOptions } from './methods/update';
+import { bulkDeleteRules } from './methods/bulk_delete';
+import { bulkEdit, BulkEditOptions } from './methods/bulk_edit';
+import { bulkEnableRules } from './methods/bulk_enable';
+import { bulkDisableRules } from './methods/bulk_disable';
+import { updateApiKey } from './methods/update_api_key';
+import { enable } from './methods/enable';
+import { disable } from './methods/disable';
+import { snooze, SnoozeParams } from './methods/snooze';
+import { unsnooze, UnsnoozeParams } from './methods/unsnooze';
+import { clearExpiredSnoozes } from './methods/clear_expired_snoozes';
+import { muteAll } from './methods/mute_all';
+import { unmuteAll } from './methods/unmute_all';
+import { muteInstance } from './methods/mute_instance';
+import { unmuteInstance } from './methods/unmute_instance';
+import { runSoon } from './methods/run_soon';
+import { listAlertTypes } from './methods/list_alert_types';
+
+export type ConstructorOptions = Omit<
+ RulesClientContext,
+ 'fieldsToExcludeFromPublicApi' | 'minimumScheduleIntervalInMs'
>;
-export type BulkEditOperation =
- | {
- operation: 'add' | 'delete' | 'set';
- field: Extract;
- value: string[];
- }
- | {
- operation: 'add' | 'set';
- field: Extract;
- value: NormalizedAlertAction[];
- }
- | {
- operation: 'set';
- field: Extract;
- value: Rule['schedule'];
- }
- | {
- operation: 'set';
- field: Extract;
- value: Rule['throttle'];
- }
- | {
- operation: 'set';
- field: Extract;
- value: Rule['notifyWhen'];
- }
- | {
- operation: 'set';
- field: Extract;
- value: RuleSnoozeSchedule;
- }
- | {
- operation: 'delete';
- field: Extract;
- value?: string[];
- }
- | {
- operation: 'set';
- field: Extract;
- value?: undefined;
- };
-
-type RuleParamsModifier = (params: Params) => Promise;
-
-export interface BulkEditOptionsFilter {
- filter?: string | KueryNode;
- operations: BulkEditOperation[];
- paramsModifier?: RuleParamsModifier;
-}
-
-export interface BulkEditOptionsIds {
- ids: string[];
- operations: BulkEditOperation[];
- paramsModifier?: RuleParamsModifier;
-}
-
-export type BulkEditOptions =
- | BulkEditOptionsFilter
- | BulkEditOptionsIds;
-
-interface BulkOptionsFilter {
- filter?: string | KueryNode;
-}
-
-interface BulkOptionsIds {
- ids?: string[];
-}
-
-export type BulkOptions = BulkOptionsFilter | BulkOptionsIds;
-
-export interface BulkOperationError {
- message: string;
- status?: number;
- rule: {
- id: string;
- name: string;
- };
-}
-
-export interface AggregateOptions extends IndexType {
- search?: string;
- defaultSearchOperator?: 'AND' | 'OR';
- searchFields?: string[];
- hasReference?: {
- type: string;
- id: string;
- };
- filter?: string | KueryNode;
-}
-
-interface IndexType {
- [key: string]: unknown;
-}
-
-export interface AggregateResult {
- alertExecutionStatus: { [status: string]: number };
- ruleLastRunOutcome: { [status: string]: number };
- ruleEnabledStatus?: { enabled: number; disabled: number };
- ruleMutedStatus?: { muted: number; unmuted: number };
- ruleSnoozedStatus?: { snoozed: number };
- ruleTags?: string[];
-}
-
-export interface FindResult {
- page: number;
- perPage: number;
- total: number;
- data: Array>;
-}
-
-interface SavedObjectOptions {
- id?: string;
- migrationVersion?: Record;
-}
-
-export interface CreateOptions {
- data: Omit<
- Rule,
- | 'id'
- | 'createdBy'
- | 'updatedBy'
- | 'createdAt'
- | 'updatedAt'
- | 'apiKey'
- | 'apiKeyOwner'
- | 'muteAll'
- | 'mutedInstanceIds'
- | 'actions'
- | 'executionStatus'
- | 'snoozeSchedule'
- | 'isSnoozedUntil'
- | 'lastRun'
- | 'nextRun'
- > & { actions: NormalizedAlertAction[] };
- options?: SavedObjectOptions;
-}
-
-export interface UpdateOptions {
- id: string;
- data: {
- name: string;
- tags: string[];
- schedule: IntervalSchedule;
- actions: NormalizedAlertAction[];
- params: Params;
- throttle?: string | null;
- notifyWhen?: RuleNotifyWhenType | null;
- };
-}
-
-export interface GetAlertSummaryParams {
- id: string;
- dateStart?: string;
- numberOfExecutions?: number;
-}
-
-export interface GetExecutionLogByIdParams {
- id: string;
- dateStart: string;
- dateEnd?: string;
- filter?: string;
- page: number;
- perPage: number;
- sort: estypes.Sort;
-}
-
-export interface GetRuleExecutionKPIParams {
- id: string;
- dateStart: string;
- dateEnd?: string;
- filter?: string;
-}
-
-export interface GetGlobalExecutionKPIParams {
- dateStart: string;
- dateEnd?: string;
- filter?: string;
- namespaces?: Array;
-}
-
-export interface GetGlobalExecutionLogParams {
- dateStart: string;
- dateEnd?: string;
- filter?: string;
- page: number;
- perPage: number;
- sort: estypes.Sort;
- namespaces?: Array;
-}
-
-export interface GetActionErrorLogByIdParams {
- id: string;
- dateStart: string;
- dateEnd?: string;
- filter?: string;
- page: number;
- perPage: number;
- sort: estypes.Sort;
- namespace?: string;
-}
-
-interface ScheduleTaskOptions {
- id: string;
- consumer: string;
- ruleTypeId: string;
- schedule: IntervalSchedule;
- throwOnConflict: boolean; // whether to throw conflict errors or swallow them
-}
-
-type BulkAction = 'DELETE' | 'ENABLE' | 'DISABLE';
-
-// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
-const extractedSavedObjectParamReferenceNamePrefix = 'param:';
+const fieldsToExcludeFromPublicApi: Array = [
+ 'monitoring',
+ 'mapped_params',
+ 'snoozeSchedule',
+ 'activeSnoozes',
+];
-// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
-const preconfiguredConnectorActionRefPrefix = 'preconfigured:';
-
-const MAX_RULES_NUMBER_FOR_BULK_OPERATION = 10000;
-const API_KEY_GENERATE_CONCURRENCY = 50;
-const RULE_TYPE_CHECKS_CONCURRENCY = 50;
-
-const actionErrorLogDefaultFilter =
- 'event.provider:actions AND ((event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout))';
-
-const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
- type: AlertingAuthorizationFilterType.KQL,
- fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' },
-};
export class RulesClient {
- private readonly logger: Logger;
- private readonly getUserName: () => Promise;
- private readonly spaceId: string;
- private readonly namespace?: string;
- private readonly taskManager: TaskManagerStartContract;
- private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
- private readonly authorization: AlertingAuthorization;
- private readonly ruleTypeRegistry: RuleTypeRegistry;
- private readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
- private readonly minimumScheduleIntervalInMs: number;
- private readonly createAPIKey: (name: string) => Promise;
- private readonly getActionsClient: () => Promise;
- private readonly actionsAuthorization: ActionsAuthorization;
- private readonly getEventLogClient: () => Promise;
- private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
- private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
- private readonly auditLogger?: AuditLogger;
- private readonly eventLogger?: IEventLogger;
- private readonly fieldsToExcludeFromPublicApi: Array = [
- 'monitoring',
- 'mapped_params',
- 'snoozeSchedule',
- 'activeSnoozes',
- ];
-
- constructor({
- ruleTypeRegistry,
- minimumScheduleInterval,
- unsecuredSavedObjectsClient,
- authorization,
- taskManager,
- logger,
- spaceId,
- namespace,
- getUserName,
- createAPIKey,
- encryptedSavedObjectsClient,
- getActionsClient,
- actionsAuthorization,
- getEventLogClient,
- kibanaVersion,
- auditLogger,
- eventLogger,
- }: ConstructorOptions) {
- this.logger = logger;
- this.getUserName = getUserName;
- this.spaceId = spaceId;
- this.namespace = namespace;
- this.taskManager = taskManager;
- this.ruleTypeRegistry = ruleTypeRegistry;
- this.minimumScheduleInterval = minimumScheduleInterval;
- this.minimumScheduleIntervalInMs = parseDuration(minimumScheduleInterval.value);
- this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
- this.authorization = authorization;
- this.createAPIKey = createAPIKey;
- this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
- this.getActionsClient = getActionsClient;
- this.actionsAuthorization = actionsAuthorization;
- this.getEventLogClient = getEventLogClient;
- this.kibanaVersion = kibanaVersion;
- this.auditLogger = auditLogger;
- this.eventLogger = eventLogger;
- }
-
- public async clone(
- id: string,
- { newId }: { newId?: string }
- ): Promise> {
- let ruleSavedObject: SavedObject;
-
- try {
- ruleSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser(
- 'alert',
- id,
- {
- namespace: this.namespace,
- }
- );
- } catch (e) {
- // We'll skip invalidating the API key since we failed to load the decrypted saved object
- this.logger.error(
- `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
- );
- // Still attempt to load the object using SOC
- ruleSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id);
- }
-
- /*
- * As the time of the creation of this PR, security solution already have a clone/duplicate API
- * with some specific business logic so to avoid weird bugs, I prefer to exclude them from this
- * functionality until we resolve our difference
- */
- if (
- isDetectionEngineAADRuleType(ruleSavedObject) ||
- ruleSavedObject.attributes.consumer === AlertConsumers.SIEM
- ) {
- throw Boom.badRequest(
- 'The clone functionality is not enable for rule who belongs to security solution'
- );
- }
- const ruleName =
- ruleSavedObject.attributes.name.indexOf('[Clone]') > 0
- ? ruleSavedObject.attributes.name
- : `${ruleSavedObject.attributes.name} [Clone]`;
- const ruleId = newId ?? SavedObjectsUtils.generateId();
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: ruleSavedObject.attributes.alertTypeId,
- consumer: ruleSavedObject.attributes.consumer,
- operation: WriteOperations.Create,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.CREATE,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
-
- this.ruleTypeRegistry.ensureRuleTypeEnabled(ruleSavedObject.attributes.alertTypeId);
- // Throws an error if alert type isn't registered
- const ruleType = this.ruleTypeRegistry.get(ruleSavedObject.attributes.alertTypeId);
- const username = await this.getUserName();
- const createTime = Date.now();
- const lastRunTimestamp = new Date();
- const legacyId = Semver.lt(this.kibanaVersion, '8.0.0') ? id : null;
- let createdAPIKey = null;
- try {
- createdAPIKey = ruleSavedObject.attributes.enabled
- ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, ruleName))
- : null;
- } catch (error) {
- throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
- }
- const rawRule: RawRule = {
- ...ruleSavedObject.attributes,
- name: ruleName,
- ...this.apiKeyAsAlertAttributes(createdAPIKey, username),
- legacyId,
- createdBy: username,
- updatedBy: username,
- createdAt: new Date(createTime).toISOString(),
- updatedAt: new Date(createTime).toISOString(),
- snoozeSchedule: [],
- muteAll: false,
- mutedInstanceIds: [],
- executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
- monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()),
- scheduledTaskId: null,
- };
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.CREATE,
- outcome: 'unknown',
- savedObject: { type: 'alert', id },
- })
- );
-
- return await this.createRuleSavedObject({
- intervalInMs: parseDuration(rawRule.schedule.interval),
- rawRule,
- references: ruleSavedObject.references,
- ruleId,
- });
- }
-
- public async create({
- data,
- options,
- }: CreateOptions): Promise> {
- const id = options?.id || SavedObjectsUtils.generateId();
-
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: data.alertTypeId,
- consumer: data.consumer,
- operation: WriteOperations.Create,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.CREATE,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
-
- this.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId);
-
- // Throws an error if alert type isn't registered
- const ruleType = this.ruleTypeRegistry.get(data.alertTypeId);
-
- const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params);
- const username = await this.getUserName();
-
- let createdAPIKey = null;
- try {
- createdAPIKey = data.enabled
- ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, data.name))
- : null;
- } catch (error) {
- throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
- }
-
- await this.validateActions(ruleType, data);
-
- // Throw error if schedule interval is less than the minimum and we are enforcing it
- const intervalInMs = parseDuration(data.schedule.interval);
- if (intervalInMs < this.minimumScheduleIntervalInMs && this.minimumScheduleInterval.enforce) {
- throw Boom.badRequest(
- `Error creating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}`
- );
- }
-
- // Extract saved object references for this rule
- const {
- references,
- params: updatedParams,
- actions,
- } = await this.extractReferences(ruleType, data.actions, validatedAlertTypeParams);
-
- const createTime = Date.now();
- const lastRunTimestamp = new Date();
- const legacyId = Semver.lt(this.kibanaVersion, '8.0.0') ? id : null;
- const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
- const throttle = data.throttle ?? null;
-
- const rawRule: RawRule = {
- ...data,
- ...this.apiKeyAsAlertAttributes(createdAPIKey, username),
- legacyId,
- actions,
- createdBy: username,
- updatedBy: username,
- createdAt: new Date(createTime).toISOString(),
- updatedAt: new Date(createTime).toISOString(),
- snoozeSchedule: [],
- params: updatedParams as RawRule['params'],
- muteAll: false,
- mutedInstanceIds: [],
- notifyWhen,
- throttle,
- executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
- monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()),
- };
-
- const mappedParams = getMappedParams(updatedParams);
-
- if (Object.keys(mappedParams).length) {
- rawRule.mapped_params = mappedParams;
- }
-
- return await this.createRuleSavedObject({
- intervalInMs,
- rawRule,
- references,
- ruleId: id,
- options,
- });
- }
-
- private async createRuleSavedObject({
- intervalInMs,
- rawRule,
- references,
- ruleId,
- options,
- }: {
- intervalInMs: number;
- rawRule: RawRule;
- references: SavedObjectReference[];
- ruleId: string;
- options?: SavedObjectOptions;
- }) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.CREATE,
- outcome: 'unknown',
- savedObject: { type: 'alert', id: ruleId },
- })
- );
-
- let createdAlert: SavedObject;
- try {
- createdAlert = await this.unsecuredSavedObjectsClient.create(
- 'alert',
- this.updateMeta(rawRule),
- {
- ...options,
- references,
- id: ruleId,
- }
- );
- } catch (e) {
- // Avoid unused API key
- await bulkMarkApiKeysForInvalidation(
- { apiKeys: rawRule.apiKey ? [rawRule.apiKey] : [] },
- this.logger,
- this.unsecuredSavedObjectsClient
- );
-
- throw e;
- }
- if (rawRule.enabled) {
- let scheduledTask;
- try {
- scheduledTask = await this.scheduleTask({
- id: createdAlert.id,
- consumer: rawRule.consumer,
- ruleTypeId: rawRule.alertTypeId,
- schedule: rawRule.schedule,
- throwOnConflict: true,
- });
- } catch (e) {
- // Cleanup data, something went wrong scheduling the task
- try {
- await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id);
- } catch (err) {
- // Skip the cleanup error and throw the task manager error to avoid confusion
- this.logger.error(
- `Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}`
- );
- }
- throw e;
- }
- await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, {
- scheduledTaskId: scheduledTask.id,
- });
- createdAlert.attributes.scheduledTaskId = scheduledTask.id;
- }
-
- // Log warning if schedule interval is less than the minimum but we're not enforcing it
- if (intervalInMs < this.minimumScheduleIntervalInMs && !this.minimumScheduleInterval.enforce) {
- this.logger.warn(
- `Rule schedule interval (${rawRule.schedule.interval}) for "${createdAlert.attributes.alertTypeId}" rule type with ID "${createdAlert.id}" is less than the minimum value (${this.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.`
- );
- }
-
- return this.getAlertFromRaw(
- createdAlert.id,
- createdAlert.attributes.alertTypeId,
- createdAlert.attributes,
- references,
- false,
- true
- );
- }
-
- public async get({
- id,
- includeLegacyId = false,
- includeSnoozeData = false,
- excludeFromPublicApi = false,
- }: {
- id: string;
- includeLegacyId?: boolean;
- includeSnoozeData?: boolean;
- excludeFromPublicApi?: boolean;
- }): Promise | SanitizedRuleWithLegacyId> {
- const result = await this.unsecuredSavedObjectsClient.get('alert', id);
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: result.attributes.alertTypeId,
- consumer: result.attributes.consumer,
- operation: ReadOperations.Get,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET,
- savedObject: { type: 'alert', id },
- })
- );
- return this.getAlertFromRaw(
- result.id,
- result.attributes.alertTypeId,
- result.attributes,
- result.references,
- includeLegacyId,
- excludeFromPublicApi,
- includeSnoozeData
- );
- }
-
- public async resolve({
- id,
- includeLegacyId,
- includeSnoozeData = false,
- }: {
- id: string;
- includeLegacyId?: boolean;
- includeSnoozeData?: boolean;
- }): Promise> {
- const { saved_object: result, ...resolveResponse } =
- await this.unsecuredSavedObjectsClient.resolve('alert', id);
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: result.attributes.alertTypeId,
- consumer: result.attributes.consumer,
- operation: ReadOperations.Get,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.RESOLVE,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.RESOLVE,
- savedObject: { type: 'alert', id },
- })
- );
-
- const rule = this.getAlertFromRaw(
- result.id,
- result.attributes.alertTypeId,
- result.attributes,
- result.references,
- includeLegacyId,
- false,
- includeSnoozeData
- );
-
- return {
- ...rule,
- ...resolveResponse,
- };
- }
-
- public async getAlertState({ id }: { id: string }): Promise {
- const alert = await this.get({ id });
- await this.authorization.ensureAuthorized({
- ruleTypeId: alert.alertTypeId,
- consumer: alert.consumer,
- operation: ReadOperations.GetRuleState,
- entity: AlertingAuthorizationEntity.Rule,
- });
- if (alert.scheduledTaskId) {
- const { state } = taskInstanceToAlertTaskInstance(
- await this.taskManager.get(alert.scheduledTaskId),
- alert
- );
- return state;
- }
- }
-
- public async getAlertSummary({
- id,
- dateStart,
- numberOfExecutions,
- }: GetAlertSummaryParams): Promise {
- this.logger.debug(`getAlertSummary(): getting alert ${id}`);
- const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
-
- await this.authorization.ensureAuthorized({
- ruleTypeId: rule.alertTypeId,
- consumer: rule.consumer,
- operation: ReadOperations.GetAlertSummary,
- entity: AlertingAuthorizationEntity.Rule,
- });
-
- const dateNow = new Date();
- const durationMillis = parseDuration(rule.schedule.interval) * (numberOfExecutions ?? 60);
- const defaultDateStart = new Date(dateNow.valueOf() - durationMillis);
- const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart);
-
- const eventLogClient = await this.getEventLogClient();
-
- this.logger.debug(`getAlertSummary(): search the event log for rule ${id}`);
- let events: IEvent[];
- let executionEvents: IEvent[];
-
- try {
- const [queryResults, executionResults] = await Promise.all([
- eventLogClient.findEventsBySavedObjectIds(
- 'alert',
- [id],
- {
- page: 1,
- per_page: 10000,
- start: parsedDateStart.toISOString(),
- sort: [{ sort_field: '@timestamp', sort_order: 'desc' }],
- end: dateNow.toISOString(),
- },
- rule.legacyId !== null ? [rule.legacyId] : undefined
- ),
- eventLogClient.findEventsBySavedObjectIds(
- 'alert',
- [id],
- {
- page: 1,
- per_page: numberOfExecutions ?? 60,
- filter: 'event.provider: alerting AND event.action:execute',
- sort: [{ sort_field: '@timestamp', sort_order: 'desc' }],
- end: dateNow.toISOString(),
- },
- rule.legacyId !== null ? [rule.legacyId] : undefined
- ),
- ]);
- events = queryResults.data;
- executionEvents = executionResults.data;
- } catch (err) {
- this.logger.debug(
- `rulesClient.getAlertSummary(): error searching event log for rule ${id}: ${err.message}`
- );
- events = [];
- executionEvents = [];
- }
-
- return alertSummaryFromEventLog({
- rule,
- events,
- executionEvents,
- dateStart: parsedDateStart.toISOString(),
- dateEnd: dateNow.toISOString(),
- });
- }
-
- public async getExecutionLogForRule({
- id,
- dateStart,
- dateEnd,
- filter,
- page,
- perPage,
- sort,
- }: GetExecutionLogByIdParams): Promise {
- this.logger.debug(`getExecutionLogForRule(): getting execution log for rule ${id}`);
- const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
-
- try {
- // Make sure user has access to this rule
- await this.authorization.ensureAuthorized({
- ruleTypeId: rule.alertTypeId,
- consumer: rule.consumer,
- operation: ReadOperations.GetExecutionLog,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_EXECUTION_LOG,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_EXECUTION_LOG,
- savedObject: { type: 'alert', id },
- })
- );
-
- // default duration of instance summary is 60 * rule interval
- const dateNow = new Date();
- const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
- const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
-
- const eventLogClient = await this.getEventLogClient();
-
- try {
- const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds(
- 'alert',
- [id],
- {
- start: parsedDateStart.toISOString(),
- end: parsedDateEnd.toISOString(),
- aggs: getExecutionLogAggregation({
- filter,
- page,
- perPage,
- sort,
- }),
- },
- rule.legacyId !== null ? [rule.legacyId] : undefined
- );
-
- return formatExecutionLogResult(aggResult);
- } catch (err) {
- this.logger.debug(
- `rulesClient.getExecutionLogForRule(): error searching event log for rule ${id}: ${err.message}`
- );
- throw err;
- }
- }
-
- public async getGlobalExecutionLogWithAuth({
- dateStart,
- dateEnd,
- filter,
- page,
- perPage,
- sort,
- namespaces,
- }: GetGlobalExecutionLogParams): Promise {
- this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
-
- let authorizationTuple;
- try {
- authorizationTuple = await this.authorization.getFindAuthorizationFilter(
- AlertingAuthorizationEntity.Alert,
- {
- type: AlertingAuthorizationFilterType.KQL,
- fieldNames: {
- ruleTypeId: 'kibana.alert.rule.rule_type_id',
- consumer: 'kibana.alert.rule.consumer',
- },
- }
- );
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG,
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_GLOBAL_EXECUTION_LOG,
- })
- );
-
- const dateNow = new Date();
- const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
- const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
-
- const eventLogClient = await this.getEventLogClient();
-
- try {
- const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
- 'alert',
- authorizationTuple.filter as KueryNode,
- {
- start: parsedDateStart.toISOString(),
- end: parsedDateEnd.toISOString(),
- aggs: getExecutionLogAggregation({
- filter,
- page,
- perPage,
- sort,
- }),
- },
- namespaces
- );
-
- return formatExecutionLogResult(aggResult);
- } catch (err) {
- this.logger.debug(
- `rulesClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}`
- );
- throw err;
- }
- }
-
- public async getActionErrorLog({
- id,
- dateStart,
- dateEnd,
- filter,
- page,
- perPage,
- sort,
- }: GetActionErrorLogByIdParams): Promise {
- this.logger.debug(`getActionErrorLog(): getting action error logs for rule ${id}`);
- const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
-
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: rule.alertTypeId,
- consumer: rule.consumer,
- operation: ReadOperations.GetActionErrorLog,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_ACTION_ERROR_LOG,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_ACTION_ERROR_LOG,
- savedObject: { type: 'alert', id },
- })
- );
-
- // default duration of instance summary is 60 * rule interval
- const dateNow = new Date();
- const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
- const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
-
- const eventLogClient = await this.getEventLogClient();
-
- try {
- const errorResult = await eventLogClient.findEventsBySavedObjectIds(
- 'alert',
- [id],
- {
- start: parsedDateStart.toISOString(),
- end: parsedDateEnd.toISOString(),
- page,
- per_page: perPage,
- filter: filter
- ? `(${actionErrorLogDefaultFilter}) AND (${filter})`
- : actionErrorLogDefaultFilter,
- sort: convertEsSortToEventLogSort(sort),
- },
- rule.legacyId !== null ? [rule.legacyId] : undefined
- );
- return formatExecutionErrorsResult(errorResult);
- } catch (err) {
- this.logger.debug(
- `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}`
- );
- throw err;
- }
- }
-
- public async getActionErrorLogWithAuth({
- id,
- dateStart,
- dateEnd,
- filter,
- page,
- perPage,
- sort,
- namespace,
- }: GetActionErrorLogByIdParams): Promise {
- this.logger.debug(`getActionErrorLogWithAuth(): getting action error logs for rule ${id}`);
-
- let authorizationTuple;
- try {
- authorizationTuple = await this.authorization.getFindAuthorizationFilter(
- AlertingAuthorizationEntity.Alert,
- {
- type: AlertingAuthorizationFilterType.KQL,
- fieldNames: {
- ruleTypeId: 'kibana.alert.rule.rule_type_id',
- consumer: 'kibana.alert.rule.consumer',
- },
- }
- );
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_ACTION_ERROR_LOG,
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_ACTION_ERROR_LOG,
- savedObject: { type: 'alert', id },
- })
- );
-
- // default duration of instance summary is 60 * rule interval
- const dateNow = new Date();
- const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
- const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
-
- const eventLogClient = await this.getEventLogClient();
-
- try {
- const errorResult = await eventLogClient.findEventsWithAuthFilter(
- 'alert',
- [id],
- authorizationTuple.filter as KueryNode,
- namespace,
- {
- start: parsedDateStart.toISOString(),
- end: parsedDateEnd.toISOString(),
- page,
- per_page: perPage,
- filter: filter
- ? `(${actionErrorLogDefaultFilter}) AND (${filter})`
- : actionErrorLogDefaultFilter,
- sort: convertEsSortToEventLogSort(sort),
- }
- );
- return formatExecutionErrorsResult(errorResult);
- } catch (err) {
- this.logger.debug(
- `rulesClient.getActionErrorLog(): error searching event log for rule ${id}: ${err.message}`
- );
- throw err;
- }
- }
-
- public async getGlobalExecutionKpiWithAuth({
- dateStart,
- dateEnd,
- filter,
- namespaces,
- }: GetGlobalExecutionKPIParams) {
- this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
-
- let authorizationTuple;
- try {
- authorizationTuple = await this.authorization.getFindAuthorizationFilter(
- AlertingAuthorizationEntity.Alert,
- {
- type: AlertingAuthorizationFilterType.KQL,
- fieldNames: {
- ruleTypeId: 'kibana.alert.rule.rule_type_id',
- consumer: 'kibana.alert.rule.consumer',
- },
- }
- );
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI,
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI,
- })
- );
-
- const dateNow = new Date();
- const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
- const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
-
- const eventLogClient = await this.getEventLogClient();
-
- try {
- const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
- 'alert',
- authorizationTuple.filter as KueryNode,
- {
- start: parsedDateStart.toISOString(),
- end: parsedDateEnd.toISOString(),
- aggs: getExecutionKPIAggregation(filter),
- },
- namespaces
- );
-
- return formatExecutionKPIResult(aggResult);
- } catch (err) {
- this.logger.debug(
- `rulesClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}`
- );
- throw err;
- }
- }
-
- public async getRuleExecutionKPI({ id, dateStart, dateEnd, filter }: GetRuleExecutionKPIParams) {
- this.logger.debug(`getRuleExecutionKPI(): getting execution KPI for rule ${id}`);
- const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
-
- try {
- // Make sure user has access to this rule
- await this.authorization.ensureAuthorized({
- ruleTypeId: rule.alertTypeId,
- consumer: rule.consumer,
- operation: ReadOperations.GetRuleExecutionKPI,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_RULE_EXECUTION_KPI,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.GET_RULE_EXECUTION_KPI,
- savedObject: { type: 'alert', id },
- })
- );
-
- // default duration of instance summary is 60 * rule interval
- const dateNow = new Date();
- const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
- const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
-
- const eventLogClient = await this.getEventLogClient();
-
- try {
- const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds(
- 'alert',
- [id],
- {
- start: parsedDateStart.toISOString(),
- end: parsedDateEnd.toISOString(),
- aggs: getExecutionKPIAggregation(filter),
- },
- rule.legacyId !== null ? [rule.legacyId] : undefined
- );
-
- return formatExecutionKPIResult(aggResult);
- } catch (err) {
- this.logger.debug(
- `rulesClient.getRuleExecutionKPI(): error searching execution KPI for rule ${id}: ${err.message}`
- );
- throw err;
- }
- }
-
- public async find({
- options: { fields, ...options } = {},
- excludeFromPublicApi = false,
- includeSnoozeData = false,
- }: {
- options?: FindOptions;
- excludeFromPublicApi?: boolean;
- includeSnoozeData?: boolean;
- } = {}): Promise> {
- let authorizationTuple;
- try {
- authorizationTuple = await this.authorization.getFindAuthorizationFilter(
- AlertingAuthorizationEntity.Rule,
- alertingAuthorizationFilterOpts
- );
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.FIND,
- error,
- })
- );
- throw error;
- }
-
- const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple;
-
- const filterKueryNode = buildKueryNodeFilter(options.filter);
- let sortField = mapSortField(options.sortField);
- if (excludeFromPublicApi) {
- try {
- validateOperationOnAttributes(
- filterKueryNode,
- sortField,
- options.searchFields,
- this.fieldsToExcludeFromPublicApi
- );
- } catch (error) {
- throw Boom.badRequest(`Error find rules: ${error.message}`);
- }
- }
-
- sortField = mapSortField(getModifiedField(options.sortField));
-
- // Generate new modified search and search fields, translating certain params properties
- // to mapped_params. Thus, allowing for sort/search/filtering on params.
- // We do the modifcation after the validate check to make sure the public API does not
- // use the mapped_params in their queries.
- options = {
- ...options,
- ...(options.searchFields && { searchFields: getModifiedSearchFields(options.searchFields) }),
- ...(options.search && { search: getModifiedSearch(options.searchFields, options.search) }),
- };
-
- // Modifies kuery node AST to translate params filter and the filter value to mapped_params.
- // This translation is done in place, and therefore is not a pure function.
- if (filterKueryNode) {
- modifyFilterKueryNode({ astFilter: filterKueryNode });
- }
-
- const {
- page,
- per_page: perPage,
- total,
- saved_objects: data,
- } = await this.unsecuredSavedObjectsClient.find({
- ...options,
- sortField,
- filter:
- (authorizationFilter && filterKueryNode
- ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
- : authorizationFilter) ?? filterKueryNode,
- fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields,
- type: 'alert',
- });
-
- const authorizedData = data.map(({ id, attributes, references }) => {
- try {
- ensureRuleTypeIsAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- AlertingAuthorizationEntity.Rule
- );
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.FIND,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
- return this.getAlertFromRaw(
- id,
- attributes.alertTypeId,
- fields ? (pick(attributes, fields) as RawRule) : attributes,
- references,
- false,
- excludeFromPublicApi,
- includeSnoozeData
- );
- });
-
- authorizedData.forEach(({ id }) =>
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.FIND,
- savedObject: { type: 'alert', id },
- })
- )
- );
-
- return {
- page,
- perPage,
- total,
- data: authorizedData,
- };
- }
-
- public async aggregate({
- options: { fields, filter, ...options } = {},
- }: { options?: AggregateOptions } = {}): Promise {
- let authorizationTuple;
- try {
- authorizationTuple = await this.authorization.getFindAuthorizationFilter(
- AlertingAuthorizationEntity.Rule,
- alertingAuthorizationFilterOpts
- );
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.AGGREGATE,
- error,
- })
- );
- throw error;
- }
-
- const { filter: authorizationFilter } = authorizationTuple;
- const filterKueryNode = buildKueryNodeFilter(filter);
-
- const resp = await this.unsecuredSavedObjectsClient.find({
- ...options,
- filter:
- authorizationFilter && filterKueryNode
- ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
- : authorizationFilter,
- page: 1,
- perPage: 0,
- type: 'alert',
- aggs: {
- status: {
- terms: { field: 'alert.attributes.executionStatus.status' },
- },
- outcome: {
- terms: { field: 'alert.attributes.lastRun.outcome' },
- },
- enabled: {
- terms: { field: 'alert.attributes.enabled' },
- },
- muted: {
- terms: { field: 'alert.attributes.muteAll' },
- },
- tags: {
- terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 },
- },
- snoozed: {
- nested: {
- path: 'alert.attributes.snoozeSchedule',
- },
- aggs: {
- count: {
- filter: {
- exists: {
- field: 'alert.attributes.snoozeSchedule.duration',
- },
- },
- },
- },
- },
- },
- });
-
- if (!resp.aggregations) {
- // Return a placeholder with all zeroes
- const placeholder: AggregateResult = {
- alertExecutionStatus: {},
- ruleLastRunOutcome: {},
- ruleEnabledStatus: {
- enabled: 0,
- disabled: 0,
- },
- ruleMutedStatus: {
- muted: 0,
- unmuted: 0,
- },
- ruleSnoozedStatus: { snoozed: 0 },
- };
-
- for (const key of RuleExecutionStatusValues) {
- placeholder.alertExecutionStatus[key] = 0;
- }
-
- return placeholder;
- }
-
- const alertExecutionStatus = resp.aggregations.status.buckets.map(
- ({ key, doc_count: docCount }) => ({
- [key]: docCount,
- })
- );
-
- const ruleLastRunOutcome = resp.aggregations.outcome.buckets.map(
- ({ key, doc_count: docCount }) => ({
- [key]: docCount,
- })
- );
-
- const ret: AggregateResult = {
- alertExecutionStatus: alertExecutionStatus.reduce(
- (acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
- {}
- ),
- ruleLastRunOutcome: ruleLastRunOutcome.reduce(
- (acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
- {}
- ),
- };
-
- // Fill missing keys with zeroes
- for (const key of RuleExecutionStatusValues) {
- if (!ret.alertExecutionStatus.hasOwnProperty(key)) {
- ret.alertExecutionStatus[key] = 0;
- }
- }
- for (const key of RuleLastRunOutcomeValues) {
- if (!ret.ruleLastRunOutcome.hasOwnProperty(key)) {
- ret.ruleLastRunOutcome[key] = 0;
- }
- }
-
- const enabledBuckets = resp.aggregations.enabled.buckets;
- ret.ruleEnabledStatus = {
- enabled: enabledBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
- disabled: enabledBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
- };
-
- const mutedBuckets = resp.aggregations.muted.buckets;
- ret.ruleMutedStatus = {
- muted: mutedBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
- unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
- };
-
- ret.ruleSnoozedStatus = {
- snoozed: resp.aggregations.snoozed?.count?.doc_count ?? 0,
- };
-
- const tagsBuckets = resp.aggregations.tags?.buckets || [];
- ret.ruleTags = tagsBuckets.map((bucket) => bucket.key);
-
- return ret;
- }
-
- public async delete({ id }: { id: string }) {
- return await retryIfConflicts(
- this.logger,
- `rulesClient.delete('${id}')`,
- async () => await this.deleteWithOCC({ id })
- );
- }
-
- private async deleteWithOCC({ id }: { id: string }) {
- let taskIdToRemove: string | undefined | null;
- let apiKeyToInvalidate: string | null = null;
- let attributes: RawRule;
-
- try {
- const decryptedAlert =
- await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, {
- namespace: this.namespace,
- });
- apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
- taskIdToRemove = decryptedAlert.attributes.scheduledTaskId;
- attributes = decryptedAlert.attributes;
- } catch (e) {
- // We'll skip invalidating the API key since we failed to load the decrypted saved object
- this.logger.error(
- `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
- );
- // Still attempt to load the scheduledTaskId using SOC
- const alert = await this.unsecuredSavedObjectsClient.get('alert', id);
- taskIdToRemove = alert.attributes.scheduledTaskId;
- attributes = alert.attributes;
- }
-
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: attributes.alertTypeId,
- consumer: attributes.consumer,
- operation: WriteOperations.Delete,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.DELETE,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.DELETE,
- outcome: 'unknown',
- savedObject: { type: 'alert', id },
- })
- );
- const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id);
-
- await Promise.all([
- taskIdToRemove ? this.taskManager.removeIfExists(taskIdToRemove) : null,
- apiKeyToInvalidate
- ? bulkMarkApiKeysForInvalidation(
- { apiKeys: [apiKeyToInvalidate] },
- this.logger,
- this.unsecuredSavedObjectsClient
- )
- : null,
- ]);
-
- return removeResult;
- }
-
- public async update({
- id,
- data,
- }: UpdateOptions): Promise> {
- return await retryIfConflicts(
- this.logger,
- `rulesClient.update('${id}')`,
- async () => await this.updateWithOCC({ id, data })
- );
- }
-
- private async updateWithOCC({
- id,
- data,
- }: UpdateOptions): Promise> {
- let alertSavedObject: SavedObject;
-
- try {
- alertSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser(
- 'alert',
- id,
- {
- namespace: this.namespace,
- }
- );
- } catch (e) {
- // We'll skip invalidating the API key since we failed to load the decrypted saved object
- this.logger.error(
- `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
- );
- // Still attempt to load the object using SOC
- alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id);
- }
-
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: alertSavedObject.attributes.alertTypeId,
- consumer: alertSavedObject.attributes.consumer,
- operation: WriteOperations.Update,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.UPDATE,
- savedObject: { type: 'alert', id },
- error,
- })
- );
- throw error;
- }
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.UPDATE,
- outcome: 'unknown',
- savedObject: { type: 'alert', id },
- })
- );
-
- this.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId);
-
- const updateResult = await this.updateAlert({ id, data }, alertSavedObject);
-
- await Promise.all([
- alertSavedObject.attributes.apiKey
- ? bulkMarkApiKeysForInvalidation(
- { apiKeys: [alertSavedObject.attributes.apiKey] },
- this.logger,
- this.unsecuredSavedObjectsClient
- )
- : null,
- (async () => {
- if (
- updateResult.scheduledTaskId &&
- updateResult.schedule &&
- !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule)
- ) {
- try {
- const { tasks } = await this.taskManager.bulkUpdateSchedules(
- [updateResult.scheduledTaskId],
- updateResult.schedule
- );
-
- this.logger.debug(
- `Rule update has rescheduled the underlying task: ${updateResult.scheduledTaskId} to run at: ${tasks?.[0]?.runAt}`
- );
- } catch (err) {
- this.logger.error(
- `Rule update failed to run its underlying task. TaskManager bulkUpdateSchedules failed with Error: ${err.message}`
- );
- }
- }
- })(),
- ]);
-
- return updateResult;
- }
-
- private async updateAlert(
- { id, data }: UpdateOptions,
- { attributes, version }: SavedObject
- ): Promise> {
- const ruleType = this.ruleTypeRegistry.get(attributes.alertTypeId);
-
- // Validate
- const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params);
- await this.validateActions(ruleType, data);
-
- // Throw error if schedule interval is less than the minimum and we are enforcing it
- const intervalInMs = parseDuration(data.schedule.interval);
- if (intervalInMs < this.minimumScheduleIntervalInMs && this.minimumScheduleInterval.enforce) {
- throw Boom.badRequest(
- `Error updating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}`
- );
- }
-
- // Extract saved object references for this rule
- const {
- references,
- params: updatedParams,
- actions,
- } = await this.extractReferences(ruleType, data.actions, validatedAlertTypeParams);
-
- const username = await this.getUserName();
-
- let createdAPIKey = null;
- try {
- createdAPIKey = attributes.enabled
- ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, data.name))
- : null;
- } catch (error) {
- throw Boom.badRequest(`Error updating rule: could not create API key - ${error.message}`);
- }
-
- const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username);
- const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
-
- let updatedObject: SavedObject;
- const createAttributes = this.updateMeta({
- ...attributes,
- ...data,
- ...apiKeyAttributes,
- params: updatedParams as RawRule['params'],
- actions,
- notifyWhen,
- updatedBy: username,
- updatedAt: new Date().toISOString(),
- });
-
- const mappedParams = getMappedParams(updatedParams);
-
- if (Object.keys(mappedParams).length) {
- createAttributes.mapped_params = mappedParams;
- }
-
- try {
- updatedObject = await this.unsecuredSavedObjectsClient.create(
- 'alert',
- createAttributes,
- {
- id,
- overwrite: true,
- version,
- references,
- }
- );
- } catch (e) {
- // Avoid unused API key
- await bulkMarkApiKeysForInvalidation(
- { apiKeys: createAttributes.apiKey ? [createAttributes.apiKey] : [] },
- this.logger,
- this.unsecuredSavedObjectsClient
- );
-
- throw e;
- }
-
- // Log warning if schedule interval is less than the minimum but we're not enforcing it
- if (intervalInMs < this.minimumScheduleIntervalInMs && !this.minimumScheduleInterval.enforce) {
- this.logger.warn(
- `Rule schedule interval (${data.schedule.interval}) for "${ruleType.id}" rule type with ID "${id}" is less than the minimum value (${this.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.`
- );
- }
-
- return this.getPartialRuleFromRaw(
- id,
- ruleType,
- updatedObject.attributes,
- updatedObject.references,
- false,
- true
- );
- }
-
- private getAuthorizationFilter = async ({ action }: { action: BulkAction }) => {
- try {
- const authorizationTuple = await this.authorization.getFindAuthorizationFilter(
- AlertingAuthorizationEntity.Rule,
- alertingAuthorizationFilterOpts
- );
- return authorizationTuple.filter;
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction[action],
- error,
- })
- );
- throw error;
- }
- };
-
- private getAndValidateCommonBulkOptions = (options: BulkOptions) => {
- const filter = (options as BulkOptionsFilter).filter;
- const ids = (options as BulkOptionsIds).ids;
-
- if (!ids && !filter) {
- throw Boom.badRequest(
- "Either 'ids' or 'filter' property in method's arguments should be provided"
- );
- }
-
- if (ids?.length === 0) {
- throw Boom.badRequest("'ids' property should not be an empty array");
- }
-
- if (ids && filter) {
- throw Boom.badRequest(
- "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments"
- );
- }
- return { ids, filter };
- };
-
- private checkAuthorizationAndGetTotal = async ({
- filter,
- action,
- }: {
- filter: KueryNode | null;
- action: BulkAction;
- }) => {
- const actionToConstantsMapping: Record<
- BulkAction,
- { WriteOperation: WriteOperations | ReadOperations; RuleAuditAction: RuleAuditAction }
- > = {
- DELETE: {
- WriteOperation: WriteOperations.BulkDelete,
- RuleAuditAction: RuleAuditAction.DELETE,
- },
- ENABLE: {
- WriteOperation: WriteOperations.BulkEnable,
- RuleAuditAction: RuleAuditAction.ENABLE,
- },
- DISABLE: {
- WriteOperation: WriteOperations.BulkDisable,
- RuleAuditAction: RuleAuditAction.DISABLE,
- },
- };
- const { aggregations, total } = await this.unsecuredSavedObjectsClient.find<
- RawRule,
- RuleBulkOperationAggregation
- >({
- filter,
- page: 1,
- perPage: 0,
- type: 'alert',
- aggs: {
- alertTypeId: {
- multi_terms: {
- terms: [
- { field: 'alert.attributes.alertTypeId' },
- { field: 'alert.attributes.consumer' },
- ],
- },
- },
- },
- });
-
- if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) {
- throw Boom.badRequest(
- `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk ${action.toLocaleLowerCase()}`
- );
- }
-
- const buckets = aggregations?.alertTypeId.buckets;
-
- if (buckets === undefined || buckets?.length === 0) {
- throw Boom.badRequest(`No rules found for bulk ${action.toLocaleLowerCase()}`);
- }
-
- await pMap(
- buckets,
- async ({ key: [ruleType, consumer, actions] }) => {
- this.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType);
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: ruleType,
- consumer,
- operation: actionToConstantsMapping[action].WriteOperation,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: actionToConstantsMapping[action].RuleAuditAction,
- error,
- })
- );
- throw error;
- }
- },
- { concurrency: RULE_TYPE_CHECKS_CONCURRENCY }
- );
- return { total };
- };
-
- public bulkDeleteRules = async (options: BulkOptions) => {
- const { ids, filter } = this.getAndValidateCommonBulkOptions(options);
-
- const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter);
- const authorizationFilter = await this.getAuthorizationFilter({ action: 'DELETE' });
-
- const kueryNodeFilterWithAuth =
- authorizationFilter && kueryNodeFilter
- ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode])
- : kueryNodeFilter;
-
- const { total } = await this.checkAuthorizationAndGetTotal({
- filter: kueryNodeFilterWithAuth,
- action: 'DELETE',
- });
-
- const { apiKeysToInvalidate, errors, taskIdsToDelete } = await retryIfBulkDeleteConflicts(
- this.logger,
- (filterKueryNode: KueryNode | null) => this.bulkDeleteWithOCC({ filter: filterKueryNode }),
- kueryNodeFilterWithAuth
- );
-
- const taskIdsFailedToBeDeleted: string[] = [];
- const taskIdsSuccessfullyDeleted: string[] = [];
- if (taskIdsToDelete.length > 0) {
- try {
- const resultFromDeletingTasks = await this.taskManager.bulkRemoveIfExist(taskIdsToDelete);
- resultFromDeletingTasks?.statuses.forEach((status) => {
- if (status.success) {
- taskIdsSuccessfullyDeleted.push(status.id);
- } else {
- taskIdsFailedToBeDeleted.push(status.id);
- }
- });
- if (taskIdsSuccessfullyDeleted.length) {
- this.logger.debug(
- `Successfully deleted schedules for underlying tasks: ${taskIdsSuccessfullyDeleted.join(
- ', '
- )}`
- );
- }
- if (taskIdsFailedToBeDeleted.length) {
- this.logger.error(
- `Failure to delete schedules for underlying tasks: ${taskIdsFailedToBeDeleted.join(
- ', '
- )}`
- );
- }
- } catch (error) {
- this.logger.error(
- `Failure to delete schedules for underlying tasks: ${taskIdsToDelete.join(
- ', '
- )}. TaskManager bulkRemoveIfExist failed with Error: ${error.message}`
- );
- }
- }
-
- await bulkMarkApiKeysForInvalidation(
- { apiKeys: apiKeysToInvalidate },
- this.logger,
- this.unsecuredSavedObjectsClient
- );
-
- return { errors, total, taskIdsFailedToBeDeleted };
- };
-
- private bulkDeleteWithOCC = async ({ filter }: { filter: KueryNode | null }) => {
- const rulesFinder =
- await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser(
- {
- filter,
- type: 'alert',
- perPage: 100,
- ...(this.namespace ? { namespaces: [this.namespace] } : undefined),
- }
- );
-
- const rules: SavedObjectsBulkDeleteObject[] = [];
- const apiKeysToInvalidate: string[] = [];
- const taskIdsToDelete: string[] = [];
- const errors: BulkOperationError[] = [];
- const apiKeyToRuleIdMapping: Record = {};
- const taskIdToRuleIdMapping: Record = {};
- const ruleNameToRuleIdMapping: Record = {};
-
- for await (const response of rulesFinder.find()) {
- for (const rule of response.saved_objects) {
- if (rule.attributes.apiKey) {
- apiKeyToRuleIdMapping[rule.id] = rule.attributes.apiKey;
- }
- if (rule.attributes.name) {
- ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
- }
- if (rule.attributes.scheduledTaskId) {
- taskIdToRuleIdMapping[rule.id] = rule.attributes.scheduledTaskId;
- }
- rules.push(rule);
-
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.DELETE,
- outcome: 'unknown',
- savedObject: { type: 'alert', id: rule.id },
- })
- );
- }
- }
-
- const result = await this.unsecuredSavedObjectsClient.bulkDelete(rules);
-
- result.statuses.forEach((status) => {
- if (status.error === undefined) {
- if (apiKeyToRuleIdMapping[status.id]) {
- apiKeysToInvalidate.push(apiKeyToRuleIdMapping[status.id]);
- }
- if (taskIdToRuleIdMapping[status.id]) {
- taskIdsToDelete.push(taskIdToRuleIdMapping[status.id]);
- }
- } else {
- errors.push({
- message: status.error.message ?? 'n/a',
- status: status.error.statusCode,
- rule: {
- id: status.id,
- name: ruleNameToRuleIdMapping[status.id] ?? 'n/a',
- },
- });
- }
- });
- return { apiKeysToInvalidate, errors, taskIdsToDelete };
- };
-
- public async bulkEdit(
- options: BulkEditOptions
- ): Promise<{
- rules: Array>;
- errors: BulkOperationError[];
- total: number;
- }> {
- const queryFilter = (options as BulkEditOptionsFilter).filter;
- const ids = (options as BulkEditOptionsIds).ids;
-
- if (ids && queryFilter) {
- throw Boom.badRequest(
- "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method arguments"
- );
- }
-
- const qNodeQueryFilter = buildKueryNodeFilter(queryFilter);
-
- const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter;
- let authorizationTuple;
- try {
- authorizationTuple = await this.authorization.getFindAuthorizationFilter(
- AlertingAuthorizationEntity.Rule,
- alertingAuthorizationFilterOpts
- );
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.BULK_EDIT,
- error,
- })
- );
- throw error;
- }
- const { filter: authorizationFilter } = authorizationTuple;
- const qNodeFilterWithAuth =
- authorizationFilter && qNodeFilter
- ? nodeBuilder.and([qNodeFilter, authorizationFilter as KueryNode])
- : qNodeFilter;
-
- const { aggregations, total } = await this.unsecuredSavedObjectsClient.find<
- RawRule,
- RuleBulkOperationAggregation
- >({
- filter: qNodeFilterWithAuth,
- page: 1,
- perPage: 0,
- type: 'alert',
- aggs: {
- alertTypeId: {
- multi_terms: {
- terms: [
- { field: 'alert.attributes.alertTypeId' },
- { field: 'alert.attributes.consumer' },
- ],
- },
- },
- },
- });
-
- if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) {
- throw Boom.badRequest(
- `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk edit`
- );
- }
- const buckets = aggregations?.alertTypeId.buckets;
-
- if (buckets === undefined) {
- throw Error('No rules found for bulk edit');
- }
-
- await pMap(
- buckets,
- async ({ key: [ruleType, consumer] }) => {
- this.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType);
-
- try {
- await this.authorization.ensureAuthorized({
- ruleTypeId: ruleType,
- consumer,
- operation: WriteOperations.BulkEdit,
- entity: AlertingAuthorizationEntity.Rule,
- });
- } catch (error) {
- this.auditLogger?.log(
- ruleAuditEvent({
- action: RuleAuditAction.BULK_EDIT,
- error,
- })
- );
- throw error;
- }
- },
- { concurrency: RULE_TYPE_CHECKS_CONCURRENCY }
- );
-
- const { apiKeysToInvalidate, results, errors } = await retryIfBulkEditConflicts(
- this.logger,
- `rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${
- options.paramsModifier ? '[Function]' : undefined
- }')`,
- (filterKueryNode: KueryNode | null) =>
- this.bulkEditOcc({
- filter: filterKueryNode,
- operations: options.operations,
- paramsModifier: options.paramsModifier,
- }),
- qNodeFilterWithAuth
- );
-
- await bulkMarkApiKeysForInvalidation(
- { apiKeys: apiKeysToInvalidate },
- this.logger,
- this.unsecuredSavedObjectsClient
- );
-
- const updatedRules = results.map(({ id, attributes, references }) => {
- return this.getAlertFromRaw(
- id,
- attributes.alertTypeId as string,
- attributes as RawRule,
- references,
- false
- );
- });
-
- // update schedules only if schedule operation is present
- const scheduleOperation = options.operations.find(
- (
- operation
- ): operation is Extract }> =>
- operation.field === 'schedule'
- );
-
- if (scheduleOperation?.value) {
- const taskIds = updatedRules.reduce((acc, rule) => {
- if (rule.scheduledTaskId) {
- acc.push(rule.scheduledTaskId);
- }
- return acc;
- }, []);
-
- try {
- await this.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value);
- this.logger.debug(
- `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}`
- );
- } catch (error) {
- this.logger.error(
- `Failure to update schedules for underlying tasks: ${taskIds.join(
- ', '
- )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}`
- );
- }
- }
-
- return { rules: updatedRules, errors, total };
- }
-
- private async bulkEditOcc({
- filter,
- operations,
- paramsModifier,
- }: {
- filter: KueryNode | null;
- operations: BulkEditOptions['operations'];
- paramsModifier: BulkEditOptions['paramsModifier'];
- }): Promise<{
- apiKeysToInvalidate: string[];
- rules: Array>;
- resultSavedObjects: Array>;
- errors: BulkOperationError[];
- }> {
- const rulesFinder =
- await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser(
- {
- filter,
- type: 'alert',
- perPage: 100,
- ...(this.namespace ? { namespaces: [this.namespace] } : undefined),
- }
- );
-
- const rules: Array