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> = []; - const errors: BulkOperationError[] = []; - const apiKeysToInvalidate: string[] = []; - const apiKeysMap = new Map(); - const username = await this.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 = this.ruleTypeRegistry.get(rule.attributes.alertTypeId); - - let attributes = cloneDeep(rule.attributes); - let ruleActions = { - actions: this.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 this.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 this.validateActions(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) < - this.minimumScheduleIntervalInMs; - if (isIntervalInvalid && this.minimumScheduleInterval.enforce) { - throw Error( - `Error updating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}` - ); - } else if (isIntervalInvalid && !this.minimumScheduleInterval.enforce) { - this.logger.warn( - `Rule schedule interval (${attributes.schedule.interval}) for "${ruleType.id}" rule type with ID "${attributes.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.` - ); - } - } - - 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 this.extractReferences( - ruleType, - ruleActions.actions, - validatedMutatedAlertTypeParams - ); - - const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation; - - // create API key - let createdAPIKey = null; - try { - createdAPIKey = shouldUpdateApiKey - ? await this.createAPIKey(this.generateAPIKeyName(ruleType.id, attributes.name)) - : null; - } catch (error) { - throw Error(`Error updating rule: could not create API key - ${error.message}`); - } - - const apiKeyAttributes = this.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 = this.updateMeta({ - ...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, - }, - }); - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); - } - }, - { concurrency: API_KEY_GENERATE_CONCURRENCY } - ); - } - - let result; - try { - result = await this.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; - }, []), - }, - this.logger, - this.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 }; - } - - private getShouldScheduleTask = async (scheduledTaskId: string | null | undefined) => { - if (!scheduledTaskId) return true; - try { - // make sure scheduledTaskId exist - await this.taskManager.get(scheduledTaskId); - return false; - } catch (err) { - return true; - } - }; - - public bulkEnableRules = async (options: BulkOptions) => { - const { ids, filter } = this.getAndValidateCommonBulkOptions(options); - - const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); - const authorizationFilter = await this.getAuthorizationFilter({ action: 'ENABLE' }); - - const kueryNodeFilterWithAuth = - authorizationFilter && kueryNodeFilter - ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) - : kueryNodeFilter; - - const { total } = await this.checkAuthorizationAndGetTotal({ - filter: kueryNodeFilterWithAuth, - action: 'ENABLE', - }); - - const { errors, rules, accListSpecificForBulkOperation } = await retryIfBulkOperationConflicts({ - action: 'ENABLE', - logger: this.logger, - bulkOperation: (filterKueryNode: KueryNode | null) => - this.bulkEnableRulesWithOCC({ filter: filterKueryNode }), - filter: kueryNodeFilterWithAuth, - }); - - const [taskIdsToEnable] = accListSpecificForBulkOperation; - - const taskIdsFailedToBeEnabled: string[] = []; - if (taskIdsToEnable.length > 0) { - try { - const resultFromEnablingTasks = await this.taskManager.bulkEnable(taskIdsToEnable); - resultFromEnablingTasks?.errors?.forEach((error) => { - taskIdsFailedToBeEnabled.push(error.task.id); - }); - if (resultFromEnablingTasks.tasks.length) { - this.logger.debug( - `Successfully enabled schedules for underlying tasks: ${resultFromEnablingTasks.tasks - .map((task) => task.id) - .join(', ')}` - ); - } - if (resultFromEnablingTasks.errors.length) { - this.logger.error( - `Failure to enable schedules for underlying tasks: ${resultFromEnablingTasks.errors - .map((error) => error.task.id) - .join(', ')}` - ); - } - } catch (error) { - taskIdsFailedToBeEnabled.push(...taskIdsToEnable); - this.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 this.getAlertFromRaw( - id, - attributes.alertTypeId as string, - attributes as RawRule, - references, - false - ); - }); - - return { errors, rules: updatedRules, total, taskIdsFailedToBeEnabled }; - }; - - private bulkEnableRulesWithOCC = async ({ filter }: { filter: KueryNode | null }) => { - const rulesFinder = - await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: 'alert', - perPage: 100, - ...(this.namespace ? { namespaces: [this.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 this.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 this.getUserName(); - - const updatedAttributes = this.updateMeta({ - ...rule.attributes, - ...(!rule.attributes.apiKey && - (await this.createNewAPIKeySet({ 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 this.getShouldScheduleTask( - rule.attributes.scheduledTaskId - ); - - let scheduledTaskId; - if (shouldScheduleTask) { - const scheduledTask = await this.scheduleTask({ - 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), - }, - }); - - this.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, - }, - }); - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.ENABLE, - error, - }) - ); - } - }); - } - - const result = await this.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] }; - }; - - private recoverRuleAlerts = async (id: string, attributes: RawRule) => { - if (!this.eventLogger || !attributes.scheduledTaskId) return; - try { - const { state } = taskInstanceToAlertTaskInstance( - await this.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: this.ruleTypeRegistry.get(attributes.alertTypeId), - consumer: attributes.consumer, - instanceId: alertId, - action: EVENT_LOG_ACTIONS.recoveredInstance, - message, - state: instanceState, - group: actionGroup, - namespace: this.namespace, - spaceId: this.spaceId, - savedObjects: [ - { - id, - type: 'alert', - typeId: attributes.alertTypeId, - relation: SAVED_OBJECT_REL_PRIMARY, - }, - ], - }); - this.eventLogger.logEvent(event); - } - } catch (error) { - // this should not block the rest of the disable process - this.logger.warn( - `rulesClient.disable('${id}') - Could not write recovery events - ${error.message}` - ); - } - }; - - public bulkDisableRules = async (options: BulkOptions) => { - const { ids, filter } = this.getAndValidateCommonBulkOptions(options); - - const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); - const authorizationFilter = await this.getAuthorizationFilter({ action: 'DISABLE' }); - - const kueryNodeFilterWithAuth = - authorizationFilter && kueryNodeFilter - ? nodeBuilder.and([kueryNodeFilter, authorizationFilter as KueryNode]) - : kueryNodeFilter; - - const { total } = await this.checkAuthorizationAndGetTotal({ - filter: kueryNodeFilterWithAuth, - action: 'DISABLE', - }); - - const { errors, rules, taskIdsToDisable, taskIdsToDelete } = await retryIfBulkDisableConflicts( - this.logger, - (filterKueryNode: KueryNode | null) => - this.bulkDisableRulesWithOCC({ filter: filterKueryNode }), - kueryNodeFilterWithAuth - ); - - if (taskIdsToDisable.length > 0) { - try { - const resultFromDisablingTasks = await this.taskManager.bulkDisable(taskIdsToDisable); - if (resultFromDisablingTasks.tasks.length) { - this.logger.debug( - `Successfully disabled schedules for underlying tasks: ${resultFromDisablingTasks.tasks - .map((task) => task.id) - .join(', ')}` - ); - } - if (resultFromDisablingTasks.errors.length) { - this.logger.error( - `Failure to disable schedules for underlying tasks: ${resultFromDisablingTasks.errors - .map((error) => error.task.id) - .join(', ')}` - ); - } - } catch (error) { - this.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 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}` - ); - } - } - - const updatedRules = rules.map(({ id, attributes, references }) => { - return this.getAlertFromRaw( - id, - attributes.alertTypeId as string, - attributes as RawRule, - references, - false - ); - }); - - return { errors, rules: updatedRules, total }; - }; - - private bulkDisableRulesWithOCC = async ({ filter }: { filter: KueryNode | null }) => { - const rulesFinder = - await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: 'alert', - perPage: 100, - ...(this.namespace ? { namespaces: [this.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; - - this.recoverRuleAlerts(rule.id, rule.attributes); - - if (rule.attributes.name) { - ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; - } - - const username = await this.getUserName(); - const updatedAttributes = this.updateMeta({ - ...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, - }, - }); - - this.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, - }, - }); - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DISABLE, - error, - }) - ); - } - }); - } - - const result = await this.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 }; - }; - - private 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, - }; - } - - public async updateApiKey({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.updateApiKey('${id}')`, - async () => await this.updateApiKeyWithOCC({ id }) - ); - } - - private async updateApiKeyWithOCC({ id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; - let attributes: RawRule; - let version: string | undefined; - - try { - const decryptedAlert = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { - namespace: this.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 - this.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 this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.UpdateApiKey, - entity: AlertingAuthorizationEntity.Rule, - }); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UPDATE_API_KEY, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - const username = await this.getUserName(); - - let createdAPIKey = null; - try { - createdAPIKey = await this.createAPIKey( - this.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 = this.updateMeta({ - ...attributes, - ...this.apiKeyAsAlertAttributes(createdAPIKey, username), - updatedAt: new Date().toISOString(), - updatedBy: username, - }); - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UPDATE_API_KEY, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - try { - await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); - } catch (e) { - // Avoid unused API key - await bulkMarkApiKeysForInvalidation( - { apiKeys: updateAttributes.apiKey ? [updateAttributes.apiKey] : [] }, - this.logger, - this.unsecuredSavedObjectsClient - ); - throw e; - } - - if (apiKeyToInvalidate) { - await bulkMarkApiKeysForInvalidation( - { apiKeys: [apiKeyToInvalidate] }, - this.logger, - this.unsecuredSavedObjectsClient - ); - } - } - - public async enable({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.enable('${id}')`, - async () => await this.enableWithOCC({ id }) - ); - } - - private async enableWithOCC({ id }: { id: string }) { - let existingApiKey: string | null = null; - let attributes: RawRule; - let version: string | undefined; - - try { - const decryptedAlert = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { - namespace: this.namespace, - }); - existingApiKey = decryptedAlert.attributes.apiKey; - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - this.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 this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Enable, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.ENABLE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.ENABLE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - if (attributes.enabled === false) { - const username = await this.getUserName(); - const now = new Date(); - - const schedule = attributes.schedule as IntervalSchedule; - - const updateAttributes = this.updateMeta({ - ...attributes, - ...(!existingApiKey && (await this.createNewAPIKeySet({ 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 this.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 this.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 this.scheduleTask({ - id, - consumer: attributes.consumer, - ruleTypeId: attributes.alertTypeId, - schedule: attributes.schedule as IntervalSchedule, - throwOnConflict: false, - }); - await this.unsecuredSavedObjectsClient.update('alert', id, { - scheduledTaskId: scheduledTask.id, - }); - } else { - // Task exists so set enabled to true - await this.taskManager.bulkEnable([attributes.scheduledTaskId!]); - } - } - - private async createNewAPIKeySet({ - attributes, - username, - }: { - attributes: RawRule; - username: string | null; - }): Promise> { - let createdAPIKey = null; - try { - createdAPIKey = await this.createAPIKey( - this.generateAPIKeyName(attributes.alertTypeId, attributes.name) - ); - } catch (error) { - throw Boom.badRequest(`Error creating API key for rule: ${error.message}`); - } - - return this.apiKeyAsAlertAttributes(createdAPIKey, username); - } - - public async disable({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.disable('${id}')`, - async () => await this.disableWithOCC({ id }) - ); - } - - private async disableWithOCC({ id }: { id: string }) { - let attributes: RawRule; - let version: string | undefined; - - try { - const decryptedAlert = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { - namespace: this.namespace, - }); - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - this.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 this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - this.recoverRuleAlerts(id, attributes); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Disable, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DISABLE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.DISABLE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - if (attributes.enabled === true) { - await this.unsecuredSavedObjectsClient.update( - 'alert', - id, - this.updateMeta({ - ...attributes, - enabled: false, - scheduledTaskId: attributes.scheduledTaskId === id ? attributes.scheduledTaskId : null, - updatedBy: await this.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 this.taskManager.removeIfExists(attributes.scheduledTaskId); - } else { - await this.taskManager.bulkDisable([attributes.scheduledTaskId]); - } - } - } - } - - public async snooze({ - id, - snoozeSchedule, - }: { - id: string; - snoozeSchedule: RuleSnoozeSchedule; - }): Promise { - const snoozeDateValidationMsg = validateSnoozeStartDate(snoozeSchedule.rRule.dtstart); - if (snoozeDateValidationMsg) { - throw new RuleMutedError(snoozeDateValidationMsg); - } - - return await retryIfConflicts( - this.logger, - `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`, - async () => await this.snoozeWithOCC({ id, snoozeSchedule }) - ); - } - - private async snoozeWithOCC({ - id, - snoozeSchedule, - }: { - id: string; - snoozeSchedule: RuleSnoozeSchedule; - }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Snooze, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.SNOOZE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.SNOOZE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); - - try { - verifySnoozeScheduleLimit(newAttrs); - } catch (error) { - throw Boom.badRequest(error.message); - } - - const updateAttributes = this.updateMeta({ - ...newAttrs, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async unsnooze({ - id, - scheduleIds, - }: { - id: string; - scheduleIds?: string[]; - }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.unsnooze('${id}')`, - async () => await this.unsnoozeWithOCC({ id, scheduleIds }) - ); - } - - private async unsnoozeWithOCC({ id, scheduleIds }: { id: string; scheduleIds?: string[] }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Unsnooze, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNSNOOZE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNSNOOZE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - const newAttrs = getUnsnoozeAttributes(attributes, scheduleIds); - - const updateAttributes = this.updateMeta({ - ...newAttrs, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public calculateIsSnoozedUntil(rule: { - muteAll: boolean; - snoozeSchedule?: RuleSnooze; - }): string | null { - const isSnoozedUntil = getRuleSnoozeEndTime(rule); - return isSnoozedUntil ? isSnoozedUntil.toISOString() : null; - } - - public async clearExpiredSnoozes({ id }: { id: string }): Promise { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - const snoozeSchedule = attributes.snoozeSchedule - ? attributes.snoozeSchedule.filter((s) => { - try { - return !isSnoozeExpired(s); - } catch (e) { - this.logger.error(`Error checking for expiration of snooze ${s.id}: ${e}`); - return true; - } - }) - : []; - - if (snoozeSchedule.length === attributes.snoozeSchedule?.length) return; - - const updateAttributes = this.updateMeta({ - snoozeSchedule, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async muteAll({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.muteAll('${id}')`, - async () => await this.muteAllWithOCC({ id }) - ); - } - - private async muteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.MuteAll, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const updateAttributes = this.updateMeta({ - muteAll: true, - mutedInstanceIds: [], - snoozeSchedule: clearUnscheduledSnooze(attributes), - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async unmuteAll({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.unmuteAll('${id}')`, - async () => await this.unmuteAllWithOCC({ id }) - ); - } - - private async unmuteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.UnmuteAll, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const updateAttributes = this.updateMeta({ - muteAll: false, - mutedInstanceIds: [], - snoozeSchedule: clearUnscheduledSnooze(attributes), - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async muteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.muteInstance('${alertId}')`, - async () => await this.muteInstanceWithOCC({ alertId, alertInstanceId }) - ); - } - - private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.MuteAlert, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE_ALERT, - savedObject: { type: 'alert', id: alertId }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.MUTE_ALERT, - outcome: 'unknown', - savedObject: { type: 'alert', id: alertId }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const mutedInstanceIds = attributes.mutedInstanceIds || []; - if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { - mutedInstanceIds.push(alertInstanceId); - await this.unsecuredSavedObjectsClient.update( - 'alert', - alertId, - this.updateMeta({ - mutedInstanceIds, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }), - { version } - ); - } - } - - public async unmuteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { - return await retryIfConflicts( - this.logger, - `rulesClient.unmuteInstance('${alertId}')`, - async () => await this.unmuteInstanceWithOCC({ alertId, alertInstanceId }) - ); - } - - private async unmuteInstanceWithOCC({ - alertId, - alertInstanceId, - }: { - alertId: string; - alertInstanceId: string; - }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); - - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.UnmuteAlert, - entity: AlertingAuthorizationEntity.Rule, - }); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE_ALERT, - savedObject: { type: 'alert', id: alertId }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.UNMUTE_ALERT, - outcome: 'unknown', - savedObject: { type: 'alert', id: alertId }, - }) - ); - - this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const mutedInstanceIds = attributes.mutedInstanceIds || []; - if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.unsecuredSavedObjectsClient.update( - 'alert', - alertId, - this.updateMeta({ - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), - }), - { version } - ); - } - } - - public async runSoon({ id }: { id: string }) { - const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); - try { - await this.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: ReadOperations.RunSoon, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.RUN_SOON, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.RUN_SOON, - outcome: 'unknown', - savedObject: { type: 'alert', id }, - }) - ); - - this.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 this.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 this.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, - }, - }); - } - } - - public async listAlertTypes() { - return await this.authorization.filterByRuleTypeAuthorization( - this.ruleTypeRegistry.list(), - [ReadOperations.Get, WriteOperations.Create], - AlertingAuthorizationEntity.Rule - ); - } + private readonly context: RulesClientContext; + + constructor(context: ConstructorOptions) { + this.context = { + ...context, + minimumScheduleIntervalInMs: parseDuration(context.minimumScheduleInterval.value), + fieldsToExcludeFromPublicApi, + }; + } + + public aggregate = (params?: { options?: AggregateOptions }) => aggregate(this.context, params); + public clone = (...args: CloneArguments) => + clone(this.context, ...args); + public create = (params: CreateOptions) => + create(this.context, params); + public delete = (params: { id: string }) => deleteRule(this.context, params); + public find = (params?: FindParams) => + find(this.context, params); + public get = (params: GetParams) => + get(this.context, params); + public resolve = (params: ResolveParams) => + resolve(this.context, params); + public update = (params: UpdateOptions) => + update(this.context, params); + + public getAlertState = (params: GetAlertStateParams) => getAlertState(this.context, params); + public getAlertSummary = (params: GetAlertSummaryParams) => getAlertSummary(this.context, params); + public getExecutionLogForRule = (params: GetExecutionLogByIdParams) => + getExecutionLogForRule(this.context, params); + public getGlobalExecutionLogWithAuth = (params: GetGlobalExecutionLogParams) => + getGlobalExecutionLogWithAuth(this.context, params); + public getRuleExecutionKPI = (params: GetRuleExecutionKPIParams) => + getRuleExecutionKPI(this.context, params); + public getGlobalExecutionKpiWithAuth = (params: GetGlobalExecutionKPIParams) => + getGlobalExecutionKpiWithAuth(this.context, params); + public getActionErrorLog = (params: GetActionErrorLogByIdParams) => + getActionErrorLog(this.context, params); + public getActionErrorLogWithAuth = (params: GetActionErrorLogByIdParams) => + getActionErrorLogWithAuth(this.context, params); + + public bulkDeleteRules = (options: BulkOptions) => bulkDeleteRules(this.context, options); + public bulkEdit = (options: BulkEditOptions) => + bulkEdit(this.context, options); + public bulkEnableRules = (options: BulkOptions) => bulkEnableRules(this.context, options); + public bulkDisableRules = (options: BulkOptions) => bulkDisableRules(this.context, options); + + public updateApiKey = (options: { id: string }) => updateApiKey(this.context, options); + + public enable = (options: { id: string }) => enable(this.context, options); + public disable = (options: { id: string }) => disable(this.context, options); + + public snooze = (options: SnoozeParams) => snooze(this.context, options); + public unsnooze = (options: UnsnoozeParams) => unsnooze(this.context, options); + + public clearExpiredSnoozes = (options: { id: string }) => + clearExpiredSnoozes(this.context, options); + + public muteAll = (options: { id: string }) => muteAll(this.context, options); + public unmuteAll = (options: { id: string }) => unmuteAll(this.context, options); + public muteInstance = (options: MuteOptions) => muteInstance(this.context, options); + public unmuteInstance = (options: MuteOptions) => unmuteInstance(this.context, options); + + public runSoon = (options: { id: string }) => runSoon(this.context, options); + + public listAlertTypes = () => listAlertTypes(this.context); public getSpaceId(): string | undefined { - return this.spaceId; - } - - private async scheduleTask(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: this.spaceId, - consumer, - }, - state: { - previousStartedAt: null, - alertTypeState: {}, - alertInstances: {}, - }, - scope: ['alerting'], - enabled: true, - }; - try { - return await this.taskManager.schedule(taskInstance); - } catch (err) { - if (err.statusCode === 409 && !throwOnConflict) { - return taskInstance; - } - throw err; - } - } - - private 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']; - } - - private getAlertFromRaw( - id: string, - ruleTypeId: string, - rawRule: RawRule, - references: SavedObjectReference[] | undefined, - includeLegacyId: boolean = false, - excludeFromPublicApi: boolean = false, - includeSnoozeData: boolean = false - ): Rule | RuleWithLegacyId { - const ruleType = this.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 = this.getPartialRuleFromRaw( - 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; - } - - private getPartialRuleFromRaw( - 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 - ? this.calculateIsSnoozedUntil({ - muteAll: partialRawRule.muteAll ?? false, - snoozeSchedule, - }) - : null; - const includeMonitoring = monitoring && !excludeFromPublicApi; - const rule = { - id, - notifyWhen, - ...omit(partialRawRule, excludeFromPublicApi ? [...this.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 ? this.injectReferencesIntoActions(id, actions, references || []) : [], - params: this.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(this.logger, id, executionStatus) } - : {}), - ...(includeMonitoring - ? { monitoring: convertMonitoringFromRawAndVerify(this.logger, id, monitoring) } - : {}), - ...(nextRun ? { nextRun: new Date(nextRun) } : {}), - }; - - return includeLegacyId - ? ({ ...rule, legacyId } as PartialRuleWithLegacyId) - : (rule as PartialRule); - } - - private async validateActions( - 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 this.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(', '), - }, - }) - ); - } - } - } - - private async extractReferences< - Params extends RuleTypeParams, - ExtractedParams extends RuleTypeParams - >( - ruleType: UntypedNormalizedRuleType, - ruleActions: NormalizedAlertAction[], - ruleParams: Params - ): Promise<{ - actions: RawRule['actions']; - params: ExtractedParams; - references: SavedObjectReference[]; - }> { - const { references: actionReferences, actions } = await this.denormalizeActions(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, - }; - } - - private 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}` - ); - } - } - - private async denormalizeActions( - alertActions: NormalizedAlertAction[] - ): Promise<{ actions: RawRule['actions']; references: SavedObjectReference[] }> { - const references: SavedObjectReference[] = []; - const actions: RawRule['actions'] = []; - if (alertActions.length) { - const actionsClient = await this.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, - }; - } - - private includeFieldsRequiredForAuthentication(fields: string[]): string[] { - return uniq([...fields, 'alertTypeId', 'consumer']); - } - - private generateAPIKeyName(alertTypeId: string, alertName: string) { - return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 }); - } - - private updateMeta>(alertAttributes: T): T { - if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) { - alertAttributes.meta = alertAttributes.meta ?? {}; - alertAttributes.meta.versionApiKeyLastmodified = this.kibanaVersion; - } - return alertAttributes; - } -} - -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; -} - -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, - }; -} - -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], - }; -} - -function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { - const snoozeSchedule = scheduleIds - ? clearScheduledSnoozesById(attributes, scheduleIds) - : clearCurrentActiveSnooze(attributes); - - return { - snoozeSchedule, - ...(!scheduleIds ? { muteAll: false } : {}), - }; -} - -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, - }; -} - -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') - : []; -} - -function clearScheduledSnoozesById(attributes: RawRule, ids: string[]) { - return attributes.snoozeSchedule - ? attributes.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id)) - : []; -} - -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; -} - -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', - }) - ); + return this.context.spaceId; } } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 4d9b84e53a2f4f..6205b4bb4ba6aa 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -6,7 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RulesClient, ConstructorOptions, CreateOptions } from '../rules_client'; +import { CreateOptions } from '../methods/create'; +import { RulesClient, ConstructorOptions } from '../rules_client'; import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index 4cfa13f69b84dd..1b65d27c8e72ed 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -54,7 +54,7 @@ beforeEach(() => { setGlobalDate(); -jest.mock('../lib/map_sort_field', () => ({ +jest.mock('../common/map_sort_field', () => ({ mapSortField: jest.fn(), })); @@ -288,7 +288,7 @@ describe('find()', () => { test('calls mapSortField', async () => { const rulesClient = new RulesClient(rulesClientParams); await rulesClient.find({ options: { sortField: 'name' } }); - expect(jest.requireMock('../lib/map_sort_field').mapSortField).toHaveBeenCalledWith('name'); + expect(jest.requireMock('../common/map_sort_field').mapSortField).toHaveBeenCalledWith('name'); }); test('should translate filter/sort/search on params to mapped_params', async () => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts index 6b635abe5d7f01..672281469d18b3 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { RulesClient, ConstructorOptions, GetActionErrorLogByIdParams } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../rules_client'; +import { GetActionErrorLogByIdParams } from '../methods/get_action_error_log'; import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { fromKueryExpression } from '@kbn/es-query'; diff --git a/x-pack/plugins/alerting/server/rules_client/types.ts b/x-pack/plugins/alerting/server/rules_client/types.ts new file mode 100644 index 00000000000000..ff59b5527f9020 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/types.ts @@ -0,0 +1,148 @@ +/* + * 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 { Logger, SavedObjectsClientContract, PluginInitializerContext } 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 { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import { IEventLogClient, IEventLogger } from '@kbn/event-log-plugin/server'; +import { AuditLogger } from '@kbn/security-plugin/server'; +import { RegistryRuleType } from '../rule_type_registry'; +import { + RuleTypeRegistry, + RuleAction, + IntervalSchedule, + SanitizedRule, + RuleSnoozeSchedule, +} from '../types'; +import { AlertingAuthorization } from '../authorization'; +import { AlertingRulesConfig } from '../config'; + +export type { + BulkEditOperation, + BulkEditFields, + BulkEditOptions, + BulkEditOptionsFilter, + BulkEditOptionsIds, +} from './methods/bulk_edit'; +export type { CreateOptions } from './methods/create'; +export type { FindOptions, FindResult } from './methods/find'; +export type { UpdateOptions } from './methods/update'; +export type { AggregateOptions, AggregateResult } from './methods/aggregate'; +export type { GetAlertSummaryParams } from './methods/get_alert_summary'; +export type { + GetExecutionLogByIdParams, + GetGlobalExecutionLogParams, +} from './methods/get_execution_log'; +export type { + GetGlobalExecutionKPIParams, + GetRuleExecutionKPIParams, +} from './methods/get_execution_kpi'; +export type { GetActionErrorLogByIdParams } from './methods/get_action_error_log'; + +export interface RulesClientContext { + readonly logger: Logger; + readonly getUserName: () => Promise; + readonly spaceId: string; + readonly namespace?: string; + readonly taskManager: TaskManagerStartContract; + readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; + readonly authorization: AlertingAuthorization; + readonly ruleTypeRegistry: RuleTypeRegistry; + readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval']; + readonly minimumScheduleIntervalInMs: number; + readonly createAPIKey: (name: string) => Promise; + readonly getActionsClient: () => Promise; + readonly actionsAuthorization: ActionsAuthorization; + readonly getEventLogClient: () => Promise; + readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + readonly auditLogger?: AuditLogger; + readonly eventLogger?: IEventLogger; + readonly fieldsToExcludeFromPublicApi: Array; +} + +export type NormalizedAlertAction = Omit; + +export interface RegistryAlertTypeWithAuth extends RegistryRuleType { + authorizedConsumers: string[]; +} +export type CreateAPIKeyResult = + | { apiKeysEnabled: false } + | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult }; +export type InvalidateAPIKeyResult = + | { apiKeysEnabled: false } + | { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult }; + +export interface RuleBulkOperationAggregation { + alertTypeId: { + buckets: Array<{ + key: string[]; + doc_count: number; + }>; + }; +} +export interface SavedObjectOptions { + id?: string; + migrationVersion?: Record; +} + +export interface ScheduleTaskOptions { + id: string; + consumer: string; + ruleTypeId: string; + schedule: IntervalSchedule; + throwOnConflict: boolean; // whether to throw conflict errors or swallow them +} + +export interface IndexType { + [key: string]: unknown; +} + +export interface MuteOptions extends IndexType { + alertId: string; + alertInstanceId: string; +} + +export interface SnoozeOptions extends IndexType { + snoozeSchedule: RuleSnoozeSchedule; +} + +export interface BulkOptionsFilter { + filter?: string | KueryNode; +} + +export interface BulkOptionsIds { + ids?: string[]; +} + +export type BulkOptions = BulkOptionsFilter | BulkOptionsIds; + +export interface BulkOperationError { + message: string; + status?: number; + rule: { + id: string; + name: string; + }; +} + +export type BulkAction = 'DELETE' | 'ENABLE' | 'DISABLE'; + +export interface RuleBulkOperationAggregation { + alertTypeId: { + buckets: Array<{ + key: string[]; + doc_count: number; + }>; + }; +} diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts b/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts index 6de67875ba2ebc..9a8967c9556ab9 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations/8.2/index.ts @@ -7,7 +7,7 @@ import { SavedObjectUnsanitizedDoc } from '@kbn/core-saved-objects-server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; -import { getMappedParams } from '../../../rules_client/lib/mapped_params_utils'; +import { getMappedParams } from '../../../rules_client/common'; import { RawRule } from '../../../types'; import { createEsoMigration, pipeMigrations } from '../utils'; diff --git a/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts index d0a5eae0fed0f6..7e4019618edfbc 100644 --- a/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts +++ b/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts @@ -18,7 +18,7 @@ import pRetry from 'p-retry'; const BEFORE_SETUP_TIMEOUT = 30 * 60 * 1000; // 30 minutes; const DOCKER_START_TIMEOUT = 5 * 60 * 1000; // 5 minutes -const DOCKER_IMAGE = `docker.elastic.co/package-registry/distribution:lite`; +const DOCKER_IMAGE = `docker.elastic.co/package-registry/distribution:production`; function firstWithTimeout(source$: Rx.Observable, errorMsg: string, ms = 30 * 1000) { return Rx.race( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 4c29d902e3883f..6664aff57b0b30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -1047,6 +1047,7 @@ export const RulesList = ({ setIsEnablingRules(false); showToast({ action: 'ENABLE', errors, total }); await refreshRules(); + onClearSelection(); }, [http, selectedIds, getFilter, setIsEnablingRules, showToast]); const onDisable = useCallback(async () => { @@ -1061,6 +1062,7 @@ export const RulesList = ({ setIsDisablingRules(false); showToast({ action: 'DISABLE', errors, total }); await refreshRules(); + onClearSelection(); }, [http, selectedIds, getFilter, setIsDisablingRules, showToast]); const onDeleteCancel = () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx index 81f62c9a3d28b5..19fe6c68f26465 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx @@ -154,7 +154,7 @@ describe.skip('Rules list bulk disable', () => { wrapper.find('[data-test-subj="showBulkActionButton"]').first().simulate('click'); }); - it('can bulk disable', async () => { + it.skip('can bulk disable', async () => { wrapper.find('button[data-test-subj="bulkDisable"]').first().simulate('click'); await act(async () => { @@ -175,6 +175,10 @@ describe.skip('Rules list bulk disable', () => { ids: [], }) ); + expect( + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').first().prop('checked') + ).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="bulkDisable"]').exists()).toBeFalsy(); }); describe('Toast', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_enable.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_enable.test.tsx index 5645b65fdbedc4..688e0ae50ec46f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_enable.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_enable.test.tsx @@ -175,6 +175,10 @@ describe.skip('Rules list bulk enable', () => { ids: [], }) ); + expect( + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').first().prop('checked') + ).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="bulkEnable"]').exists()).toBeFalsy(); }); describe('Toast', () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts index 2f72e9ea41da19..86b249dc6bc79f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts @@ -20,22 +20,17 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC await tearDown(getService); }); - // loadTestFile(require.resolve('./find')); - // loadTestFile(require.resolve('./create')); - // loadTestFile(require.resolve('./delete')); - // loadTestFile(require.resolve('./disable')); - // loadTestFile(require.resolve('./enable')); - // loadTestFile(require.resolve('./execution_status')); - // loadTestFile(require.resolve('./get')); - // loadTestFile(require.resolve('./get_alert_state')); - // loadTestFile(require.resolve('./get_alert_summary')); - // loadTestFile(require.resolve('./rule_types')); - // loadTestFile(require.resolve('./bulk_edit')); - // loadTestFile(require.resolve('./bulk_delete')); - // loadTestFile(require.resolve('./bulk_enable')); - // loadTestFile(require.resolve('./bulk_disable')); - // loadTestFile(require.resolve('./retain_api_key')); - loadTestFile(require.resolve('./clone')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./disable')); + loadTestFile(require.resolve('./enable')); + loadTestFile(require.resolve('./execution_status')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); + loadTestFile(require.resolve('./get_alert_summary')); + loadTestFile(require.resolve('./rule_types')); + loadTestFile(require.resolve('./retain_api_key')); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts new file mode 100644 index 00000000000000..f999da061b90b3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts @@ -0,0 +1,19 @@ +/* + * 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 { createTestConfig } from '../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('./tests')], + useDedicatedTaskRunner: true, +}); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_delete.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_disable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_enable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/clone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/clone.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts new file mode 100644 index 00000000000000..0dd1ec2531733e --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerts - Group 3', () => { + describe('alerts', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./bulk_edit')); + loadTestFile(require.resolve('./bulk_delete')); + loadTestFile(require.resolve('./bulk_enable')); + loadTestFile(require.resolve('./bulk_disable')); + loadTestFile(require.resolve('./clone')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/index.ts new file mode 100644 index 00000000000000..c6b0d233a6041b --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration security and spaces enabled - Group 3', function () { + loadTestFile(require.resolve('./alerting')); + }); +} diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index a0882254d53e31..2d5f5be792a888 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -18,7 +18,7 @@ const getFullPath = (relativePath: string) => path.join(path.dirname(__filename) // This hash comes from the latest successful build of the Production Distribution of the Package Registry, for // example: https://internal-ci.elastic.co/blue/organizations/jenkins/package_storage%2Findexing-job/detail/main/1884/pipeline/147. // It should be updated any time there is a new package published. -export const dockerImage = 'docker.elastic.co/package-registry/distribution:lite'; +export const dockerImage = 'docker.elastic.co/package-registry/distribution:production'; export const BUNDLED_PACKAGE_DIR = '/tmp/fleet_bundled_packages'; diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index 6afa7e4a437f92..ea6a865ecbc58c 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -14,7 +14,7 @@ import { pageObjects } from './page_objects'; // This hash comes from the latest successful build of the Production Distribution of the Package Registry, for // example: https://internal-ci.elastic.co/blue/organizations/jenkins/package_storage%2Findexing-job/detail/main/1884/pipeline/147. // It should be updated any time there is a new package published. -export const dockerImage = 'docker.elastic.co/package-registry/distribution:lite'; +export const dockerImage = 'docker.elastic.co/package-registry/distribution:production'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/scalability/config.ts b/x-pack/test/scalability/config.ts index 49bcfee2cf199f..0d3303faf5970a 100644 --- a/x-pack/test/scalability/config.ts +++ b/x-pack/test/scalability/config.ts @@ -62,8 +62,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...(!!AGGS_SHARD_DELAY ? ['--data.search.aggs.shardDelay.enabled=true'] : []), ...(!!DISABLE_PLUGINS ? ['--plugins.initialize=false'] : []), ], - // delay shutdown to ensure that APM can report the data it collects during test execution - delayShutdown: 90_000, }, }; } diff --git a/x-pack/test/scalability/runner.ts b/x-pack/test/scalability/runner.ts index e09a9d438b410d..5882237ade467b 100644 --- a/x-pack/test/scalability/runner.ts +++ b/x-pack/test/scalability/runner.ts @@ -6,6 +6,7 @@ */ import { withProcRunner } from '@kbn/dev-proc-runner'; +import path from 'path'; import { FtrProviderContext } from './ftr_provider_context'; /** @@ -19,6 +20,7 @@ export async function ScalabilityTestRunner( gatlingProjectRootPath: string ) { const log = getService('log'); + const gatlingReportBaseDir = path.parse(scalabilityJsonPath).name; log.info(`Running scalability test with json file: '${scalabilityJsonPath}'`); @@ -28,6 +30,7 @@ export async function ScalabilityTestRunner( args: [ 'gatling:test', '-q', + `-Dgatling.core.outputDirectoryBaseName=${gatlingReportBaseDir}`, '-Dgatling.simulationClass=org.kibanaLoadTest.simulation.generic.GenericJourney', `-DjourneyPath=${scalabilityJsonPath}`, ], diff --git a/yarn.lock b/yarn.lock index a0b8800f50d461..a18e013d8f9679 100644 --- a/yarn.lock +++ b/yarn.lock @@ -113,21 +113,21 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.20.2", "@babel/core@^7.7.2", "@babel/core@^7.7.5": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92" - integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g== +"@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.20.5", "@babel/core@^7.7.2", "@babel/core@^7.7.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" + integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.2" + "@babel/generator" "^7.20.5" "@babel/helper-compilation-targets" "^7.20.0" "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.1" - "@babel/parser" "^7.20.2" + "@babel/helpers" "^7.20.5" + "@babel/parser" "^7.20.5" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -150,12 +150,12 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.20.1", "@babel/generator@^7.20.2", "@babel/generator@^7.20.4", "@babel/generator@^7.7.2": - version "7.20.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" - integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.20.5", "@babel/generator@^7.7.2": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" + integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== dependencies: - "@babel/types" "^7.20.2" + "@babel/types" "^7.20.5" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -370,14 +370,14 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helpers@^7.12.5", "@babel/helpers@^7.20.1": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9" - integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg== +"@babel/helpers@^7.12.5", "@babel/helpers@^7.20.5": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" + integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== dependencies: "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.0" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" "@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": version "7.18.6" @@ -388,10 +388,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2", "@babel/parser@^7.20.3": - version "7.20.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" - integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== +"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" + integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -1177,12 +1177,12 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" - integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== dependencies: - regenerator-runtime "^0.13.10" + regenerator-runtime "^0.13.11" "@babel/template@^7.12.7", "@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3": version "7.18.10" @@ -1193,26 +1193,26 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.10.3", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" - integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== +"@babel/traverse@^7.10.3", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" + integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.1" + "@babel/generator" "^7.20.5" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.1" - "@babel/types" "^7.20.0" + "@babel/parser" "^7.20.5" + "@babel/types" "^7.20.5" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" - integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== +"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" + integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== dependencies: "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" @@ -9295,7 +9295,7 @@ axobject-query@^2.2.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== -babel-jest@^29.2.2, babel-jest@^29.3.1: +babel-jest@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" integrity sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA== @@ -12819,22 +12819,7 @@ ejs@^3.1.6, ejs@^3.1.8: dependencies: jake "^10.8.5" -elastic-apm-http-client@11.0.2, elastic-apm-http-client@^11.0.1: - version "11.0.2" - resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-11.0.2.tgz#576521443d4f3c733b5220ae8175bf5538870cf5" - integrity sha512-Wiqwi4lnhjkILtP54wIbdY0X3Lv+x9JID42zYBI3g7BGRWUu4pPcTjJStWT/muMW57cdimHUektD3tOMFogprQ== - dependencies: - agentkeepalive "^4.2.1" - breadth-filter "^2.0.0" - end-of-stream "^1.4.4" - fast-safe-stringify "^2.0.7" - fast-stream-to-buffer "^1.0.0" - object-filter-sequence "^1.0.0" - readable-stream "^3.4.0" - semver "^6.3.0" - stream-chopper "^3.0.1" - -elastic-apm-http-client@11.0.3: +elastic-apm-http-client@11.0.3, elastic-apm-http-client@^11.0.1: version "11.0.3" resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-11.0.3.tgz#1d357af449d66695ef10019c21efe6377ad8815e" integrity sha512-y+P9ByvfxjZbnLejgGaCAnwEe+FWMVshoMmjeLEEEVlQTLiFUHy7vhYyCQVqgbZzQ6zpaGPqPU2woKglKW4RHw== @@ -12849,45 +12834,7 @@ elastic-apm-http-client@11.0.3: semver "^6.3.0" stream-chopper "^3.0.1" -elastic-apm-node@^3.38.0: - version "3.40.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.40.0.tgz#ed805ec817db7687ba9a77bcc0db6131e8cbc8cf" - integrity sha512-gs9Z7boZW2o3ZMVbdjoJKXv4F2AcfMh52DW1WxEE/FSFa6lymj6GmCEFywuP8SqdpRZbh6yohJoGOpl7sheNJg== - dependencies: - "@elastic/ecs-pino-format" "^1.2.0" - "@opentelemetry/api" "^1.1.0" - after-all-results "^2.0.0" - async-cache "^1.1.0" - async-value-promise "^1.1.1" - basic-auth "^2.0.1" - cookie "^0.5.0" - core-util-is "^1.0.2" - elastic-apm-http-client "11.0.2" - end-of-stream "^1.4.4" - error-callsites "^2.0.4" - error-stack-parser "^2.0.6" - escape-string-regexp "^4.0.0" - fast-safe-stringify "^2.0.7" - http-headers "^3.0.2" - is-native "^1.0.1" - lru-cache "^6.0.0" - measured-reporting "^1.51.1" - monitor-event-loop-delay "^1.0.0" - object-filter-sequence "^1.0.0" - object-identity-map "^1.0.2" - original-url "^1.2.3" - pino "^6.11.2" - relative-microtime "^2.0.0" - require-in-the-middle "^5.2.0" - semver "^6.3.0" - set-cookie-serde "^1.0.0" - shallow-clone-shim "^2.0.0" - source-map "^0.8.0-beta.0" - sql-summary "^1.0.1" - traverse "^0.6.6" - unicode-byte-truncate "^1.0.0" - -elastic-apm-node@^3.40.1: +elastic-apm-node@^3.38.0, elastic-apm-node@^3.40.1: version "3.40.1" resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.40.1.tgz#ae3669d480fdacf62ace40d12a6f1a3c46b37940" integrity sha512-vdyEZ7BPKJP2a1PkCsg350XXGZj03bwOiGrZdqgflocYxns5QwFbhvMKaVq7hWWWS8/sACesrLLELyQgdOpFsw== @@ -23394,10 +23341,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: - version "0.13.10" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" - integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regenerator-transform@^0.15.0: version "0.15.0"