[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:
Zacqary Adam Xeper 2022-12-06 15:51:27 -06:00 committed by GitHub
parent 87d1e8b6db
commit a3220fe1b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 5407 additions and 4511 deletions

View file

@ -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(),

View 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.
*/
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,
};
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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 });
}

View file

@ -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 };
};

View file

@ -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']);
}

View 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';

View file

@ -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}`
);
}
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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',
})
);
}
}

View file

@ -6,3 +6,4 @@
*/
export * from './rules_client';
export * from './types';

View file

@ -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 };
};

View file

@ -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);
}

View file

@ -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
);
}

View 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 { 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,
};
}

View file

@ -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,
};
}

View file

@ -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>);
}

View file

@ -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;
}
};

View file

@ -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';

View file

@ -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}`
);
}
};

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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(', '),
},
})
);
}
}
}

View 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;
}

View file

@ -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 };
};

View file

@ -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 };
};

View 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 };
}

View 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 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] };
};

View file

@ -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
);
}

View 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,
});
}

View 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,
});
}

View file

@ -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;
}

View file

@ -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]);
}
}
}
}

View 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!]);
}
}

View 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,
};
}

View 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
);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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(),
});
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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
);
}

View file

@ -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
);
}

View file

@ -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 }
);
}
}

View file

@ -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,
};
}

View file

@ -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,
},
});
}
}

View 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
);
}

View file

@ -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
);
}

View file

@ -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 }
);
}
}

View file

@ -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
);
}

View 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
);
}

View file

@ -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

View file

@ -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';

View file

@ -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 () => {

View file

@ -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';

View 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;
}>;
};
}

View file

@ -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';