mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Alerting] Split RulesClient methods into multiple files (#146612)
## Summary Closes #136141 This splits `rules_client.ts`, a file of over 5000 lines, into multiple files. It does this by converting all of the `RulesClient` classes into functions that take a `context` value, which consists of all the RulesClient's `private` variables. - `RulesClient` `public` class methods are replaced with higher-order functions that pass the `context` value into these newly split functions. These are stored in the ~`rules_client` folder root~ `methods` directory - `private` methods that required access to `this` are stored in the `lib` folder, and now take a `context` - The previous `lib` folder is renamed to `common`, and consists of any and all functions that don't require access to the `context`. Several former `private` methods that didn't actually invoke `this` have been moved to `common`. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
87d1e8b6db
commit
a3220fe1b6
80 changed files with 5407 additions and 4511 deletions
|
@ -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(),
|
||||
|
|
|
@ -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<RawRule, 'apiKey' | 'apiKeyOwner'> {
|
||||
return apiKey && apiKey.apiKeysEnabled
|
||||
? {
|
||||
apiKeyOwner: username,
|
||||
apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'),
|
||||
}
|
||||
: {
|
||||
apiKeyOwner: null,
|
||||
apiKey: null,
|
||||
};
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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 });
|
||||
}
|
|
@ -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 };
|
||||
};
|
|
@ -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']);
|
||||
}
|
24
x-pack/plugins/alerting/server/rules_client/common/index.ts
Normal file
24
x-pack/plugins/alerting/server/rules_client/common/index.ts
Normal file
|
@ -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';
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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<RawRule>) {
|
||||
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',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './rules_client';
|
||||
export * from './types';
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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<Pick<RawRule, 'apiKey' | 'apiKeyOwner'>> {
|
||||
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);
|
||||
}
|
|
@ -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<Params extends RuleTypeParams = never>(
|
||||
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<RawRule>;
|
||||
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<RawRule>('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<Params>(
|
||||
context,
|
||||
createdAlert.id,
|
||||
createdAlert.attributes.alertTypeId,
|
||||
createdAlert.attributes,
|
||||
references,
|
||||
false,
|
||||
true
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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<Params extends RuleTypeParams>(
|
||||
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<Params>(
|
||||
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<Params extends RuleTypeParams>(
|
||||
context: RulesClientContext,
|
||||
id: string,
|
||||
ruleType: UntypedNormalizedRuleType,
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
meta,
|
||||
notifyWhen,
|
||||
legacyId,
|
||||
scheduledTaskId,
|
||||
params,
|
||||
executionStatus,
|
||||
monitoring,
|
||||
nextRun,
|
||||
schedule,
|
||||
actions,
|
||||
snoozeSchedule,
|
||||
...partialRawRule
|
||||
}: Partial<RawRule>,
|
||||
references: SavedObjectReference[] | undefined,
|
||||
includeLegacyId: boolean = false,
|
||||
excludeFromPublicApi: boolean = false,
|
||||
includeSnoozeData: boolean = false
|
||||
): PartialRule<Params> | PartialRuleWithLegacyId<Params> {
|
||||
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<Params>)
|
||||
: (rule as PartialRule<Params>);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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<Record<string, RawAlert>, 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}`
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<T extends Partial<RawRule>>(
|
||||
context: RulesClientContext,
|
||||
alertAttributes: T
|
||||
): T {
|
||||
if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) {
|
||||
alertAttributes.meta = alertAttributes.meta ?? {};
|
||||
alertAttributes.meta.versionApiKeyLastmodified = context.kibanaVersion;
|
||||
}
|
||||
return alertAttributes;
|
||||
}
|
|
@ -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<RawRule, 'notifyWhen' | 'throttle'> & { actions: NormalizedAlertAction[] }
|
||||
): Promise<void> {
|
||||
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(', '),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
223
x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts
Normal file
223
x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts
Normal file
|
@ -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<AggregateResult> {
|
||||
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<RawRule, RuleAggregation>({
|
||||
...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;
|
||||
}
|
|
@ -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<RawRule>(
|
||||
{
|
||||
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<string, string> = {};
|
||||
const taskIdToRuleIdMapping: Record<string, string> = {};
|
||||
const ruleNameToRuleIdMapping: Record<string, string> = {};
|
||||
|
||||
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 };
|
||||
};
|
|
@ -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<RawRule>(
|
||||
{
|
||||
filter,
|
||||
type: 'alert',
|
||||
perPage: 100,
|
||||
...(context.namespace ? { namespaces: [context.namespace] } : undefined),
|
||||
}
|
||||
);
|
||||
|
||||
const rulesToDisable: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
const errors: BulkOperationError[] = [];
|
||||
const ruleNameToRuleIdMapping: Record<string, string> = {};
|
||||
|
||||
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<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
|
||||
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 };
|
||||
};
|
544
x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts
Normal file
544
x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts
Normal file
|
@ -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<BulkEditFields, 'tags'>;
|
||||
value: string[];
|
||||
}
|
||||
| {
|
||||
operation: 'add' | 'set';
|
||||
field: Extract<BulkEditFields, 'actions'>;
|
||||
value: NormalizedAlertAction[];
|
||||
}
|
||||
| {
|
||||
operation: 'set';
|
||||
field: Extract<BulkEditFields, 'schedule'>;
|
||||
value: Rule['schedule'];
|
||||
}
|
||||
| {
|
||||
operation: 'set';
|
||||
field: Extract<BulkEditFields, 'throttle'>;
|
||||
value: Rule['throttle'];
|
||||
}
|
||||
| {
|
||||
operation: 'set';
|
||||
field: Extract<BulkEditFields, 'notifyWhen'>;
|
||||
value: Rule['notifyWhen'];
|
||||
}
|
||||
| {
|
||||
operation: 'set';
|
||||
field: Extract<BulkEditFields, 'snoozeSchedule'>;
|
||||
value: RuleSnoozeSchedule;
|
||||
}
|
||||
| {
|
||||
operation: 'delete';
|
||||
field: Extract<BulkEditFields, 'snoozeSchedule'>;
|
||||
value?: string[];
|
||||
}
|
||||
| {
|
||||
operation: 'set';
|
||||
field: Extract<BulkEditFields, 'apiKey'>;
|
||||
value?: undefined;
|
||||
};
|
||||
|
||||
type RuleParamsModifier<Params extends RuleTypeParams> = (params: Params) => Promise<Params>;
|
||||
|
||||
export interface BulkEditOptionsFilter<Params extends RuleTypeParams> {
|
||||
filter?: string | KueryNode;
|
||||
operations: BulkEditOperation[];
|
||||
paramsModifier?: RuleParamsModifier<Params>;
|
||||
}
|
||||
|
||||
export interface BulkEditOptionsIds<Params extends RuleTypeParams> {
|
||||
ids: string[];
|
||||
operations: BulkEditOperation[];
|
||||
paramsModifier?: RuleParamsModifier<Params>;
|
||||
}
|
||||
|
||||
export type BulkEditOptions<Params extends RuleTypeParams> =
|
||||
| BulkEditOptionsFilter<Params>
|
||||
| BulkEditOptionsIds<Params>;
|
||||
|
||||
export async function bulkEdit<Params extends RuleTypeParams>(
|
||||
context: RulesClientContext,
|
||||
options: BulkEditOptions<Params>
|
||||
): Promise<{
|
||||
rules: Array<SanitizedRule<Params>>;
|
||||
errors: BulkOperationError[];
|
||||
total: number;
|
||||
}> {
|
||||
const queryFilter = (options as BulkEditOptionsFilter<Params>).filter;
|
||||
const ids = (options as BulkEditOptionsIds<Params>).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<Params>(
|
||||
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<BulkEditOperation, { field: Extract<BulkEditFields, 'schedule'> }> =>
|
||||
operation.field === 'schedule'
|
||||
);
|
||||
|
||||
if (scheduleOperation?.value) {
|
||||
const taskIds = updatedRules.reduce<string[]>((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<Params extends RuleTypeParams>(
|
||||
context: RulesClientContext,
|
||||
{
|
||||
filter,
|
||||
operations,
|
||||
paramsModifier,
|
||||
}: {
|
||||
filter: KueryNode | null;
|
||||
operations: BulkEditOptions<Params>['operations'];
|
||||
paramsModifier: BulkEditOptions<Params>['paramsModifier'];
|
||||
}
|
||||
): Promise<{
|
||||
apiKeysToInvalidate: string[];
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RawRule>>;
|
||||
resultSavedObjects: Array<SavedObjectsUpdateResponse<RawRule>>;
|
||||
errors: BulkOperationError[];
|
||||
}> {
|
||||
const rulesFinder =
|
||||
await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<RawRule>(
|
||||
{
|
||||
filter,
|
||||
type: 'alert',
|
||||
perPage: 100,
|
||||
...(context.namespace ? { namespaces: [context.namespace] } : undefined),
|
||||
}
|
||||
);
|
||||
|
||||
const rules: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
const errors: BulkOperationError[] = [];
|
||||
const apiKeysToInvalidate: string[] = [];
|
||||
const apiKeysMap = new Map<string, { oldApiKey?: string; newApiKey?: string }>();
|
||||
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<string[]>((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 };
|
||||
}
|
|
@ -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<RawRule>(
|
||||
{
|
||||
filter,
|
||||
type: 'alert',
|
||||
perPage: 100,
|
||||
...(context.namespace ? { namespaces: [context.namespace] } : undefined),
|
||||
}
|
||||
);
|
||||
|
||||
const rulesToEnable: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
const taskIdsToEnable: string[] = [];
|
||||
const errors: BulkOperationError[] = [];
|
||||
const ruleNameToRuleIdMapping: Record<string, string> = {};
|
||||
|
||||
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<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
|
||||
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] };
|
||||
};
|
|
@ -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<void> {
|
||||
const { attributes, version } = await context.unsecuredSavedObjectsClient.get<RawRule>(
|
||||
'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
|
||||
);
|
||||
}
|
131
x-pack/plugins/alerting/server/rules_client/methods/clone.ts
Normal file
131
x-pack/plugins/alerting/server/rules_client/methods/clone.ts
Normal file
|
@ -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<Params extends RuleTypeParams = never>(
|
||||
context: RulesClientContext,
|
||||
id: string,
|
||||
{ newId }: { newId?: string }
|
||||
): Promise<SanitizedRule<Params>> {
|
||||
let ruleSavedObject: SavedObject<RawRule>;
|
||||
|
||||
try {
|
||||
ruleSavedObject = await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>(
|
||||
'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<RawRule>('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,
|
||||
});
|
||||
}
|
147
x-pack/plugins/alerting/server/rules_client/methods/create.ts
Normal file
147
x-pack/plugins/alerting/server/rules_client/methods/create.ts
Normal file
|
@ -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<RuleAction, 'actionTypeId'>;
|
||||
interface SavedObjectOptions {
|
||||
id?: string;
|
||||
migrationVersion?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CreateOptions<Params extends RuleTypeParams> {
|
||||
data: Omit<
|
||||
Rule<Params>,
|
||||
| 'id'
|
||||
| 'createdBy'
|
||||
| 'updatedBy'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'apiKey'
|
||||
| 'apiKeyOwner'
|
||||
| 'muteAll'
|
||||
| 'mutedInstanceIds'
|
||||
| 'actions'
|
||||
| 'executionStatus'
|
||||
| 'snoozeSchedule'
|
||||
| 'isSnoozedUntil'
|
||||
| 'lastRun'
|
||||
| 'nextRun'
|
||||
> & { actions: NormalizedAlertAction[] };
|
||||
options?: SavedObjectOptions;
|
||||
}
|
||||
|
||||
export async function create<Params extends RuleTypeParams = never>(
|
||||
context: RulesClientContext,
|
||||
{ data, options }: CreateOptions<Params>
|
||||
): Promise<SanitizedRule<Params>> {
|
||||
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,
|
||||
});
|
||||
}
|
|
@ -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<RawRule>('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<RawRule>('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;
|
||||
}
|
|
@ -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<void> {
|
||||
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<RawRule>('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<RawRule>('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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
142
x-pack/plugins/alerting/server/rules_client/methods/enable.ts
Normal file
142
x-pack/plugins/alerting/server/rules_client/methods/enable.ts
Normal file
|
@ -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<void> {
|
||||
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<RawRule>('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<RawRule>('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!]);
|
||||
}
|
||||
}
|
179
x-pack/plugins/alerting/server/rules_client/methods/find.ts
Normal file
179
x-pack/plugins/alerting/server/rules_client/methods/find.ts
Normal file
|
@ -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<Params extends RuleTypeParams> {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
data: Array<SanitizedRule<Params>>;
|
||||
}
|
||||
|
||||
export async function find<Params extends RuleTypeParams = never>(
|
||||
context: RulesClientContext,
|
||||
{
|
||||
options: { fields, ...options } = {},
|
||||
excludeFromPublicApi = false,
|
||||
includeSnoozeData = false,
|
||||
}: FindParams = {}
|
||||
): Promise<FindResult<Params>> {
|
||||
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<RawRule>({
|
||||
...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<Params>(
|
||||
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,
|
||||
};
|
||||
}
|
64
x-pack/plugins/alerting/server/rules_client/methods/get.ts
Normal file
64
x-pack/plugins/alerting/server/rules_client/methods/get.ts
Normal file
|
@ -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<Params extends RuleTypeParams = never>(
|
||||
context: RulesClientContext,
|
||||
{
|
||||
id,
|
||||
includeLegacyId = false,
|
||||
includeSnoozeData = false,
|
||||
excludeFromPublicApi = false,
|
||||
}: GetParams
|
||||
): Promise<SanitizedRule<Params> | SanitizedRuleWithLegacyId<Params>> {
|
||||
const result = await context.unsecuredSavedObjectsClient.get<RawRule>('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<Params>(
|
||||
context,
|
||||
result.id,
|
||||
result.attributes.alertTypeId,
|
||||
result.attributes,
|
||||
result.references,
|
||||
includeLegacyId,
|
||||
excludeFromPublicApi,
|
||||
includeSnoozeData
|
||||
);
|
||||
}
|
|
@ -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<IExecutionErrorsResult> {
|
||||
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<IExecutionErrorsResult> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<RuleTaskState | void> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<AlertSummary> {
|
||||
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(),
|
||||
});
|
||||
}
|
|
@ -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<string | undefined>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<string | undefined>;
|
||||
}
|
||||
|
||||
export async function getExecutionLogForRule(
|
||||
context: RulesClientContext,
|
||||
{ id, dateStart, dateEnd, filter, page, perPage, sort }: GetExecutionLogByIdParams
|
||||
): Promise<IExecutionLogResult> {
|
||||
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<IExecutionLogResult> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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<void> {
|
||||
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<RawRule>(
|
||||
'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
|
||||
);
|
||||
}
|
|
@ -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<void> {
|
||||
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<Rule>(
|
||||
'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 }
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<Params extends RuleTypeParams = never>(
|
||||
context: RulesClientContext,
|
||||
{ id, includeLegacyId, includeSnoozeData = false }: ResolveParams
|
||||
): Promise<ResolvedSanitizedRule<Params>> {
|
||||
const { saved_object: result, ...resolveResponse } =
|
||||
await context.unsecuredSavedObjectsClient.resolve<RawRule>('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<Params>(
|
||||
context,
|
||||
result.id,
|
||||
result.attributes.alertTypeId,
|
||||
result.attributes,
|
||||
result.references,
|
||||
includeLegacyId,
|
||||
false,
|
||||
includeSnoozeData
|
||||
);
|
||||
|
||||
return {
|
||||
...rule,
|
||||
...resolveResponse,
|
||||
};
|
||||
}
|
|
@ -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<Rule>('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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
109
x-pack/plugins/alerting/server/rules_client/methods/snooze.ts
Normal file
109
x-pack/plugins/alerting/server/rules_client/methods/snooze.ts
Normal file
|
@ -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<void> {
|
||||
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<RawRule>(
|
||||
'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
|
||||
);
|
||||
}
|
|
@ -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<void> {
|
||||
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<RawRule>(
|
||||
'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
|
||||
);
|
||||
}
|
|
@ -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<void> {
|
||||
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<Rule>(
|
||||
'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<RawRule>(
|
||||
'alert',
|
||||
alertId,
|
||||
updateMeta(context, {
|
||||
updatedBy: await context.getUserName(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
|
||||
}),
|
||||
{ version }
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<void> {
|
||||
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<RawRule>(
|
||||
'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
|
||||
);
|
||||
}
|
240
x-pack/plugins/alerting/server/rules_client/methods/update.ts
Normal file
240
x-pack/plugins/alerting/server/rules_client/methods/update.ts
Normal file
|
@ -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<Params extends RuleTypeParams> {
|
||||
id: string;
|
||||
data: {
|
||||
name: string;
|
||||
tags: string[];
|
||||
schedule: IntervalSchedule;
|
||||
actions: NormalizedAlertAction[];
|
||||
params: Params;
|
||||
throttle?: string | null;
|
||||
notifyWhen?: RuleNotifyWhenType | null;
|
||||
};
|
||||
}
|
||||
|
||||
export async function update<Params extends RuleTypeParams = never>(
|
||||
context: RulesClientContext,
|
||||
{ id, data }: UpdateOptions<Params>
|
||||
): Promise<PartialRule<Params>> {
|
||||
return await retryIfConflicts(
|
||||
context.logger,
|
||||
`rulesClient.update('${id}')`,
|
||||
async () => await updateWithOCC<Params>(context, { id, data })
|
||||
);
|
||||
}
|
||||
|
||||
async function updateWithOCC<Params extends RuleTypeParams>(
|
||||
context: RulesClientContext,
|
||||
{ id, data }: UpdateOptions<Params>
|
||||
): Promise<PartialRule<Params>> {
|
||||
let alertSavedObject: SavedObject<RawRule>;
|
||||
|
||||
try {
|
||||
alertSavedObject =
|
||||
await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>('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<RawRule>('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<Params>(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<Params extends RuleTypeParams>(
|
||||
context: RulesClientContext,
|
||||
{ id, data }: UpdateOptions<Params>,
|
||||
{ attributes, version }: SavedObject<RawRule>
|
||||
): Promise<PartialRule<Params>> {
|
||||
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<RawRule>;
|
||||
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<RawRule>(
|
||||
'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
|
||||
);
|
||||
}
|
|
@ -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<void> {
|
||||
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<RawRule>('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<RawRule>('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
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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';
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
148
x-pack/plugins/alerting/server/rules_client/types.ts
Normal file
148
x-pack/plugins/alerting/server/rules_client/types.ts
Normal file
|
@ -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<string | null>;
|
||||
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<CreateAPIKeyResult>;
|
||||
readonly getActionsClient: () => Promise<ActionsClient>;
|
||||
readonly actionsAuthorization: ActionsAuthorization;
|
||||
readonly getEventLogClient: () => Promise<IEventLogClient>;
|
||||
readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
||||
readonly auditLogger?: AuditLogger;
|
||||
readonly eventLogger?: IEventLogger;
|
||||
readonly fieldsToExcludeFromPublicApi: Array<keyof SanitizedRule>;
|
||||
}
|
||||
|
||||
export type NormalizedAlertAction = Omit<RuleAction, 'actionTypeId'>;
|
||||
|
||||
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<string, string>;
|
||||
}
|
||||
|
||||
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;
|
||||
}>;
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue