mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RAM][HTTP Versioning] Version Rule Bulk Edit Endpoint (#161912)
## Summary Resolves: https://github.com/elastic/kibana/issues/161395 Parent Issue: https://github.com/elastic/kibana/issues/157883 Adds versioned types to the rule `bulk_edit` endpoint. This PR also moves around the folder structure slightly, by adding a sub folder for the `data`/`route`/`application` methods: ## Before  ## After  Notice I added a `methods` folder to contain the methods, I did the same for the `data` and `route` folders as well. I think this improves the hierarchy of these modules, If folks are ok with it then I will update the doc with the new folder structure. ### 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: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
71ebc38c42
commit
d4e6a19798
101 changed files with 2289 additions and 1253 deletions
12
x-pack/plugins/alerting/common/routes/r_rule/index.ts
Normal file
12
x-pack/plugins/alerting/common/routes/r_rule/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
export { rRuleSchema } from './schemas/latest';
|
||||
export type { RRule } from './types/latest';
|
||||
|
||||
export { rRuleSchema as rRuleSchemaV1 } from './schemas/v1';
|
||||
export type { RRule as RRuleV1 } from './types/latest';
|
51
x-pack/plugins/alerting/common/routes/r_rule/schemas/v1.ts
Normal file
51
x-pack/plugins/alerting/common/routes/r_rule/schemas/v1.ts
Normal 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 { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
validateStartDateV1,
|
||||
validateEndDateV1,
|
||||
createValidateRecurrenceByV1,
|
||||
} from '../validation';
|
||||
|
||||
export const rRuleSchema = schema.object({
|
||||
dtstart: schema.string({ validate: validateStartDateV1 }),
|
||||
tzid: schema.string(),
|
||||
freq: schema.maybe(
|
||||
schema.oneOf([schema.literal(0), schema.literal(1), schema.literal(2), schema.literal(3)])
|
||||
),
|
||||
interval: schema.maybe(
|
||||
schema.number({
|
||||
validate: (interval: number) => {
|
||||
if (interval < 1) return 'rRule interval must be > 0';
|
||||
},
|
||||
})
|
||||
),
|
||||
until: schema.maybe(schema.string({ validate: validateEndDateV1 })),
|
||||
count: schema.maybe(
|
||||
schema.number({
|
||||
validate: (count: number) => {
|
||||
if (count < 1) return 'rRule count must be > 0';
|
||||
},
|
||||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRecurrenceByV1('byweekday'),
|
||||
})
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRecurrenceByV1('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRecurrenceByV1('bymonth'),
|
||||
})
|
||||
),
|
||||
});
|
10
x-pack/plugins/alerting/common/routes/r_rule/types/v1.ts
Normal file
10
x-pack/plugins/alerting/common/routes/r_rule/types/v1.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { rRuleSchemaV1 } from '..';
|
||||
|
||||
export type RRule = TypeOf<typeof rRuleSchemaV1>;
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { validateStartDate } from './validate_start_date/latest';
|
||||
export { validateEndDate } from './validate_end_date/latest';
|
||||
export { createValidateRecurrenceBy } from './validate_recurrence_by/latest';
|
||||
|
||||
export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1';
|
||||
export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1';
|
||||
export { createValidateRecurrenceBy as createValidateRecurrenceByV1 } from './validate_recurrence_by/v1';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 const validateEndDate = (date: string) => {
|
||||
const parsedValue = Date.parse(date);
|
||||
if (isNaN(parsedValue)) return `Invalid date: ${date}`;
|
||||
if (parsedValue <= Date.now()) return `Invalid snooze date as it is in the past: ${date}`;
|
||||
return;
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 const validateRecurrenceBy = <T>(name: string, array: T[]) => {
|
||||
if (array.length === 0) {
|
||||
return `rRule ${name} cannot be empty`;
|
||||
}
|
||||
};
|
||||
|
||||
export const createValidateRecurrenceBy = <T>(name: string) => {
|
||||
return (array: T[]) => validateRecurrenceBy(name, array);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const validateStartDate = (date: string) => {
|
||||
const parsedValue = Date.parse(date);
|
||||
if (isNaN(parsedValue)) return `Invalid date: ${date}`;
|
||||
return;
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 {
|
||||
ruleSnoozeScheduleSchema,
|
||||
bulkEditOperationsSchema,
|
||||
bulkEditRulesRequestBodySchema,
|
||||
} from './schemas/latest';
|
||||
export type {
|
||||
RuleSnoozeSchedule,
|
||||
BulkEditRulesRequestBody,
|
||||
BulkEditRulesResponse,
|
||||
} from './types/latest';
|
||||
|
||||
export {
|
||||
ruleSnoozeScheduleSchema as ruleSnoozeScheduleSchemaV1,
|
||||
bulkEditOperationsSchema as bulkEditOperationsSchemaV1,
|
||||
bulkEditRulesRequestBodySchema as bulkEditRulesRequestBodySchemaV1,
|
||||
} from './schemas/v1';
|
||||
export type {
|
||||
RuleSnoozeSchedule as RuleSnoozeScheduleV1,
|
||||
BulkEditRulesRequestBody as BulkEditRulesRequestBodyV1,
|
||||
BulkEditRulesResponse as BulkEditRulesResponseV1,
|
||||
} from './types/v1';
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { validateDurationV1, validateNotifyWhenV1 } from '../../../validation';
|
||||
import { validateSnoozeScheduleV1 } from '../validation';
|
||||
import { rRuleSchemaV1 } from '../../../../r_rule';
|
||||
import { ruleNotifyWhenV1 } from '../../../response';
|
||||
|
||||
const notifyWhenSchema = schema.oneOf(
|
||||
[
|
||||
schema.literal(ruleNotifyWhenV1.CHANGE),
|
||||
schema.literal(ruleNotifyWhenV1.ACTIVE),
|
||||
schema.literal(ruleNotifyWhenV1.THROTTLE),
|
||||
],
|
||||
{ validate: validateNotifyWhenV1 }
|
||||
);
|
||||
|
||||
export const scheduleIdsSchema = schema.maybe(schema.arrayOf(schema.string()));
|
||||
|
||||
export const ruleSnoozeScheduleSchema = schema.object({
|
||||
id: schema.maybe(schema.string()),
|
||||
duration: schema.number(),
|
||||
rRule: rRuleSchemaV1,
|
||||
});
|
||||
|
||||
const ruleSnoozeScheduleSchemaWithValidation = schema.object(
|
||||
{
|
||||
id: schema.maybe(schema.string()),
|
||||
duration: schema.number(),
|
||||
rRule: rRuleSchemaV1,
|
||||
},
|
||||
{ validate: validateSnoozeScheduleV1 }
|
||||
);
|
||||
|
||||
const ruleActionSchema = schema.object({
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
frequency: schema.maybe(
|
||||
schema.object({
|
||||
summary: schema.boolean(),
|
||||
throttle: schema.nullable(schema.string()),
|
||||
notifyWhen: notifyWhenSchema,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const bulkEditOperationsSchema = schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.object({
|
||||
operation: schema.oneOf([
|
||||
schema.literal('add'),
|
||||
schema.literal('delete'),
|
||||
schema.literal('set'),
|
||||
]),
|
||||
field: schema.literal('tags'),
|
||||
value: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.oneOf([schema.literal('add'), schema.literal('set')]),
|
||||
field: schema.literal('actions'),
|
||||
value: schema.arrayOf(ruleActionSchema),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('schedule'),
|
||||
value: schema.object({ interval: schema.string({ validate: validateDurationV1 }) }),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('throttle'),
|
||||
value: schema.nullable(schema.string()),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('notifyWhen'),
|
||||
value: notifyWhenSchema,
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.oneOf([schema.literal('set')]),
|
||||
field: schema.literal('snoozeSchedule'),
|
||||
value: ruleSnoozeScheduleSchemaWithValidation,
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.oneOf([schema.literal('delete')]),
|
||||
field: schema.literal('snoozeSchedule'),
|
||||
value: schema.maybe(scheduleIdsSchema),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('apiKey'),
|
||||
}),
|
||||
]),
|
||||
{ minSize: 1 }
|
||||
);
|
||||
|
||||
export const bulkEditRulesRequestBodySchema = schema.object({
|
||||
filter: schema.maybe(schema.string()),
|
||||
ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
operations: bulkEditOperationsSchema,
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { TypeOf } from '@kbn/config-schema';
|
||||
import { RuleParamsV1, RuleResponseV1 } from '../../../response';
|
||||
import { ruleSnoozeScheduleSchemaV1, bulkEditRulesRequestBodySchemaV1 } from '..';
|
||||
|
||||
export type RuleSnoozeSchedule = TypeOf<typeof ruleSnoozeScheduleSchemaV1>;
|
||||
export type BulkEditRulesRequestBody = TypeOf<typeof bulkEditRulesRequestBodySchemaV1>;
|
||||
|
||||
interface BulkEditActionSkippedResult {
|
||||
id: RuleResponseV1['id'];
|
||||
name?: RuleResponseV1['name'];
|
||||
skip_reason: 'RULE_NOT_MODIFIED';
|
||||
}
|
||||
|
||||
interface BulkEditOperationError {
|
||||
message: string;
|
||||
status?: number;
|
||||
rule: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BulkEditRulesResponse<Params extends RuleParamsV1 = never> {
|
||||
body: {
|
||||
rules: Array<RuleResponseV1<Params>>;
|
||||
skipped: BulkEditActionSkippedResult[];
|
||||
errors: BulkEditOperationError[];
|
||||
total: number;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { validateSnoozeSchedule } from './validate_snooze_schedule/latest';
|
||||
|
||||
export { validateSnoozeSchedule as validateSnoozeScheduleV1 } from './validate_snooze_schedule/v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { Frequency } from '@kbn/rrule';
|
||||
import moment from 'moment';
|
||||
import { RuleSnoozeScheduleV1 } from '../..';
|
||||
|
||||
export const validateSnoozeSchedule = (schedule: RuleSnoozeScheduleV1) => {
|
||||
const intervalIsDaily = schedule.rRule.freq === Frequency.DAILY;
|
||||
const durationInDays = moment.duration(schedule.duration, 'milliseconds').asDays();
|
||||
if (intervalIsDaily && schedule.rRule.interval && durationInDays >= schedule.rRule.interval) {
|
||||
return 'Recurrence interval must be longer than the snooze duration';
|
||||
}
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ruleNotifyWhenV1 } from '../../rule_response';
|
||||
import { ruleNotifyWhenV1 } from '../../../response';
|
||||
import {
|
||||
validateNotifyWhenV1,
|
||||
validateDurationV1,
|
||||
validateHoursV1,
|
||||
validateTimezoneV1,
|
||||
} from '../../validation';
|
||||
} from '../../../validation';
|
||||
|
||||
export const notifyWhenSchema = schema.oneOf(
|
||||
[
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -5,12 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { RuleParamsV1, RuleResponseV1 } from '../../rule_response';
|
||||
import { RuleParamsV1, RuleResponseV1 } from '../../../response';
|
||||
import {
|
||||
actionSchema as actionSchemaV1,
|
||||
actionFrequencySchema as actionFrequencySchemaV1,
|
||||
createParamsSchema as createParamsSchemaV1,
|
||||
createBodySchema as createBodySchemaV1,
|
||||
actionSchemaV1,
|
||||
actionFrequencySchemaV1,
|
||||
createParamsSchemaV1,
|
||||
createBodySchemaV1,
|
||||
} from '..';
|
||||
|
||||
export type CreateRuleAction = TypeOf<typeof actionSchemaV1>;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -14,9 +14,10 @@ export {
|
|||
monitoringSchema,
|
||||
rRuleSchema,
|
||||
ruleResponseSchema,
|
||||
ruleSnoozeScheduleSchema,
|
||||
} from './schemas/latest';
|
||||
|
||||
export type { RuleParams, RuleResponse } from './types/latest';
|
||||
export type { RuleParams, RuleResponse, RuleSnoozeSchedule } from './types/latest';
|
||||
|
||||
export {
|
||||
ruleNotifyWhen,
|
||||
|
@ -43,6 +44,7 @@ export {
|
|||
monitoringSchema as monitoringSchemaV1,
|
||||
rRuleSchema as rRuleSchemaV1,
|
||||
ruleResponseSchema as ruleResponseSchemaV1,
|
||||
ruleSnoozeScheduleSchema as ruleSnoozeScheduleSchemaV1,
|
||||
} from './schemas/v1';
|
||||
|
||||
export {
|
||||
|
@ -61,4 +63,8 @@ export type {
|
|||
RuleExecutionStatusWarningReason as RuleExecutionStatusWarningReasonV1,
|
||||
} from './constants/v1';
|
||||
|
||||
export type { RuleParams as RuleParamsV1, RuleResponse as RuleResponseV1 } from './types/v1';
|
||||
export type {
|
||||
RuleParams as RuleParamsV1,
|
||||
RuleResponse as RuleResponseV1,
|
||||
RuleSnoozeSchedule as RuleSnoozeScheduleV1,
|
||||
} from './types/v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -217,7 +217,7 @@ export const rRuleSchema = schema.object({
|
|||
bysecond: schema.arrayOf(schema.number()),
|
||||
});
|
||||
|
||||
const snoozeScheduleSchema = schema.object({
|
||||
export const ruleSnoozeScheduleSchema = schema.object({
|
||||
duration: schema.number(),
|
||||
rRule: rRuleSchema,
|
||||
id: schema.maybe(schema.string()),
|
||||
|
@ -248,7 +248,7 @@ export const ruleResponseSchema = schema.object({
|
|||
muted_alert_ids: schema.arrayOf(schema.string()),
|
||||
execution_status: ruleExecutionStatusSchema,
|
||||
monitoring: schema.maybe(monitoringSchema),
|
||||
snooze_schedule: schema.maybe(schema.arrayOf(snoozeScheduleSchema)),
|
||||
snooze_schedule: schema.maybe(schema.arrayOf(ruleSnoozeScheduleSchema)),
|
||||
active_snoozes: schema.maybe(schema.arrayOf(schema.string())),
|
||||
is_snoozed_until: schema.maybe(schema.nullable(schema.string())),
|
||||
last_run: schema.maybe(schema.nullable(ruleLastRunSchema)),
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { ruleParamsSchemaV1, ruleResponseSchemaV1 } from '..';
|
||||
import { ruleParamsSchemaV1, ruleResponseSchemaV1, ruleSnoozeScheduleSchemaV1 } from '..';
|
||||
|
||||
export type RuleParams = TypeOf<typeof ruleParamsSchemaV1>;
|
||||
export type RuleSnoozeSchedule = TypeOf<typeof ruleSnoozeScheduleSchemaV1>;
|
||||
type RuleResponseSchemaType = TypeOf<typeof ruleResponseSchemaV1>;
|
||||
|
||||
export interface RuleResponse<Params extends RuleParams = never> {
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ruleNotifyWhenV1, RuleNotifyWhenV1 } from '../../rule_response';
|
||||
import { ruleNotifyWhenV1, RuleNotifyWhenV1 } from '../../response';
|
||||
|
||||
export function validateNotifyWhen(notifyWhen: string) {
|
||||
if (Object.values(ruleNotifyWhenV1).includes(notifyWhen as RuleNotifyWhenV1)) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { rRuleSchema } from './r_rule_schema';
|
||||
export { rRuleRequestSchema } from './r_rule_request_schema';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { validateStartDate, validateEndDate, createValidateRecurrenceBy } from '../validation';
|
||||
|
||||
export const rRuleRequestSchema = schema.object({
|
||||
dtstart: schema.string({ validate: validateStartDate }),
|
||||
tzid: schema.string(),
|
||||
freq: schema.maybe(
|
||||
schema.oneOf([schema.literal(0), schema.literal(1), schema.literal(2), schema.literal(3)])
|
||||
),
|
||||
interval: schema.maybe(
|
||||
schema.number({
|
||||
validate: (interval: number) => {
|
||||
if (interval < 1) return 'rRule interval must be > 0';
|
||||
},
|
||||
})
|
||||
),
|
||||
until: schema.maybe(schema.string({ validate: validateEndDate })),
|
||||
count: schema.maybe(
|
||||
schema.number({
|
||||
validate: (count: number) => {
|
||||
if (count < 1) return 'rRule count must be > 0';
|
||||
},
|
||||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRecurrenceBy('byweekday'),
|
||||
})
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRecurrenceBy('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
validate: createValidateRecurrenceBy('bymonth'),
|
||||
})
|
||||
),
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const rRuleSchema = schema.object({
|
||||
dtstart: schema.string(),
|
||||
tzid: schema.string(),
|
||||
freq: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal(0),
|
||||
schema.literal(1),
|
||||
schema.literal(2),
|
||||
schema.literal(3),
|
||||
schema.literal(4),
|
||||
schema.literal(5),
|
||||
schema.literal(6),
|
||||
])
|
||||
),
|
||||
until: schema.maybe(schema.string()),
|
||||
count: schema.maybe(schema.number()),
|
||||
interval: schema.maybe(schema.number()),
|
||||
wkst: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal('MO'),
|
||||
schema.literal('TU'),
|
||||
schema.literal('WE'),
|
||||
schema.literal('TH'),
|
||||
schema.literal('FR'),
|
||||
schema.literal('SA'),
|
||||
schema.literal('SU'),
|
||||
])
|
||||
),
|
||||
byweekday: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))),
|
||||
bymonth: schema.maybe(schema.arrayOf(schema.number())),
|
||||
bysetpos: schema.maybe(schema.arrayOf(schema.number())),
|
||||
bymonthday: schema.arrayOf(schema.number()),
|
||||
byyearday: schema.arrayOf(schema.number()),
|
||||
byweekno: schema.arrayOf(schema.number()),
|
||||
byhour: schema.arrayOf(schema.number()),
|
||||
byminute: schema.arrayOf(schema.number()),
|
||||
bysecond: schema.arrayOf(schema.number()),
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 type { RRule } from './r_rule';
|
||||
export type { RRuleRequest } from './r_rule_request';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { TypeOf } from '@kbn/config-schema';
|
||||
import { rRuleSchema } from '../schemas/r_rule_schema';
|
||||
|
||||
export type RRule = TypeOf<typeof rRuleSchema>;
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { rRuleRequestSchema } from '../schemas/r_rule_request_schema';
|
||||
|
||||
export type RRuleRequest = TypeOf<typeof rRuleRequestSchema>;
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { validateStartDate } from './validate_start_date';
|
||||
export { validateEndDate } from './validate_end_date';
|
||||
export { validateRecurrenceBy, createValidateRecurrenceBy } from './validate_recurrence_by';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 const validateEndDate = (date: string) => {
|
||||
const parsedValue = Date.parse(date);
|
||||
if (isNaN(parsedValue)) return `Invalid date: ${date}`;
|
||||
if (parsedValue <= Date.now()) return `Invalid snooze date as it is in the past: ${date}`;
|
||||
return;
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 const validateRecurrenceBy = <T>(name: string, array: T[]) => {
|
||||
if (array.length === 0) {
|
||||
return `rRule ${name} cannot be empty`;
|
||||
}
|
||||
};
|
||||
|
||||
export const createValidateRecurrenceBy = <T>(name: string) => {
|
||||
return (array: T[]) => validateRecurrenceBy(name, array);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const validateStartDate = (date: string) => {
|
||||
const parsedValue = Date.parse(date);
|
||||
if (isNaN(parsedValue)) return `Invalid date: ${date}`;
|
||||
return;
|
||||
};
|
|
@ -6,27 +6,33 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { omit } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { RulesClient, ConstructorOptions } from '../../../../rules_client/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';
|
||||
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
|
||||
import { RecoveredActionGroup, RuleTypeParams } from '../../../common';
|
||||
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
|
||||
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
|
||||
import { RecoveredActionGroup, RuleTypeParams } from '../../../../../common';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
|
||||
import { AlertingAuthorization } from '../../../../authorization/alerting_authorization';
|
||||
import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { NormalizedAlertAction } from '../types';
|
||||
import { enabledRule1, enabledRule2, siemRule1, siemRule2 } from './test_helpers';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
import { migrateLegacyActionsMock } from '../lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
|
||||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { NormalizedAlertAction } from '../../../../rules_client/types';
|
||||
import {
|
||||
enabledRule1,
|
||||
enabledRule2,
|
||||
siemRule1,
|
||||
siemRule2,
|
||||
} from '../../../../rules_client/tests/test_helpers';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { migrateLegacyActionsMock } from '../../../../rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
|
@ -37,11 +43,11 @@ jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
|||
resultedReferences: [],
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../lib/snooze/is_snooze_active', () => ({
|
||||
jest.mock('../../../../lib/snooze/is_snooze_active', () => ({
|
||||
isSnoozeActive: jest.fn(),
|
||||
}));
|
||||
|
||||
|
@ -50,7 +56,7 @@ jest.mock('uuid', () => {
|
|||
return { v4: () => `${uuid++}` };
|
||||
});
|
||||
|
||||
const { isSnoozeActive } = jest.requireMock('../../lib/snooze/is_snooze_active');
|
||||
const { isSnoozeActive } = jest.requireMock('../../../../lib/snooze/is_snooze_active');
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -105,10 +111,21 @@ describe('bulkEdit()', () => {
|
|||
attributes: {
|
||||
enabled: false,
|
||||
tags: ['foo'],
|
||||
createdBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
legacyId: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
snoozeSchedule: [],
|
||||
alertTypeId: 'myType',
|
||||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -244,6 +261,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -301,6 +322,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -354,6 +379,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -621,8 +650,15 @@ describe('bulkEdit()', () => {
|
|||
],
|
||||
{ overwrite: true }
|
||||
);
|
||||
|
||||
expect(result.rules[0]).toEqual({
|
||||
...existingRule.attributes,
|
||||
...omit(existingRule.attributes, 'legacyId'),
|
||||
createdAt: new Date(existingRule.attributes.createdAt),
|
||||
updatedAt: new Date(existingRule.attributes.updatedAt),
|
||||
executionStatus: {
|
||||
...existingRule.attributes.executionStatus,
|
||||
lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate),
|
||||
},
|
||||
actions: [existingAction, { ...newAction, uuid: '222' }],
|
||||
id: existingRule.id,
|
||||
snoozeSchedule: [],
|
||||
|
@ -827,7 +863,13 @@ describe('bulkEdit()', () => {
|
|||
{ overwrite: true }
|
||||
);
|
||||
expect(result.rules[0]).toEqual({
|
||||
...existingRule.attributes,
|
||||
...omit(existingRule.attributes, 'legacyId'),
|
||||
createdAt: new Date(existingRule.attributes.createdAt),
|
||||
updatedAt: new Date(existingRule.attributes.updatedAt),
|
||||
executionStatus: {
|
||||
...existingRule.attributes.executionStatus,
|
||||
lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate),
|
||||
},
|
||||
actions: [
|
||||
existingAction,
|
||||
{
|
||||
|
@ -875,6 +917,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {
|
||||
index: ['test-1', 'test-2', 'test-4', 'test-5'],
|
||||
},
|
||||
|
@ -940,6 +986,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {
|
||||
index: ['test-1'],
|
||||
},
|
||||
|
@ -1040,6 +1090,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -1414,7 +1468,7 @@ describe('bulkEdit()', () => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0][0].attributes as any)
|
||||
.snoozeSchedule
|
||||
).toBeUndefined();
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1491,6 +1545,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {
|
||||
index: ['index-1', 'index-2', 'index-3'],
|
||||
},
|
||||
|
@ -1559,6 +1617,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {
|
||||
index: ['index-1', 'index-2'],
|
||||
},
|
||||
|
@ -1628,6 +1690,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: {
|
||||
index: ['index-1', 'index-2', 'index-3'],
|
||||
},
|
||||
|
@ -2041,6 +2107,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: { index: ['test-index-*'] },
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -2437,6 +2507,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: { index: ['test-index-*'] },
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -2514,6 +2588,10 @@ describe('bulkEdit()', () => {
|
|||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
params: { index: ['test-index-*'] },
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
|
@ -2551,6 +2629,10 @@ describe('bulkEdit()', () => {
|
|||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
schedule: { interval: '1m' },
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
consumer: 'myApp',
|
||||
params: { index: ['test-index-*'] },
|
||||
throttle: null,
|
|
@ -0,0 +1,890 @@
|
|||
/*
|
||||
* 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,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { BulkActionSkipResult } from '../../../../../common/bulk_edit';
|
||||
import { RuleTypeRegistry } 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 '../../../../rules_client/common/audit_events';
|
||||
import {
|
||||
retryIfBulkEditConflicts,
|
||||
applyBulkEditOperation,
|
||||
buildKueryNodeFilter,
|
||||
injectReferencesIntoActions,
|
||||
getBulkSnooze,
|
||||
getBulkUnsnooze,
|
||||
verifySnoozeScheduleLimit,
|
||||
} from '../../../../rules_client/common';
|
||||
import {
|
||||
alertingAuthorizationFilterOpts,
|
||||
MAX_RULES_NUMBER_FOR_BULK_OPERATION,
|
||||
RULE_TYPE_CHECKS_CONCURRENCY,
|
||||
API_KEY_GENERATE_CONCURRENCY,
|
||||
} from '../../../../rules_client/common/constants';
|
||||
import { getMappedParams } from '../../../../rules_client/common/mapped_params_utils';
|
||||
import {
|
||||
extractReferences,
|
||||
validateActions,
|
||||
updateMeta,
|
||||
addGeneratedActionValues,
|
||||
createNewAPIKeySet,
|
||||
} from '../../../../rules_client/lib';
|
||||
import {
|
||||
BulkOperationError,
|
||||
RuleBulkOperationAggregation,
|
||||
RulesClientContext,
|
||||
NormalizedAlertActionWithGeneratedValues,
|
||||
} from '../../../../rules_client/types';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import {
|
||||
BulkEditFields,
|
||||
BulkEditOperation,
|
||||
BulkEditOptionsFilter,
|
||||
BulkEditOptionsIds,
|
||||
ParamsModifier,
|
||||
ShouldIncrementRevision,
|
||||
} from './types';
|
||||
import { RawRuleAction, RawRule, SanitizedRule } from '../../../../types';
|
||||
import { ruleNotifyWhen } from '../../constants';
|
||||
import { ruleDomainSchema } from '../../schemas';
|
||||
import { RuleParams, RuleDomain, RuleSnoozeSchedule } from '../../types';
|
||||
import { findRulesSo, bulkCreateRulesSo } from '../../../../data/rule';
|
||||
import { RuleAttributes, RuleActionAttributes } from '../../../../data/rule/types';
|
||||
import {
|
||||
transformRuleAttributesToRuleDomain,
|
||||
transformRuleDomainToRuleAttributes,
|
||||
transformRuleDomainToRule,
|
||||
} from '../../transforms';
|
||||
|
||||
export const bulkEditFieldsToExcludeFromRevisionUpdates = new Set(['snoozeSchedule', 'apiKey']);
|
||||
|
||||
type ApiKeysMap = Map<
|
||||
string,
|
||||
{
|
||||
oldApiKey?: string;
|
||||
newApiKey?: string;
|
||||
oldApiKeyCreatedByUser?: boolean | null;
|
||||
newApiKeyCreatedByUser?: boolean | null;
|
||||
}
|
||||
>;
|
||||
|
||||
type ApiKeyAttributes = Pick<RuleAttributes, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'>;
|
||||
|
||||
type RuleType = ReturnType<RuleTypeRegistry['get']>;
|
||||
|
||||
// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
|
||||
export interface BulkEditResult<Params extends RuleParams> {
|
||||
rules: Array<SanitizedRule<Params>>;
|
||||
skipped: BulkActionSkipResult[];
|
||||
errors: BulkOperationError[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type BulkEditOptions<Params extends RuleParams> =
|
||||
| BulkEditOptionsFilter<Params>
|
||||
| BulkEditOptionsIds<Params>;
|
||||
|
||||
export async function bulkEditRules<Params extends RuleParams>(
|
||||
context: RulesClientContext,
|
||||
options: BulkEditOptions<Params>
|
||||
): Promise<BulkEditResult<Params>> {
|
||||
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 findRulesSo<RuleBulkOperationAggregation>({
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
savedObjectsFindOptions: {
|
||||
filter: qNodeFilterWithAuth,
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
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, skipped } = await retryIfBulkEditConflicts(
|
||||
context.logger,
|
||||
`rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${
|
||||
options.paramsModifier ? '[Function]' : undefined
|
||||
}', shouldIncrementRevision=${options.shouldIncrementRevision ? '[Function]' : undefined}')`,
|
||||
(filterKueryNode: KueryNode | null) =>
|
||||
bulkEditRulesOcc(context, {
|
||||
filter: filterKueryNode,
|
||||
operations: options.operations,
|
||||
paramsModifier: options.paramsModifier,
|
||||
shouldIncrementRevision: options.shouldIncrementRevision,
|
||||
}),
|
||||
qNodeFilterWithAuth
|
||||
);
|
||||
|
||||
if (apiKeysToInvalidate.length > 0) {
|
||||
await bulkMarkApiKeysForInvalidation(
|
||||
{ apiKeys: apiKeysToInvalidate },
|
||||
context.logger,
|
||||
context.unsecuredSavedObjectsClient
|
||||
);
|
||||
}
|
||||
|
||||
const updatedRules = results.map(({ id, attributes, references }) => {
|
||||
// TODO (http-versioning): alertTypeId should never be null, but we need to
|
||||
// fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject
|
||||
// when we are doing the bulk create and this should fix itself
|
||||
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId!);
|
||||
const ruleDomain = transformRuleAttributesToRuleDomain<Params>(attributes as RuleAttributes, {
|
||||
id,
|
||||
logger: context.logger,
|
||||
ruleType,
|
||||
references,
|
||||
omitGeneratedValues: false,
|
||||
});
|
||||
try {
|
||||
ruleDomainSchema.validate(ruleDomain);
|
||||
} catch (e) {
|
||||
context.logger.warn(`Error validating bulk edited rule domain object for id: ${id}, ${e}`);
|
||||
}
|
||||
return ruleDomain;
|
||||
});
|
||||
|
||||
// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
|
||||
const publicRules = updatedRules.map((rule: RuleDomain<Params>) => {
|
||||
return transformRuleDomainToRule<Params>(rule);
|
||||
}) as Array<SanitizedRule<Params>>;
|
||||
|
||||
await bulkUpdateSchedules(context, options.operations, updatedRules);
|
||||
|
||||
return { rules: publicRules, skipped, errors, total };
|
||||
}
|
||||
|
||||
async function bulkEditRulesOcc<Params extends RuleParams>(
|
||||
context: RulesClientContext,
|
||||
{
|
||||
filter,
|
||||
operations,
|
||||
paramsModifier,
|
||||
shouldIncrementRevision,
|
||||
}: {
|
||||
filter: KueryNode | null;
|
||||
operations: BulkEditOperation[];
|
||||
paramsModifier?: ParamsModifier<Params>;
|
||||
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
|
||||
}
|
||||
): Promise<{
|
||||
apiKeysToInvalidate: string[];
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>;
|
||||
resultSavedObjects: Array<SavedObjectsUpdateResponse<RuleAttributes>>;
|
||||
errors: BulkOperationError[];
|
||||
skipped: BulkActionSkipResult[];
|
||||
}> {
|
||||
const rulesFinder =
|
||||
await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<RuleAttributes>(
|
||||
{
|
||||
filter,
|
||||
type: 'alert',
|
||||
perPage: 100,
|
||||
...(context.namespace ? { namespaces: [context.namespace] } : undefined),
|
||||
}
|
||||
);
|
||||
|
||||
const rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>> = [];
|
||||
const skipped: BulkActionSkipResult[] = [];
|
||||
const errors: BulkOperationError[] = [];
|
||||
const apiKeysMap: ApiKeysMap = new Map();
|
||||
const username = await context.getUserName();
|
||||
|
||||
for await (const response of rulesFinder.find()) {
|
||||
await pMap(
|
||||
response.saved_objects,
|
||||
async (rule: SavedObjectsFindResult<RuleAttributes>) =>
|
||||
updateRuleAttributesAndParamsInMemory({
|
||||
context,
|
||||
rule,
|
||||
operations,
|
||||
paramsModifier,
|
||||
apiKeysMap,
|
||||
rules,
|
||||
skipped,
|
||||
errors,
|
||||
username,
|
||||
shouldIncrementRevision,
|
||||
}),
|
||||
{ concurrency: API_KEY_GENERATE_CONCURRENCY }
|
||||
);
|
||||
}
|
||||
await rulesFinder.close();
|
||||
|
||||
const { result, apiKeysToInvalidate } =
|
||||
rules.length > 0
|
||||
? await saveBulkUpdatedRules(context, rules, apiKeysMap)
|
||||
: {
|
||||
result: { saved_objects: [] },
|
||||
apiKeysToInvalidate: [],
|
||||
};
|
||||
|
||||
return {
|
||||
apiKeysToInvalidate,
|
||||
resultSavedObjects: result.saved_objects,
|
||||
errors,
|
||||
rules,
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
async function bulkUpdateSchedules<Params extends RuleParams>(
|
||||
context: RulesClientContext,
|
||||
operations: BulkEditOperation[],
|
||||
updatedRules: Array<RuleDomain<Params>>
|
||||
): Promise<void> {
|
||||
const scheduleOperation = operations.find(
|
||||
(
|
||||
operation
|
||||
): operation is Extract<BulkEditOperation, { field: Extract<BulkEditFields, 'schedule'> }> =>
|
||||
operation.field === 'schedule'
|
||||
);
|
||||
|
||||
if (!scheduleOperation?.value) {
|
||||
return;
|
||||
}
|
||||
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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>({
|
||||
context,
|
||||
rule,
|
||||
operations,
|
||||
paramsModifier,
|
||||
apiKeysMap,
|
||||
rules,
|
||||
skipped,
|
||||
errors,
|
||||
username,
|
||||
shouldIncrementRevision = () => true,
|
||||
}: {
|
||||
context: RulesClientContext;
|
||||
rule: SavedObjectsFindResult<RuleAttributes>;
|
||||
operations: BulkEditOperation[];
|
||||
paramsModifier?: ParamsModifier<Params>;
|
||||
apiKeysMap: ApiKeysMap;
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>;
|
||||
skipped: BulkActionSkipResult[];
|
||||
errors: BulkOperationError[];
|
||||
username: string | null;
|
||||
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
if (rule.attributes.apiKey) {
|
||||
apiKeysMap.set(rule.id, {
|
||||
oldApiKey: rule.attributes.apiKey,
|
||||
oldApiKeyCreatedByUser: rule.attributes.apiKeyCreatedByUser,
|
||||
});
|
||||
}
|
||||
|
||||
const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId);
|
||||
|
||||
await ensureAuthorizationForBulkUpdate(context, operations, rule);
|
||||
|
||||
// migrate legacy actions only for SIEM rules
|
||||
// TODO (http-versioning) Remove RawRuleAction and RawRule casts
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
actions: rule.attributes.actions as RawRuleAction[],
|
||||
references: rule.references,
|
||||
attributes: rule.attributes as RawRule,
|
||||
});
|
||||
|
||||
if (migratedActions.hasLegacyActions) {
|
||||
rule.attributes.actions = migratedActions.resultedActions;
|
||||
rule.references = migratedActions.resultedReferences;
|
||||
}
|
||||
|
||||
const ruleActions = injectReferencesIntoActions(
|
||||
rule.id,
|
||||
rule.attributes.actions || [],
|
||||
rule.references || []
|
||||
);
|
||||
|
||||
const ruleDomain: RuleDomain<Params> = transformRuleAttributesToRuleDomain<Params>(
|
||||
rule.attributes,
|
||||
{
|
||||
id: rule.id,
|
||||
logger: context.logger,
|
||||
ruleType: context.ruleTypeRegistry.get(rule.attributes.alertTypeId),
|
||||
references: rule.references,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
rule: updatedRule,
|
||||
ruleActions: updatedRuleActions,
|
||||
hasUpdateApiKeyOperation,
|
||||
isAttributesUpdateSkipped,
|
||||
} = await getUpdatedAttributesFromOperations<Params>({
|
||||
context,
|
||||
operations,
|
||||
rule: ruleDomain,
|
||||
ruleActions,
|
||||
ruleType,
|
||||
});
|
||||
|
||||
validateScheduleInterval(context, updatedRule.schedule.interval, ruleType.id, rule.id);
|
||||
|
||||
const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier
|
||||
? await paramsModifier(updatedRule.params)
|
||||
: {
|
||||
modifiedParams: updatedRule.params,
|
||||
isParamsUpdateSkipped: true,
|
||||
};
|
||||
|
||||
// Increment revision if params ended up being modified AND it wasn't already incremented as part of attribute update
|
||||
if (
|
||||
shouldIncrementRevision(ruleParams) &&
|
||||
!isParamsUpdateSkipped &&
|
||||
rule.attributes.revision === updatedRule.revision
|
||||
) {
|
||||
updatedRule.revision += 1;
|
||||
}
|
||||
|
||||
// If neither attributes nor parameters were updated, mark
|
||||
// the rule as skipped and continue to the next rule.
|
||||
if (isAttributesUpdateSkipped && isParamsUpdateSkipped) {
|
||||
skipped.push({
|
||||
id: rule.id,
|
||||
name: rule.attributes.name,
|
||||
skip_reason: 'RULE_NOT_MODIFIED',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,
|
||||
updatedRuleActions as NormalizedAlertActionWithGeneratedValues[],
|
||||
validatedMutatedAlertTypeParams
|
||||
);
|
||||
|
||||
const ruleAttributes = transformRuleDomainToRuleAttributes(updatedRule, {
|
||||
legacyId: rule.attributes.legacyId,
|
||||
actionsWithRefs: rawAlertActions,
|
||||
paramsWithRefs: updatedParams as RuleAttributes['params'],
|
||||
});
|
||||
|
||||
const { apiKeyAttributes } = await prepareApiKeys(
|
||||
context,
|
||||
rule,
|
||||
ruleType,
|
||||
apiKeysMap,
|
||||
ruleAttributes,
|
||||
hasUpdateApiKeyOperation,
|
||||
username
|
||||
);
|
||||
|
||||
const { updatedAttributes } = updateAttributes(
|
||||
context,
|
||||
ruleAttributes,
|
||||
apiKeyAttributes,
|
||||
updatedParams,
|
||||
rawAlertActions,
|
||||
username
|
||||
);
|
||||
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureAuthorizationForBulkUpdate(
|
||||
context: RulesClientContext,
|
||||
operations: BulkEditOperation[],
|
||||
rule: SavedObjectsFindResult<RuleAttributes>
|
||||
): Promise<void> {
|
||||
if (rule.attributes.actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operation of operations) {
|
||||
const { field } = operation;
|
||||
if (field === 'snoozeSchedule' || field === 'apiKey') {
|
||||
try {
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
break;
|
||||
} catch (error) {
|
||||
throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getUpdatedAttributesFromOperations<Params extends RuleParams>({
|
||||
context,
|
||||
operations,
|
||||
rule,
|
||||
ruleActions,
|
||||
ruleType,
|
||||
}: {
|
||||
context: RulesClientContext;
|
||||
operations: BulkEditOperation[];
|
||||
rule: RuleDomain<Params>;
|
||||
ruleActions: RuleDomain['actions'];
|
||||
ruleType: RuleType;
|
||||
}) {
|
||||
let updatedRule = cloneDeep(rule);
|
||||
let updatedRuleActions = ruleActions;
|
||||
let hasUpdateApiKeyOperation = false;
|
||||
let isAttributesUpdateSkipped = true;
|
||||
|
||||
for (const operation of operations) {
|
||||
// Check if the update should be skipped for the current action.
|
||||
// If it should, save the skip reasons in attributesUpdateSkipReasons
|
||||
// and continue to the next operation before without
|
||||
// the `isAttributesUpdateSkipped` flag to false.
|
||||
switch (operation.field) {
|
||||
case 'actions': {
|
||||
const updatedOperation = {
|
||||
...operation,
|
||||
value: addGeneratedActionValues(operation.value),
|
||||
};
|
||||
|
||||
try {
|
||||
await validateActions(context, ruleType, {
|
||||
...updatedRule,
|
||||
actions: updatedOperation.value,
|
||||
});
|
||||
} catch (e) {
|
||||
// If validateActions fails on the first attempt, it may be because of legacy rule-level frequency params
|
||||
updatedRule = await attemptToMigrateLegacyFrequency(
|
||||
context,
|
||||
updatedOperation,
|
||||
updatedRule,
|
||||
ruleType
|
||||
);
|
||||
}
|
||||
|
||||
const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation(
|
||||
updatedOperation,
|
||||
{
|
||||
actions: updatedRuleActions,
|
||||
}
|
||||
);
|
||||
if (isAttributeModified) {
|
||||
updatedRuleActions = modifiedAttributes.actions;
|
||||
isAttributesUpdateSkipped = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'snoozeSchedule': {
|
||||
// Silently skip adding snooze or snooze schedules on security
|
||||
// rules until we implement snoozing of their rules
|
||||
if (updatedRule.consumer === AlertConsumers.SIEM) {
|
||||
// While the rule is technically not updated, we are still marking
|
||||
// the rule as updated in case of snoozing, until support
|
||||
// for snoozing is added.
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
if (operation.operation === 'set') {
|
||||
const snoozeAttributes = getBulkSnooze<Params>(
|
||||
updatedRule,
|
||||
operation.value as RuleSnoozeSchedule
|
||||
);
|
||||
try {
|
||||
verifySnoozeScheduleLimit(snoozeAttributes.snoozeSchedule);
|
||||
} catch (error) {
|
||||
throw Error(`Error updating rule: could not add snooze - ${error.message}`);
|
||||
}
|
||||
updatedRule = {
|
||||
...updatedRule,
|
||||
muteAll: snoozeAttributes.muteAll,
|
||||
snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'],
|
||||
};
|
||||
}
|
||||
if (operation.operation === 'delete') {
|
||||
const idsToDelete = operation.value && [...operation.value];
|
||||
if (idsToDelete?.length === 0) {
|
||||
updatedRule.snoozeSchedule?.forEach((schedule) => {
|
||||
if (schedule.id) {
|
||||
idsToDelete.push(schedule.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
const snoozeAttributes = getBulkUnsnooze(updatedRule, idsToDelete);
|
||||
updatedRule = {
|
||||
...updatedRule,
|
||||
muteAll: snoozeAttributes.muteAll,
|
||||
snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'],
|
||||
};
|
||||
}
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
case 'apiKey': {
|
||||
hasUpdateApiKeyOperation = true;
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (operation.field === 'schedule') {
|
||||
validateScheduleOperation(operation.value, updatedRule.actions, rule.id);
|
||||
}
|
||||
const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation(
|
||||
operation,
|
||||
updatedRule
|
||||
);
|
||||
|
||||
if (isAttributeModified) {
|
||||
updatedRule = {
|
||||
...updatedRule,
|
||||
...modifiedAttributes,
|
||||
};
|
||||
isAttributesUpdateSkipped = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only increment revision if update wasn't skipped and `operation.field` should result in a revision increment
|
||||
if (
|
||||
!isAttributesUpdateSkipped &&
|
||||
!bulkEditFieldsToExcludeFromRevisionUpdates.has(operation.field) &&
|
||||
rule.revision - updatedRule.revision === 0
|
||||
) {
|
||||
updatedRule.revision += 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
rule: updatedRule,
|
||||
ruleActions: updatedRuleActions,
|
||||
hasUpdateApiKeyOperation,
|
||||
isAttributesUpdateSkipped,
|
||||
};
|
||||
}
|
||||
|
||||
function validateScheduleInterval(
|
||||
context: RulesClientContext,
|
||||
scheduleInterval: string,
|
||||
ruleTypeId: string,
|
||||
ruleId: string
|
||||
): void {
|
||||
if (!scheduleInterval) {
|
||||
return;
|
||||
}
|
||||
const isIntervalInvalid = parseDuration(scheduleInterval) < 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 (${scheduleInterval}) for "${ruleTypeId}" rule type with ID "${ruleId}" 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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that updated schedule interval is not longer than any of the existing action frequencies
|
||||
* @param schedule Schedule interval that user tries to set
|
||||
* @param actions Rule actions
|
||||
*/
|
||||
function validateScheduleOperation(
|
||||
schedule: RuleDomain['schedule'],
|
||||
actions: RuleDomain['actions'],
|
||||
ruleId: string
|
||||
): void {
|
||||
const scheduleInterval = parseDuration(schedule.interval);
|
||||
const actionsWithInvalidThrottles = [];
|
||||
|
||||
for (const action of actions) {
|
||||
// check for actions throttled shorter than the rule schedule
|
||||
if (
|
||||
action.frequency?.notifyWhen === ruleNotifyWhen.THROTTLE &&
|
||||
parseDuration(action.frequency.throttle!) < scheduleInterval
|
||||
) {
|
||||
actionsWithInvalidThrottles.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
if (actionsWithInvalidThrottles.length > 0) {
|
||||
throw Error(
|
||||
`Error updating rule with ID "${ruleId}": the interval ${schedule.interval} is longer than the action frequencies`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareApiKeys(
|
||||
context: RulesClientContext,
|
||||
rule: SavedObjectsFindResult<RuleAttributes>,
|
||||
ruleType: RuleType,
|
||||
apiKeysMap: ApiKeysMap,
|
||||
attributes: RuleAttributes,
|
||||
hasUpdateApiKeyOperation: boolean,
|
||||
username: string | null
|
||||
): Promise<{ apiKeyAttributes: ApiKeyAttributes }> {
|
||||
const apiKeyAttributes = await createNewAPIKeySet(context, {
|
||||
id: ruleType.id,
|
||||
ruleName: attributes.name,
|
||||
username,
|
||||
shouldUpdateApiKey: attributes.enabled || hasUpdateApiKeyOperation,
|
||||
errorMessage: 'Error updating rule: could not create API key',
|
||||
});
|
||||
|
||||
// collect generated API keys
|
||||
if (apiKeyAttributes.apiKey) {
|
||||
apiKeysMap.set(rule.id, {
|
||||
...apiKeysMap.get(rule.id),
|
||||
newApiKey: apiKeyAttributes.apiKey,
|
||||
newApiKeyCreatedByUser: apiKeyAttributes.apiKeyCreatedByUser,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
apiKeyAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
function updateAttributes(
|
||||
context: RulesClientContext,
|
||||
attributes: RuleAttributes,
|
||||
apiKeyAttributes: ApiKeyAttributes,
|
||||
updatedParams: RuleParams,
|
||||
rawAlertActions: RuleActionAttributes[],
|
||||
username: string | null
|
||||
): {
|
||||
updatedAttributes: RuleAttributes;
|
||||
} {
|
||||
// get notifyWhen
|
||||
const notifyWhen = getRuleNotifyWhenType(
|
||||
attributes.notifyWhen ?? null,
|
||||
attributes.throttle ?? null
|
||||
);
|
||||
|
||||
// TODO (http-versioning) Remove casts when updateMeta has been converted
|
||||
const castedAttributes = attributes as RawRule;
|
||||
const updatedAttributes = updateMeta(context, {
|
||||
...castedAttributes,
|
||||
...apiKeyAttributes,
|
||||
params: updatedParams as RawRule['params'],
|
||||
actions: rawAlertActions as RawRule['actions'],
|
||||
notifyWhen,
|
||||
updatedBy: username,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}) as RuleAttributes;
|
||||
|
||||
// add mapped_params
|
||||
const mappedParams = getMappedParams(updatedParams);
|
||||
|
||||
if (Object.keys(mappedParams).length) {
|
||||
updatedAttributes.mapped_params = mappedParams;
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
async function saveBulkUpdatedRules(
|
||||
context: RulesClientContext,
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>,
|
||||
apiKeysMap: ApiKeysMap
|
||||
) {
|
||||
const apiKeysToInvalidate: string[] = [];
|
||||
let result;
|
||||
try {
|
||||
// TODO (http-versioning): for whatever reasoning we are using SavedObjectsBulkUpdateObject
|
||||
// everywhere when it should be SavedObjectsBulkCreateObject. We need to fix it in
|
||||
// bulk_disable, bulk_enable, etc. to fix this cast
|
||||
result = await bulkCreateRulesSo({
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
bulkCreateRuleAttributes: rules as Array<SavedObjectsBulkCreateObject<RuleAttributes>>,
|
||||
savedObjectsBulkCreateOptions: { overwrite: true },
|
||||
});
|
||||
} catch (e) {
|
||||
// avoid unused newly generated API keys
|
||||
if (apiKeysMap.size > 0) {
|
||||
await bulkMarkApiKeysForInvalidation(
|
||||
{
|
||||
apiKeys: Array.from(apiKeysMap.values())
|
||||
.filter((value) => value.newApiKey && !value.newApiKeyCreatedByUser)
|
||||
.map((value) => value.newApiKey as string),
|
||||
},
|
||||
context.logger,
|
||||
context.unsecuredSavedObjectsClient
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
result.saved_objects.map(({ id, error }) => {
|
||||
const oldApiKey = apiKeysMap.get(id)?.oldApiKey;
|
||||
const oldApiKeyCreatedByUser = apiKeysMap.get(id)?.oldApiKeyCreatedByUser;
|
||||
const newApiKey = apiKeysMap.get(id)?.newApiKey;
|
||||
const newApiKeyCreatedByUser = apiKeysMap.get(id)?.newApiKeyCreatedByUser;
|
||||
|
||||
// if SO wasn't saved and has new API key it will be invalidated
|
||||
if (error && newApiKey && !newApiKeyCreatedByUser) {
|
||||
apiKeysToInvalidate.push(newApiKey);
|
||||
// if SO saved and has old Api Key it will be invalidate
|
||||
} else if (!error && oldApiKey && !oldApiKeyCreatedByUser) {
|
||||
apiKeysToInvalidate.push(oldApiKey);
|
||||
}
|
||||
});
|
||||
|
||||
return { result, apiKeysToInvalidate };
|
||||
}
|
||||
|
||||
async function attemptToMigrateLegacyFrequency<Params extends RuleParams>(
|
||||
context: RulesClientContext,
|
||||
operation: BulkEditOperation,
|
||||
rule: RuleDomain<Params>,
|
||||
ruleType: RuleType
|
||||
) {
|
||||
if (operation.field !== 'actions')
|
||||
throw new Error('Can only perform frequency migration on an action operation');
|
||||
// Try to remove the rule-level frequency params, and then validate actions
|
||||
if (typeof rule.notifyWhen !== 'undefined') rule.notifyWhen = undefined;
|
||||
if (rule.throttle) rule.throttle = undefined;
|
||||
await validateActions(context, ruleType, {
|
||||
...rule,
|
||||
actions: operation.value,
|
||||
});
|
||||
return rule;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 type { BulkEditRuleSnoozeSchedule, BulkEditOperation } from './types';
|
||||
export { bulkEditRules } from './bulk_edit_rules';
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { rRuleRequestSchema } from '../../../../r_rule/schemas';
|
||||
import { notifyWhenSchema } from '../../../schemas';
|
||||
import { validateDuration } from '../../../validation';
|
||||
import { validateSnoozeSchedule } from '../validation';
|
||||
|
||||
export const scheduleIdsSchema = schema.maybe(schema.arrayOf(schema.string()));
|
||||
|
||||
export const bulkEditRuleSnoozeScheduleSchema = schema.object({
|
||||
id: schema.maybe(schema.string()),
|
||||
duration: schema.number(),
|
||||
rRule: rRuleRequestSchema,
|
||||
});
|
||||
const bulkEditRuleSnoozeScheduleSchemaWithValidation = schema.object(
|
||||
{
|
||||
id: schema.maybe(schema.string()),
|
||||
duration: schema.number(),
|
||||
rRule: rRuleRequestSchema,
|
||||
},
|
||||
{ validate: validateSnoozeSchedule }
|
||||
);
|
||||
|
||||
const bulkEditActionSchema = schema.object({
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
frequency: schema.maybe(
|
||||
schema.object({
|
||||
summary: schema.boolean(),
|
||||
throttle: schema.nullable(schema.string()),
|
||||
notifyWhen: notifyWhenSchema,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const bulkEditTagSchema = schema.object({
|
||||
operation: schema.oneOf([schema.literal('add'), schema.literal('delete'), schema.literal('set')]),
|
||||
field: schema.literal('tags'),
|
||||
value: schema.arrayOf(schema.string()),
|
||||
});
|
||||
|
||||
const bulkEditActionsSchema = schema.object({
|
||||
operation: schema.oneOf([schema.literal('add'), schema.literal('set')]),
|
||||
field: schema.literal('actions'),
|
||||
value: schema.arrayOf(bulkEditActionSchema),
|
||||
});
|
||||
|
||||
const bulkEditScheduleSchema = schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('schedule'),
|
||||
value: schema.object({ interval: schema.string({ validate: validateDuration }) }),
|
||||
});
|
||||
|
||||
const bulkEditThrottleSchema = schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('throttle'),
|
||||
value: schema.nullable(schema.string()),
|
||||
});
|
||||
|
||||
const bulkEditNotifyWhenSchema = schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('notifyWhen'),
|
||||
value: notifyWhenSchema,
|
||||
});
|
||||
|
||||
const bulkEditSnoozeSchema = schema.object({
|
||||
operation: schema.oneOf([schema.literal('set')]),
|
||||
field: schema.literal('snoozeSchedule'),
|
||||
value: bulkEditRuleSnoozeScheduleSchemaWithValidation,
|
||||
});
|
||||
|
||||
const bulkEditUnsnoozeSchema = schema.object({
|
||||
operation: schema.oneOf([schema.literal('delete')]),
|
||||
field: schema.literal('snoozeSchedule'),
|
||||
value: schema.maybe(scheduleIdsSchema),
|
||||
});
|
||||
|
||||
const bulkEditApiKeySchema = schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('apiKey'),
|
||||
});
|
||||
|
||||
export const bulkEditOperationSchema = schema.oneOf([
|
||||
bulkEditTagSchema,
|
||||
bulkEditActionsSchema,
|
||||
bulkEditScheduleSchema,
|
||||
bulkEditThrottleSchema,
|
||||
bulkEditNotifyWhenSchema,
|
||||
bulkEditSnoozeSchema,
|
||||
bulkEditUnsnoozeSchema,
|
||||
bulkEditApiKeySchema,
|
||||
]);
|
||||
|
||||
export const bulkEditOperationsSchema = schema.arrayOf(bulkEditOperationSchema, { minSize: 1 });
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export {
|
||||
bulkEditRuleSnoozeScheduleSchema,
|
||||
bulkEditOperationsSchema,
|
||||
bulkEditOperationSchema,
|
||||
} from './bulk_edit_rules_option_schemas';
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { TypeOf } from '@kbn/config-schema';
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import {
|
||||
bulkEditRuleSnoozeScheduleSchema,
|
||||
bulkEditOperationsSchema,
|
||||
bulkEditOperationSchema,
|
||||
} from '../schemas';
|
||||
import { RuleParams, RuleDomain, Rule } from '../../../types';
|
||||
|
||||
export type BulkEditRuleSnoozeSchedule = TypeOf<typeof bulkEditRuleSnoozeScheduleSchema>;
|
||||
export type BulkEditOperation = TypeOf<typeof bulkEditOperationSchema>;
|
||||
export type BulkEditOperations = TypeOf<typeof bulkEditOperationsSchema>;
|
||||
|
||||
export type ParamsModifier<Params extends RuleParams> = (
|
||||
params: Params
|
||||
) => Promise<ParamsModifierResult<Params>>;
|
||||
|
||||
interface ParamsModifierResult<Params extends RuleParams> {
|
||||
modifiedParams: Params;
|
||||
isParamsUpdateSkipped: boolean;
|
||||
}
|
||||
|
||||
export type ShouldIncrementRevision<Params extends RuleParams> = (params?: Params) => boolean;
|
||||
|
||||
export type BulkEditFields = keyof Pick<
|
||||
RuleDomain,
|
||||
'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey'
|
||||
>;
|
||||
|
||||
export interface BulkEditOptionsCommon<Params extends RuleParams> {
|
||||
operations: BulkEditOperation[];
|
||||
paramsModifier?: ParamsModifier<Params>;
|
||||
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
|
||||
}
|
||||
|
||||
export type BulkEditOptionsFilter<Params extends RuleParams> = BulkEditOptionsCommon<Params> & {
|
||||
filter?: string | KueryNode;
|
||||
};
|
||||
|
||||
export type BulkEditOptionsIds<Params extends RuleParams> = BulkEditOptionsCommon<Params> & {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
export type BulkEditSkipReason = 'RULE_NOT_MODIFIED';
|
||||
|
||||
export interface BulkActionSkipResult {
|
||||
id: Rule['id'];
|
||||
name?: Rule['name'];
|
||||
skip_reason: BulkEditSkipReason;
|
||||
}
|
||||
|
||||
export interface BulkOperationError {
|
||||
message: string;
|
||||
status?: number;
|
||||
rule: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export type {
|
||||
BulkEditRuleSnoozeSchedule,
|
||||
BulkEditOperation,
|
||||
BulkEditOperations,
|
||||
BulkEditFields,
|
||||
BulkEditOptionsFilter,
|
||||
BulkEditOptionsIds,
|
||||
BulkActionSkipResult,
|
||||
BulkEditOptionsCommon,
|
||||
BulkOperationError,
|
||||
ParamsModifier,
|
||||
ShouldIncrementRevision,
|
||||
} from './bulk_edit_rules_options';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { validateSnoozeSchedule } from './validate_snooze_schedule';
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { Frequency } from '@kbn/rrule';
|
||||
import moment from 'moment';
|
||||
import { BulkEditRuleSnoozeSchedule } from '../types';
|
||||
|
||||
export const validateSnoozeSchedule = (schedule: BulkEditRuleSnoozeSchedule) => {
|
||||
const intervalIsDaily = schedule.rRule.freq === Frequency.DAILY;
|
||||
const durationInDays = moment.duration(schedule.duration, 'milliseconds').asDays();
|
||||
if (intervalIsDaily && schedule.rRule.interval && durationInDays >= schedule.rRule.interval) {
|
||||
return 'Recurrence interval must be longer than the snooze duration';
|
||||
}
|
||||
};
|
|
@ -7,24 +7,24 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { CreateRuleParams } from './create_rule';
|
||||
import { RulesClient, ConstructorOptions } from '../../../rules_client';
|
||||
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';
|
||||
import { alertingAuthorizationMock } from '../../../authorization/alerting_authorization.mock';
|
||||
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
|
||||
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { AlertingAuthorization } from '../../../authorization/alerting_authorization';
|
||||
import { AlertingAuthorization } from '../../../../authorization/alerting_authorization';
|
||||
import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { ruleNotifyWhen } from '../constants';
|
||||
import { ruleNotifyWhen } from '../../constants';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from '../../../rules_client/tests/lib';
|
||||
import { RecoveredActionGroup } from '../../../../common';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { getRuleExecutionStatusPending, getDefaultMonitoring } from '../../../lib';
|
||||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
import { RecoveredActionGroup } from '../../../../../common';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { getRuleExecutionStatusPending, getDefaultMonitoring } from '../../../../lib';
|
||||
|
||||
jest.mock('../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
|
@ -8,30 +8,34 @@ import Semver from 'semver';
|
|||
import Boom from '@hapi/boom';
|
||||
import { SavedObject, SavedObjectsUtils } from '@kbn/core/server';
|
||||
import { withSpan } from '@kbn/apm-utils';
|
||||
import { parseDuration } from '../../../../common/parse_duration';
|
||||
import { WriteOperations, AlertingAuthorizationEntity } from '../../../authorization';
|
||||
import { validateRuleTypeParams, getRuleNotifyWhenType, getDefaultMonitoring } from '../../../lib';
|
||||
import { getRuleExecutionStatusPending } from '../../../lib/rule_execution_status';
|
||||
import { parseDuration } from '../../../../../common/parse_duration';
|
||||
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
|
||||
import {
|
||||
validateRuleTypeParams,
|
||||
getRuleNotifyWhenType,
|
||||
getDefaultMonitoringRuleDomainProperties,
|
||||
} from '../../../../lib';
|
||||
import { getRuleExecutionStatusPending } from '../../../../lib/rule_execution_status';
|
||||
import {
|
||||
extractReferences,
|
||||
validateActions,
|
||||
addGeneratedActionValues,
|
||||
} from '../../../rules_client/lib';
|
||||
import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../../../rules_client/common';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../rules_client/common/audit_events';
|
||||
import { RulesClientContext } from '../../../rules_client/types';
|
||||
import { Rule, RuleDomain, RuleParams } from '../types';
|
||||
import { SanitizedRule } from '../../../types';
|
||||
} from '../../../../rules_client/lib';
|
||||
import { generateAPIKeyName, apiKeyAsRuleDomainProperties } from '../../../../rules_client/common';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
import { RulesClientContext } from '../../../../rules_client/types';
|
||||
import { RuleDomain, RuleParams } from '../../types';
|
||||
import { SanitizedRule } from '../../../../types';
|
||||
import {
|
||||
transformRuleAttributesToRuleDomain,
|
||||
transformRuleDomainToRuleAttributes,
|
||||
transformRuleDomainToRule,
|
||||
} from '../transforms';
|
||||
import { ruleDomainSchema } from '../schemas';
|
||||
import { RuleAttributes } from '../../../data/rule/types';
|
||||
} from '../../transforms';
|
||||
import { ruleDomainSchema } from '../../schemas';
|
||||
import { RuleAttributes } from '../../../../data/rule/types';
|
||||
import type { CreateRuleData } from './types';
|
||||
import { createRuleDataSchema } from './schemas';
|
||||
import { createRuleSavedObject } from '../../../rules_client/lib';
|
||||
import { createRuleSavedObject } from '../../../../rules_client/lib';
|
||||
|
||||
export interface CreateRuleOptions {
|
||||
id?: string;
|
||||
|
@ -142,7 +146,9 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
const ruleAttributes = transformRuleDomainToRuleAttributes(
|
||||
{
|
||||
...data,
|
||||
...apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey),
|
||||
// TODO (http-versioning) create a rule domain version of this function
|
||||
// Right now this works because the 2 types can interop but it's not ideal
|
||||
...apiKeyAsRuleDomainProperties(createdAPIKey, username, isAuthTypeApiKey),
|
||||
id,
|
||||
createdBy: username,
|
||||
updatedBy: username,
|
||||
|
@ -154,13 +160,13 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
notifyWhen,
|
||||
throttle,
|
||||
executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
|
||||
monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()) as Rule['monitoring'],
|
||||
monitoring: getDefaultMonitoringRuleDomainProperties(lastRunTimestamp.toISOString()),
|
||||
revision: 0,
|
||||
running: false,
|
||||
},
|
||||
{
|
||||
legacyId,
|
||||
actionsWithRefs: actions as RuleAttributes['actions'],
|
||||
actionsWithRefs: actions,
|
||||
paramsWithRefs: updatedParams,
|
||||
}
|
||||
);
|
||||
|
@ -193,7 +199,7 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
try {
|
||||
ruleDomainSchema.validate(ruleDomain);
|
||||
} catch (e) {
|
||||
context.logger.warn(`Error validating rule domain object for id: ${id}, ${e}`);
|
||||
context.logger.warn(`Error validating created rule domain object for id: ${id}, ${e}`);
|
||||
}
|
||||
|
||||
// Convert domain rule to rule (Remove certain properties)
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { validateDuration } from '../../../../../common/routes/rule/validation';
|
||||
import { notifyWhenSchema, actionAlertsFilterSchema } from '../../schemas';
|
||||
import { validateDuration } from '../../../validation';
|
||||
import { notifyWhenSchema, actionAlertsFilterSchema } from '../../../schemas';
|
||||
|
||||
export const createRuleDataSchema = schema.object({
|
||||
name: schema.string(),
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { createRuleDataSchema } from '../schemas';
|
||||
import { RuleParams } from '../../types';
|
||||
import { RuleParams } from '../../../types';
|
||||
|
||||
type CreateRuleDataType = TypeOf<typeof createRuleDataSchema>;
|
||||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
export {
|
||||
ruleParamsSchema,
|
||||
rRuleSchema,
|
||||
snoozeScheduleSchema,
|
||||
ruleExecutionStatusSchema,
|
||||
ruleLastRunSchema,
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ruleExecutionStatusErrorReason,
|
||||
ruleExecutionStatusWarningReason,
|
||||
} from '../constants';
|
||||
import { rRuleSchema } from '../../r_rule/schemas';
|
||||
import { dateSchema } from './date_schema';
|
||||
import { notifyWhenSchema } from './notify_when_schema';
|
||||
import { actionDomainSchema, actionSchema } from './action_schemas';
|
||||
|
@ -122,45 +123,6 @@ export const monitoringSchema = schema.object({
|
|||
}),
|
||||
});
|
||||
|
||||
export const rRuleSchema = schema.object({
|
||||
dtstart: schema.string(),
|
||||
tzid: schema.string(),
|
||||
freq: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal(0),
|
||||
schema.literal(1),
|
||||
schema.literal(2),
|
||||
schema.literal(3),
|
||||
schema.literal(4),
|
||||
schema.literal(5),
|
||||
schema.literal(6),
|
||||
])
|
||||
),
|
||||
until: schema.maybe(schema.string()),
|
||||
count: schema.maybe(schema.number()),
|
||||
interval: schema.maybe(schema.number()),
|
||||
wkst: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal('MO'),
|
||||
schema.literal('TU'),
|
||||
schema.literal('WE'),
|
||||
schema.literal('TH'),
|
||||
schema.literal('FR'),
|
||||
schema.literal('SA'),
|
||||
schema.literal('SU'),
|
||||
])
|
||||
),
|
||||
byweekday: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))),
|
||||
bymonth: schema.maybe(schema.arrayOf(schema.number())),
|
||||
bysetpos: schema.maybe(schema.arrayOf(schema.number())),
|
||||
bymonthday: schema.arrayOf(schema.number()),
|
||||
byyearday: schema.arrayOf(schema.number()),
|
||||
byweekno: schema.arrayOf(schema.number()),
|
||||
byhour: schema.arrayOf(schema.number()),
|
||||
byminute: schema.arrayOf(schema.number()),
|
||||
bysecond: schema.arrayOf(schema.number()),
|
||||
});
|
||||
|
||||
export const snoozeScheduleSchema = schema.object({
|
||||
duration: schema.number(),
|
||||
rRule: rRuleSchema,
|
||||
|
|
|
@ -5,4 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export type { Rule, RuleDomain, RuleLastRun, Monitoring, RuleParams, RuleNotifyWhen } from './rule';
|
||||
export type {
|
||||
Rule,
|
||||
RuleDomain,
|
||||
RuleLastRun,
|
||||
Monitoring,
|
||||
RuleParams,
|
||||
RuleNotifyWhen,
|
||||
RuleSnoozeSchedule,
|
||||
} from './rule';
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from '../constants';
|
||||
import {
|
||||
ruleParamsSchema,
|
||||
rRuleSchema,
|
||||
snoozeScheduleSchema,
|
||||
ruleExecutionStatusSchema,
|
||||
ruleLastRunSchema,
|
||||
|
@ -36,8 +35,7 @@ export type RuleExecutionStatusWarningReason =
|
|||
typeof ruleExecutionStatusWarningReason[keyof typeof ruleExecutionStatusWarningReason];
|
||||
|
||||
export type RuleParams = TypeOf<typeof ruleParamsSchema>;
|
||||
export type RRule = TypeOf<typeof rRuleSchema>;
|
||||
export type SnoozeSchedule = TypeOf<typeof snoozeScheduleSchema>;
|
||||
export type RuleSnoozeSchedule = TypeOf<typeof snoozeScheduleSchema>;
|
||||
export type RuleLastRun = TypeOf<typeof ruleLastRunSchema>;
|
||||
export type Monitoring = TypeOf<typeof monitoringSchema>;
|
||||
export type Action = TypeOf<typeof actionSchema>;
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { validateDuration } from './validate_duration';
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const SECONDS_REGEX = /^[1-9][0-9]*s$/;
|
||||
const MINUTES_REGEX = /^[1-9][0-9]*m$/;
|
||||
const HOURS_REGEX = /^[1-9][0-9]*h$/;
|
||||
const DAYS_REGEX = /^[1-9][0-9]*d$/;
|
||||
|
||||
export function validateDuration(duration: string) {
|
||||
if (duration.match(SECONDS_REGEX)) {
|
||||
return;
|
||||
}
|
||||
if (duration.match(MINUTES_REGEX)) {
|
||||
return;
|
||||
}
|
||||
if (duration.match(HOURS_REGEX)) {
|
||||
return;
|
||||
}
|
||||
if (duration.match(DAYS_REGEX)) {
|
||||
return;
|
||||
}
|
||||
return 'string is not a valid duration: ' + duration;
|
||||
}
|
|
@ -5,9 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { createRuleSo } from './create_rule_so';
|
||||
export type { CreateRuleSoParams } from './create_rule_so';
|
||||
export { updateRuleSo } from './update_rule_so';
|
||||
export type { UpdateRuleSoParams } from './update_rule_so';
|
||||
export { deleteRuleSo } from './delete_rule_so';
|
||||
export type { DeleteRuleSoParams } from './delete_rule_so';
|
||||
export { createRuleSo } from './methods/create_rule_so';
|
||||
export type { CreateRuleSoParams } from './methods/create_rule_so';
|
||||
export { updateRuleSo } from './methods/update_rule_so';
|
||||
export type { UpdateRuleSoParams } from './methods/update_rule_so';
|
||||
export { deleteRuleSo } from './methods/delete_rule_so';
|
||||
export type { DeleteRuleSoParams } from './methods/delete_rule_so';
|
||||
export { findRulesSo } from './methods/find_rules_so';
|
||||
export type { FindRulesSoParams } from './methods/find_rules_so';
|
||||
export { bulkCreateRulesSo } from './methods/bulk_create_rule_so';
|
||||
export type { BulkCreateRulesSoParams } from './methods/bulk_create_rule_so';
|
||||
|
|
|
@ -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 {
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { RuleAttributes } from '../types';
|
||||
|
||||
export interface BulkCreateRulesSoParams {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
bulkCreateRuleAttributes: Array<SavedObjectsBulkCreateObject<RuleAttributes>>;
|
||||
savedObjectsBulkCreateOptions?: SavedObjectsCreateOptions;
|
||||
}
|
||||
|
||||
export const bulkCreateRulesSo = (
|
||||
params: BulkCreateRulesSoParams
|
||||
): Promise<SavedObjectsBulkResponse<RuleAttributes>> => {
|
||||
const { savedObjectsClient, bulkCreateRuleAttributes, savedObjectsBulkCreateOptions } = params;
|
||||
|
||||
return savedObjectsClient.bulkCreate<RuleAttributes>(
|
||||
bulkCreateRuleAttributes,
|
||||
savedObjectsBulkCreateOptions
|
||||
);
|
||||
};
|
|
@ -10,16 +10,16 @@ import {
|
|||
SavedObjectsCreateOptions,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { RuleAttributes } from './types';
|
||||
import { RuleAttributes } from '../types';
|
||||
|
||||
export interface CreateRuleSoParams {
|
||||
savedObjectClient: SavedObjectsClientContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
ruleAttributes: RuleAttributes;
|
||||
savedObjectCreateOptions?: SavedObjectsCreateOptions;
|
||||
savedObjectsCreateOptions?: SavedObjectsCreateOptions;
|
||||
}
|
||||
|
||||
export const createRuleSo = (params: CreateRuleSoParams): Promise<SavedObject<RuleAttributes>> => {
|
||||
const { savedObjectClient, ruleAttributes, savedObjectCreateOptions } = params;
|
||||
const { savedObjectsClient, ruleAttributes, savedObjectsCreateOptions } = params;
|
||||
|
||||
return savedObjectClient.create('alert', ruleAttributes, savedObjectCreateOptions);
|
||||
return savedObjectsClient.create('alert', ruleAttributes, savedObjectsCreateOptions);
|
||||
};
|
|
@ -8,13 +8,13 @@
|
|||
import { SavedObjectsClientContract, SavedObjectsDeleteOptions } from '@kbn/core/server';
|
||||
|
||||
export interface DeleteRuleSoParams {
|
||||
savedObjectClient: SavedObjectsClientContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
id: string;
|
||||
savedObjectDeleteOptions?: SavedObjectsDeleteOptions;
|
||||
savedObjectsDeleteOptions?: SavedObjectsDeleteOptions;
|
||||
}
|
||||
|
||||
export const deleteRuleSo = (params: DeleteRuleSoParams): Promise<{}> => {
|
||||
const { savedObjectClient, id, savedObjectDeleteOptions } = params;
|
||||
const { savedObjectsClient, id, savedObjectsDeleteOptions } = params;
|
||||
|
||||
return savedObjectClient.delete('alert', id, savedObjectDeleteOptions);
|
||||
return savedObjectsClient.delete('alert', id, savedObjectsDeleteOptions);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 {
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { RuleAttributes } from '../types';
|
||||
|
||||
export interface FindRulesSoParams {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
savedObjectsFindOptions: Omit<SavedObjectsFindOptions, 'type'>;
|
||||
}
|
||||
|
||||
export const findRulesSo = <RuleAggregation = Record<string, unknown>>(
|
||||
params: FindRulesSoParams
|
||||
): Promise<SavedObjectsFindResponse<RuleAttributes, RuleAggregation>> => {
|
||||
const { savedObjectsClient, savedObjectsFindOptions } = params;
|
||||
|
||||
return savedObjectsClient.find<RuleAttributes, RuleAggregation>({
|
||||
...savedObjectsFindOptions,
|
||||
type: 'alert',
|
||||
});
|
||||
};
|
|
@ -10,24 +10,24 @@ import {
|
|||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { RuleAttributes } from './types';
|
||||
import { RuleAttributes } from '../types';
|
||||
|
||||
export interface UpdateRuleSoParams {
|
||||
savedObjectClient: SavedObjectsClientContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
id: string;
|
||||
updateRuleAttributes: Partial<RuleAttributes>;
|
||||
savedObjectUpdateOptions?: SavedObjectsUpdateOptions<RuleAttributes>;
|
||||
savedObjectsUpdateOptions?: SavedObjectsUpdateOptions<RuleAttributes>;
|
||||
}
|
||||
|
||||
export const updateRuleSo = (
|
||||
params: UpdateRuleSoParams
|
||||
): Promise<SavedObjectsUpdateResponse<RuleAttributes>> => {
|
||||
const { savedObjectClient, id, updateRuleAttributes, savedObjectUpdateOptions } = params;
|
||||
const { savedObjectsClient, id, updateRuleAttributes, savedObjectsUpdateOptions } = params;
|
||||
|
||||
return savedObjectClient.update<RuleAttributes>(
|
||||
return savedObjectsClient.update<RuleAttributes>(
|
||||
'alert',
|
||||
id,
|
||||
updateRuleAttributes,
|
||||
savedObjectUpdateOptions
|
||||
savedObjectsUpdateOptions
|
||||
);
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
export type {
|
||||
RuleNotifyWhenAttributes,
|
||||
RuleLastRunOutcomeValuesAttributes,
|
||||
RuleActionAttributes,
|
||||
RuleExecutionStatusValuesAttributes,
|
||||
RuleExecutionStatusErrorReasonAttributes,
|
||||
RuleExecutionStatusWarningReasonAttributes,
|
||||
|
|
|
@ -144,7 +144,7 @@ interface AlertsFilterAttributes {
|
|||
timeframe?: AlertsFilterTimeFrameAttributes;
|
||||
}
|
||||
|
||||
interface RuleActionAttributes {
|
||||
export interface RuleActionAttributes {
|
||||
uuid: string;
|
||||
group: string;
|
||||
actionRef: string;
|
||||
|
|
|
@ -36,14 +36,7 @@ export type {
|
|||
export { RuleNotifyWhen } from '../common';
|
||||
export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config';
|
||||
export type { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
export type {
|
||||
FindResult,
|
||||
BulkEditOperation,
|
||||
BulkOperationError,
|
||||
BulkEditOptions,
|
||||
BulkEditOptionsFilter,
|
||||
BulkEditOptionsIds,
|
||||
} from './rules_client';
|
||||
export type { FindResult, BulkEditOperation, BulkOperationError } from './rules_client';
|
||||
export type { Rule } from './application/rule/types';
|
||||
export type { PublicAlert as Alert } from './alert';
|
||||
export { parseDuration, isRuleSnoozed } from './lib';
|
||||
|
|
|
@ -31,6 +31,7 @@ export { lastRunFromState, lastRunFromError, lastRunToRaw } from './last_run_sta
|
|||
export {
|
||||
resetMonitoringLastRun,
|
||||
getDefaultMonitoring,
|
||||
getDefaultMonitoringRuleDomainProperties,
|
||||
convertMonitoringFromRawAndVerify,
|
||||
} from './monitoring';
|
||||
export { getNextRun } from './next_run';
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
RuleMonitoringHistory,
|
||||
RuleMonitoringLastRunMetrics,
|
||||
} from '../types';
|
||||
import { RuleDomain } from '../application/rule/types';
|
||||
|
||||
const INITIAL_LAST_RUN_METRICS: RuleMonitoringLastRunMetrics = {
|
||||
duration: 0,
|
||||
|
@ -37,6 +38,23 @@ export const getDefaultMonitoring = (timestamp: string): RawRuleMonitoring => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getDefaultMonitoringRuleDomainProperties = (
|
||||
timestamp: string
|
||||
): RuleDomain['monitoring'] => {
|
||||
return {
|
||||
run: {
|
||||
history: [],
|
||||
calculated_metrics: {
|
||||
success_ratio: 0,
|
||||
},
|
||||
last_run: {
|
||||
timestamp,
|
||||
metrics: INITIAL_LAST_RUN_METRICS,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const resetMonitoringLastRun = (monitoring: RuleMonitoring): RawRuleMonitoring => {
|
||||
const { run, ...restMonitoring } = monitoring;
|
||||
const { last_run: lastRun, ...restRun } = run;
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
|
||||
import { ILicenseState, RuleTypeDisabledError, validateDurationSchema } from '../lib';
|
||||
import { verifyAccessAndContext, rewriteRule, handleDisabledApiKeysError } from './lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types';
|
||||
import { snoozeScheduleSchema } from './snooze_rule';
|
||||
import { scheduleIdsSchema } from './unsnooze_rule';
|
||||
|
||||
const ruleActionSchema = schema.object({
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
frequency: schema.maybe(
|
||||
schema.object({
|
||||
summary: schema.boolean(),
|
||||
throttle: schema.nullable(schema.string()),
|
||||
notifyWhen: schema.oneOf([
|
||||
schema.literal('onActionGroupChange'),
|
||||
schema.literal('onActiveAlert'),
|
||||
schema.literal('onThrottleInterval'),
|
||||
]),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const operationsSchema = schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.object({
|
||||
operation: schema.oneOf([
|
||||
schema.literal('add'),
|
||||
schema.literal('delete'),
|
||||
schema.literal('set'),
|
||||
]),
|
||||
field: schema.literal('tags'),
|
||||
value: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.oneOf([schema.literal('add'), schema.literal('set')]),
|
||||
field: schema.literal('actions'),
|
||||
value: schema.arrayOf(ruleActionSchema),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('schedule'),
|
||||
value: schema.object({ interval: schema.string({ validate: validateDurationSchema }) }),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('throttle'),
|
||||
value: schema.nullable(schema.string()),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('notifyWhen'),
|
||||
value: schema.nullable(
|
||||
schema.oneOf([
|
||||
schema.literal('onActionGroupChange'),
|
||||
schema.literal('onActiveAlert'),
|
||||
schema.literal('onThrottleInterval'),
|
||||
])
|
||||
),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.oneOf([schema.literal('set')]),
|
||||
field: schema.literal('snoozeSchedule'),
|
||||
value: snoozeScheduleSchema,
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.oneOf([schema.literal('delete')]),
|
||||
field: schema.literal('snoozeSchedule'),
|
||||
value: schema.maybe(scheduleIdsSchema),
|
||||
}),
|
||||
schema.object({
|
||||
operation: schema.literal('set'),
|
||||
field: schema.literal('apiKey'),
|
||||
}),
|
||||
]),
|
||||
{ minSize: 1 }
|
||||
);
|
||||
|
||||
const bodySchema = schema.object({
|
||||
filter: schema.maybe(schema.string()),
|
||||
ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
operations: operationsSchema,
|
||||
});
|
||||
|
||||
interface BuildBulkEditRulesRouteParams {
|
||||
licenseState: ILicenseState;
|
||||
path: string;
|
||||
router: IRouter<AlertingRequestHandlerContext>;
|
||||
}
|
||||
|
||||
const buildBulkEditRulesRoute = ({ licenseState, path, router }: BuildBulkEditRulesRouteParams) => {
|
||||
router.post(
|
||||
{
|
||||
path,
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
},
|
||||
},
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesClient = (await context.alerting).getRulesClient();
|
||||
const { filter, operations, ids } = req.body;
|
||||
|
||||
try {
|
||||
const bulkEditResults = await rulesClient.bulkEdit({
|
||||
filter,
|
||||
ids: ids as string[],
|
||||
operations,
|
||||
});
|
||||
return res.ok({
|
||||
body: { ...bulkEditResults, rules: bulkEditResults.rules.map(rewriteRule) },
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof RuleTypeDisabledError) {
|
||||
return e.sendResponse(res);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const bulkEditInternalRulesRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) =>
|
||||
buildBulkEditRulesRoute({
|
||||
licenseState,
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_edit`,
|
||||
router,
|
||||
});
|
|
@ -13,7 +13,7 @@ import { Observable } from 'rxjs';
|
|||
import { ILicenseState } from '../lib';
|
||||
import { defineLegacyRoutes } from './legacy';
|
||||
import { AlertingRequestHandlerContext } from '../types';
|
||||
import { createRuleRoute } from './rule/create';
|
||||
import { createRuleRoute } from './rule/apis/create';
|
||||
import { getRuleRoute, getInternalRuleRoute } from './get_rule';
|
||||
import { updateRuleRoute } from './update_rule';
|
||||
import { deleteRuleRoute } from './delete_rule';
|
||||
|
@ -36,7 +36,7 @@ import { muteAlertRoute } from './mute_alert';
|
|||
import { unmuteAllRuleRoute } from './unmute_all_rule';
|
||||
import { unmuteAlertRoute } from './unmute_alert';
|
||||
import { updateRuleApiKeyRoute } from './update_rule_api_key';
|
||||
import { bulkEditInternalRulesRoute } from './bulk_edit_rules';
|
||||
import { bulkEditInternalRulesRoute } from './rule/apis/bulk_edit/bulk_edit_rules_route';
|
||||
import { snoozeRuleRoute } from './snooze_rule';
|
||||
import { unsnoozeRuleRoute } from './unsnooze_rule';
|
||||
import { runSoonRoute } from './run_soon';
|
||||
|
|
|
@ -7,23 +7,23 @@
|
|||
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { bulkEditInternalRulesRoute } from './bulk_edit_rules';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { SanitizedRule } from '../types';
|
||||
import { bulkEditInternalRulesRoute } from './bulk_edit_rules_route';
|
||||
import { licenseStateMock } from '../../../../lib/license_state.mock';
|
||||
import { verifyApiAccess } from '../../../../lib/license_api_access';
|
||||
import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled';
|
||||
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../../../../rules_client.mock';
|
||||
import { SanitizedRule } from '../../../../types';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
jest.mock('../lib/license_api_access', () => ({
|
||||
jest.mock('../../../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('bulkEditInternalRulesRoute', () => {
|
||||
describe('bulkEditRulesRoute', () => {
|
||||
const mockedAlert: SanitizedRule<{}> = {
|
||||
id: '1',
|
||||
alertTypeId: '1',
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { IRouter } from '@kbn/core/server';
|
||||
|
||||
import { ILicenseState, RuleTypeDisabledError } from '../../../../lib';
|
||||
import { verifyAccessAndContext, handleDisabledApiKeysError } from '../../../lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
|
||||
|
||||
import {
|
||||
bulkEditRulesRequestBodySchemaV1,
|
||||
BulkEditRulesRequestBodyV1,
|
||||
BulkEditRulesResponseV1,
|
||||
} from '../../../../../common/routes/rule/apis/bulk_edit';
|
||||
import { Rule } from '../../../../application/rule/types';
|
||||
import type { RuleParamsV1 } from '../../../../../common/routes/rule/response';
|
||||
|
||||
import { transformRuleToRuleResponseV1 } from '../../transforms';
|
||||
|
||||
interface BuildBulkEditRulesRouteParams {
|
||||
licenseState: ILicenseState;
|
||||
path: string;
|
||||
router: IRouter<AlertingRequestHandlerContext>;
|
||||
}
|
||||
|
||||
const buildBulkEditRulesRoute = ({ licenseState, path, router }: BuildBulkEditRulesRouteParams) => {
|
||||
router.post(
|
||||
{
|
||||
path,
|
||||
validate: {
|
||||
body: bulkEditRulesRequestBodySchemaV1,
|
||||
},
|
||||
},
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesClient = (await context.alerting).getRulesClient();
|
||||
const bulkEditData: BulkEditRulesRequestBodyV1 = req.body;
|
||||
|
||||
const { filter, operations, ids } = bulkEditData;
|
||||
|
||||
try {
|
||||
const bulkEditResults = await rulesClient.bulkEdit<RuleParamsV1>({
|
||||
filter,
|
||||
ids,
|
||||
operations,
|
||||
});
|
||||
|
||||
const resultBody: BulkEditRulesResponseV1<RuleParamsV1> = {
|
||||
body: {
|
||||
...bulkEditResults,
|
||||
rules: bulkEditResults.rules.map((rule) => {
|
||||
// TODO (http-versioning): Remove this cast, this enables us to move forward
|
||||
// without fixing all of other solution types
|
||||
return transformRuleToRuleResponseV1<RuleParamsV1>(rule as Rule<RuleParamsV1>);
|
||||
}),
|
||||
},
|
||||
};
|
||||
return res.ok(resultBody);
|
||||
} catch (e) {
|
||||
if (e instanceof RuleTypeDisabledError) {
|
||||
return e.sendResponse(res);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const bulkEditInternalRulesRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) =>
|
||||
buildBulkEditRulesRoute({
|
||||
licenseState,
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_edit`,
|
||||
router,
|
||||
});
|
|
@ -8,20 +8,20 @@
|
|||
import { pick } from 'lodash';
|
||||
import { createRuleRoute } from './create_rule_route';
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../../../lib/license_state.mock';
|
||||
import { verifyApiAccess } from '../../../lib/license_api_access';
|
||||
import { mockHandlerArguments } from '../../_mock_handler_arguments';
|
||||
import type { CreateRuleRequestBodyV1 } from '../../../../common/routes/rule/create';
|
||||
import { rulesClientMock } from '../../../rules_client.mock';
|
||||
import { RuleTypeDisabledError } from '../../../lib';
|
||||
import { AsApiContract } from '../../lib';
|
||||
import { SanitizedRule } from '../../../types';
|
||||
import { licenseStateMock } from '../../../../lib/license_state.mock';
|
||||
import { verifyApiAccess } from '../../../../lib/license_api_access';
|
||||
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
|
||||
import type { CreateRuleRequestBodyV1 } from '../../../../../common/routes/rule/apis/create';
|
||||
import { rulesClientMock } from '../../../../rules_client.mock';
|
||||
import { RuleTypeDisabledError } from '../../../../lib';
|
||||
import { AsApiContract } from '../../../lib';
|
||||
import { SanitizedRule } from '../../../../types';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
|
||||
jest.mock('../../../lib/license_api_access', () => ({
|
||||
jest.mock('../../../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
|
@ -5,24 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RuleTypeDisabledError } from '../../../lib';
|
||||
import { RuleTypeDisabledError } from '../../../../lib';
|
||||
import {
|
||||
handleDisabledApiKeysError,
|
||||
verifyAccessAndContext,
|
||||
countUsageOfPredefinedIds,
|
||||
} from '../../lib';
|
||||
import { BASE_ALERTING_API_PATH } from '../../../types';
|
||||
import { RouteOptions } from '../..';
|
||||
} from '../../../lib';
|
||||
import { BASE_ALERTING_API_PATH } from '../../../../types';
|
||||
import { RouteOptions } from '../../..';
|
||||
import type {
|
||||
CreateRuleRequestBodyV1,
|
||||
CreateRuleRequestParamsV1,
|
||||
CreateRuleResponseV1,
|
||||
} from '../../../../common/routes/rule/create';
|
||||
import { createBodySchemaV1, createParamsSchemaV1 } from '../../../../common/routes/rule/create';
|
||||
import type { RuleParamsV1 } from '../../../../common/routes/rule/rule_response';
|
||||
import { Rule } from '../../../application/rule/types';
|
||||
} from '../../../../../common/routes/rule/apis/create';
|
||||
import {
|
||||
createBodySchemaV1,
|
||||
createParamsSchemaV1,
|
||||
} from '../../../../../common/routes/rule/apis/create';
|
||||
import type { RuleParamsV1 } from '../../../../../common/routes/rule/response';
|
||||
import { Rule } from '../../../../application/rule/types';
|
||||
import { transformCreateBodyV1 } from './transforms';
|
||||
import { transformRuleToRuleResponseV1 } from '../transforms';
|
||||
import { transformRuleToRuleResponseV1 } from '../../transforms';
|
||||
|
||||
export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOptions) => {
|
||||
router.post(
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -8,9 +8,9 @@
|
|||
import type {
|
||||
CreateRuleActionV1,
|
||||
CreateRuleRequestBodyV1,
|
||||
} from '../../../../../../common/routes/rule/create';
|
||||
import type { CreateRuleData } from '../../../../../application/rule/create';
|
||||
import type { RuleParams } from '../../../../../application/rule/types';
|
||||
} from '../../../../../../../common/routes/rule/apis/create';
|
||||
import type { CreateRuleData } from '../../../../../../application/rule/methods/create';
|
||||
import type { RuleParams } from '../../../../../../application/rule/types';
|
||||
|
||||
const transformCreateBodyActions = (actions: CreateRuleActionV1[]): CreateRuleData['actions'] => {
|
||||
if (!actions) return [];
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RuleResponseV1, RuleParamsV1 } from '../../../../../common/routes/rule/rule_response';
|
||||
import { RuleResponseV1, RuleParamsV1 } from '../../../../../common/routes/rule/response';
|
||||
import { Rule, RuleLastRun, RuleParams } from '../../../../application/rule/types';
|
||||
|
||||
const transformRuleLastRun = (lastRun: RuleLastRun): RuleResponseV1['last_run'] => {
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
|
||||
import { RawRule } from '../../types';
|
||||
import { CreateAPIKeyResult } from '../types';
|
||||
import { RuleDomain } from '../../application/rule/types';
|
||||
|
||||
/**
|
||||
* @deprecated TODO (http-versioning) make sure this is deprecated
|
||||
* once all of the RawRules are phased out
|
||||
*/
|
||||
export function apiKeyAsAlertAttributes(
|
||||
apiKey: CreateAPIKeyResult | null,
|
||||
username: string | null,
|
||||
|
@ -25,3 +30,21 @@ export function apiKeyAsAlertAttributes(
|
|||
apiKeyCreatedByUser: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function apiKeyAsRuleDomainProperties(
|
||||
apiKey: CreateAPIKeyResult | null,
|
||||
username: string | null,
|
||||
createdByUser: boolean
|
||||
): Pick<RuleDomain, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'> {
|
||||
return apiKey && apiKey.apiKeysEnabled
|
||||
? {
|
||||
apiKeyOwner: username,
|
||||
apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'),
|
||||
apiKeyCreatedByUser: createdByUser,
|
||||
}
|
||||
: {
|
||||
apiKeyOwner: null,
|
||||
apiKey: null,
|
||||
apiKeyCreatedByUser: null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,7 +32,9 @@ export const applyBulkEditOperation = <R extends object>(operation: BulkEditOper
|
|||
|
||||
switch (operation.operation) {
|
||||
case 'set':
|
||||
set(rule, operation.field, operation.value);
|
||||
if (operation.field !== 'apiKey') {
|
||||
set(rule, operation.field, operation.value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
|
|
|
@ -13,7 +13,10 @@ 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 {
|
||||
apiKeyAsAlertAttributes,
|
||||
apiKeyAsRuleDomainProperties,
|
||||
} from './api_key_as_alert_attributes';
|
||||
export * from './inject_references';
|
||||
export { parseDate } from './parse_date';
|
||||
export { includeFieldsRequiredForAuthentication } from './include_fields_required_for_authentication';
|
||||
|
|
|
@ -10,6 +10,7 @@ import { omit } from 'lodash';
|
|||
import { SavedObjectReference, SavedObjectAttributes } from '@kbn/core/server';
|
||||
import { UntypedNormalizedRuleType } from '../../rule_type_registry';
|
||||
import { Rule, RawRule, RuleTypeParams } from '../../types';
|
||||
import { RuleActionAttributes } from '../../data/rule/types';
|
||||
import {
|
||||
preconfiguredConnectorActionRefPrefix,
|
||||
extractedSavedObjectParamReferenceNamePrefix,
|
||||
|
@ -17,7 +18,7 @@ import {
|
|||
|
||||
export function injectReferencesIntoActions(
|
||||
alertId: string,
|
||||
actions: RawRule['actions'],
|
||||
actions: RawRule['actions'] | RuleActionAttributes[],
|
||||
references: SavedObjectReference[]
|
||||
) {
|
||||
return actions.map((action) => {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Logger, SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from
|
|||
import { BulkActionSkipResult } from '../../../common/bulk_edit';
|
||||
import { convertRuleIdsToKueryNode } from '../../lib';
|
||||
import { BulkOperationError } from '../types';
|
||||
import { RawRule } from '../../types';
|
||||
import { RuleAttributes } from '../../data/rule/types';
|
||||
import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry';
|
||||
|
||||
// max number of failed SO ids in one retry filter
|
||||
|
@ -20,15 +20,15 @@ const MaxIdsNumberInRetryFilter = 1000;
|
|||
|
||||
type BulkEditOperation = (filter: KueryNode | null) => Promise<{
|
||||
apiKeysToInvalidate: string[];
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RawRule>>;
|
||||
resultSavedObjects: Array<SavedObjectsUpdateResponse<RawRule>>;
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>;
|
||||
resultSavedObjects: Array<SavedObjectsUpdateResponse<RuleAttributes>>;
|
||||
errors: BulkOperationError[];
|
||||
skipped: BulkActionSkipResult[];
|
||||
}>;
|
||||
|
||||
interface ReturnRetry {
|
||||
apiKeysToInvalidate: string[];
|
||||
results: Array<SavedObjectsUpdateResponse<RawRule>>;
|
||||
results: Array<SavedObjectsUpdateResponse<RuleAttributes>>;
|
||||
errors: BulkOperationError[];
|
||||
skipped: BulkActionSkipResult[];
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export const retryIfBulkEditConflicts = async (
|
|||
filter: KueryNode | null,
|
||||
retries: number = RETRY_IF_CONFLICTS_ATTEMPTS,
|
||||
accApiKeysToInvalidate: string[] = [],
|
||||
accResults: Array<SavedObjectsUpdateResponse<RawRule>> = [],
|
||||
accResults: Array<SavedObjectsUpdateResponse<RuleAttributes>> = [],
|
||||
accErrors: BulkOperationError[] = [],
|
||||
accSkipped: BulkActionSkipResult[] = []
|
||||
): Promise<ReturnRetry> => {
|
||||
|
|
|
@ -7,8 +7,16 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RawRule, RuleSnoozeSchedule } from '../../types';
|
||||
import {
|
||||
RuleDomain,
|
||||
RuleParams,
|
||||
RuleSnoozeSchedule as RuleDomainSnoozeSchedule,
|
||||
} from '../../application/rule/types';
|
||||
import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed';
|
||||
|
||||
/**
|
||||
* @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types
|
||||
*/
|
||||
export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) {
|
||||
// If duration is -1, instead mute all
|
||||
const { id: snoozeId, duration } = snoozeSchedule;
|
||||
|
@ -16,34 +24,40 @@ export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSno
|
|||
if (duration === -1) {
|
||||
return {
|
||||
muteAll: true,
|
||||
snoozeSchedule: clearUnscheduledSnooze(attributes),
|
||||
snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes),
|
||||
};
|
||||
}
|
||||
return {
|
||||
snoozeSchedule: (snoozeId
|
||||
? clearScheduledSnoozesById(attributes, [snoozeId])
|
||||
: clearUnscheduledSnooze(attributes)
|
||||
? clearScheduledSnoozesAttributesById(attributes, [snoozeId])
|
||||
: clearUnscheduledSnoozeAttributes(attributes)
|
||||
).concat(snoozeSchedule),
|
||||
muteAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) {
|
||||
export function getBulkSnooze<Params extends RuleParams>(
|
||||
rule: RuleDomain<Params>,
|
||||
snoozeSchedule: RuleDomainSnoozeSchedule
|
||||
): {
|
||||
muteAll: RuleDomain<Params>['muteAll'];
|
||||
snoozeSchedule: RuleDomain<Params>['snoozeSchedule'];
|
||||
} {
|
||||
// If duration is -1, instead mute all
|
||||
const { id: snoozeId, duration } = snoozeSchedule;
|
||||
|
||||
if (duration === -1) {
|
||||
return {
|
||||
muteAll: true,
|
||||
snoozeSchedule: clearUnscheduledSnooze(attributes),
|
||||
snoozeSchedule: clearUnscheduledSnooze<Params>(rule),
|
||||
};
|
||||
}
|
||||
|
||||
// Bulk adding snooze schedule, don't touch the existing snooze/indefinite snooze
|
||||
if (snoozeId) {
|
||||
const existingSnoozeSchedules = attributes.snoozeSchedule || [];
|
||||
const existingSnoozeSchedules = rule.snoozeSchedule || [];
|
||||
return {
|
||||
muteAll: attributes.muteAll,
|
||||
muteAll: rule.muteAll,
|
||||
snoozeSchedule: [...existingSnoozeSchedules, snoozeSchedule],
|
||||
};
|
||||
}
|
||||
|
@ -51,14 +65,17 @@ export function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: Rul
|
|||
// Bulk snoozing, don't touch the existing snooze schedules
|
||||
return {
|
||||
muteAll: false,
|
||||
snoozeSchedule: [...clearUnscheduledSnooze(attributes), snoozeSchedule],
|
||||
snoozeSchedule: [...(clearUnscheduledSnooze<Params>(rule) || []), snoozeSchedule],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types
|
||||
*/
|
||||
export function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) {
|
||||
const snoozeSchedule = scheduleIds
|
||||
? clearScheduledSnoozesById(attributes, scheduleIds)
|
||||
: clearCurrentActiveSnooze(attributes);
|
||||
? clearScheduledSnoozesAttributesById(attributes, scheduleIds)
|
||||
: clearCurrentActiveSnoozeAttributes(attributes);
|
||||
|
||||
return {
|
||||
snoozeSchedule,
|
||||
|
@ -66,43 +83,65 @@ export function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[
|
|||
};
|
||||
}
|
||||
|
||||
export function getBulkUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) {
|
||||
export function getBulkUnsnooze<Params extends RuleParams>(
|
||||
rule: RuleDomain<Params>,
|
||||
scheduleIds?: string[]
|
||||
) {
|
||||
// Bulk removing snooze schedules, don't touch the current snooze/indefinite snooze
|
||||
if (scheduleIds) {
|
||||
const newSchedules = clearScheduledSnoozesById(attributes, scheduleIds);
|
||||
const newSchedules = clearScheduledSnoozesById(rule, scheduleIds);
|
||||
// Unscheduled snooze is also known as snooze now
|
||||
const unscheduledSnooze =
|
||||
attributes.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || [];
|
||||
const unscheduledSnooze = rule.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || [];
|
||||
|
||||
return {
|
||||
snoozeSchedule: [...unscheduledSnooze, ...newSchedules],
|
||||
muteAll: attributes.muteAll,
|
||||
muteAll: rule.muteAll,
|
||||
};
|
||||
}
|
||||
|
||||
// Bulk unsnoozing, don't touch current snooze schedules that are NOT active
|
||||
return {
|
||||
snoozeSchedule: clearCurrentActiveSnooze(attributes),
|
||||
snoozeSchedule: clearCurrentActiveSnooze(rule),
|
||||
muteAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearUnscheduledSnooze(attributes: RawRule) {
|
||||
/**
|
||||
* @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types
|
||||
*/
|
||||
export function clearUnscheduledSnoozeAttributes(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[]) {
|
||||
export function clearUnscheduledSnooze<Params extends RuleParams>(rule: RuleDomain<Params>) {
|
||||
return rule.snoozeSchedule ? rule.snoozeSchedule.filter((s) => typeof s.id !== 'undefined') : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types
|
||||
*/
|
||||
export function clearScheduledSnoozesAttributesById(attributes: RawRule, ids: string[]) {
|
||||
return attributes.snoozeSchedule
|
||||
? attributes.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id))
|
||||
: [];
|
||||
}
|
||||
|
||||
export function clearCurrentActiveSnooze(attributes: RawRule) {
|
||||
export function clearScheduledSnoozesById<Params extends RuleParams>(
|
||||
rule: RuleDomain<Params>,
|
||||
ids: string[]
|
||||
) {
|
||||
return rule.snoozeSchedule ? rule.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types
|
||||
*/
|
||||
export function clearCurrentActiveSnoozeAttributes(attributes: RawRule) {
|
||||
// First attempt to cancel a simple (unscheduled) snooze
|
||||
const clearedUnscheduledSnoozes = clearUnscheduledSnooze(attributes);
|
||||
const clearedUnscheduledSnoozes = clearUnscheduledSnoozeAttributes(attributes);
|
||||
// Now clear any scheduled snoozes that are currently active and never recur
|
||||
const activeSnoozes = getActiveScheduledSnoozes(attributes);
|
||||
const activeSnoozeIds = activeSnoozes?.map((s) => s.id) ?? [];
|
||||
|
@ -127,7 +166,37 @@ export function clearCurrentActiveSnooze(attributes: RawRule) {
|
|||
return clearedSnoozesAndSkippedRecurringSnoozes;
|
||||
}
|
||||
|
||||
export function verifySnoozeScheduleLimit(attributes: Partial<RawRule>) {
|
||||
export function clearCurrentActiveSnooze<Params extends RuleParams>(rule: RuleDomain<Params>) {
|
||||
// First attempt to cancel a simple (unscheduled) snooze
|
||||
const clearedUnscheduledSnoozes = clearUnscheduledSnooze(rule);
|
||||
// Now clear any scheduled snoozes that are currently active and never recur
|
||||
const activeSnoozes = getActiveScheduledSnoozes(rule);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types
|
||||
*/
|
||||
export function verifySnoozeAttributeScheduleLimit(attributes: Partial<RawRule>) {
|
||||
const schedules = attributes.snoozeSchedule?.filter((snooze) => snooze.id);
|
||||
if (schedules && schedules.length > 5) {
|
||||
throw Error(
|
||||
|
@ -137,3 +206,16 @@ export function verifySnoozeScheduleLimit(attributes: Partial<RawRule>) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function verifySnoozeScheduleLimit<Params extends RuleParams>(
|
||||
snoozeSchedule: RuleDomain<Params>['snoozeSchedule']
|
||||
) {
|
||||
const schedules = 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',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,8 +68,8 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
|
|||
() =>
|
||||
createRuleSo({
|
||||
ruleAttributes: updateMeta(context, rawRule as RawRule) as RuleAttributes,
|
||||
savedObjectClient: context.unsecuredSavedObjectsClient,
|
||||
savedObjectCreateOptions: {
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
savedObjectsCreateOptions: {
|
||||
...options,
|
||||
references,
|
||||
id: ruleId,
|
||||
|
@ -101,7 +101,7 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
|
|||
// Cleanup data, something went wrong scheduling the task
|
||||
try {
|
||||
await deleteRuleSo({
|
||||
savedObjectClient: context.unsecuredSavedObjectsClient,
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
id: createdAlert.id,
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -115,7 +115,7 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
|
|||
|
||||
await withSpan({ name: 'unsecuredSavedObjectsClient.update', type: 'rules' }, () =>
|
||||
updateRuleSo({
|
||||
savedObjectClient: context.unsecuredSavedObjectsClient,
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
id: createdAlert.id,
|
||||
updateRuleAttributes: {
|
||||
scheduledTaskId,
|
||||
|
|
|
@ -5,898 +5,9 @@
|
|||
* 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,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { BulkActionSkipResult } from '../../../common/bulk_edit';
|
||||
import {
|
||||
RawRule,
|
||||
SanitizedRule,
|
||||
RuleTypeParams,
|
||||
Rule,
|
||||
RuleSnoozeSchedule,
|
||||
RuleWithLegacyId,
|
||||
RuleTypeRegistry,
|
||||
RawRuleAction,
|
||||
RuleNotifyWhen,
|
||||
} 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,
|
||||
getBulkSnoozeAttributes,
|
||||
getBulkUnsnoozeAttributes,
|
||||
verifySnoozeScheduleLimit,
|
||||
injectReferencesIntoParams,
|
||||
} 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,
|
||||
addGeneratedActionValues,
|
||||
createNewAPIKeySet,
|
||||
} from '../lib';
|
||||
import {
|
||||
NormalizedAlertAction,
|
||||
BulkOperationError,
|
||||
RuleBulkOperationAggregation,
|
||||
RulesClientContext,
|
||||
NormalizedAlertActionWithGeneratedValues,
|
||||
} from '../types';
|
||||
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
|
||||
export type BulkEditFields = keyof Pick<
|
||||
Rule,
|
||||
'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey'
|
||||
>;
|
||||
|
||||
export const bulkEditFieldsToExcludeFromRevisionUpdates: ReadonlySet<BulkEditOperation['field']> =
|
||||
new Set(['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 ApiKeysMap = Map<
|
||||
string,
|
||||
{
|
||||
oldApiKey?: string;
|
||||
newApiKey?: string;
|
||||
oldApiKeyCreatedByUser?: boolean | null;
|
||||
newApiKeyCreatedByUser?: boolean | null;
|
||||
}
|
||||
>;
|
||||
|
||||
type ApiKeyAttributes = Pick<RawRule, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'>;
|
||||
|
||||
type RuleType = ReturnType<RuleTypeRegistry['get']>;
|
||||
|
||||
// TODO (http-versioning): This file exists only to provide the type export for
|
||||
// security solution, once we version all of our types we can remove this file
|
||||
export interface RuleParamsModifierResult<Params> {
|
||||
modifiedParams: Params;
|
||||
isParamsUpdateSkipped: boolean;
|
||||
}
|
||||
|
||||
export type RuleParamsModifier<Params extends RuleTypeParams> = (
|
||||
params: Params
|
||||
) => Promise<RuleParamsModifierResult<Params>>;
|
||||
|
||||
export type ShouldIncrementRevision<Params extends RuleTypeParams> = (
|
||||
params?: RuleTypeParams
|
||||
) => boolean;
|
||||
|
||||
export interface BulkEditOptionsFilter<Params extends RuleTypeParams> {
|
||||
filter?: string | KueryNode;
|
||||
operations: BulkEditOperation[];
|
||||
paramsModifier?: RuleParamsModifier<Params>;
|
||||
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
|
||||
}
|
||||
|
||||
export interface BulkEditOptionsIds<Params extends RuleTypeParams> {
|
||||
ids: string[];
|
||||
operations: BulkEditOperation[];
|
||||
paramsModifier?: RuleParamsModifier<Params>;
|
||||
shouldIncrementRevision?: ShouldIncrementRevision<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>>;
|
||||
skipped: BulkActionSkipResult[];
|
||||
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, skipped } = await retryIfBulkEditConflicts(
|
||||
context.logger,
|
||||
`rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${
|
||||
options.paramsModifier ? '[Function]' : undefined
|
||||
}', shouldIncrementRevision=${options.shouldIncrementRevision ? '[Function]' : undefined}')`,
|
||||
(filterKueryNode: KueryNode | null) =>
|
||||
bulkEditOcc(context, {
|
||||
filter: filterKueryNode,
|
||||
operations: options.operations,
|
||||
paramsModifier: options.paramsModifier,
|
||||
shouldIncrementRevision: options.shouldIncrementRevision,
|
||||
}),
|
||||
qNodeFilterWithAuth
|
||||
);
|
||||
|
||||
if (apiKeysToInvalidate.length > 0) {
|
||||
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,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
await bulkUpdateSchedules(context, options.operations, updatedRules);
|
||||
|
||||
return { rules: updatedRules, skipped, errors, total };
|
||||
}
|
||||
|
||||
async function bulkEditOcc<Params extends RuleTypeParams>(
|
||||
context: RulesClientContext,
|
||||
{
|
||||
filter,
|
||||
operations,
|
||||
paramsModifier,
|
||||
shouldIncrementRevision,
|
||||
}: {
|
||||
filter: KueryNode | null;
|
||||
operations: BulkEditOptions<Params>['operations'];
|
||||
paramsModifier: BulkEditOptions<Params>['paramsModifier'];
|
||||
shouldIncrementRevision?: BulkEditOptions<Params>['shouldIncrementRevision'];
|
||||
}
|
||||
): Promise<{
|
||||
apiKeysToInvalidate: string[];
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RawRule>>;
|
||||
resultSavedObjects: Array<SavedObjectsUpdateResponse<RawRule>>;
|
||||
errors: BulkOperationError[];
|
||||
skipped: BulkActionSkipResult[];
|
||||
}> {
|
||||
const rulesFinder =
|
||||
await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<RawRule>(
|
||||
{
|
||||
filter,
|
||||
type: 'alert',
|
||||
perPage: 100,
|
||||
...(context.namespace ? { namespaces: [context.namespace] } : undefined),
|
||||
}
|
||||
);
|
||||
|
||||
const rules: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
const skipped: BulkActionSkipResult[] = [];
|
||||
const errors: BulkOperationError[] = [];
|
||||
const apiKeysMap: ApiKeysMap = new Map();
|
||||
const username = await context.getUserName();
|
||||
|
||||
for await (const response of rulesFinder.find()) {
|
||||
await pMap(
|
||||
response.saved_objects,
|
||||
async (rule: SavedObjectsFindResult<RawRule>) =>
|
||||
updateRuleAttributesAndParamsInMemory({
|
||||
context,
|
||||
rule,
|
||||
operations,
|
||||
paramsModifier,
|
||||
apiKeysMap,
|
||||
rules,
|
||||
skipped,
|
||||
errors,
|
||||
username,
|
||||
shouldIncrementRevision,
|
||||
}),
|
||||
{ concurrency: API_KEY_GENERATE_CONCURRENCY }
|
||||
);
|
||||
}
|
||||
await rulesFinder.close();
|
||||
|
||||
const { result, apiKeysToInvalidate } =
|
||||
rules.length > 0
|
||||
? await saveBulkUpdatedRules(context, rules, apiKeysMap)
|
||||
: {
|
||||
result: { saved_objects: [] },
|
||||
apiKeysToInvalidate: [],
|
||||
};
|
||||
|
||||
return {
|
||||
apiKeysToInvalidate,
|
||||
resultSavedObjects: result.saved_objects,
|
||||
errors,
|
||||
rules,
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
async function bulkUpdateSchedules(
|
||||
context: RulesClientContext,
|
||||
operations: BulkEditOperation[],
|
||||
updatedRules: Array<Rule | RuleWithLegacyId>
|
||||
): Promise<void> {
|
||||
const scheduleOperation = operations.find(
|
||||
(
|
||||
operation
|
||||
): operation is Extract<BulkEditOperation, { field: Extract<BulkEditFields, 'schedule'> }> =>
|
||||
operation.field === 'schedule'
|
||||
);
|
||||
|
||||
if (!scheduleOperation?.value) {
|
||||
return;
|
||||
}
|
||||
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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRuleAttributesAndParamsInMemory<Params extends RuleTypeParams>({
|
||||
context,
|
||||
rule,
|
||||
operations,
|
||||
paramsModifier,
|
||||
apiKeysMap,
|
||||
rules,
|
||||
skipped,
|
||||
errors,
|
||||
username,
|
||||
shouldIncrementRevision = () => true,
|
||||
}: {
|
||||
context: RulesClientContext;
|
||||
rule: SavedObjectsFindResult<RawRule>;
|
||||
operations: BulkEditOptions<Params>['operations'];
|
||||
paramsModifier: BulkEditOptions<Params>['paramsModifier'];
|
||||
apiKeysMap: ApiKeysMap;
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RawRule>>;
|
||||
skipped: BulkActionSkipResult[];
|
||||
errors: BulkOperationError[];
|
||||
username: string | null;
|
||||
shouldIncrementRevision: BulkEditOptions<Params>['shouldIncrementRevision'];
|
||||
}): Promise<void> {
|
||||
try {
|
||||
if (rule.attributes.apiKey) {
|
||||
apiKeysMap.set(rule.id, {
|
||||
oldApiKey: rule.attributes.apiKey,
|
||||
oldApiKeyCreatedByUser: rule.attributes.apiKeyCreatedByUser,
|
||||
});
|
||||
}
|
||||
|
||||
const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId);
|
||||
|
||||
await ensureAuthorizationForBulkUpdate(context, operations, rule);
|
||||
|
||||
// migrate legacy actions only for SIEM rules
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
actions: rule.attributes.actions,
|
||||
references: rule.references,
|
||||
attributes: rule.attributes,
|
||||
});
|
||||
|
||||
if (migratedActions.hasLegacyActions) {
|
||||
rule.attributes.actions = migratedActions.resultedActions;
|
||||
rule.references = migratedActions.resultedReferences;
|
||||
}
|
||||
|
||||
const { attributes, ruleActions, hasUpdateApiKeyOperation, isAttributesUpdateSkipped } =
|
||||
await getUpdatedAttributesFromOperations(context, operations, rule, ruleType);
|
||||
|
||||
validateScheduleInterval(context, attributes.schedule.interval, ruleType.id, rule.id);
|
||||
|
||||
const params = injectReferencesIntoParams<Params, RuleTypeParams>(
|
||||
rule.id,
|
||||
ruleType,
|
||||
attributes.params,
|
||||
rule.references || []
|
||||
);
|
||||
const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier
|
||||
? await paramsModifier(params)
|
||||
: {
|
||||
modifiedParams: params,
|
||||
isParamsUpdateSkipped: true,
|
||||
};
|
||||
|
||||
// Increment revision if params ended up being modified AND it wasn't already incremented as part of attribute update
|
||||
if (
|
||||
shouldIncrementRevision(ruleParams) &&
|
||||
!isParamsUpdateSkipped &&
|
||||
rule.attributes.revision === attributes.revision
|
||||
) {
|
||||
attributes.revision += 1;
|
||||
}
|
||||
|
||||
// If neither attributes nor parameters were updated, mark
|
||||
// the rule as skipped and continue to the next rule.
|
||||
if (isAttributesUpdateSkipped && isParamsUpdateSkipped) {
|
||||
skipped.push({
|
||||
id: rule.id,
|
||||
name: rule.attributes.name,
|
||||
skip_reason: 'RULE_NOT_MODIFIED',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 as NormalizedAlertActionWithGeneratedValues[],
|
||||
validatedMutatedAlertTypeParams
|
||||
);
|
||||
|
||||
const { apiKeyAttributes } = await prepareApiKeys(
|
||||
context,
|
||||
rule,
|
||||
ruleType,
|
||||
apiKeysMap,
|
||||
attributes,
|
||||
hasUpdateApiKeyOperation,
|
||||
username
|
||||
);
|
||||
|
||||
const { updatedAttributes } = updateAttributes(
|
||||
context,
|
||||
attributes,
|
||||
apiKeyAttributes,
|
||||
updatedParams,
|
||||
rawAlertActions,
|
||||
username
|
||||
);
|
||||
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureAuthorizationForBulkUpdate(
|
||||
context: RulesClientContext,
|
||||
operations: BulkEditOperation[],
|
||||
rule: SavedObjectsFindResult<RawRule>
|
||||
): Promise<void> {
|
||||
if (rule.attributes.actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operation of operations) {
|
||||
const { field } = operation;
|
||||
if (field === 'snoozeSchedule' || field === 'apiKey') {
|
||||
try {
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
break;
|
||||
} catch (error) {
|
||||
throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getUpdatedAttributesFromOperations(
|
||||
context: RulesClientContext,
|
||||
operations: BulkEditOperation[],
|
||||
rule: SavedObjectsFindResult<RawRule>,
|
||||
ruleType: RuleType
|
||||
) {
|
||||
let attributes = cloneDeep(rule.attributes);
|
||||
let ruleActions = {
|
||||
actions: injectReferencesIntoActions(
|
||||
rule.id,
|
||||
rule.attributes.actions || [],
|
||||
rule.references || []
|
||||
),
|
||||
};
|
||||
|
||||
let hasUpdateApiKeyOperation = false;
|
||||
let isAttributesUpdateSkipped = true;
|
||||
|
||||
for (const operation of operations) {
|
||||
// Check if the update should be skipped for the current action.
|
||||
// If it should, save the skip reasons in attributesUpdateSkipReasons
|
||||
// and continue to the next operation before without
|
||||
// the `isAttributesUpdateSkipped` flag to false.
|
||||
switch (operation.field) {
|
||||
case 'actions': {
|
||||
const updatedOperation = {
|
||||
...operation,
|
||||
value: addGeneratedActionValues(operation.value),
|
||||
};
|
||||
|
||||
try {
|
||||
await validateActions(context, ruleType, {
|
||||
...attributes,
|
||||
actions: updatedOperation.value,
|
||||
});
|
||||
} catch (e) {
|
||||
// If validateActions fails on the first attempt, it may be because of legacy rule-level frequency params
|
||||
attributes = await attemptToMigrateLegacyFrequency(
|
||||
context,
|
||||
updatedOperation,
|
||||
attributes,
|
||||
ruleType
|
||||
);
|
||||
}
|
||||
|
||||
const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation(
|
||||
updatedOperation,
|
||||
ruleActions
|
||||
);
|
||||
if (isAttributeModified) {
|
||||
ruleActions = modifiedAttributes;
|
||||
isAttributesUpdateSkipped = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'snoozeSchedule': {
|
||||
// Silently skip adding snooze or snooze schedules on security
|
||||
// rules until we implement snoozing of their rules
|
||||
if (rule.attributes.consumer === AlertConsumers.SIEM) {
|
||||
// While the rule is technically not updated, we are still marking
|
||||
// the rule as updated in case of snoozing, until support
|
||||
// for snoozing is added.
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
if (operation.operation === 'set') {
|
||||
const snoozeAttributes = getBulkSnoozeAttributes(rule.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),
|
||||
};
|
||||
}
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
case 'apiKey': {
|
||||
hasUpdateApiKeyOperation = true;
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (operation.field === 'schedule') {
|
||||
validateScheduleOperation(operation.value, attributes.actions, rule.id);
|
||||
}
|
||||
const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation(
|
||||
operation,
|
||||
rule.attributes
|
||||
);
|
||||
|
||||
if (isAttributeModified) {
|
||||
attributes = {
|
||||
...attributes,
|
||||
...modifiedAttributes,
|
||||
};
|
||||
isAttributesUpdateSkipped = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only increment revision if update wasn't skipped and `operation.field` should result in a revision increment
|
||||
if (
|
||||
!isAttributesUpdateSkipped &&
|
||||
!bulkEditFieldsToExcludeFromRevisionUpdates.has(operation.field) &&
|
||||
rule.attributes.revision - attributes.revision === 0
|
||||
) {
|
||||
attributes.revision += 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
attributes,
|
||||
ruleActions,
|
||||
hasUpdateApiKeyOperation,
|
||||
isAttributesUpdateSkipped,
|
||||
};
|
||||
}
|
||||
|
||||
function validateScheduleInterval(
|
||||
context: RulesClientContext,
|
||||
scheduleInterval: string,
|
||||
ruleTypeId: string,
|
||||
ruleId: string
|
||||
): void {
|
||||
if (!scheduleInterval) {
|
||||
return;
|
||||
}
|
||||
const isIntervalInvalid = parseDuration(scheduleInterval) < 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 (${scheduleInterval}) for "${ruleTypeId}" rule type with ID "${ruleId}" 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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that updated schedule interval is not longer than any of the existing action frequencies
|
||||
* @param schedule Schedule interval that user tries to set
|
||||
* @param actions Rule actions
|
||||
*/
|
||||
function validateScheduleOperation(
|
||||
schedule: RawRule['schedule'],
|
||||
actions: RawRule['actions'],
|
||||
ruleId: string
|
||||
): void {
|
||||
const scheduleInterval = parseDuration(schedule.interval);
|
||||
const actionsWithInvalidThrottles = [];
|
||||
|
||||
for (const action of actions) {
|
||||
// check for actions throttled shorter than the rule schedule
|
||||
if (
|
||||
action.frequency?.notifyWhen === RuleNotifyWhen.THROTTLE &&
|
||||
parseDuration(action.frequency.throttle!) < scheduleInterval
|
||||
) {
|
||||
actionsWithInvalidThrottles.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
if (actionsWithInvalidThrottles.length > 0) {
|
||||
throw Error(
|
||||
`Error updating rule with ID "${ruleId}": the interval ${schedule.interval} is longer than the action frequencies`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareApiKeys(
|
||||
context: RulesClientContext,
|
||||
rule: SavedObjectsFindResult<RawRule>,
|
||||
ruleType: RuleType,
|
||||
apiKeysMap: ApiKeysMap,
|
||||
attributes: RawRule,
|
||||
hasUpdateApiKeyOperation: boolean,
|
||||
username: string | null
|
||||
): Promise<{ apiKeyAttributes: ApiKeyAttributes }> {
|
||||
const apiKeyAttributes = await createNewAPIKeySet(context, {
|
||||
id: ruleType.id,
|
||||
ruleName: attributes.name,
|
||||
username,
|
||||
shouldUpdateApiKey: attributes.enabled || hasUpdateApiKeyOperation,
|
||||
errorMessage: 'Error updating rule: could not create API key',
|
||||
});
|
||||
|
||||
// collect generated API keys
|
||||
if (apiKeyAttributes.apiKey) {
|
||||
apiKeysMap.set(rule.id, {
|
||||
...apiKeysMap.get(rule.id),
|
||||
newApiKey: apiKeyAttributes.apiKey,
|
||||
newApiKeyCreatedByUser: apiKeyAttributes.apiKeyCreatedByUser,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
apiKeyAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
function updateAttributes(
|
||||
context: RulesClientContext,
|
||||
attributes: RawRule,
|
||||
apiKeyAttributes: ApiKeyAttributes,
|
||||
updatedParams: RuleTypeParams,
|
||||
rawAlertActions: RawRuleAction[],
|
||||
username: string | null
|
||||
): {
|
||||
updatedAttributes: RawRule;
|
||||
} {
|
||||
// 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;
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
async function saveBulkUpdatedRules(
|
||||
context: RulesClientContext,
|
||||
rules: Array<SavedObjectsBulkUpdateObject<RawRule>>,
|
||||
apiKeysMap: ApiKeysMap
|
||||
) {
|
||||
const apiKeysToInvalidate: string[] = [];
|
||||
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())
|
||||
.filter((value) => value.newApiKey && !value.newApiKeyCreatedByUser)
|
||||
.map((value) => value.newApiKey as string),
|
||||
},
|
||||
context.logger,
|
||||
context.unsecuredSavedObjectsClient
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
result.saved_objects.map(({ id, error }) => {
|
||||
const oldApiKey = apiKeysMap.get(id)?.oldApiKey;
|
||||
const oldApiKeyCreatedByUser = apiKeysMap.get(id)?.oldApiKeyCreatedByUser;
|
||||
const newApiKey = apiKeysMap.get(id)?.newApiKey;
|
||||
const newApiKeyCreatedByUser = apiKeysMap.get(id)?.newApiKeyCreatedByUser;
|
||||
|
||||
// if SO wasn't saved and has new API key it will be invalidated
|
||||
if (error && newApiKey && !newApiKeyCreatedByUser) {
|
||||
apiKeysToInvalidate.push(newApiKey);
|
||||
// if SO saved and has old Api Key it will be invalidate
|
||||
} else if (!error && oldApiKey && !oldApiKeyCreatedByUser) {
|
||||
apiKeysToInvalidate.push(oldApiKey);
|
||||
}
|
||||
});
|
||||
|
||||
return { result, apiKeysToInvalidate };
|
||||
}
|
||||
|
||||
async function attemptToMigrateLegacyFrequency(
|
||||
context: RulesClientContext,
|
||||
operation: BulkEditOperation,
|
||||
attributes: SavedObjectsFindResult<RawRule>['attributes'],
|
||||
ruleType: RuleType
|
||||
) {
|
||||
if (operation.field !== 'actions')
|
||||
throw new Error('Can only perform frequency migration on an action operation');
|
||||
// Try to remove the rule-level frequency params, and then validate actions
|
||||
if (typeof attributes.notifyWhen !== 'undefined') attributes.notifyWhen = undefined;
|
||||
if (attributes.throttle) attributes.throttle = undefined;
|
||||
await validateActions(context, ruleType, {
|
||||
...attributes,
|
||||
actions: operation.value,
|
||||
});
|
||||
return attributes;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { partiallyUpdateAlert } from '../../saved_objects';
|
|||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { updateMeta } from '../lib';
|
||||
import { clearUnscheduledSnooze } from '../common';
|
||||
import { clearUnscheduledSnoozeAttributes } from '../common';
|
||||
|
||||
export async function muteAll(context: RulesClientContext, { id }: { id: string }): Promise<void> {
|
||||
return await retryIfConflicts(
|
||||
|
@ -63,7 +63,7 @@ async function muteAllWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
const updateAttributes = updateMeta(context, {
|
||||
muteAll: true,
|
||||
mutedInstanceIds: [],
|
||||
snoozeSchedule: clearUnscheduledSnooze(attributes),
|
||||
snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes),
|
||||
updatedBy: await context.getUserName(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ 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 { getSnoozeAttributes, verifySnoozeAttributeScheduleLimit } from '../common';
|
||||
import { updateMeta } from '../lib';
|
||||
|
||||
export interface SnoozeParams {
|
||||
|
@ -88,7 +88,7 @@ async function snoozeWithOCC(
|
|||
const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule);
|
||||
|
||||
try {
|
||||
verifySnoozeScheduleLimit(newAttrs);
|
||||
verifySnoozeAttributeScheduleLimit(newAttrs);
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(error.message);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { partiallyUpdateAlert } from '../../saved_objects';
|
|||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { updateMeta } from '../lib';
|
||||
import { clearUnscheduledSnooze } from '../common';
|
||||
import { clearUnscheduledSnoozeAttributes } from '../common';
|
||||
|
||||
export async function unmuteAll(
|
||||
context: RulesClientContext,
|
||||
|
@ -66,7 +66,7 @@ async function unmuteAllWithOCC(context: RulesClientContext, { id }: { id: strin
|
|||
const updateAttributes = updateMeta(context, {
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
snoozeSchedule: clearUnscheduledSnooze(attributes),
|
||||
snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes),
|
||||
updatedBy: await context.getUserName(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import Boom from '@hapi/boom';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import type { ShouldIncrementRevision } from './bulk_edit';
|
||||
import {
|
||||
PartialRule,
|
||||
RawRule,
|
||||
|
@ -35,6 +34,8 @@ import {
|
|||
migrateLegacyActions,
|
||||
} from '../lib';
|
||||
|
||||
type ShouldIncrementRevision = (params?: RuleTypeParams) => boolean;
|
||||
|
||||
export interface UpdateOptions<Params extends RuleTypeParams> {
|
||||
id: string;
|
||||
data: {
|
||||
|
@ -47,7 +48,7 @@ export interface UpdateOptions<Params extends RuleTypeParams> {
|
|||
notifyWhen?: RuleNotifyWhenType | null;
|
||||
};
|
||||
allowMissingConnectorSecrets?: boolean;
|
||||
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
|
||||
shouldIncrementRevision?: ShouldIncrementRevision;
|
||||
}
|
||||
|
||||
export async function update<Params extends RuleTypeParams = never>(
|
||||
|
|
|
@ -10,7 +10,7 @@ import { parseDuration } from '../../common/parse_duration';
|
|||
import { RulesClientContext, BulkOptions, MuteOptions } from './types';
|
||||
|
||||
import { clone, CloneArguments } from './methods/clone';
|
||||
import { createRule, CreateRuleParams } from '../application/rule/create';
|
||||
import { createRule, CreateRuleParams } from '../application/rule/methods/create';
|
||||
import { get, GetParams } from './methods/get';
|
||||
import { resolve, ResolveParams } from './methods/resolve';
|
||||
import { getAlertState, GetAlertStateParams } from './methods/get_alert_state';
|
||||
|
@ -37,7 +37,10 @@ import { aggregate, AggregateParams } from './methods/aggregate';
|
|||
import { deleteRule } from './methods/delete';
|
||||
import { update, UpdateOptions } from './methods/update';
|
||||
import { bulkDeleteRules } from './methods/bulk_delete';
|
||||
import { bulkEdit, BulkEditOptions } from './methods/bulk_edit';
|
||||
import {
|
||||
bulkEditRules,
|
||||
BulkEditOptions,
|
||||
} from '../application/rule/methods/bulk_edit/bulk_edit_rules';
|
||||
import { bulkEnableRules } from './methods/bulk_enable';
|
||||
import { bulkDisableRules } from './methods/bulk_disable';
|
||||
import { updateApiKey } from './methods/update_api_key';
|
||||
|
@ -136,7 +139,7 @@ export class RulesClient {
|
|||
|
||||
public bulkDeleteRules = (options: BulkOptions) => bulkDeleteRules(this.context, options);
|
||||
public bulkEdit = <Params extends RuleTypeParams>(options: BulkEditOptions<Params>) =>
|
||||
bulkEdit<Params>(this.context, options);
|
||||
bulkEditRules<Params>(this.context, options);
|
||||
public bulkEnableRules = (options: BulkOptions) => bulkEnableRules(this.context, options);
|
||||
public bulkDisableRules = (options: BulkOptions) => bulkDisableRules(this.context, options);
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue