mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[RAM][Security Solution][Alerts] moves legacy actions migration to rulesClient (#153101)
## Summary - this PR is the first part of work related to conditional logic actions. The rest of PRs, will be merged after this one. As they depend on a work implemented here. [List of tickets](https://github.com/elastic/security-team/issues/2894#issuecomment-1480253677) - addresses https://github.com/elastic/kibana/issues/151919 - moves code related to legacy actions migration from D&R to RulesClient, [details](https://github.com/elastic/kibana/issues/151919#issuecomment-1473913699) - similarly to D&R part, now rulesClient read APIs, would return legacy actions within rule - similarly, every mutation API in rulesClient, would migrate legacy actions, and remove sidecar SO - each migrated legacy action will have also [`frequency` object](https://github.com/elastic/kibana/blob/8.7/x-pack/plugins/alerting/server/types.ts#L234-L238), that would allow to have notifyWhen/throttle on action level once https://github.com/elastic/kibana/issues/151916 is implemented, which is targeted in 8.8, right after this PR. But before it's merged, `frequency` is getting removed in [update/bulk_edit/create APIs](https://github.com/elastic/kibana/blob/8.7/x-pack/plugins/alerting/server/rules_client/methods/update.ts#L151-L160). Hence it's not reflected in most of the tests at this point. - cleanup of legacy actions related code in D&R - adds unit tests for RulesClient - keeps functional/e2e tests in D&R Changes in behaviour, introduced in this PR: - since, migration happens within single rulesClient API call, revision in migrated rule will increment by `1` only. - legacy actions from sidecar SO, now will be merged with rules actions, if there any. Before, in the previous implementation, there was inconsistency in a way how legacy and rules actions were treated. - On read: actions existing in rule, [would take precedence over legacy ones ](https://github.com/elastic/kibana/blob/8.7/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts#L94-L114) - On migration: SO actions [only saved](https://github.com/elastic/kibana/blob/8.7/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/rule_actions/legacy_action_migration.ts#L114). If any actions present in rule, they will be lost. Here is an example video from **main** branch <details> <summary>Here is an example video from MAIN branch, where action in rule is overwritten by legacy action</summary> https://user-images.githubusercontent.com/92328789/230397535-d3fcd644-7cf9-4970-a573-18fd8c9f2235.mov </details> So, depends on sequence of events, different actions could be saved for identical use case: rule has both legacy and existing action. - if rule migrated through update API, existing in rule action will be saved - if rule migrated through enable/disable API, bulk edit, legacy action will be saved In this implementation, both existing in rule and legacy actions will be merged, to prevent loss of actions <details> <summary>Here is an example video from this PR branch, where actions are merged</summary> <video src="https://user-images.githubusercontent.com/92328789/230405569-f1da38e9-4e36-46a8-9654-f664e0a31063.mov" /> </details> - when duplicating rule, we don't migrate source rule anymore. It can lead to unwanted API key regeneration, with possible loss of privileges, earlier associated with the source rule. As part of UX, when duplicating any entity, users would not be expecting source entity to be changed TODO: - performance improvement issue for future https://github.com/elastic/kibana/issues/154438 - currently, in main branch, when migration is performed through rule enabling, actions not showing anymore in UI. Relevant ticket is https://github.com/elastic/kibana/issues/154567 I haven't fixed it in this PR, as in[ the next one ](https://github.com/elastic/kibana/pull/153113), we will display notifyWhen/throttle on action level in UI, rather than on rule level. So, once that PR is merged, actions should be displayed in new UI ### Checklist Delete any items that are not applicable to this PR. - [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 ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
parent
1b36fb83c4
commit
f8c16c159c
81 changed files with 3044 additions and 1976 deletions
|
@ -15,5 +15,7 @@ export { checkAuthorizationAndGetTotal } from './check_authorization_and_get_tot
|
|||
export { scheduleTask } from './schedule_task';
|
||||
export { createNewAPIKeySet } from './create_new_api_key_set';
|
||||
export { recoverRuleAlerts } from './recover_rule_alerts';
|
||||
export { migrateLegacyActions } from './siem_legacy_actions/migrate_legacy_actions';
|
||||
export { formatLegacyActions } from './siem_legacy_actions/format_legacy_actions';
|
||||
export { addGeneratedActionValues } from './add_generated_action_values';
|
||||
export { incrementRevision } from './increment_revision';
|
||||
|
|
|
@ -5,23 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectsFindOptions, SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import type { SavedObjectsFindResult, SavedObjectAttribute } from '@kbn/core/server';
|
||||
|
||||
import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyGetBulkRuleActionsSavedObject } from './legacy_get_bulk_rule_actions_saved_object';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRulesActionsSavedObject } from './legacy_get_rule_actions_saved_object';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyRuleActionsSavedObjectType } from './legacy_saved_object_mappings';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types';
|
||||
import { Rule } from '../../../types';
|
||||
|
||||
describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
||||
import {
|
||||
legacyGetBulkRuleActionsSavedObject,
|
||||
LegacyActionsObj,
|
||||
formatLegacyActions,
|
||||
} from './format_legacy_actions';
|
||||
import { legacyRuleActionsSavedObjectType } from './types';
|
||||
|
||||
describe('legacyGetBulkRuleActionsSavedObject', () => {
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
type FuncReturn = Record<string, LegacyRulesActionsSavedObject>;
|
||||
type FuncReturn = Record<string, LegacyActionsObj>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.createLogger();
|
||||
|
@ -34,10 +34,9 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('calls "savedObjectsClient.find" with the expected "hasReferences"', () => {
|
||||
legacyGetBulkRuleActionsSavedObject({ alertIds: ['123'], savedObjectsClient, logger });
|
||||
const [[arg1]] = savedObjectsClient.find.mock.calls;
|
||||
expect(arg1).toEqual<SavedObjectsFindOptions>({
|
||||
test('calls "savedObjectsClient.find" with the expected "hasReferences"', async () => {
|
||||
await legacyGetBulkRuleActionsSavedObject({ alertIds: ['123'], savedObjectsClient, logger });
|
||||
expect(savedObjectsClient.find).toHaveBeenCalledWith({
|
||||
hasReference: [{ id: '123', type: 'alert' }],
|
||||
perPage: 10000,
|
||||
type: legacyRuleActionsSavedObjectType,
|
||||
|
@ -45,9 +44,7 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
|
||||
test('returns nothing transformed through the find if it does not return any matches against the alert id', async () => {
|
||||
const savedObjects: Array<
|
||||
SavedObjectsFindResult<LegacyIRuleActionsAttributesSavedObjectAttributes>
|
||||
> = [];
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [];
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
|
@ -64,9 +61,7 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
|
||||
test('returns 1 action transformed through the find if 1 was found for 1 single alert id', async () => {
|
||||
const savedObjects: Array<
|
||||
SavedObjectsFindResult<LegacyIRuleActionsAttributesSavedObjectAttributes>
|
||||
> = [
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [
|
||||
{
|
||||
score: 0,
|
||||
id: '123',
|
||||
|
@ -111,12 +106,15 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
expect(returnValue).toEqual<FuncReturn>({
|
||||
'alert-123': {
|
||||
id: '123',
|
||||
alertThrottle: '1d',
|
||||
ruleThrottle: '1d',
|
||||
actions: [
|
||||
legacyRuleActions: [
|
||||
{
|
||||
action_type_id: 'action_type_1',
|
||||
actionTypeId: 'action_type_1',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
group: 'group_1',
|
||||
id: 'action-123',
|
||||
params: {},
|
||||
|
@ -127,9 +125,7 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
|
||||
test('returns 1 action transformed through the find for 2 alerts with 1 action each', async () => {
|
||||
const savedObjects: Array<
|
||||
SavedObjectsFindResult<LegacyIRuleActionsAttributesSavedObjectAttributes>
|
||||
> = [
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [
|
||||
{
|
||||
score: 0,
|
||||
id: '123',
|
||||
|
@ -203,12 +199,16 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
expect(returnValue).toEqual<FuncReturn>({
|
||||
'alert-123': {
|
||||
id: '123',
|
||||
alertThrottle: '1d',
|
||||
ruleThrottle: '1d',
|
||||
actions: [
|
||||
|
||||
legacyRuleActions: [
|
||||
{
|
||||
action_type_id: 'action_type_1',
|
||||
actionTypeId: 'action_type_1',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
group: 'group_1',
|
||||
id: 'action-123',
|
||||
params: {},
|
||||
|
@ -216,12 +216,15 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
],
|
||||
},
|
||||
'alert-456': {
|
||||
id: '456',
|
||||
alertThrottle: '1d',
|
||||
ruleThrottle: '1d',
|
||||
actions: [
|
||||
legacyRuleActions: [
|
||||
{
|
||||
action_type_id: 'action_type_2',
|
||||
actionTypeId: 'action_type_2',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
group: 'group_2',
|
||||
id: 'action-456',
|
||||
params: {},
|
||||
|
@ -232,9 +235,7 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
|
||||
test('returns 2 actions transformed through the find if they were found for 1 single alert id', async () => {
|
||||
const savedObjects: Array<
|
||||
SavedObjectsFindResult<LegacyIRuleActionsAttributesSavedObjectAttributes>
|
||||
> = [
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [
|
||||
{
|
||||
score: 0,
|
||||
id: '123',
|
||||
|
@ -290,18 +291,26 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
expect(returnValue).toEqual<FuncReturn>({
|
||||
'alert-123': {
|
||||
id: '123',
|
||||
alertThrottle: '1d',
|
||||
ruleThrottle: '1d',
|
||||
actions: [
|
||||
legacyRuleActions: [
|
||||
{
|
||||
action_type_id: 'action_type_1',
|
||||
actionTypeId: 'action_type_1',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
group: 'group_1',
|
||||
id: 'action-123',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
action_type_id: 'action_type_2',
|
||||
actionTypeId: 'action_type_2',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
group: 'group_2',
|
||||
id: 'action-456',
|
||||
params: {},
|
||||
|
@ -312,9 +321,7 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
|
||||
test('returns only 1 action if for some unusual reason the actions reference is missing an item for 1 single alert id', async () => {
|
||||
const savedObjects: Array<
|
||||
SavedObjectsFindResult<LegacyIRuleActionsAttributesSavedObjectAttributes>
|
||||
> = [
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [
|
||||
{
|
||||
score: 0,
|
||||
id: '123',
|
||||
|
@ -366,12 +373,15 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
expect(returnValue).toEqual<FuncReturn>({
|
||||
'alert-123': {
|
||||
id: '123',
|
||||
alertThrottle: '1d',
|
||||
ruleThrottle: '1d',
|
||||
actions: [
|
||||
legacyRuleActions: [
|
||||
{
|
||||
action_type_id: 'action_type_1',
|
||||
actionTypeId: 'action_type_1',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
group: 'group_1',
|
||||
id: 'action-123',
|
||||
params: {},
|
||||
|
@ -382,9 +392,7 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
|
||||
test('returns only 1 action if for some unusual reason the action is missing from the attributes', async () => {
|
||||
const savedObjects: Array<
|
||||
SavedObjectsFindResult<LegacyIRuleActionsAttributesSavedObjectAttributes>
|
||||
> = [
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [
|
||||
{
|
||||
score: 0,
|
||||
id: '123',
|
||||
|
@ -435,12 +443,15 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
expect(returnValue).toEqual<FuncReturn>({
|
||||
'alert-123': {
|
||||
id: '123',
|
||||
alertThrottle: '1d',
|
||||
ruleThrottle: '1d',
|
||||
actions: [
|
||||
legacyRuleActions: [
|
||||
{
|
||||
action_type_id: 'action_type_1',
|
||||
actionTypeId: 'action_type_1',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
group: 'group_1',
|
||||
id: 'action-123',
|
||||
params: {},
|
||||
|
@ -451,9 +462,7 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
});
|
||||
|
||||
test('returns nothing if the alert id is missing within the references array', async () => {
|
||||
const savedObjects: Array<
|
||||
SavedObjectsFindResult<LegacyIRuleActionsAttributesSavedObjectAttributes>
|
||||
> = [
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [
|
||||
{
|
||||
score: 0,
|
||||
id: '123',
|
||||
|
@ -495,3 +504,112 @@ describe('legacy_get_bulk_rule_actions_saved_object', () => {
|
|||
expect(returnValue).toEqual<FuncReturn>({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatLegacyActions', () => {
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.createLogger();
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
});
|
||||
|
||||
it('should return not modified rule when error is thrown within method', async () => {
|
||||
savedObjectsClient.find.mockRejectedValueOnce(new Error('test failure'));
|
||||
const mockRules = [{ id: 'mock-id0' }, { id: 'mock-id1' }] as Rule[];
|
||||
expect(
|
||||
await formatLegacyActions(mockRules, {
|
||||
logger,
|
||||
savedObjectsClient,
|
||||
})
|
||||
).toEqual(mockRules);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`formatLegacyActions(): Failed to read legacy actions for SIEM rules mock-id0, mock-id1: test failure`
|
||||
);
|
||||
});
|
||||
|
||||
it('should format rule correctly', async () => {
|
||||
const savedObjects: Array<SavedObjectsFindResult<SavedObjectAttribute>> = [
|
||||
{
|
||||
score: 0,
|
||||
id: '123',
|
||||
type: legacyRuleActionsSavedObjectType,
|
||||
references: [
|
||||
{
|
||||
name: 'alert_0',
|
||||
id: 'alert-123',
|
||||
type: 'alert',
|
||||
},
|
||||
{
|
||||
name: 'action_0',
|
||||
id: 'action-123',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'group_1',
|
||||
params: {},
|
||||
action_type_id: 'action_type_1',
|
||||
actionRef: 'action_0',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '1d',
|
||||
alertThrottle: '1d',
|
||||
},
|
||||
},
|
||||
];
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
page: 1,
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
|
||||
const mockRules = [
|
||||
{
|
||||
id: 'alert-123',
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: 'action_type_2',
|
||||
group: 'group_1',
|
||||
id: 'action-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as Rule[];
|
||||
const migratedRules = await formatLegacyActions(mockRules, {
|
||||
logger,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
expect(migratedRules).toEqual([
|
||||
{
|
||||
// actions have been merged
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: 'action_type_2',
|
||||
group: 'group_1',
|
||||
id: 'action-456',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
actionTypeId: 'action_type_1',
|
||||
frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '1d' },
|
||||
group: 'group_1',
|
||||
id: 'action-123',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
id: 'alert-123',
|
||||
// muteAll set to false
|
||||
muteAll: false,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
throttle: '1d',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { chunk } from 'lodash';
|
||||
import type { SavedObjectsFindOptionsReference, Logger } from '@kbn/core/server';
|
||||
import pMap from 'p-map';
|
||||
import { RuleAction, Rule } from '../../../types';
|
||||
import type { RuleExecutorServices } from '../../..';
|
||||
import { injectReferencesIntoActions } from '../../common';
|
||||
import { transformToNotifyWhen } from './transform_to_notify_when';
|
||||
import { transformFromLegacyActions } from './transform_legacy_actions';
|
||||
import { LegacyIRuleActionsAttributes, legacyRuleActionsSavedObjectType } from './types';
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
interface LegacyGetBulkRuleActionsSavedObject {
|
||||
alertIds: string[];
|
||||
savedObjectsClient: RuleExecutorServices['savedObjectsClient'];
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export interface LegacyActionsObj {
|
||||
ruleThrottle: string | null;
|
||||
legacyRuleActions: RuleAction[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
* this function finds all legacy actions associated with rules in bulk
|
||||
* it's useful for such methods as find, so we do not request legacy actions in a separate request per rule
|
||||
* @params params.alertIds - list of rule ids to look for legacy actions for
|
||||
* @params params.savedObjectsClient - savedObjectsClient
|
||||
* @params params.logger - logger
|
||||
* @returns map of legacy actions objects per rule with legacy actions
|
||||
*/
|
||||
export const legacyGetBulkRuleActionsSavedObject = async ({
|
||||
alertIds,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
}: LegacyGetBulkRuleActionsSavedObject): Promise<Record<string, LegacyActionsObj>> => {
|
||||
const references = alertIds.map<SavedObjectsFindOptionsReference>((alertId) => ({
|
||||
id: alertId,
|
||||
type: 'alert',
|
||||
}));
|
||||
const errors: unknown[] = [];
|
||||
const results = await pMap(
|
||||
chunk(references, 1000),
|
||||
async (referencesChunk) => {
|
||||
try {
|
||||
return savedObjectsClient.find<LegacyIRuleActionsAttributes>({
|
||||
// here we looking legacyRuleActionsSavedObjectType, as not all of rules create `siem.notifications`
|
||||
// more information on that can be found in https://github.com/elastic/kibana/pull/130511 PR
|
||||
type: legacyRuleActionsSavedObjectType,
|
||||
perPage: 10000,
|
||||
hasReference: referencesChunk,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error fetching rule actions: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
errors.push(error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
{ concurrency: 1 }
|
||||
);
|
||||
const actionSavedObjects = results.flat().flatMap((r) => r.saved_objects);
|
||||
|
||||
if (errors.length) {
|
||||
throw new AggregateError(errors, 'Error fetching rule actions');
|
||||
}
|
||||
|
||||
return actionSavedObjects.reduce((acc: { [key: string]: LegacyActionsObj }, savedObject) => {
|
||||
const ruleAlertId = savedObject.references.find((reference) => {
|
||||
// Find the first rule alert and assume that is the one we want since we should only ever have 1.
|
||||
return reference.type === 'alert';
|
||||
});
|
||||
// We check to ensure we have found a "ruleAlertId" and hopefully we have.
|
||||
const ruleAlertIdKey = ruleAlertId != null ? ruleAlertId.id : undefined;
|
||||
if (ruleAlertIdKey != null) {
|
||||
const legacyRawActions = transformFromLegacyActions(
|
||||
savedObject.attributes,
|
||||
savedObject.references
|
||||
);
|
||||
acc[ruleAlertIdKey] = {
|
||||
ruleThrottle: savedObject.attributes.ruleThrottle,
|
||||
legacyRuleActions: injectReferencesIntoActions(
|
||||
ruleAlertIdKey,
|
||||
legacyRawActions,
|
||||
savedObject.references
|
||||
) // remove uuid from action, as this uuid is not persistent
|
||||
.map(({ uuid, ...action }) => action),
|
||||
};
|
||||
} else {
|
||||
logger.error(
|
||||
`Security Solution notification (Legacy) Was expecting to find a reference of type "alert" within ${savedObject.references} but did not. Skipping this notification.`
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
* formats rules with associated SIEM legacy actions, if any legacy actions present
|
||||
* @param rules - list of rules to format
|
||||
* @param params - logger, savedObjectsClient
|
||||
* @returns
|
||||
*/
|
||||
export const formatLegacyActions = async <T extends Rule>(
|
||||
rules: T[],
|
||||
{ logger, savedObjectsClient }: Omit<LegacyGetBulkRuleActionsSavedObject, 'alertIds'>
|
||||
): Promise<T[]> => {
|
||||
try {
|
||||
const res = await legacyGetBulkRuleActionsSavedObject({
|
||||
alertIds: rules.map((rule) => rule.id),
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
return rules.map((rule) => {
|
||||
const legacyRuleActionsMatch = res[rule.id];
|
||||
if (!legacyRuleActionsMatch) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
const { legacyRuleActions, ruleThrottle } = legacyRuleActionsMatch;
|
||||
return {
|
||||
...rule,
|
||||
actions: [...rule.actions, ...legacyRuleActions],
|
||||
throttle: (legacyRuleActions.length ? ruleThrottle : rule.throttle) ?? 'no_actions',
|
||||
notifyWhen: transformToNotifyWhen(ruleThrottle),
|
||||
// muteAll property is disregarded in further rule processing in Security Solution when legacy actions are present.
|
||||
// So it should be safe to set it as false, so it won't be displayed to user as w/o actions see transformFromAlertThrottle method
|
||||
muteAll: legacyRuleActions.length ? false : rule.muteAll,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
const ruleIds = rules.map((rule) => rule.id).join(', ');
|
||||
logger.error(
|
||||
`formatLegacyActions(): Failed to read legacy actions for SIEM rules ${ruleIds}: ${e.message}`
|
||||
);
|
||||
return rules;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,357 @@
|
|||
/*
|
||||
* 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 { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
|
||||
import { migrateLegacyActions } from './migrate_legacy_actions';
|
||||
import { retrieveMigratedLegacyActions } from './retrieve_migrated_legacy_actions';
|
||||
import { injectReferencesIntoActions } from '../../common';
|
||||
|
||||
import { validateActions } from '../validate_actions';
|
||||
import { RulesClientContext } from '../..';
|
||||
import { RawRuleAction, RawRule } from '../../../types';
|
||||
|
||||
import { UntypedNormalizedRuleType } from '../../../rule_type_registry';
|
||||
import { RecoveredActionGroup } from '../../../../common';
|
||||
|
||||
jest.mock('./retrieve_migrated_legacy_actions', () => ({
|
||||
retrieveMigratedLegacyActions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../validate_actions', () => ({
|
||||
validateActions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common', () => ({
|
||||
injectReferencesIntoActions: jest.fn(),
|
||||
}));
|
||||
|
||||
const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
||||
id: 'test',
|
||||
name: 'My test rule',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
getSummarizedAlerts: jest.fn(),
|
||||
};
|
||||
|
||||
const context = {
|
||||
ruleTypeRegistry: {
|
||||
get: () => ruleType,
|
||||
},
|
||||
logger: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
} as unknown as RulesClientContext;
|
||||
|
||||
const ruleId = 'rule_id_1';
|
||||
|
||||
const attributes = {
|
||||
alertTypeId: 'siem.query',
|
||||
consumer: AlertConsumers.SIEM,
|
||||
} as unknown as RawRule;
|
||||
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValue({
|
||||
legacyActions: [],
|
||||
legacyActionsReferences: [],
|
||||
});
|
||||
|
||||
const legacyActionsMock: RawRuleAction[] = [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
uuid: '11403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
actionRef: 'action_0',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const legacyReferencesMock: SavedObjectReference[] = [
|
||||
{
|
||||
id: 'cc85da20-d480-11ed-8e69-1df522116c28',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
];
|
||||
|
||||
const existingActionsMock: RawRuleAction[] = [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
body: {
|
||||
test_web_hook: 'alert.id - {{alert.id}}',
|
||||
},
|
||||
},
|
||||
actionTypeId: '.webhook',
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
actionRef: 'action_0',
|
||||
},
|
||||
];
|
||||
|
||||
const referencesMock: SavedObjectReference[] = [
|
||||
{
|
||||
id: 'b2fd3f90-cd81-11ed-9f6d-a746729ca213',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
];
|
||||
|
||||
describe('migrateLegacyActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return empty migratedActions when error is thrown within method', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockRejectedValueOnce(new Error('test failure'));
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(migratedActions).toEqual({
|
||||
resultedActions: [],
|
||||
hasLegacyActions: false,
|
||||
resultedReferences: [],
|
||||
});
|
||||
expect(context.logger.error).toHaveBeenCalledWith(
|
||||
`migrateLegacyActions(): Failed to migrate legacy actions for SIEM rule ${ruleId}: test failure`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return earley empty migratedActions when consumer is not SIEM', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValue({
|
||||
legacyActions: [],
|
||||
legacyActionsReferences: [],
|
||||
});
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes: { ...attributes, consumer: 'mine' },
|
||||
});
|
||||
|
||||
expect(migratedActions).toEqual({
|
||||
resultedActions: [],
|
||||
hasLegacyActions: false,
|
||||
resultedReferences: [],
|
||||
});
|
||||
expect(retrieveMigratedLegacyActions).not.toHaveBeenCalled();
|
||||
expect(validateActions).not.toHaveBeenCalled();
|
||||
expect(injectReferencesIntoActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call retrieveMigratedLegacyActions with correct rule id', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValue({
|
||||
legacyActions: [],
|
||||
legacyActionsReferences: [],
|
||||
});
|
||||
await migrateLegacyActions(context, { ruleId, attributes });
|
||||
|
||||
expect(retrieveMigratedLegacyActions).toHaveBeenCalledWith(context, { ruleId });
|
||||
});
|
||||
|
||||
it('should not call validateActions and injectReferencesIntoActions if skipActionsValidation=true', async () => {
|
||||
await migrateLegacyActions(context, { ruleId, attributes, skipActionsValidation: true });
|
||||
|
||||
expect(validateActions).not.toHaveBeenCalled();
|
||||
expect(injectReferencesIntoActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call validateActions and injectReferencesIntoActions if attributes provided', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValueOnce({
|
||||
legacyActions: legacyActionsMock,
|
||||
legacyActionsReferences: legacyReferencesMock,
|
||||
});
|
||||
|
||||
(injectReferencesIntoActions as jest.Mock).mockReturnValue('actions-with-references');
|
||||
await migrateLegacyActions(context, { ruleId, attributes });
|
||||
|
||||
expect(validateActions).toHaveBeenCalledWith(context, ruleType, {
|
||||
...attributes,
|
||||
actions: 'actions-with-references',
|
||||
});
|
||||
|
||||
expect(injectReferencesIntoActions).toHaveBeenCalledWith(
|
||||
'rule_id_1',
|
||||
[
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
uuid: '11403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
},
|
||||
],
|
||||
[{ id: 'cc85da20-d480-11ed-8e69-1df522116c28', name: 'action_0', type: 'action' }]
|
||||
);
|
||||
});
|
||||
|
||||
it('should set frequency props from rule level to existing actions', async () => {
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
actions: existingActionsMock,
|
||||
references: referencesMock,
|
||||
attributes: { ...attributes, throttle: '1h', notifyWhen: 'onThrottleInterval' },
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', false);
|
||||
expect(result).toHaveProperty('resultedReferences', referencesMock);
|
||||
expect(result).toHaveProperty('resultedActions', [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.webhook',
|
||||
group: 'default',
|
||||
params: { body: { test_web_hook: 'alert.id - {{alert.id}}' } },
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1h',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return correct response when legacy actions empty and existing empty', async () => {
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
actions: existingActionsMock,
|
||||
references: referencesMock,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', false);
|
||||
expect(result).toHaveProperty('resultedReferences', referencesMock);
|
||||
expect(result).toHaveProperty('resultedActions', [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.webhook',
|
||||
group: 'default',
|
||||
params: { body: { test_web_hook: 'alert.id - {{alert.id}}' } },
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
summary: true,
|
||||
throttle: null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return correct response when legacy actions empty and existing actions empty', async () => {
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', false);
|
||||
expect(result).toHaveProperty('resultedReferences', []);
|
||||
expect(result).toHaveProperty('resultedActions', []);
|
||||
});
|
||||
it('should return correct response when existing actions empty and legacy present', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValueOnce({
|
||||
legacyActions: legacyActionsMock,
|
||||
legacyActionsReferences: legacyReferencesMock,
|
||||
});
|
||||
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', true);
|
||||
expect(result).toHaveProperty('resultedReferences', legacyReferencesMock);
|
||||
expect(result).toHaveProperty('resultedActions', legacyActionsMock);
|
||||
});
|
||||
it('should merge actions and references correctly when existing and legacy actions both present', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValueOnce({
|
||||
legacyActions: legacyActionsMock,
|
||||
legacyActionsReferences: legacyReferencesMock,
|
||||
});
|
||||
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
actions: existingActionsMock,
|
||||
references: referencesMock,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result.resultedReferences[0].name).toBe('action_0');
|
||||
expect(result.resultedReferences[1].name).toBe('action_1');
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', true);
|
||||
|
||||
// ensure references are correct
|
||||
expect(result.resultedReferences[0].name).toBe('action_0');
|
||||
expect(result.resultedReferences[1].name).toBe('action_1');
|
||||
expect(result).toHaveProperty('resultedReferences', [
|
||||
{
|
||||
id: 'b2fd3f90-cd81-11ed-9f6d-a746729ca213',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
{
|
||||
id: 'cc85da20-d480-11ed-8e69-1df522116c28',
|
||||
name: 'action_1',
|
||||
type: 'action',
|
||||
},
|
||||
]);
|
||||
|
||||
// ensure actionsRefs are correct
|
||||
expect(result.resultedActions[0].actionRef).toBe('action_0');
|
||||
expect(result.resultedActions[1].actionRef).toBe('action_1');
|
||||
expect(result).toHaveProperty('resultedActions', [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.webhook',
|
||||
group: 'default',
|
||||
params: { body: { test_web_hook: 'alert.id - {{alert.id}}' } },
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
summary: true,
|
||||
throttle: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
actionRef: 'action_1',
|
||||
actionTypeId: '.email',
|
||||
frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '1d' },
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
uuid: '11403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { RulesClientContext } from '../..';
|
||||
import { RawRuleAction, RawRule } from '../../../types';
|
||||
import { validateActions } from '../validate_actions';
|
||||
import { injectReferencesIntoActions } from '../../common';
|
||||
import { retrieveMigratedLegacyActions } from './retrieve_migrated_legacy_actions';
|
||||
|
||||
type MigrateLegacyActions = (
|
||||
context: RulesClientContext,
|
||||
params: {
|
||||
ruleId: string;
|
||||
references?: SavedObjectReference[];
|
||||
actions?: RawRuleAction[];
|
||||
attributes: RawRule;
|
||||
skipActionsValidation?: boolean;
|
||||
}
|
||||
) => Promise<{
|
||||
resultedActions: RawRuleAction[];
|
||||
resultedReferences: SavedObjectReference[];
|
||||
hasLegacyActions: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
* migrates SIEM legacy actions and merges rule actions and references
|
||||
* @param context RulesClient context
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const migrateLegacyActions: MigrateLegacyActions = async (
|
||||
context: RulesClientContext,
|
||||
{ ruleId, actions = [], references = [], attributes, skipActionsValidation }
|
||||
) => {
|
||||
try {
|
||||
if (attributes.consumer !== AlertConsumers.SIEM) {
|
||||
return {
|
||||
resultedActions: [],
|
||||
hasLegacyActions: false,
|
||||
resultedReferences: [],
|
||||
};
|
||||
}
|
||||
|
||||
const { legacyActions, legacyActionsReferences } = await retrieveMigratedLegacyActions(
|
||||
context,
|
||||
{
|
||||
ruleId,
|
||||
}
|
||||
);
|
||||
|
||||
// sometimes we don't need to validate legacy actions. For example, when delete rules or update rule from payload
|
||||
if (skipActionsValidation !== true) {
|
||||
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId);
|
||||
await validateActions(context, ruleType, {
|
||||
...attributes,
|
||||
// set to undefined to avoid both per-actin and rule level values clashing
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
actions: injectReferencesIntoActions(ruleId, legacyActions, legacyActionsReferences),
|
||||
});
|
||||
}
|
||||
|
||||
// fix references for a case when actions present in a rule
|
||||
if (actions.length) {
|
||||
legacyActions.forEach((legacyAction, i) => {
|
||||
const oldReference = legacyAction.actionRef;
|
||||
const legacyReference = legacyActionsReferences.find(({ name }) => name === oldReference);
|
||||
const newReference = `action_${actions.length + i}`;
|
||||
legacyAction.actionRef = newReference;
|
||||
|
||||
if (legacyReference) {
|
||||
legacyReference.name = newReference;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// put frequencies into existing actions
|
||||
// the situation when both action in rule nad legacy exist should be rare one
|
||||
// but if it happens, we put frequency in existing actions per-action level
|
||||
const existingActionsWithFrequencies: RawRuleAction[] = actions.map((action) => ({
|
||||
...action,
|
||||
frequency: {
|
||||
summary: true,
|
||||
notifyWhen: attributes.notifyWhen ?? 'onActiveAlert',
|
||||
throttle: attributes.throttle ?? null,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
resultedActions: [...existingActionsWithFrequencies, ...legacyActions],
|
||||
hasLegacyActions: legacyActions.length > 0,
|
||||
resultedReferences: [...references, ...legacyActionsReferences],
|
||||
};
|
||||
} catch (e) {
|
||||
context.logger.error(
|
||||
`migrateLegacyActions(): Failed to migrate legacy actions for SIEM rule ${ruleId}: ${e.message}`
|
||||
);
|
||||
|
||||
return {
|
||||
resultedActions: [],
|
||||
hasLegacyActions: false,
|
||||
resultedReferences: [],
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,368 @@
|
|||
/*
|
||||
* 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 {
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectAttribute,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import type { LegacyRuleNotificationAlertType } from './types';
|
||||
|
||||
export const migrateLegacyActionsMock = {
|
||||
legacyActions: [],
|
||||
legacyActionsReferences: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetHourlyNotificationResult = (
|
||||
id = '456',
|
||||
ruleId = '123'
|
||||
): LegacyRuleNotificationAlertType => ({
|
||||
id,
|
||||
name: 'Notification for Rule Test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: `${ruleId}`,
|
||||
},
|
||||
schedule: {
|
||||
interval: '1h',
|
||||
},
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2020-03-21T11:15:13.530Z'),
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7',
|
||||
updatedAt: new Date('2020-03-21T12:37:08.730Z'),
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
revision: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetWeeklyNotificationResult = (
|
||||
id = '456',
|
||||
ruleId = '123'
|
||||
): LegacyRuleNotificationAlertType => ({
|
||||
id,
|
||||
name: 'Notification for Rule Test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: `${ruleId}`,
|
||||
},
|
||||
schedule: {
|
||||
interval: '7d',
|
||||
},
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2020-03-21T11:15:13.530Z'),
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7',
|
||||
updatedAt: new Date('2020-03-21T12:37:08.730Z'),
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
revision: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetDailyNotificationResult = (
|
||||
id = '456',
|
||||
ruleId = '123'
|
||||
): LegacyRuleNotificationAlertType => ({
|
||||
id,
|
||||
name: 'Notification for Rule Test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: `${ruleId}`,
|
||||
},
|
||||
schedule: {
|
||||
interval: '1d',
|
||||
},
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2020-03-21T11:15:13.530Z'),
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7',
|
||||
updatedAt: new Date('2020-03-21T12:37:08.730Z'),
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
revision: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleNoActionsSOResult = (
|
||||
ruleId = '123'
|
||||
): SavedObjectsFindResult<SavedObjectAttribute> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_NO_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [],
|
||||
ruleThrottle: 'no_actions',
|
||||
alertThrottle: null,
|
||||
},
|
||||
references: [{ id: ruleId, type: 'alert', name: 'alert_0' }],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleEveryRunSOResult = (
|
||||
ruleId = '123'
|
||||
): SavedObjectsFindResult<SavedObjectAttribute> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: 'rule',
|
||||
alertThrottle: null,
|
||||
},
|
||||
references: [{ id: ruleId, type: 'alert', name: 'alert_0' }],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleHourlyActionsSOResult = (
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResult<SavedObjectAttribute> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '1h',
|
||||
alertThrottle: '1h',
|
||||
},
|
||||
references: [
|
||||
{ id: ruleId, type: 'alert', name: 'alert_0' },
|
||||
{ id: connectorId, type: 'action', name: 'action_0' },
|
||||
],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleDailyActionsSOResult = (
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResult<SavedObjectAttribute> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '1d',
|
||||
alertThrottle: '1d',
|
||||
},
|
||||
references: [
|
||||
{ id: ruleId, type: 'alert', name: 'alert_0' },
|
||||
{ id: connectorId, type: 'action', name: 'action_0' },
|
||||
],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleWeeklyActionsSOResult = (
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResult<SavedObjectAttribute> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '7d',
|
||||
alertThrottle: '7d',
|
||||
},
|
||||
references: [
|
||||
{ id: ruleId, type: 'alert', name: 'alert_0' },
|
||||
{ id: connectorId, type: 'action', name: 'action_0' },
|
||||
],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
const getLegacyActionSOs = (ruleId = '123', connectorId = '456') => ({
|
||||
none: () => legacyGetSiemNotificationRuleNoActionsSOResult(ruleId),
|
||||
rule: () => legacyGetSiemNotificationRuleEveryRunSOResult(ruleId),
|
||||
hourly: () => legacyGetSiemNotificationRuleHourlyActionsSOResult(ruleId, connectorId),
|
||||
daily: () => legacyGetSiemNotificationRuleDailyActionsSOResult(ruleId, connectorId),
|
||||
weekly: () => legacyGetSiemNotificationRuleWeeklyActionsSOResult(ruleId, connectorId),
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleActionsSOResultWithSingleHit = (
|
||||
actionTypes: Array<'none' | 'rule' | 'daily' | 'hourly' | 'weekly'>,
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResponse<SavedObjectAttribute> => {
|
||||
const actions = getLegacyActionSOs(ruleId, connectorId);
|
||||
|
||||
return {
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
total: 1,
|
||||
saved_objects: actionTypes.map((type) => actions[type]()),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* 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 { RulesClientContext } from '../..';
|
||||
import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import {
|
||||
legacyGetDailyNotificationResult,
|
||||
legacyGetHourlyNotificationResult,
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit,
|
||||
legacyGetWeeklyNotificationResult,
|
||||
} from './retrieve_migrated_legacy_actions.mock';
|
||||
|
||||
import { retrieveMigratedLegacyActions } from './retrieve_migrated_legacy_actions';
|
||||
|
||||
import { find } from '../../methods/find';
|
||||
import { deleteRule } from '../../methods/delete';
|
||||
|
||||
jest.mock('../../methods/find', () => {
|
||||
return {
|
||||
find: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../methods/delete', () => {
|
||||
return {
|
||||
deleteRule: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const findMock = find as jest.Mock;
|
||||
const deleteRuleMock = deleteRule as jest.Mock;
|
||||
|
||||
const getEmptyFindResult = () => ({
|
||||
page: 0,
|
||||
per_page: 0,
|
||||
total: 0,
|
||||
data: [],
|
||||
});
|
||||
|
||||
describe('Legacy rule action migration logic', () => {
|
||||
describe('legacyMigrate', () => {
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.createLogger();
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
});
|
||||
|
||||
const ruleId = '123';
|
||||
const connectorId = '456';
|
||||
|
||||
test('it does no cleanup or migration if no legacy remnants found', async () => {
|
||||
findMock.mockResolvedValueOnce(getEmptyFindResult());
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
page: 1,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
const migratedProperties = await retrieveMigratedLegacyActions(
|
||||
{
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
logger,
|
||||
} as unknown as RulesClientContext,
|
||||
{ ruleId }
|
||||
);
|
||||
|
||||
expect(deleteRuleMock).not.toHaveBeenCalled();
|
||||
expect(savedObjectsClient.delete).not.toHaveBeenCalled();
|
||||
expect(migratedProperties).toEqual({ legacyActions: [], legacyActionsReferences: [] });
|
||||
});
|
||||
|
||||
// Even if a rule is created with no actions pre 7.16, a
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
test('it migrates a rule with no actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
findMock.mockResolvedValueOnce(getEmptyFindResult());
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['none'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedProperties = await retrieveMigratedLegacyActions(
|
||||
{
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
logger,
|
||||
} as unknown as RulesClientContext,
|
||||
{ ruleId }
|
||||
);
|
||||
|
||||
expect(deleteRuleMock).not.toHaveBeenCalled();
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_NO_ACTIONS'
|
||||
);
|
||||
expect(migratedProperties).toEqual({ legacyActions: [], legacyActionsReferences: [] });
|
||||
});
|
||||
|
||||
test('it migrates a rule with every rule run action', async () => {
|
||||
// siem.notifications is not created for a rule with actions run every rule run
|
||||
findMock.mockResolvedValueOnce(getEmptyFindResult());
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['rule'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedProperties = await retrieveMigratedLegacyActions(
|
||||
{
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
logger,
|
||||
} as unknown as RulesClientContext,
|
||||
{ ruleId }
|
||||
);
|
||||
|
||||
expect(deleteRuleMock).not.toHaveBeenCalled();
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS'
|
||||
);
|
||||
|
||||
expect(migratedProperties).toEqual({ legacyActions: [], legacyActionsReferences: [] });
|
||||
});
|
||||
|
||||
test('it migrates a rule with daily legacy actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
findMock.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: [legacyGetDailyNotificationResult(connectorId, ruleId)],
|
||||
});
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['daily'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedProperties = await retrieveMigratedLegacyActions(
|
||||
{
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
logger,
|
||||
} as unknown as RulesClientContext,
|
||||
{ ruleId }
|
||||
);
|
||||
|
||||
expect(deleteRuleMock).toHaveBeenCalledWith(expect.any(Object), { id: '456' });
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS'
|
||||
);
|
||||
|
||||
expect(migratedProperties).toEqual({
|
||||
legacyActions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.email',
|
||||
frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '1d' },
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
],
|
||||
legacyActionsReferences: [{ id: '456', name: 'action_0', type: 'action' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('it migrates a rule with hourly legacy actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
findMock.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: [legacyGetHourlyNotificationResult(connectorId, ruleId)],
|
||||
});
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['hourly'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedProperties = await retrieveMigratedLegacyActions(
|
||||
{
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
logger,
|
||||
} as unknown as RulesClientContext,
|
||||
{ ruleId }
|
||||
);
|
||||
|
||||
expect(deleteRuleMock).toHaveBeenCalledWith(expect.any(Object), { id: '456' });
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS'
|
||||
);
|
||||
expect(migratedProperties).toEqual({
|
||||
legacyActions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.email',
|
||||
frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '1h' },
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
],
|
||||
legacyActionsReferences: [{ id: '456', name: 'action_0', type: 'action' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('it migrates a rule with weekly legacy actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
findMock.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: [legacyGetWeeklyNotificationResult(connectorId, ruleId)],
|
||||
});
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['weekly'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedProperties = await retrieveMigratedLegacyActions(
|
||||
{
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
logger,
|
||||
} as unknown as RulesClientContext,
|
||||
{ ruleId }
|
||||
);
|
||||
|
||||
expect(deleteRuleMock).toHaveBeenCalledWith(expect.any(Object), { id: '456' });
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS'
|
||||
);
|
||||
expect(migratedProperties).toEqual({
|
||||
legacyActions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.email',
|
||||
frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '7d' },
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
],
|
||||
legacyActionsReferences: [{ id: '456', name: 'action_0', type: 'action' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { RulesClientContext } from '../..';
|
||||
import { RawRuleAction } from '../../../types';
|
||||
import { find } from '../../methods/find';
|
||||
import { deleteRule } from '../../methods/delete';
|
||||
import { LegacyIRuleActionsAttributes, legacyRuleActionsSavedObjectType } from './types';
|
||||
import { transformFromLegacyActions } from './transform_legacy_actions';
|
||||
|
||||
type RetrieveMigratedLegacyActions = (
|
||||
context: RulesClientContext,
|
||||
{ ruleId }: { ruleId: string }
|
||||
) => Promise<{ legacyActions: RawRuleAction[]; legacyActionsReferences: SavedObjectReference[] }>;
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
* retrieves legacy actions for SIEM rule and deletes associated sidecar SO
|
||||
* @param context RulesClient context
|
||||
* @param params.ruleId - id of rule to be migrated
|
||||
* @returns
|
||||
*/
|
||||
export const retrieveMigratedLegacyActions: RetrieveMigratedLegacyActions = async (
|
||||
context,
|
||||
{ ruleId }
|
||||
) => {
|
||||
const { unsecuredSavedObjectsClient } = context;
|
||||
try {
|
||||
if (ruleId == null) {
|
||||
return { legacyActions: [], legacyActionsReferences: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* On update / patch I'm going to take the actions as they are, better off taking rules client.find (siem.notification) result
|
||||
* and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actual value (1hr etc..)
|
||||
* Then use the rules client to delete the siem.notification
|
||||
* Then with the legacy Rule Actions saved object type, just delete it.
|
||||
*/
|
||||
// find it using the references array, not params.ruleAlertId
|
||||
const [siemNotification, legacyRuleActionsSO] = await Promise.all([
|
||||
find(context, {
|
||||
options: {
|
||||
filter: 'alert.attributes.alertTypeId:(siem.notifications)',
|
||||
hasReference: {
|
||||
type: 'alert',
|
||||
id: ruleId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
unsecuredSavedObjectsClient.find<LegacyIRuleActionsAttributes>({
|
||||
type: legacyRuleActionsSavedObjectType,
|
||||
hasReference: {
|
||||
type: 'alert',
|
||||
id: ruleId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const siemNotificationsExist = siemNotification != null && siemNotification.data.length > 0;
|
||||
const legacyRuleNotificationSOsExist =
|
||||
legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0;
|
||||
|
||||
// Assumption: if no legacy sidecar SO or notification rule types exist
|
||||
// that reference the rule in question, assume rule actions are not legacy
|
||||
if (!siemNotificationsExist && !legacyRuleNotificationSOsExist) {
|
||||
return { legacyActions: [], legacyActionsReferences: [] };
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
// If the legacy notification rule type ("siem.notification") exist,
|
||||
// migration and cleanup are needed
|
||||
siemNotificationsExist && deleteRule(context, { id: siemNotification.data[0].id }),
|
||||
// Delete the legacy sidecar SO if it exists
|
||||
legacyRuleNotificationSOsExist &&
|
||||
unsecuredSavedObjectsClient.delete(
|
||||
legacyRuleActionsSavedObjectType,
|
||||
legacyRuleActionsSO.saved_objects[0].id
|
||||
),
|
||||
]);
|
||||
|
||||
// If legacy notification sidecar ("siem-detection-engine-rule-actions")
|
||||
// exist, migration is needed
|
||||
if (legacyRuleNotificationSOsExist) {
|
||||
// If "siem-detection-engine-rule-actions" notes that `ruleThrottle` is
|
||||
// "no_actions" or "rule", rule has no actions or rule is set to run
|
||||
// action on every rule run. In these cases, sidecar deletion is the only
|
||||
// cleanup needed and updates to the "throttle" and "notifyWhen". "siem.notification" are
|
||||
// not created for these action types
|
||||
if (
|
||||
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'no_actions' ||
|
||||
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'rule'
|
||||
) {
|
||||
return { legacyActions: [], legacyActionsReferences: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
legacyActions: transformFromLegacyActions(
|
||||
legacyRuleActionsSO.saved_objects[0].attributes,
|
||||
legacyRuleActionsSO.saved_objects[0].references
|
||||
),
|
||||
legacyActionsReferences:
|
||||
// only action references need to be saved
|
||||
legacyRuleActionsSO.saved_objects[0].references.filter(({ type }) => type === 'action') ??
|
||||
[],
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
context.logger.debug(`Migration has failed for rule ${ruleId}: ${e.message}`);
|
||||
}
|
||||
|
||||
return { legacyActions: [], legacyActionsReferences: [] };
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
|
||||
import { transformFromLegacyActions } from './transform_legacy_actions';
|
||||
import { transformToNotifyWhen } from './transform_to_notify_when';
|
||||
import { LegacyIRuleActionsAttributes } from './types';
|
||||
|
||||
jest.mock('./transform_to_notify_when', () => ({
|
||||
transformToNotifyWhen: jest.fn(),
|
||||
}));
|
||||
|
||||
const legacyActionsAttr: LegacyIRuleActionsAttributes = {
|
||||
actions: [
|
||||
{
|
||||
group: 'group_1',
|
||||
params: {},
|
||||
action_type_id: 'action_type_1',
|
||||
actionRef: 'action_0',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '1d',
|
||||
alertThrottle: '1d',
|
||||
};
|
||||
|
||||
const references: SavedObjectReference[] = [
|
||||
{
|
||||
name: 'action_0',
|
||||
id: 'action-123',
|
||||
type: 'action',
|
||||
},
|
||||
];
|
||||
|
||||
describe('transformFromLegacyActions', () => {
|
||||
it('should throw error if if references are empty', () => {
|
||||
const executor = () => {
|
||||
return transformFromLegacyActions(legacyActionsAttr, []);
|
||||
};
|
||||
expect(executor).toThrow('Connector reference id not found.');
|
||||
});
|
||||
it('should return empty list of actions if legacy actions do not have correct references', () => {
|
||||
const actions = transformFromLegacyActions(legacyActionsAttr, [
|
||||
{
|
||||
name: 'alert_0',
|
||||
id: 'alert-1',
|
||||
type: 'alert',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(actions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return notifyWhen as result of transformToNotifyWhen if it is not null', () => {
|
||||
(transformToNotifyWhen as jest.Mock).mockReturnValueOnce('onActiveAlert');
|
||||
const actions = transformFromLegacyActions(legacyActionsAttr, references);
|
||||
|
||||
expect(transformToNotifyWhen).toHaveBeenCalledWith('1d');
|
||||
expect(actions[0].frequency?.notifyWhen).toBe('onActiveAlert');
|
||||
});
|
||||
|
||||
it('should return notifyWhen as onThrottleInterval when transformToNotifyWhen returns null', () => {
|
||||
(transformToNotifyWhen as jest.Mock).mockReturnValueOnce(null);
|
||||
const actions = transformFromLegacyActions(legacyActionsAttr, references);
|
||||
|
||||
expect(actions[0].frequency?.notifyWhen).toBe('onThrottleInterval');
|
||||
});
|
||||
|
||||
it('should return transformed legacy actions', () => {
|
||||
(transformToNotifyWhen as jest.Mock).mockReturnValue('onThrottleInterval');
|
||||
|
||||
const actions = transformFromLegacyActions(legacyActionsAttr, references);
|
||||
|
||||
expect(actions).toEqual([
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'action_type_1',
|
||||
frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '1d' },
|
||||
group: 'group_1',
|
||||
params: {},
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { v4 } from 'uuid';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
import { RawRuleAction } from '../../../types';
|
||||
import { transformToNotifyWhen } from './transform_to_notify_when';
|
||||
import { LegacyIRuleActionsAttributes } from './types';
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
* transforms siem legacy actions {@link LegacyIRuleActionsAttributes} objects into {@link RawRuleAction}
|
||||
* @param legacyActionsAttr
|
||||
* @param references
|
||||
* @returns array of RawRuleAction
|
||||
*/
|
||||
export const transformFromLegacyActions = (
|
||||
legacyActionsAttr: LegacyIRuleActionsAttributes,
|
||||
references: SavedObjectReference[]
|
||||
): RawRuleAction[] => {
|
||||
const actionReference = references.reduce<Record<string, SavedObjectReference>>(
|
||||
(acc, reference) => {
|
||||
acc[reference.name] = reference;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (isEmpty(actionReference)) {
|
||||
throw new Error(`Connector reference id not found.`);
|
||||
}
|
||||
|
||||
return legacyActionsAttr.actions.reduce<RawRuleAction[]>((acc, action) => {
|
||||
const { actionRef, action_type_id: actionTypeId, group, params } = action;
|
||||
if (!actionReference[actionRef]) {
|
||||
return acc;
|
||||
}
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
group,
|
||||
params,
|
||||
uuid: v4(),
|
||||
actionRef,
|
||||
actionTypeId,
|
||||
frequency: {
|
||||
summary: true,
|
||||
notifyWhen: transformToNotifyWhen(legacyActionsAttr.ruleThrottle) ?? 'onThrottleInterval',
|
||||
throttle: legacyActionsAttr.ruleThrottle,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { transformToNotifyWhen } from './transform_to_notify_when';
|
||||
|
||||
describe('transformToNotifyWhen', () => {
|
||||
it('should return null when throttle is null OR no_actions', () => {
|
||||
expect(transformToNotifyWhen(null)).toBeNull();
|
||||
expect(transformToNotifyWhen('no_actions')).toBeNull();
|
||||
});
|
||||
it('should return onActiveAlert when throttle is rule', () => {
|
||||
expect(transformToNotifyWhen('rule')).toBe('onActiveAlert');
|
||||
});
|
||||
it('should return onThrottleInterval for other throttle values', () => {
|
||||
expect(transformToNotifyWhen('1h')).toBe('onThrottleInterval');
|
||||
expect(transformToNotifyWhen('1m')).toBe('onThrottleInterval');
|
||||
expect(transformToNotifyWhen('1d')).toBe('onThrottleInterval');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { RuleNotifyWhenType } from '../../../../common';
|
||||
|
||||
/**
|
||||
* Given a throttle from a "security_solution" rule this will transform it into an "alerting" notifyWhen
|
||||
* on their saved object.
|
||||
* @params throttle The throttle from a "security_solution" rule
|
||||
* @returns The correct "NotifyWhen" for a Kibana alerting.
|
||||
*/
|
||||
export const transformToNotifyWhen = (
|
||||
throttle: string | null | undefined
|
||||
): RuleNotifyWhenType | null => {
|
||||
if (throttle == null || throttle === 'no_actions') {
|
||||
return null; // Although I return null, this does not change the value of the "notifyWhen" and it keeps the current value of "notifyWhen"
|
||||
} else if (throttle === 'rule') {
|
||||
return 'onActiveAlert';
|
||||
} else {
|
||||
return 'onThrottleInterval';
|
||||
}
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { RuleActionParams } from '../../../types';
|
||||
import type { RuleTypeParams } from '../../..';
|
||||
import type { Rule } from '../../../../common';
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyRuleActionsSavedObjectType = 'siem-detection-engine-rule-actions';
|
||||
|
||||
/**
|
||||
* This is how it is stored on disk in its "raw format" for 7.16+
|
||||
* @deprecated Remove this once the legacy notification/side car is gone
|
||||
*/
|
||||
export interface LegacyRuleAlertSavedObjectAction {
|
||||
group: string;
|
||||
params: RuleActionParams;
|
||||
action_type_id: string;
|
||||
actionRef: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations.
|
||||
* @deprecated Remove this once the legacy notification/side car is gone
|
||||
*/
|
||||
export interface LegacyIRuleActionsAttributes extends Record<string, unknown> {
|
||||
actions: LegacyRuleAlertSavedObjectAction[];
|
||||
ruleThrottle: string;
|
||||
alertThrottle: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
interface LegacyRuleNotificationAlertTypeParams extends RuleTypeParams {
|
||||
ruleAlertId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export type LegacyRuleNotificationAlertType = Rule<LegacyRuleNotificationAlertTypeParams>;
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import pMap from 'p-map';
|
||||
import { KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { SavedObjectsBulkUpdateObject } from '@kbn/core/server';
|
||||
import { withSpan } from '@kbn/apm-utils';
|
||||
|
@ -13,7 +13,13 @@ import { convertRuleIdsToKueryNode } from '../../lib';
|
|||
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { tryToRemoveTasks } from '../common';
|
||||
import { getAuthorizationFilter, checkAuthorizationAndGetTotal, getAlertFromRaw } from '../lib';
|
||||
import { API_KEY_GENERATE_CONCURRENCY } from '../common/constants';
|
||||
import {
|
||||
getAuthorizationFilter,
|
||||
checkAuthorizationAndGetTotal,
|
||||
getAlertFromRaw,
|
||||
migrateLegacyActions,
|
||||
} from '../lib';
|
||||
import {
|
||||
retryIfBulkOperationConflicts,
|
||||
buildKueryNodeFilter,
|
||||
|
@ -166,6 +172,20 @@ const bulkDeleteWithOCC = async (
|
|||
});
|
||||
const rules = rulesToDelete.filter((rule) => deletedRuleIds.includes(rule.id));
|
||||
|
||||
// migrate legacy actions only for SIEM rules
|
||||
await pMap(
|
||||
rules,
|
||||
async (rule) => {
|
||||
await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
attributes: rule.attributes as RawRule,
|
||||
skipActionsValidation: true,
|
||||
});
|
||||
},
|
||||
// max concurrency for bulk edit operations, that is limited by api key generations, should be sufficient for bulk migrations
|
||||
{ concurrency: API_KEY_GENERATE_CONCURRENCY }
|
||||
);
|
||||
|
||||
return {
|
||||
errors,
|
||||
rules,
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { SavedObjectsBulkUpdateObject } from '@kbn/core/server';
|
||||
import { withSpan } from '@kbn/apm-utils';
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
getAlertFromRaw,
|
||||
recoverRuleAlerts,
|
||||
updateMeta,
|
||||
migrateLegacyActions,
|
||||
} from '../lib';
|
||||
import { BulkOptions, BulkOperationError, RulesClientContext } from '../types';
|
||||
import { tryToRemoveTasks } from '../common';
|
||||
|
@ -119,8 +119,22 @@ const bulkDisableRulesWithOCC = async (
|
|||
ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
|
||||
}
|
||||
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
actions: rule.attributes.actions,
|
||||
references: rule.references,
|
||||
attributes: rule.attributes,
|
||||
});
|
||||
|
||||
const updatedAttributes = updateMeta(context, {
|
||||
...rule.attributes,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? {
|
||||
actions: migratedActions.resultedActions,
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
}
|
||||
: {}),
|
||||
enabled: false,
|
||||
scheduledTaskId:
|
||||
rule.attributes.scheduledTaskId === rule.id
|
||||
|
@ -135,6 +149,9 @@ const bulkDisableRulesWithOCC = async (
|
|||
attributes: {
|
||||
...updatedAttributes,
|
||||
},
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { references: migratedActions.resultedReferences }
|
||||
: {}),
|
||||
});
|
||||
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -69,6 +69,8 @@ import {
|
|||
NormalizedAlertActionWithGeneratedValues,
|
||||
} from '../types';
|
||||
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
|
||||
export type BulkEditFields = keyof Pick<
|
||||
Rule,
|
||||
'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey'
|
||||
|
@ -449,6 +451,19 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleTypePara
|
|||
|
||||
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);
|
||||
|
||||
|
@ -625,7 +640,9 @@ async function getUpdatedAttributesFromOperations(
|
|||
|
||||
// TODO https://github.com/elastic/kibana/issues/148414
|
||||
// If any action-level frequencies get pushed into a SIEM rule, strip their frequencies
|
||||
const firstFrequency = updatedOperation.value[0]?.frequency;
|
||||
const firstFrequency = updatedOperation.value.find(
|
||||
(action) => action?.frequency
|
||||
)?.frequency;
|
||||
if (rule.attributes.consumer === AlertConsumers.SIEM && firstFrequency) {
|
||||
ruleActions.actions = ruleActions.actions.map((action) => omit(action, 'frequency'));
|
||||
if (!attributes.notifyWhen) {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import pMap from 'p-map';
|
||||
import { KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { SavedObjectsBulkUpdateObject } from '@kbn/core/server';
|
||||
|
@ -26,6 +25,7 @@ import {
|
|||
scheduleTask,
|
||||
updateMeta,
|
||||
createNewAPIKeySet,
|
||||
migrateLegacyActions,
|
||||
} from '../lib';
|
||||
import { RulesClientContext, BulkOperationError, BulkOptions } from '../types';
|
||||
|
||||
|
@ -143,6 +143,13 @@ const bulkEnableRulesWithOCC = async (
|
|||
ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
|
||||
}
|
||||
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
actions: rule.attributes.actions,
|
||||
references: rule.references,
|
||||
attributes: rule.attributes,
|
||||
});
|
||||
|
||||
const updatedAttributes = updateMeta(context, {
|
||||
...rule.attributes,
|
||||
...(!rule.attributes.apiKey &&
|
||||
|
@ -152,6 +159,13 @@ const bulkEnableRulesWithOCC = async (
|
|||
username,
|
||||
shouldUpdateApiKey: true,
|
||||
}))),
|
||||
...(migratedActions.hasLegacyActions
|
||||
? {
|
||||
actions: migratedActions.resultedActions,
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
}
|
||||
: {}),
|
||||
enabled: true,
|
||||
updatedBy: username,
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
@ -187,6 +201,9 @@ const bulkEnableRulesWithOCC = async (
|
|||
...updatedAttributes,
|
||||
...(scheduledTaskId ? { scheduledTaskId } : undefined),
|
||||
},
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { references: migratedActions.resultedReferences }
|
||||
: {}),
|
||||
});
|
||||
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -113,7 +113,7 @@ export async function create<Params extends RuleTypeParams = never>(
|
|||
|
||||
// TODO https://github.com/elastic/kibana/issues/148414
|
||||
// If any action-level frequencies get pushed into a SIEM rule, strip their frequencies
|
||||
const firstFrequency = data.actions[0]?.frequency;
|
||||
const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency;
|
||||
if (data.consumer === AlertConsumers.SIEM && firstFrequency) {
|
||||
data.actions = data.actions.map((action) => omit(action, 'frequency'));
|
||||
if (!data.notifyWhen) {
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { RawRule } from '../../types';
|
||||
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
|
||||
export async function deleteRule(context: RulesClientContext, { id }: { id: string }) {
|
||||
return await retryIfConflicts(
|
||||
|
@ -64,6 +66,11 @@ async function deleteWithOCC(context: RulesClientContext, { id }: { id: string }
|
|||
throw error;
|
||||
}
|
||||
|
||||
// migrate legacy actions only for SIEM rules
|
||||
if (attributes.consumer === AlertConsumers.SIEM) {
|
||||
await migrateLegacyActions(context, { ruleId: id, attributes, skipActionsValidation: true });
|
||||
}
|
||||
|
||||
context.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.DELETE,
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
|
||||
import { RawRule } from '../../types';
|
||||
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
|
||||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { recoverRuleAlerts, updateMeta } from '../lib';
|
||||
import { recoverRuleAlerts, updateMeta, migrateLegacyActions } from '../lib';
|
||||
|
||||
export async function disable(context: RulesClientContext, { id }: { id: string }): Promise<void> {
|
||||
return await retryIfConflicts(
|
||||
|
@ -23,6 +24,7 @@ export async function disable(context: RulesClientContext, { id }: { id: string
|
|||
async function disableWithOCC(context: RulesClientContext, { id }: { id: string }) {
|
||||
let attributes: RawRule;
|
||||
let version: string | undefined;
|
||||
let references: SavedObjectReference[];
|
||||
|
||||
try {
|
||||
const decryptedAlert =
|
||||
|
@ -31,12 +33,14 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
});
|
||||
attributes = decryptedAlert.attributes;
|
||||
version = decryptedAlert.version;
|
||||
references = decryptedAlert.references;
|
||||
} catch (e) {
|
||||
context.logger.error(`disable(): Failed to load API key of alert ${id}: ${e.message}`);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await context.unsecuredSavedObjectsClient.get<RawRule>('alert', id);
|
||||
attributes = alert.attributes;
|
||||
version = alert.version;
|
||||
references = alert.references;
|
||||
}
|
||||
|
||||
await recoverRuleAlerts(context, id, attributes);
|
||||
|
@ -70,6 +74,13 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
|
||||
|
||||
if (attributes.enabled === true) {
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: id,
|
||||
actions: attributes.actions,
|
||||
references,
|
||||
attributes,
|
||||
});
|
||||
|
||||
await context.unsecuredSavedObjectsClient.update(
|
||||
'alert',
|
||||
id,
|
||||
|
@ -80,8 +91,16 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
updatedBy: await context.getUserName(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
nextRun: null,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { actions: migratedActions.resultedActions, throttle: undefined, notifyWhen: undefined }
|
||||
: {}),
|
||||
}),
|
||||
{ version }
|
||||
{
|
||||
version,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { references: migratedActions.resultedReferences }
|
||||
: {}),
|
||||
}
|
||||
);
|
||||
|
||||
// If the scheduledTaskId does not match the rule id, we should
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { RawRule, IntervalSchedule } from '../../types';
|
||||
import { resetMonitoringLastRun, getNextRun } from '../../lib';
|
||||
|
@ -12,7 +12,7 @@ import { WriteOperations, AlertingAuthorizationEntity } from '../../authorizatio
|
|||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { updateMeta, createNewAPIKeySet, scheduleTask } from '../lib';
|
||||
import { updateMeta, createNewAPIKeySet, scheduleTask, migrateLegacyActions } from '../lib';
|
||||
|
||||
export async function enable(context: RulesClientContext, { id }: { id: string }): Promise<void> {
|
||||
return await retryIfConflicts(
|
||||
|
@ -26,6 +26,7 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string }
|
|||
let existingApiKey: string | null = null;
|
||||
let attributes: RawRule;
|
||||
let version: string | undefined;
|
||||
let references: SavedObjectReference[];
|
||||
|
||||
try {
|
||||
const decryptedAlert =
|
||||
|
@ -35,12 +36,14 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string }
|
|||
existingApiKey = decryptedAlert.attributes.apiKey;
|
||||
attributes = decryptedAlert.attributes;
|
||||
version = decryptedAlert.version;
|
||||
references = decryptedAlert.references;
|
||||
} catch (e) {
|
||||
context.logger.error(`enable(): Failed to load API key of alert ${id}: ${e.message}`);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await context.unsecuredSavedObjectsClient.get<RawRule>('alert', id);
|
||||
attributes = alert.attributes;
|
||||
version = alert.version;
|
||||
references = alert.references;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -76,6 +79,13 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string }
|
|||
context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
|
||||
|
||||
if (attributes.enabled === false) {
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: id,
|
||||
actions: attributes.actions,
|
||||
references,
|
||||
attributes,
|
||||
});
|
||||
|
||||
const username = await context.getUserName();
|
||||
const now = new Date();
|
||||
|
||||
|
@ -107,7 +117,29 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string }
|
|||
});
|
||||
|
||||
try {
|
||||
await context.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
|
||||
// to mitigate AAD issues(actions property is not used for encrypting API key in partial SO update)
|
||||
// we call create with overwrite=true
|
||||
if (migratedActions.hasLegacyActions) {
|
||||
await context.unsecuredSavedObjectsClient.create<RawRule>(
|
||||
'alert',
|
||||
{
|
||||
...updateAttributes,
|
||||
actions: migratedActions.resultedActions,
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
},
|
||||
{
|
||||
id,
|
||||
overwrite: true,
|
||||
version,
|
||||
references: migratedActions.resultedReferences,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await context.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, {
|
||||
version,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ import Boom from '@hapi/boom';
|
|||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { pick } from 'lodash';
|
||||
import { KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { RawRule, RuleTypeParams, SanitizedRule } from '../../types';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { RawRule, RuleTypeParams, SanitizedRule, Rule } from '../../types';
|
||||
import { AlertingAuthorizationEntity } from '../../authorization';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import {
|
||||
|
@ -27,6 +28,7 @@ import {
|
|||
import { alertingAuthorizationFilterOpts } from '../common/constants';
|
||||
import { getAlertFromRaw } from '../lib/get_alert_from_raw';
|
||||
import type { IndexType, RulesClientContext } from '../types';
|
||||
import { formatLegacyActions } from '../lib';
|
||||
|
||||
export interface FindParams {
|
||||
options?: FindOptions;
|
||||
|
@ -132,6 +134,8 @@ export async function find<Params extends RuleTypeParams = never>(
|
|||
type: 'alert',
|
||||
});
|
||||
|
||||
const siemRules: Rule[] = [];
|
||||
|
||||
const authorizedData = data.map(({ id, attributes, references }) => {
|
||||
try {
|
||||
ensureRuleTypeIsAuthorized(
|
||||
|
@ -149,7 +153,8 @@ export async function find<Params extends RuleTypeParams = never>(
|
|||
);
|
||||
throw error;
|
||||
}
|
||||
return getAlertFromRaw<Params>(
|
||||
|
||||
const rule = getAlertFromRaw<Params>(
|
||||
context,
|
||||
id,
|
||||
attributes.alertTypeId,
|
||||
|
@ -159,6 +164,13 @@ export async function find<Params extends RuleTypeParams = never>(
|
|||
excludeFromPublicApi,
|
||||
includeSnoozeData
|
||||
);
|
||||
|
||||
// collect SIEM rule for further formatting legacy actions
|
||||
if (attributes.consumer === AlertConsumers.SIEM) {
|
||||
siemRules.push(rule);
|
||||
}
|
||||
|
||||
return rule;
|
||||
});
|
||||
|
||||
authorizedData.forEach(({ id }) =>
|
||||
|
@ -170,6 +182,27 @@ export async function find<Params extends RuleTypeParams = never>(
|
|||
)
|
||||
);
|
||||
|
||||
// format legacy actions for SIEM rules, if there any
|
||||
if (siemRules.length) {
|
||||
const formattedRules = await formatLegacyActions(siemRules, {
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
logger: context.logger,
|
||||
});
|
||||
|
||||
const formattedRulesMap = formattedRules.reduce<Record<string, Rule>>((acc, rule) => {
|
||||
acc[rule.id] = rule;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
page,
|
||||
perPage,
|
||||
total,
|
||||
// replace siem formatted rules
|
||||
data: authorizedData.map((rule) => formattedRulesMap[rule.id] ?? rule),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
perPage,
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RawRule, SanitizedRule, RuleTypeParams, SanitizedRuleWithLegacyId } from '../../types';
|
||||
import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { getAlertFromRaw } from '../lib/get_alert_from_raw';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { formatLegacyActions } from '../lib';
|
||||
|
||||
export interface GetParams {
|
||||
id: string;
|
||||
|
@ -51,7 +53,7 @@ export async function get<Params extends RuleTypeParams = never>(
|
|||
savedObject: { type: 'alert', id },
|
||||
})
|
||||
);
|
||||
return getAlertFromRaw<Params>(
|
||||
const rule = getAlertFromRaw<Params>(
|
||||
context,
|
||||
result.id,
|
||||
result.attributes.alertTypeId,
|
||||
|
@ -61,4 +63,16 @@ export async function get<Params extends RuleTypeParams = never>(
|
|||
excludeFromPublicApi,
|
||||
includeSnoozeData
|
||||
);
|
||||
|
||||
// format legacy actions for SIEM rules
|
||||
if (result.attributes.consumer === AlertConsumers.SIEM) {
|
||||
const [migratedRule] = await formatLegacyActions([rule], {
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
logger: context.logger,
|
||||
});
|
||||
|
||||
return migratedRule;
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RawRule, RuleTypeParams, ResolvedSanitizedRule } from '../../types';
|
||||
import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { getAlertFromRaw } from '../lib/get_alert_from_raw';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { formatLegacyActions } from '../lib';
|
||||
|
||||
export interface ResolveParams {
|
||||
id: string;
|
||||
|
@ -58,6 +61,19 @@ export async function resolve<Params extends RuleTypeParams = never>(
|
|||
includeSnoozeData
|
||||
);
|
||||
|
||||
// format legacy actions for SIEM rules
|
||||
if (result.attributes.consumer === AlertConsumers.SIEM) {
|
||||
const [migratedRule] = await formatLegacyActions([rule], {
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
logger: context.logger,
|
||||
});
|
||||
|
||||
return {
|
||||
...migratedRule,
|
||||
...resolveResponse,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...rule,
|
||||
...resolveResponse,
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
addGeneratedActionValues,
|
||||
incrementRevision,
|
||||
createNewAPIKeySet,
|
||||
migrateLegacyActions,
|
||||
} from '../lib';
|
||||
|
||||
export interface UpdateOptions<Params extends RuleTypeParams> {
|
||||
|
@ -115,10 +116,24 @@ async function updateWithOCC<Params extends RuleTypeParams>(
|
|||
|
||||
context.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId);
|
||||
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: id,
|
||||
attributes: alertSavedObject.attributes,
|
||||
});
|
||||
|
||||
const updateResult = await updateAlert<Params>(
|
||||
context,
|
||||
{ id, data, allowMissingConnectorSecrets, shouldIncrementRevision },
|
||||
alertSavedObject
|
||||
migratedActions.hasLegacyActions
|
||||
? {
|
||||
...alertSavedObject,
|
||||
attributes: {
|
||||
...alertSavedObject.attributes,
|
||||
notifyWhen: undefined,
|
||||
throttle: undefined,
|
||||
},
|
||||
}
|
||||
: alertSavedObject
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
|
@ -173,7 +188,7 @@ async function updateAlert<Params extends RuleTypeParams>(
|
|||
|
||||
// TODO https://github.com/elastic/kibana/issues/148414
|
||||
// If any action-level frequencies get pushed into a SIEM rule, strip their frequencies
|
||||
const firstFrequency = data.actions[0]?.frequency;
|
||||
const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency;
|
||||
if (attributes.consumer === AlertConsumers.SIEM && firstFrequency) {
|
||||
data.actions = data.actions.map((action) => omit(action, 'frequency'));
|
||||
if (!attributes.notifyWhen) {
|
||||
|
|
|
@ -25,7 +25,20 @@ import {
|
|||
enabledRule2,
|
||||
returnedRule1,
|
||||
returnedRule2,
|
||||
siemRule1,
|
||||
} from './test_helpers';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -453,6 +466,46 @@ describe('bulkDelete', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield { saved_objects: [enabledRule1, enabledRule2, siemRule1] };
|
||||
},
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({
|
||||
statuses: [
|
||||
{ id: enabledRule1.id, type: 'alert', success: true },
|
||||
{ id: enabledRule2.id, type: 'alert', success: true },
|
||||
{ id: siemRule1.id, type: 'alert', success: true },
|
||||
],
|
||||
});
|
||||
|
||||
await rulesClient.bulkDeleteRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(3);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: enabledRule1.id,
|
||||
skipActionsValidation: true,
|
||||
attributes: enabledRule1.attributes,
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: enabledRule2.id,
|
||||
skipActionsValidation: true,
|
||||
attributes: enabledRule2.attributes,
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: siemRule1.id,
|
||||
skipActionsValidation: true,
|
||||
attributes: siemRule1.attributes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditLogger', () => {
|
||||
jest.spyOn(auditLogger, 'log').mockImplementation();
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
|
@ -29,7 +29,16 @@ import {
|
|||
savedObjectWith500Error,
|
||||
returnedDisabledRule1,
|
||||
returnedDisabledRule2,
|
||||
siemRule1,
|
||||
siemRule2,
|
||||
} from './test_helpers';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -126,6 +135,11 @@ describe('bulkDisableRules', () => {
|
|||
});
|
||||
mockCreatePointInTimeFinderAsInternalUser();
|
||||
mockUnsecuredSavedObjectFind(2);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should disable two rule', async () => {
|
||||
|
@ -598,4 +612,49 @@ describe('bulkDisableRules', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield { saved_objects: [enabledRule1, enabledRule2, siemRule1, siemRule2] };
|
||||
},
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [enabledRule1, enabledRule2, siemRule1, siemRule2],
|
||||
});
|
||||
|
||||
await rulesClient.bulkDisableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(4);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: enabledRule1.attributes,
|
||||
ruleId: enabledRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: enabledRule2.attributes,
|
||||
ruleId: enabledRule2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
|
@ -21,6 +22,20 @@ 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';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -196,6 +211,8 @@ describe('bulkEdit()', () => {
|
|||
},
|
||||
producer: 'alerts',
|
||||
});
|
||||
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);
|
||||
});
|
||||
|
||||
describe('tags operations', () => {
|
||||
|
@ -2491,4 +2508,53 @@ describe('bulkEdit()', () => {
|
|||
expect(taskManager.bulkUpdateSchedules).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield { saved_objects: [enabledRule1, enabledRule2, siemRule1, siemRule2] };
|
||||
},
|
||||
});
|
||||
|
||||
await rulesClient.bulkEdit({
|
||||
operations: [
|
||||
{
|
||||
field: 'tags',
|
||||
operation: 'set',
|
||||
value: ['test-tag'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(4);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: enabledRule1.attributes,
|
||||
ruleId: enabledRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: enabledRule2.attributes,
|
||||
ruleId: enabledRule2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
|
@ -29,8 +29,17 @@ import {
|
|||
savedObjectWith500Error,
|
||||
returnedRule1,
|
||||
returnedRule2,
|
||||
siemRule1,
|
||||
siemRule2,
|
||||
} from './test_helpers';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -78,6 +87,11 @@ beforeEach(() => {
|
|||
} as unknown as BulkUpdateTaskResult)
|
||||
);
|
||||
(auditLogger.log as jest.Mock).mockClear();
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
setGlobalDate();
|
||||
|
@ -779,4 +793,43 @@ describe('bulkEnableRules', () => {
|
|||
expect(auditLogger.log.mock.calls[0][0]?.event?.outcome).toEqual('failure');
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield { saved_objects: [disabledRule1, siemRule1, siemRule2] };
|
||||
},
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [disabledRule1, siemRule1, siemRule2],
|
||||
});
|
||||
|
||||
await rulesClient.bulkEnableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(3);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: disabledRule1.attributes,
|
||||
ruleId: disabledRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
|
@ -17,6 +19,18 @@ import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup } from './lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -215,6 +229,27 @@ describe('delete()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
const existingDecryptedSiemAlert = {
|
||||
...existingDecryptedAlert,
|
||||
attributes: { ...existingDecryptedAlert.attributes, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
existingDecryptedSiemAlert
|
||||
);
|
||||
|
||||
await rulesClient.delete({ id: '1' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: '1',
|
||||
skipActionsValidation: true,
|
||||
attributes: existingDecryptedSiemAlert.attributes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to delete this type of alert under the consumer', async () => {
|
||||
await rulesClient.delete({ id: '1' });
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
@ -18,6 +19,14 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
|||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { eventLoggerMock } from '@kbn/event-log-plugin/server/event_logger.mock';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
import { migrateLegacyActionsMock } from '../lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -122,6 +131,11 @@ describe('disable()', () => {
|
|||
rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValue(existingRule);
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedRule);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
|
@ -573,4 +587,35 @@ describe('disable()', () => {
|
|||
);
|
||||
expect(taskManager.bulkDisable).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
const existingDecryptedSiemRule = {
|
||||
...existingDecryptedRule,
|
||||
attributes: { ...existingDecryptedRule.attributes, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedSiemRule);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);
|
||||
|
||||
await rulesClient.disable({ id: '1' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
actions: [
|
||||
{
|
||||
actionRef: '1',
|
||||
actionTypeId: '1',
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
ruleId: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
@ -17,6 +18,14 @@ import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
|||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { migrateLegacyActions } from '../lib';
|
||||
import { migrateLegacyActionsMock } from '../lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -122,6 +131,11 @@ describe('enable()', () => {
|
|||
apiKeysEnabled: false,
|
||||
});
|
||||
taskManager.get.mockResolvedValue(mockTask);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
|
@ -658,4 +672,58 @@ describe('enable()', () => {
|
|||
scheduledTaskId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValueOnce({
|
||||
hasLegacyActions: true,
|
||||
resultedActions: ['fake-action-1'],
|
||||
resultedReferences: ['fake-ref-1'],
|
||||
});
|
||||
|
||||
const existingDecryptedSiemRule = {
|
||||
...existingRule,
|
||||
attributes: { ...existingRule.attributes, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedSiemRule);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);
|
||||
|
||||
await rulesClient.enable({ id: '1' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
actions: [
|
||||
{
|
||||
actionRef: '1',
|
||||
actionTypeId: '1',
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
ruleId: '1',
|
||||
});
|
||||
// to mitigate AAD issues, we call create with overwrite=true and actions related props
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
expect.objectContaining({
|
||||
...existingDecryptedSiemRule.attributes,
|
||||
actions: ['fake-action-1'],
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
enabled: true,
|
||||
}),
|
||||
{
|
||||
id: existingDecryptedSiemRule.id,
|
||||
overwrite: true,
|
||||
references: ['fake-ref-1'],
|
||||
version: existingDecryptedSiemRule.version,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,14 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
|||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { RecoveredActionGroup } from '../../../common';
|
||||
import { RegistryRuleType } from '../../rule_type_registry';
|
||||
import { enabledRule1, enabledRule2, siemRule1, siemRule2 } from './test_helpers';
|
||||
import { formatLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/format_legacy_actions', () => {
|
||||
return {
|
||||
formatLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -806,4 +814,39 @@ describe('find()', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
|
||||
(formatLegacyActions as jest.Mock).mockResolvedValueOnce([
|
||||
{ ...siemRule1, migrated: true },
|
||||
{ ...siemRule2, migrated: true },
|
||||
]);
|
||||
|
||||
unsecuredSavedObjectsClient.find.mockReset();
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [enabledRule1, enabledRule2, siemRule1, siemRule2].map((r) => ({
|
||||
...r,
|
||||
score: 1,
|
||||
})),
|
||||
});
|
||||
|
||||
const result = await rulesClient.find({ options: {} });
|
||||
|
||||
expect(formatLegacyActions).toHaveBeenCalledTimes(1);
|
||||
expect(formatLegacyActions).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({ id: siemRule1.id }),
|
||||
expect.objectContaining({ id: siemRule2.id }),
|
||||
],
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(result.data[2]).toEqual(expect.objectContaining({ id: siemRule1.id, migrated: true }));
|
||||
expect(result.data[3]).toEqual(expect.objectContaining({ id: siemRule2.id, migrated: true }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
@ -17,6 +18,13 @@ import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { RecoveredActionGroup } from '../../../common';
|
||||
import { formatLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/format_legacy_actions', () => {
|
||||
return {
|
||||
formatLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -509,4 +517,72 @@ describe('get()', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
const rule = {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('should call formatLegacyActions if consumer is SIEM', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
...rule,
|
||||
attributes: {
|
||||
...rule.attributes,
|
||||
consumer: AlertConsumers.SIEM,
|
||||
},
|
||||
});
|
||||
(formatLegacyActions as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
id: 'migrated_rule_mock',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await rulesClient.get({ id: '1' });
|
||||
|
||||
expect(formatLegacyActions).toHaveBeenCalledWith(
|
||||
[expect.objectContaining({ id: '1' })],
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'migrated_rule_mock',
|
||||
});
|
||||
});
|
||||
|
||||
test('should not call formatLegacyActions if consumer is not SIEM', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(rule);
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.get({ id: '1' });
|
||||
|
||||
expect(formatLegacyActions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
@ -17,6 +18,13 @@ import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { RecoveredActionGroup } from '../../../common';
|
||||
import { formatLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/format_legacy_actions', () => {
|
||||
return {
|
||||
formatLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -583,4 +591,82 @@ describe('resolve()', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
const rule = {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('should call formatLegacyActions if consumer is SIEM', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: {
|
||||
...rule,
|
||||
attributes: {
|
||||
...rule.attributes,
|
||||
consumer: AlertConsumers.SIEM,
|
||||
},
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
(formatLegacyActions as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
id: 'migrated_rule_mock',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await rulesClient.resolve({ id: '1' });
|
||||
|
||||
expect(formatLegacyActions).toHaveBeenCalledWith(
|
||||
[expect.objectContaining({ id: '1' })],
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'migrated_rule_mock',
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
});
|
||||
|
||||
test('should not call formatLegacyActions if consumer is not SIEM', async () => {
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: rule,
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.resolve({ id: '1' });
|
||||
|
||||
expect(formatLegacyActions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { SavedObject } from '@kbn/core-saved-objects-server';
|
||||
|
||||
|
@ -41,6 +42,20 @@ export const defaultRule = {
|
|||
version: '1',
|
||||
};
|
||||
|
||||
export const siemRule1 = {
|
||||
...defaultRule,
|
||||
attributes: {
|
||||
...defaultRule.attributes,
|
||||
consumer: AlertConsumers.SIEM,
|
||||
},
|
||||
id: 'siem-id1',
|
||||
};
|
||||
|
||||
export const siemRule2 = {
|
||||
...siemRule1,
|
||||
id: 'siem-id2',
|
||||
};
|
||||
|
||||
export const enabledRule1 = {
|
||||
...defaultRule,
|
||||
attributes: {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
|
@ -22,6 +23,13 @@ import { TaskStatus } from '@kbn/task-manager-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 { migrateLegacyActions } from '../lib';
|
||||
|
||||
jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@kbn/core-saved-objects-utils-server', () => {
|
||||
const actual = jest.requireActual('@kbn/core-saved-objects-utils-server');
|
||||
|
@ -164,6 +172,11 @@ describe('update()', () => {
|
|||
},
|
||||
producer: 'alerts',
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('updates given parameters', async () => {
|
||||
|
@ -2734,6 +2747,64 @@ describe('update()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
enabled: true,
|
||||
schedule: { interval: '1m' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
scheduledTaskId: 'task-123',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
const existingDecryptedSiemAlert = {
|
||||
...existingDecryptedAlert,
|
||||
attributes: { ...existingDecryptedAlert, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
existingDecryptedSiemAlert
|
||||
);
|
||||
|
||||
actionsClient.getBulk.mockReset();
|
||||
actionsClient.isPreconfigured.mockReset();
|
||||
|
||||
await rulesClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
schedule: { interval: '1m' },
|
||||
name: 'abc',
|
||||
tags: ['foo'],
|
||||
params: {
|
||||
bar: true,
|
||||
risk_score: 40,
|
||||
severity: 'low',
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: '1',
|
||||
attributes: existingDecryptedSiemAlert.attributes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the authentication API key function if the user is authenticated using an api key', async () => {
|
||||
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
|
||||
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
|
||||
|
|
|
@ -19,17 +19,6 @@ import type { ExceptionListClient } from '@kbn/lists-plugin/server';
|
|||
import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines';
|
||||
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
import { getQueryRuleParams } from '../../../rule_schema/mocks';
|
||||
import { legacyMigrate } from '../../../rule_management';
|
||||
|
||||
jest.mock('../../../rule_management/logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual(
|
||||
'../../../rule_management/logic/rule_actions/legacy_action_migration'
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../logic/rule_assets/prebuilt_rule_assets_client', () => {
|
||||
return {
|
||||
|
@ -105,8 +94,6 @@ describe('add_prepackaged_rules_route', () => {
|
|||
errors: [],
|
||||
});
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
|
||||
);
|
||||
|
|
|
@ -14,21 +14,10 @@ import {
|
|||
import { updatePrebuiltRules } from './update_prebuilt_rules';
|
||||
import { patchRules } from '../../../rule_management/logic/crud/patch_rules';
|
||||
import { getPrebuiltRuleMock, getPrebuiltThreatMatchRuleMock } from '../../mocks';
|
||||
import { legacyMigrate } from '../../../rule_management';
|
||||
import { getQueryRuleParams, getThreatRuleParams } from '../../../rule_schema/mocks';
|
||||
import { getThreatRuleParams } from '../../../rule_schema/mocks';
|
||||
|
||||
jest.mock('../../../rule_management/logic/crud/patch_rules');
|
||||
|
||||
jest.mock('../../../rule_management/logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual(
|
||||
'../../../rule_management/logic/rule_actions/legacy_action_migration'
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('updatePrebuiltRules', () => {
|
||||
let rulesClient: ReturnType<typeof rulesClientMock.create>;
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
|
@ -36,8 +25,6 @@ describe('updatePrebuiltRules', () => {
|
|||
beforeEach(() => {
|
||||
rulesClient = rulesClientMock.create();
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
});
|
||||
|
||||
it('should omit actions and enabled when calling patchRules', async () => {
|
||||
|
@ -82,7 +69,6 @@ describe('updatePrebuiltRules', () => {
|
|||
...getFindResultWithSingleHit(),
|
||||
data: [getRuleMock(getThreatRuleParams())],
|
||||
});
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getThreatRuleParams()));
|
||||
|
||||
await updatePrebuiltRules(rulesClient, savedObjectsClient, [
|
||||
{ ...prepackagedRule, ...updatedThreatParams },
|
||||
|
|
|
@ -12,7 +12,6 @@ import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server';
|
|||
import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions';
|
||||
import { MAX_RULES_TO_UPDATE_IN_PARALLEL } from '../../../../../../common/constants';
|
||||
|
||||
import { legacyMigrate } from '../../../rule_management';
|
||||
import { createRules } from '../../../rule_management/logic/crud/create_rules';
|
||||
import { readRules } from '../../../rule_management/logic/crud/read_rules';
|
||||
import { patchRules } from '../../../rule_management/logic/crud/patch_rules';
|
||||
|
@ -62,22 +61,16 @@ const createPromises = (
|
|||
id: undefined,
|
||||
});
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule: existingRule,
|
||||
});
|
||||
|
||||
if (!migratedRule) {
|
||||
if (!existingRule) {
|
||||
throw new PrepackagedRulesError(`Failed to find rule ${rule.rule_id}`, 500);
|
||||
}
|
||||
|
||||
// If we're trying to change the type of a prepackaged rule, we need to delete the old one
|
||||
// and replace it with the new rule, keeping the enabled setting, actions, throttle, id,
|
||||
// and exception lists from the old rule
|
||||
if (rule.type !== migratedRule.params.type) {
|
||||
if (rule.type !== existingRule.params.type) {
|
||||
await deleteRules({
|
||||
ruleId: migratedRule.id,
|
||||
ruleId: existingRule.id,
|
||||
rulesClient,
|
||||
});
|
||||
|
||||
|
@ -87,14 +80,14 @@ const createPromises = (
|
|||
...rule,
|
||||
// Force the prepackaged rule to use the enabled state from the existing rule,
|
||||
// regardless of what the prepackaged rule says
|
||||
enabled: migratedRule.enabled,
|
||||
actions: migratedRule.actions.map(transformAlertToRuleAction),
|
||||
enabled: existingRule.enabled,
|
||||
actions: existingRule.actions.map(transformAlertToRuleAction),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return patchRules({
|
||||
rulesClient,
|
||||
existingRule: migratedRule,
|
||||
existingRule,
|
||||
nextParams: {
|
||||
...rule,
|
||||
// Force enabled to use the enabled state from the existing rule by passing in undefined to patchRules
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { SavedObjectsFindResponse, SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import type { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
|
||||
import { ruleTypeMappings } from '@kbn/securitysolution-rules';
|
||||
import type { SanitizedRule, ResolvedSanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
|
@ -42,10 +42,7 @@ import { getFinalizeSignalsMigrationSchemaMock } from '../../../../../common/det
|
|||
import { getSignalsMigrationStatusSchemaMock } from '../../../../../common/detection_engine/schemas/request/get_signals_migration_status_schema.mock';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type {
|
||||
LegacyRuleNotificationAlertType,
|
||||
LegacyIRuleActionsAttributes,
|
||||
} from '../../rule_actions_legacy';
|
||||
import type { LegacyRuleNotificationAlertType } from '../../rule_actions_legacy';
|
||||
import type { HapiReadableStream } from '../../rule_management/logic/import/hapi_readable_stream';
|
||||
import type { RuleAlertType, RuleParams } from '../../rule_schema';
|
||||
import { getQueryRuleParams } from '../../rule_schema/mocks';
|
||||
|
@ -556,153 +553,6 @@ export const legacyGetNotificationResult = ({
|
|||
revision: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetHourlyNotificationResult = (
|
||||
id = '456',
|
||||
ruleId = '123'
|
||||
): LegacyRuleNotificationAlertType => ({
|
||||
id,
|
||||
name: 'Notification for Rule Test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: `${ruleId}`,
|
||||
},
|
||||
schedule: {
|
||||
interval: '1h',
|
||||
},
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2020-03-21T11:15:13.530Z'),
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7',
|
||||
updatedAt: new Date('2020-03-21T12:37:08.730Z'),
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
revision: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetDailyNotificationResult = (
|
||||
id = '456',
|
||||
ruleId = '123'
|
||||
): LegacyRuleNotificationAlertType => ({
|
||||
id,
|
||||
name: 'Notification for Rule Test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: `${ruleId}`,
|
||||
},
|
||||
schedule: {
|
||||
interval: '1d',
|
||||
},
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2020-03-21T11:15:13.530Z'),
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7',
|
||||
updatedAt: new Date('2020-03-21T12:37:08.730Z'),
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
revision: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetWeeklyNotificationResult = (
|
||||
id = '456',
|
||||
ruleId = '123'
|
||||
): LegacyRuleNotificationAlertType => ({
|
||||
id,
|
||||
name: 'Notification for Rule Test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: `${ruleId}`,
|
||||
},
|
||||
schedule: {
|
||||
interval: '7d',
|
||||
},
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2020-03-21T11:15:13.530Z'),
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7',
|
||||
updatedAt: new Date('2020-03-21T12:37:08.730Z'),
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
revision: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
|
@ -714,205 +564,3 @@ export const legacyGetFindNotificationsResultWithSingleHit = (
|
|||
total: 1,
|
||||
data: [legacyGetNotificationResult({ ruleId })],
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleNoActionsSOResult = (
|
||||
ruleId = '123'
|
||||
): SavedObjectsFindResult<LegacyIRuleActionsAttributes> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_NO_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [],
|
||||
ruleThrottle: 'no_actions',
|
||||
alertThrottle: null,
|
||||
},
|
||||
references: [{ id: ruleId, type: 'alert', name: 'alert_0' }],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleEveryRunSOResult = (
|
||||
ruleId = '123'
|
||||
): SavedObjectsFindResult<LegacyIRuleActionsAttributes> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: 'rule',
|
||||
alertThrottle: null,
|
||||
},
|
||||
references: [{ id: ruleId, type: 'alert', name: 'alert_0' }],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleHourlyActionsSOResult = (
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResult<LegacyIRuleActionsAttributes> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '1h',
|
||||
alertThrottle: '1h',
|
||||
},
|
||||
references: [
|
||||
{ id: ruleId, type: 'alert', name: 'alert_0' },
|
||||
{ id: connectorId, type: 'action', name: 'action_0' },
|
||||
],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleDailyActionsSOResult = (
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResult<LegacyIRuleActionsAttributes> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '1d',
|
||||
alertThrottle: '1d',
|
||||
},
|
||||
references: [
|
||||
{ id: ruleId, type: 'alert', name: 'alert_0' },
|
||||
{ id: connectorId, type: 'action', name: 'action_0' },
|
||||
],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleWeeklyActionsSOResult = (
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResult<LegacyIRuleActionsAttributes> => ({
|
||||
type: 'siem-detection-engine-rule-actions',
|
||||
id: 'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
ruleThrottle: '7d',
|
||||
alertThrottle: '7d',
|
||||
},
|
||||
references: [
|
||||
{ id: ruleId, type: 'alert', name: 'alert_0' },
|
||||
{ id: connectorId, type: 'action', name: 'action_0' },
|
||||
],
|
||||
migrationVersion: {
|
||||
'siem-detection-engine-rule-actions': '7.11.2',
|
||||
},
|
||||
coreMigrationVersion: '7.15.2',
|
||||
updated_at: '2022-03-31T19:06:40.473Z',
|
||||
version: 'WzIzNywxXQ==',
|
||||
score: 0,
|
||||
});
|
||||
|
||||
const getLegacyActionSOs = (ruleId = '123', connectorId = '456') => ({
|
||||
none: () => legacyGetSiemNotificationRuleNoActionsSOResult(ruleId),
|
||||
rule: () => legacyGetSiemNotificationRuleEveryRunSOResult(ruleId),
|
||||
hourly: () => legacyGetSiemNotificationRuleHourlyActionsSOResult(ruleId, connectorId),
|
||||
daily: () => legacyGetSiemNotificationRuleDailyActionsSOResult(ruleId, connectorId),
|
||||
weekly: () => legacyGetSiemNotificationRuleWeeklyActionsSOResult(ruleId, connectorId),
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetSiemNotificationRuleActionsSOResultWithSingleHit = (
|
||||
actionTypes: Array<'none' | 'rule' | 'daily' | 'hourly' | 'weekly'>,
|
||||
ruleId = '123',
|
||||
connectorId = '456'
|
||||
): SavedObjectsFindResponse<LegacyIRuleActionsAttributes> => {
|
||||
const actions = getLegacyActionSOs(ruleId, connectorId);
|
||||
|
||||
return {
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
total: 1,
|
||||
saved_objects: actionTypes.map((type) => actions[type]()),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -21,8 +21,6 @@ export { scheduleNotificationActions } from './logic/notifications/schedule_noti
|
|||
export { scheduleThrottledNotificationActions } from './logic/notifications/schedule_throttle_notification_actions';
|
||||
export { getNotificationResultsLink } from './logic/notifications/utils';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
export { legacyGetBulkRuleActionsSavedObject } from './logic/rule_actions/legacy_get_bulk_rule_actions_saved_object';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
export type { LegacyRulesActionsSavedObject } from './logic/rule_actions/legacy_get_rule_actions_saved_object';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
|
|
|
@ -1,84 +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 { chunk } from 'lodash';
|
||||
import type { SavedObjectsFindOptionsReference, Logger } from '@kbn/core/server';
|
||||
|
||||
import type { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyRuleActionsSavedObjectType } from './legacy_saved_object_mappings';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyGetRuleActionsFromSavedObject } from './legacy_utils';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRulesActionsSavedObject } from './legacy_get_rule_actions_saved_object';
|
||||
import { initPromisePool } from '../../../../../utils/promise_pool';
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
interface LegacyGetBulkRuleActionsSavedObject {
|
||||
alertIds: string[];
|
||||
savedObjectsClient: RuleExecutorServices['savedObjectsClient'];
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
*/
|
||||
export const legacyGetBulkRuleActionsSavedObject = async ({
|
||||
alertIds,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
}: LegacyGetBulkRuleActionsSavedObject): Promise<Record<string, LegacyRulesActionsSavedObject>> => {
|
||||
const references = alertIds.map<SavedObjectsFindOptionsReference>((alertId) => ({
|
||||
id: alertId,
|
||||
type: 'alert',
|
||||
}));
|
||||
const { results, errors } = await initPromisePool({
|
||||
concurrency: 1,
|
||||
items: chunk(references, 1000),
|
||||
executor: (referencesChunk) =>
|
||||
savedObjectsClient
|
||||
.find<LegacyIRuleActionsAttributesSavedObjectAttributes>({
|
||||
type: legacyRuleActionsSavedObjectType,
|
||||
perPage: 10000,
|
||||
hasReference: referencesChunk,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
`Error fetching rule actions: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
throw error;
|
||||
}),
|
||||
});
|
||||
if (errors.length) {
|
||||
throw new AggregateError(errors, 'Error fetching rule actions');
|
||||
}
|
||||
|
||||
const savedObjects = results.flatMap(({ result }) => result.saved_objects);
|
||||
return savedObjects.reduce(
|
||||
(acc: { [key: string]: LegacyRulesActionsSavedObject }, savedObject) => {
|
||||
const ruleAlertId = savedObject.references.find((reference) => {
|
||||
// Find the first rule alert and assume that is the one we want since we should only ever have 1.
|
||||
return reference.type === 'alert';
|
||||
});
|
||||
// We check to ensure we have found a "ruleAlertId" and hopefully we have.
|
||||
const ruleAlertIdKey = ruleAlertId != null ? ruleAlertId.id : undefined;
|
||||
if (ruleAlertIdKey != null) {
|
||||
acc[ruleAlertIdKey] = legacyGetRuleActionsFromSavedObject(savedObject, logger);
|
||||
} else {
|
||||
logger.error(
|
||||
`Security Solution notification (Legacy) Was expecting to find a reference of type "alert" within ${savedObject.references} but did not. Skipping this notification.`
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
|
@ -17,7 +17,6 @@ import {
|
|||
getBulkActionEditRequest,
|
||||
getFindResultWithSingleHit,
|
||||
getFindResultWithMultiHits,
|
||||
getRuleMock,
|
||||
} from '../../../../routes/__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__';
|
||||
import { performBulkActionRoute } from './route';
|
||||
|
@ -27,21 +26,10 @@ import {
|
|||
} from '../../../../../../../common/detection_engine/rule_management/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
|
||||
jest.mock('../../../../../machine_learning/authz');
|
||||
jest.mock('../../../logic/crud/read_rules', () => ({ readRules: jest.fn() }));
|
||||
|
||||
jest.mock('../../../logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual('../../../logic/rule_actions/legacy_action_migration');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Perform bulk action route', () => {
|
||||
const readRulesMock = readRules as jest.Mock;
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
|
@ -55,7 +43,6 @@ describe('Perform bulk action route', () => {
|
|||
logger = loggingSystemMock.createLogger();
|
||||
({ clients, context } = requestContextMock.createTools());
|
||||
ml = mlServicesMock.createSetupContract();
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
performBulkActionRoute(server.router, ml, logger);
|
||||
|
|
|
@ -8,17 +8,12 @@
|
|||
import { truncate } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { BadRequestError, transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type {
|
||||
IKibanaResponse,
|
||||
KibanaResponseFactory,
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/server';
|
||||
|
||||
import type { RulesClient, BulkOperationError } from '@kbn/alerting-plugin/server';
|
||||
import type { SanitizedRule, BulkActionSkipResult } from '@kbn/alerting-plugin/common';
|
||||
import type { BulkActionSkipResult } from '@kbn/alerting-plugin/common';
|
||||
import { AbortError } from '@kbn/kibana-utils-plugin/common';
|
||||
import type { RuleAlertType, RuleParams } from '../../../../rule_schema';
|
||||
import type { RuleAlertType } from '../../../../rule_schema';
|
||||
import type { BulkActionsDryRunErrCode } from '../../../../../../../common/constants';
|
||||
import {
|
||||
DETECTION_ENGINE_RULES_BULK_ACTION,
|
||||
|
@ -52,8 +47,6 @@ import { readRules } from '../../../logic/crud/read_rules';
|
|||
import { getExportByObjectIds } from '../../../logic/export/get_export_by_object_ids';
|
||||
import { buildSiemResponse } from '../../../../routes/utils';
|
||||
import { internalRuleToAPIResponse } from '../../../normalization/rule_converters';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
import { bulkEditRules } from '../../../logic/bulk_actions/bulk_edit_rules';
|
||||
import type { DryRunError } from '../../../logic/bulk_actions/dry_run';
|
||||
import {
|
||||
|
@ -234,39 +227,6 @@ const fetchRulesByQueryOrIds = async ({
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method to migrate any legacy actions a rule may have. If no actions or no legacy actions
|
||||
* no migration is performed.
|
||||
* @params rulesClient
|
||||
* @params savedObjectsClient
|
||||
* @params rule - rule to be migrated
|
||||
* @returns The migrated rule
|
||||
*/
|
||||
export const migrateRuleActions = async ({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
}: {
|
||||
rulesClient: RulesClient;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
rule: RuleAlertType;
|
||||
}): Promise<SanitizedRule<RuleParams>> => {
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
|
||||
// This should only be hit if `rule` passed into `legacyMigrate`
|
||||
// is `null` or `rule.id` is null which right now, as typed, should not occur
|
||||
// but catching if does, in which case something upstream would be breaking down
|
||||
if (migratedRule == null) {
|
||||
throw new Error(`An error occurred processing rule with id:${rule.id}`);
|
||||
}
|
||||
|
||||
return migratedRule;
|
||||
};
|
||||
|
||||
export const performBulkActionRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
ml: SetupPlugins['ml'],
|
||||
|
@ -359,31 +319,10 @@ export const performBulkActionRoute = (
|
|||
mlAuthz,
|
||||
});
|
||||
|
||||
// migrate legacy rule actions
|
||||
const migrationOutcome = await initPromisePool({
|
||||
concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL,
|
||||
items: rules,
|
||||
executor: async (rule) => {
|
||||
// actions only get fired when rule running, so we should be fine to migrate only enabled
|
||||
if (rule.enabled) {
|
||||
return migrateRuleActions({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
} else {
|
||||
return rule;
|
||||
}
|
||||
},
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
return buildBulkResponse(response, {
|
||||
updated: migrationOutcome.results
|
||||
.filter(({ result }) => result)
|
||||
.map(({ result }) => result),
|
||||
updated: rules,
|
||||
skipped,
|
||||
errors: [...errors, ...migrationOutcome.errors],
|
||||
errors,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -413,18 +352,12 @@ export const performBulkActionRoute = (
|
|||
return rule;
|
||||
}
|
||||
|
||||
const migratedRule = await migrateRuleActions({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
|
||||
if (!migratedRule.enabled) {
|
||||
await rulesClient.enable({ id: migratedRule.id });
|
||||
if (!rule.enabled) {
|
||||
await rulesClient.enable({ id: rule.id });
|
||||
}
|
||||
|
||||
return {
|
||||
...migratedRule,
|
||||
...rule,
|
||||
enabled: true,
|
||||
};
|
||||
},
|
||||
|
@ -446,18 +379,12 @@ export const performBulkActionRoute = (
|
|||
return rule;
|
||||
}
|
||||
|
||||
const migratedRule = await migrateRuleActions({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
|
||||
if (migratedRule.enabled) {
|
||||
await rulesClient.disable({ id: migratedRule.id });
|
||||
if (rule.enabled) {
|
||||
await rulesClient.disable({ id: rule.id });
|
||||
}
|
||||
|
||||
return {
|
||||
...migratedRule,
|
||||
...rule,
|
||||
enabled: false,
|
||||
};
|
||||
},
|
||||
|
@ -478,14 +405,8 @@ export const performBulkActionRoute = (
|
|||
return null;
|
||||
}
|
||||
|
||||
const migratedRule = await migrateRuleActions({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
|
||||
await deleteRules({
|
||||
ruleId: migratedRule.id,
|
||||
ruleId: rule.id,
|
||||
rulesClient,
|
||||
});
|
||||
|
||||
|
@ -509,18 +430,14 @@ export const performBulkActionRoute = (
|
|||
if (isDryRun) {
|
||||
return rule;
|
||||
}
|
||||
const migratedRule = await migrateRuleActions({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
|
||||
let shouldDuplicateExceptions = true;
|
||||
if (body.duplicate !== undefined) {
|
||||
shouldDuplicateExceptions = body.duplicate.include_exceptions;
|
||||
}
|
||||
|
||||
const duplicateRuleToCreate = await duplicateRule({
|
||||
rule: migratedRule,
|
||||
rule,
|
||||
});
|
||||
|
||||
const createdRule = await rulesClient.create({
|
||||
|
|
|
@ -30,8 +30,6 @@ import {
|
|||
} from '../../../../routes/utils';
|
||||
import { deleteRules } from '../../../logic/crud/delete_rules';
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
|
||||
|
||||
type Config = RouteConfig<unknown, unknown, BulkDeleteRulesRequestBody, 'delete' | 'post'>;
|
||||
|
@ -64,7 +62,6 @@ export const bulkDeleteRulesRoute = (router: SecuritySolutionPluginRouter, logge
|
|||
const ctx = await context.resolve(['core', 'securitySolution', 'alerting']);
|
||||
|
||||
const rulesClient = ctx.alerting.getRulesClient();
|
||||
const savedObjectsClient = ctx.core.savedObjects.client;
|
||||
|
||||
const rules = await Promise.all(
|
||||
request.body.map(async (payloadRule) => {
|
||||
|
@ -81,21 +78,16 @@ export const bulkDeleteRulesRoute = (router: SecuritySolutionPluginRouter, logge
|
|||
|
||||
try {
|
||||
const rule = await readRules({ rulesClient, id, ruleId });
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
if (!migratedRule) {
|
||||
if (!rule) {
|
||||
return getIdBulkError({ id, ruleId });
|
||||
}
|
||||
|
||||
await deleteRules({
|
||||
ruleId: migratedRule.id,
|
||||
ruleId: rule.id,
|
||||
rulesClient,
|
||||
});
|
||||
|
||||
return transformValidateBulkError(idOrRuleIdOrUnknown, migratedRule);
|
||||
return transformValidateBulkError(idOrRuleIdOrUnknown, rule);
|
||||
} catch (err) {
|
||||
return transformBulkError(idOrRuleIdOrUnknown, err);
|
||||
}
|
||||
|
|
|
@ -23,19 +23,9 @@ import { bulkPatchRulesRoute } from './route';
|
|||
import { getCreateRulesSchemaMock } from '../../../../../../../common/detection_engine/rule_schema/mocks';
|
||||
import { getMlRuleParams, getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
|
||||
jest.mock('../../../../../machine_learning/authz');
|
||||
|
||||
jest.mock('../../../logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual('../../../logic/rule_actions/legacy_action_migration');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Bulk patch rules route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -50,8 +40,6 @@ describe('Bulk patch rules route', () => {
|
|||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
|
||||
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // update succeeds
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
bulkPatchRulesRoute(server.router, ml, logger);
|
||||
});
|
||||
|
||||
|
@ -66,7 +54,6 @@ describe('Bulk patch rules route', () => {
|
|||
|
||||
test('returns an error in the response when updating a single rule that does not exist', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
const response = await server.inject(
|
||||
getPatchBulkRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
|
@ -86,7 +73,6 @@ describe('Bulk patch rules route', () => {
|
|||
...getFindResultWithSingleHit(),
|
||||
data: [getRuleMock(getMlRuleParams())],
|
||||
});
|
||||
(legacyMigrate as jest.Mock).mockResolvedValueOnce(getRuleMock(getMlRuleParams()));
|
||||
const request = requestMock.create({
|
||||
method: 'patch',
|
||||
path: `${DETECTION_ENGINE_RULES_URL}/bulk_update`,
|
||||
|
@ -167,8 +153,6 @@ describe('Bulk patch rules route', () => {
|
|||
|
||||
describe('request validation', () => {
|
||||
test('rejects payloads with no ID', async () => {
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const request = requestMock.create({
|
||||
method: 'patch',
|
||||
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
|
||||
|
|
|
@ -24,8 +24,6 @@ import { getIdBulkError } from '../../../utils/utils';
|
|||
import { transformValidateBulkError } from '../../../utils/validate';
|
||||
import { patchRules } from '../../../logic/crud/patch_rules';
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
|
||||
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
|
||||
import { validateRulesWithDuplicatedDefaultExceptionsList } from '../../../logic/exceptions/validate_rules_with_duplicated_default_exceptions_list';
|
||||
|
@ -98,14 +96,8 @@ export const bulkPatchRulesRoute = (
|
|||
ruleId: payloadRule.id,
|
||||
});
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule: existingRule,
|
||||
});
|
||||
|
||||
const rule = await patchRules({
|
||||
existingRule: migratedRule,
|
||||
existingRule,
|
||||
rulesClient,
|
||||
nextParams: payloadRule,
|
||||
});
|
||||
|
|
|
@ -21,19 +21,9 @@ import type { BulkError } from '../../../../routes/utils';
|
|||
import { getCreateRulesSchemaMock } from '../../../../../../../common/detection_engine/rule_schema/mocks';
|
||||
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
|
||||
jest.mock('../../../../../machine_learning/authz');
|
||||
|
||||
jest.mock('../../../logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual('../../../logic/rule_actions/legacy_action_migration');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Bulk update rules route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -50,8 +40,6 @@ describe('Bulk update rules route', () => {
|
|||
|
||||
clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index');
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
bulkUpdateRulesRoute(server.router, ml, logger);
|
||||
});
|
||||
|
||||
|
@ -66,7 +54,6 @@ describe('Bulk update rules route', () => {
|
|||
|
||||
test('returns 200 as a response when updating a single rule that does not exist', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const expected: BulkError[] = [
|
||||
{
|
||||
|
@ -130,8 +117,6 @@ describe('Bulk update rules route', () => {
|
|||
|
||||
describe('request validation', () => {
|
||||
test('rejects payloads with no ID', async () => {
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const noIdRequest = requestMock.create({
|
||||
method: 'put',
|
||||
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
|
||||
|
|
|
@ -28,8 +28,6 @@ import {
|
|||
createBulkErrorObject,
|
||||
} from '../../../../routes/utils';
|
||||
import { updateRules } from '../../../logic/crud/update_rules';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
|
||||
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
|
||||
|
@ -103,15 +101,9 @@ export const bulkUpdateRulesRoute = (
|
|||
ruleId: payloadRule.id,
|
||||
});
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule: existingRule,
|
||||
});
|
||||
|
||||
const rule = await updateRules({
|
||||
rulesClient,
|
||||
existingRule: migratedRule,
|
||||
existingRule,
|
||||
ruleUpdate: payloadRule,
|
||||
});
|
||||
if (rule != null) {
|
||||
|
|
|
@ -13,21 +13,10 @@ import {
|
|||
getFindResultWithSingleHit,
|
||||
getDeleteRequestById,
|
||||
getEmptySavedObjectsResponse,
|
||||
getRuleMock,
|
||||
} from '../../../../routes/__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__';
|
||||
import { deleteRuleRoute } from './route';
|
||||
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
|
||||
jest.mock('../../../logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual('../../../logic/rule_actions/legacy_action_migration');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Delete rule route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
|
@ -40,8 +29,6 @@ describe('Delete rule route', () => {
|
|||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse());
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
deleteRuleRoute(server.router);
|
||||
});
|
||||
|
||||
|
@ -67,7 +54,7 @@ describe('Delete rule route', () => {
|
|||
|
||||
test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const response = await server.inject(
|
||||
getDeleteRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
|
|
|
@ -19,8 +19,6 @@ import { buildSiemResponse } from '../../../../routes/utils';
|
|||
|
||||
import { deleteRules } from '../../../logic/crud/delete_rules';
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
import { getIdError, transform } from '../../../utils/utils';
|
||||
|
||||
export const deleteRuleRoute = (router: SecuritySolutionPluginRouter) => {
|
||||
|
@ -46,16 +44,10 @@ export const deleteRuleRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
|
||||
const ctx = await context.resolve(['core', 'securitySolution', 'alerting']);
|
||||
const rulesClient = ctx.alerting.getRulesClient();
|
||||
const savedObjectsClient = ctx.core.savedObjects.client;
|
||||
|
||||
const rule = await readRules({ rulesClient, id, ruleId });
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
|
||||
if (!migratedRule) {
|
||||
if (!rule) {
|
||||
const error = getIdError({ id, ruleId });
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
|
@ -64,11 +56,11 @@ export const deleteRuleRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
}
|
||||
|
||||
await deleteRules({
|
||||
ruleId: migratedRule.id,
|
||||
ruleId: rule.id,
|
||||
rulesClient,
|
||||
});
|
||||
|
||||
const transformed = transform(migratedRule);
|
||||
const transformed = transform(rule);
|
||||
if (transformed == null) {
|
||||
return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' });
|
||||
} else {
|
||||
|
|
|
@ -21,9 +21,6 @@ import { buildSiemResponse } from '../../../../routes/utils';
|
|||
import { buildRouteValidation } from '../../../../../../utils/build_validation/route_validation';
|
||||
import { transformFindAlerts } from '../../../utils/utils';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyGetBulkRuleActionsSavedObject } from '../../../../rule_actions_legacy';
|
||||
|
||||
export const findRulesRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => {
|
||||
router.get(
|
||||
{
|
||||
|
@ -49,7 +46,6 @@ export const findRulesRoute = (router: SecuritySolutionPluginRouter, logger: Log
|
|||
const { query } = request;
|
||||
const ctx = await context.resolve(['core', 'securitySolution', 'alerting']);
|
||||
const rulesClient = ctx.alerting.getRulesClient();
|
||||
const savedObjectsClient = ctx.core.savedObjects.client;
|
||||
|
||||
const rules = await findRules({
|
||||
rulesClient,
|
||||
|
@ -61,15 +57,7 @@ export const findRulesRoute = (router: SecuritySolutionPluginRouter, logger: Log
|
|||
fields: query.fields,
|
||||
});
|
||||
|
||||
const ruleIds = rules.data.map((rule) => rule.id);
|
||||
|
||||
const ruleActions = await legacyGetBulkRuleActionsSavedObject({
|
||||
alertIds: ruleIds,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
const transformed = transformFindAlerts(rules, ruleActions);
|
||||
const transformed = transformFindAlerts(rules);
|
||||
if (transformed == null) {
|
||||
return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' });
|
||||
} else {
|
||||
|
|
|
@ -22,21 +22,11 @@ import {
|
|||
} from '../../../../routes/__mocks__/request_responses';
|
||||
|
||||
import { getMlRuleParams, getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
|
||||
import { patchRuleRoute } from './route';
|
||||
|
||||
jest.mock('../../../../../machine_learning/authz');
|
||||
|
||||
jest.mock('../../../logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual('../../../logic/rule_actions/legacy_action_migration');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Patch rule route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -51,8 +41,6 @@ describe('Patch rule route', () => {
|
|||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule
|
||||
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // successful update
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
patchRuleRoute(server.router, ml);
|
||||
});
|
||||
|
||||
|
@ -67,7 +55,6 @@ describe('Patch rule route', () => {
|
|||
|
||||
test('returns 404 when updating a single rule that does not exist', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
const response = await server.inject(
|
||||
getPatchRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
|
@ -80,7 +67,6 @@ describe('Patch rule route', () => {
|
|||
});
|
||||
|
||||
test('returns error if requesting a non-rule', async () => {
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
clients.rulesClient.find.mockResolvedValue(nonRuleFindResult());
|
||||
const response = await server.inject(
|
||||
getPatchRequest(),
|
||||
|
@ -114,7 +100,6 @@ describe('Patch rule route', () => {
|
|||
...getFindResultWithSingleHit(),
|
||||
data: [getRuleMock(getMlRuleParams())],
|
||||
});
|
||||
(legacyMigrate as jest.Mock).mockResolvedValueOnce(getRuleMock(getMlRuleParams()));
|
||||
const request = requestMock.create({
|
||||
method: 'patch',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
|
|
|
@ -24,8 +24,6 @@ import { readRules } from '../../../logic/crud/read_rules';
|
|||
import { patchRules } from '../../../logic/crud/patch_rules';
|
||||
import { checkDefaultRuleExceptionListReferences } from '../../../logic/exceptions/check_for_default_rule_exception_list';
|
||||
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
import { getIdError } from '../../../utils/utils';
|
||||
import { transformValidate } from '../../../utils/validate';
|
||||
|
||||
|
@ -83,15 +81,9 @@ export const patchRuleRoute = (router: SecuritySolutionPluginRouter, ml: SetupPl
|
|||
ruleId: params.id,
|
||||
});
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule: existingRule,
|
||||
});
|
||||
|
||||
const rule = await patchRules({
|
||||
rulesClient,
|
||||
existingRule: migratedRule,
|
||||
existingRule,
|
||||
nextParams: params,
|
||||
});
|
||||
if (rule != null && rule.enabled != null && rule.name != null) {
|
||||
|
|
|
@ -20,8 +20,6 @@ import { buildSiemResponse } from '../../../../routes/utils';
|
|||
import { getIdError, transform } from '../../../utils/utils';
|
||||
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyGetRuleActionsSavedObject } from '../../../../rule_actions_legacy';
|
||||
|
||||
export const readRuleRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => {
|
||||
router.get(
|
||||
|
@ -45,7 +43,6 @@ export const readRuleRoute = (router: SecuritySolutionPluginRouter, logger: Logg
|
|||
|
||||
try {
|
||||
const rulesClient = (await context.alerting).getRulesClient();
|
||||
const savedObjectsClient = (await context.core).savedObjects.client;
|
||||
|
||||
const rule = await readRules({
|
||||
id,
|
||||
|
@ -53,13 +50,7 @@ export const readRuleRoute = (router: SecuritySolutionPluginRouter, logger: Logg
|
|||
ruleId,
|
||||
});
|
||||
if (rule != null) {
|
||||
const legacyRuleActions = await legacyGetRuleActionsSavedObject({
|
||||
savedObjectsClient,
|
||||
ruleAlertId: rule.id,
|
||||
logger,
|
||||
});
|
||||
|
||||
const transformed = transform(rule, legacyRuleActions);
|
||||
const transformed = transform(rule);
|
||||
if (transformed == null) {
|
||||
return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' });
|
||||
} else {
|
||||
|
|
|
@ -20,19 +20,9 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../../../common/constant
|
|||
import { updateRuleRoute } from './route';
|
||||
import { getUpdateRulesSchemaMock } from '../../../../../../../common/detection_engine/rule_schema/mocks';
|
||||
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
|
||||
jest.mock('../../../../../machine_learning/authz');
|
||||
|
||||
jest.mock('../../../logic/rule_actions/legacy_action_migration', () => {
|
||||
const actual = jest.requireActual('../../../logic/rule_actions/legacy_action_migration');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Update rule route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -48,8 +38,6 @@ describe('Update rule route', () => {
|
|||
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // successful update
|
||||
clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index');
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
updateRuleRoute(server.router, ml);
|
||||
});
|
||||
|
||||
|
@ -64,7 +52,7 @@ describe('Update rule route', () => {
|
|||
|
||||
test('returns 404 when updating a single rule that does not exist', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const response = await server.inject(
|
||||
getUpdateRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
|
@ -78,7 +66,6 @@ describe('Update rule route', () => {
|
|||
});
|
||||
|
||||
test('returns error when updating non-rule', async () => {
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(null);
|
||||
clients.rulesClient.find.mockResolvedValue(nonRuleFindResult());
|
||||
const response = await server.inject(
|
||||
getUpdateRequest(),
|
||||
|
|
|
@ -20,8 +20,7 @@ import { getIdError } from '../../../utils/utils';
|
|||
import { transformValidate } from '../../../utils/validate';
|
||||
import { updateRules } from '../../../logic/crud/update_rules';
|
||||
import { buildRouteValidation } from '../../../../../../utils/build_validation/route_validation';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../../../logic/rule_actions/legacy_action_migration';
|
||||
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
import { checkDefaultRuleExceptionListReferences } from '../../../logic/exceptions/check_for_default_rule_exception_list';
|
||||
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
|
||||
|
@ -72,14 +71,9 @@ export const updateRuleRoute = (router: SecuritySolutionPluginRouter, ml: SetupP
|
|||
id: request.body.id,
|
||||
});
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule: existingRule,
|
||||
});
|
||||
const rule = await updateRules({
|
||||
rulesClient,
|
||||
existingRule: migratedRule,
|
||||
existingRule,
|
||||
ruleUpdate: request.body,
|
||||
});
|
||||
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
|
||||
export * from './api/register_routes';
|
||||
|
||||
// TODO: https://github.com/elastic/kibana/pull/142950
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
export { legacyMigrate } from './logic/rule_actions/legacy_action_migration';
|
||||
|
||||
// TODO: https://github.com/elastic/kibana/pull/142950
|
||||
// TODO: Revisit and consider moving to the rule_schema subdomain
|
||||
export {
|
||||
|
@ -18,3 +14,5 @@ export {
|
|||
typeSpecificCamelToSnake,
|
||||
convertCreateAPIToInternalSchema,
|
||||
} from './normalization/rule_converters';
|
||||
|
||||
export { transformFromAlertThrottle, transformToNotifyWhen } from './normalization/rule_actions';
|
||||
|
|
|
@ -17,9 +17,6 @@ import { transformAlertsToRules, transformRuleToExportableFormat } from '../../u
|
|||
import { getRuleExceptionsForExport } from './get_export_rule_exceptions';
|
||||
import { getRuleActionConnectorsForExport } from './get_export_rule_action_connectors';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyGetBulkRuleActionsSavedObject } from '../../../rule_actions_legacy';
|
||||
|
||||
export const getExportAll = async (
|
||||
rulesClient: RulesClient,
|
||||
exceptionsClient: ExceptionListClient | undefined,
|
||||
|
@ -35,15 +32,8 @@ export const getExportAll = async (
|
|||
actionConnectors: string;
|
||||
}> => {
|
||||
const ruleAlertTypes = await getNonPackagedRules({ rulesClient });
|
||||
const alertIds = ruleAlertTypes.map((rule) => rule.id);
|
||||
const rules = transformAlertsToRules(ruleAlertTypes);
|
||||
|
||||
// Gather actions
|
||||
const legacyActions = await legacyGetBulkRuleActionsSavedObject({
|
||||
alertIds,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
});
|
||||
const rules = transformAlertsToRules(ruleAlertTypes, legacyActions);
|
||||
const exportRules = rules.map((r) => transformRuleToExportableFormat(r));
|
||||
|
||||
// Gather exceptions
|
||||
|
|
|
@ -22,8 +22,6 @@ import { transformRuleToExportableFormat } from '../../utils/utils';
|
|||
import { getRuleExceptionsForExport } from './get_export_rule_exceptions';
|
||||
import { getRuleActionConnectorsForExport } from './get_export_rule_action_connectors';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyGetBulkRuleActionsSavedObject } from '../../../rule_actions_legacy';
|
||||
import { internalRuleToAPIResponse } from '../../normalization/rule_converters';
|
||||
import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema';
|
||||
|
||||
|
@ -123,12 +121,6 @@ export const getRulesFromObjects = async (
|
|||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
});
|
||||
const alertIds = rules.data.map((rule) => rule.id);
|
||||
const legacyActions = await legacyGetBulkRuleActionsSavedObject({
|
||||
alertIds,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
const alertsAndErrors = objects.map(({ rule_id: ruleId }) => {
|
||||
const matchingRule = rules.data.find((rule) => rule.params.ruleId === ruleId);
|
||||
|
@ -139,9 +131,7 @@ export const getRulesFromObjects = async (
|
|||
) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
rule: transformRuleToExportableFormat(
|
||||
internalRuleToAPIResponse(matchingRule, legacyActions[matchingRule.id])
|
||||
),
|
||||
rule: transformRuleToExportableFormat(internalRuleToAPIResponse(matchingRule)),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
|
|
@ -16,8 +16,6 @@ import type { RulesClient } from '@kbn/alerting-plugin/server';
|
|||
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
|
||||
|
||||
import type { RuleToImport } from '../../../../../../common/detection_engine/rule_management';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate } from '../rule_actions/legacy_action_migration';
|
||||
import type { ImportRuleResponse } from '../../../routes/utils';
|
||||
import { createBulkErrorObject } from '../../../routes/utils';
|
||||
import { createRules } from '../crud/create_rules';
|
||||
|
@ -128,14 +126,9 @@ export const importRules = async ({
|
|||
status_code: 200,
|
||||
});
|
||||
} else if (rule != null && overwriteRules) {
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
await patchRules({
|
||||
rulesClient,
|
||||
existingRule: migratedRule,
|
||||
existingRule: rule,
|
||||
nextParams: {
|
||||
...parsedRule,
|
||||
exceptions_list: [...exceptions],
|
||||
|
|
|
@ -1,486 +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 { requestContextMock } from '../../../routes/__mocks__';
|
||||
import {
|
||||
getEmptyFindResult,
|
||||
legacyGetDailyNotificationResult,
|
||||
legacyGetHourlyNotificationResult,
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit,
|
||||
legacyGetWeeklyNotificationResult,
|
||||
} from '../../../routes/__mocks__/request_responses';
|
||||
|
||||
import type { RuleAlertType } from '../../../rule_schema';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyMigrate, getUpdatedActionsParams } from './legacy_action_migration';
|
||||
|
||||
const getRuleLegacyActions = (): RuleAlertType =>
|
||||
({
|
||||
id: '123',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
name: 'Simple Rule Query',
|
||||
tags: ['__internal_rule_id:ruleId', '__internal_immutable:false'],
|
||||
alertTypeId: 'siem.queryRule',
|
||||
consumer: 'siem',
|
||||
enabled: true,
|
||||
throttle: '1h',
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
monitoring: { execution: { history: [], calculated_metrics: { success_ratio: 0 } } },
|
||||
mapped_params: { risk_score: 1, severity: '60-high' },
|
||||
schedule: { interval: '5m' },
|
||||
actions: [],
|
||||
params: {
|
||||
author: [],
|
||||
description: 'Simple Rule Query',
|
||||
ruleId: 'ruleId',
|
||||
falsePositives: [],
|
||||
from: 'now-6m',
|
||||
immutable: false,
|
||||
outputIndex: '.siem-signals-default',
|
||||
maxSignals: 100,
|
||||
riskScore: 1,
|
||||
riskScoreMapping: [],
|
||||
severity: 'high',
|
||||
severityMapping: [],
|
||||
threat: [],
|
||||
to: 'now',
|
||||
references: [],
|
||||
version: 1,
|
||||
exceptionsList: [],
|
||||
type: 'query',
|
||||
language: 'kuery',
|
||||
index: ['auditbeat-*'],
|
||||
query: 'user.name: root or user.name: admin',
|
||||
},
|
||||
snoozeEndTime: null,
|
||||
updatedAt: '2022-03-31T21:47:25.695Z',
|
||||
createdAt: '2022-03-31T21:47:16.379Z',
|
||||
scheduledTaskId: '21bb9b60-b13c-11ec-99d0-asdfasdfasf',
|
||||
executionStatus: {
|
||||
status: 'pending',
|
||||
lastExecutionDate: '2022-03-31T21:47:25.695Z',
|
||||
lastDuration: 0,
|
||||
},
|
||||
} as unknown as RuleAlertType);
|
||||
|
||||
describe('Legacy rule action migration logic', () => {
|
||||
describe('legacyMigrate', () => {
|
||||
const ruleId = '123';
|
||||
const connectorId = '456';
|
||||
const { clients } = requestContextMock.createTools();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('it does no cleanup or migration if no legacy reminants found', async () => {
|
||||
clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult());
|
||||
clients.savedObjectsClient.find.mockResolvedValueOnce({
|
||||
page: 0,
|
||||
per_page: 0,
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
const rule = {
|
||||
...getRuleLegacyActions(),
|
||||
id: ruleId,
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
muteAll: true,
|
||||
} as RuleAlertType;
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient: clients.rulesClient,
|
||||
savedObjectsClient: clients.savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(clients.rulesClient.delete).not.toHaveBeenCalled();
|
||||
expect(clients.savedObjectsClient.delete).not.toHaveBeenCalled();
|
||||
expect(migratedRule).toEqual(rule);
|
||||
});
|
||||
|
||||
// Even if a rule is created with no actions pre 7.16, a
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
test('it migrates a rule with no actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult());
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
clients.savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['none'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient: clients.rulesClient,
|
||||
savedObjectsClient: clients.savedObjectsClient,
|
||||
rule: {
|
||||
...getRuleLegacyActions(),
|
||||
id: ruleId,
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
muteAll: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(clients.rulesClient.delete).not.toHaveBeenCalled();
|
||||
expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_NO_ACTIONS'
|
||||
);
|
||||
expect(migratedRule?.actions).toEqual([]);
|
||||
expect(migratedRule?.throttle).toBeNull();
|
||||
expect(migratedRule?.muteAll).toBeTruthy();
|
||||
expect(migratedRule?.notifyWhen).toEqual('onActiveAlert');
|
||||
});
|
||||
|
||||
test('it migrates a rule with every rule run action', async () => {
|
||||
// siem.notifications is not created for a rule with actions run every rule run
|
||||
clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult());
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
clients.savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['rule'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient: clients.rulesClient,
|
||||
savedObjectsClient: clients.savedObjectsClient,
|
||||
rule: {
|
||||
...getRuleLegacyActions(),
|
||||
id: ruleId,
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: '.email',
|
||||
params: {
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
},
|
||||
id: connectorId,
|
||||
group: 'default',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
muteAll: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(clients.rulesClient.delete).not.toHaveBeenCalled();
|
||||
expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS'
|
||||
);
|
||||
expect(migratedRule?.actions).toEqual([
|
||||
{
|
||||
id: connectorId,
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(migratedRule?.notifyWhen).toEqual('onActiveAlert');
|
||||
expect(migratedRule?.throttle).toBeNull();
|
||||
expect(migratedRule?.muteAll).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it migrates a rule with daily legacy actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
clients.rulesClient.find.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: [legacyGetDailyNotificationResult(connectorId, ruleId)],
|
||||
});
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
clients.savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['daily'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient: clients.rulesClient,
|
||||
savedObjectsClient: clients.savedObjectsClient,
|
||||
rule: {
|
||||
...getRuleLegacyActions(),
|
||||
id: ruleId,
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
});
|
||||
|
||||
expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' });
|
||||
expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS'
|
||||
);
|
||||
expect(migratedRule?.actions).toEqual([
|
||||
{
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(migratedRule?.throttle).toEqual('1d');
|
||||
expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval');
|
||||
expect(migratedRule?.muteAll).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it migrates a rule with hourly legacy actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
clients.rulesClient.find.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: [legacyGetHourlyNotificationResult(connectorId, ruleId)],
|
||||
});
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
clients.savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['hourly'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient: clients.rulesClient,
|
||||
savedObjectsClient: clients.savedObjectsClient,
|
||||
rule: {
|
||||
...getRuleLegacyActions(),
|
||||
id: ruleId,
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
});
|
||||
|
||||
expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' });
|
||||
expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS'
|
||||
);
|
||||
expect(migratedRule?.actions).toEqual([
|
||||
{
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(migratedRule?.throttle).toEqual('1h');
|
||||
expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval');
|
||||
expect(migratedRule?.muteAll).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it migrates a rule with weekly legacy actions', async () => {
|
||||
// siem.notifications is not created for a rule with no actions
|
||||
clients.rulesClient.find.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: [legacyGetWeeklyNotificationResult(connectorId, ruleId)],
|
||||
});
|
||||
// siem-detection-engine-rule-actions SO is still created
|
||||
clients.savedObjectsClient.find.mockResolvedValueOnce(
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['weekly'], ruleId, connectorId)
|
||||
);
|
||||
|
||||
const migratedRule = await legacyMigrate({
|
||||
rulesClient: clients.rulesClient,
|
||||
savedObjectsClient: clients.savedObjectsClient,
|
||||
rule: {
|
||||
...getRuleLegacyActions(),
|
||||
id: ruleId,
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
});
|
||||
|
||||
expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' });
|
||||
expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith(
|
||||
'siem-detection-engine-rule-actions',
|
||||
'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS'
|
||||
);
|
||||
expect(migratedRule?.actions).toEqual([
|
||||
{
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(migratedRule?.throttle).toEqual('7d');
|
||||
expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval');
|
||||
expect(migratedRule?.muteAll).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpdatedActionsParams', () => {
|
||||
it('updates one action', () => {
|
||||
const { id, ...rule } = {
|
||||
...getRuleLegacyActions(),
|
||||
id: '123',
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
} as RuleAlertType;
|
||||
|
||||
expect(
|
||||
getUpdatedActionsParams({
|
||||
rule: {
|
||||
...rule,
|
||||
id,
|
||||
},
|
||||
ruleThrottle: '1h',
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['a@a.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: '61ec7a40-b076-11ec-bb3f-1f063f8e06cf',
|
||||
type: 'alert',
|
||||
name: 'alert_0',
|
||||
},
|
||||
{
|
||||
id: '1234',
|
||||
type: 'action',
|
||||
name: 'action_0',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
...rule,
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
id: '1234',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['a@a.com'],
|
||||
},
|
||||
},
|
||||
],
|
||||
throttle: '1h',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates multiple actions', () => {
|
||||
const { id, ...rule } = {
|
||||
...getRuleLegacyActions(),
|
||||
id: '123',
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
} as RuleAlertType;
|
||||
|
||||
expect(
|
||||
getUpdatedActionsParams({
|
||||
rule: {
|
||||
...rule,
|
||||
id,
|
||||
},
|
||||
ruleThrottle: '1h',
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Rule email',
|
||||
},
|
||||
action_type_id: '.email',
|
||||
},
|
||||
{
|
||||
actionRef: 'action_1',
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
},
|
||||
action_type_id: '.slack',
|
||||
},
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: '064e3160-b076-11ec-bb3f-1f063f8e06cf',
|
||||
type: 'alert',
|
||||
name: 'alert_0',
|
||||
},
|
||||
{
|
||||
id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf',
|
||||
type: 'action',
|
||||
name: 'action_0',
|
||||
},
|
||||
{
|
||||
id: '207fa0e0-c04e-11ec-8a52-4fb92379525a',
|
||||
type: 'action',
|
||||
name: 'action_1',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
...rule,
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Rule email',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
},
|
||||
{
|
||||
actionTypeId: '.slack',
|
||||
group: 'default',
|
||||
id: '207fa0e0-c04e-11ec-8a52-4fb92379525a',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
},
|
||||
},
|
||||
],
|
||||
throttle: '1h',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,178 +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 { isEmpty } from 'lodash/fp';
|
||||
|
||||
import type { RuleAction } from '@kbn/alerting-plugin/common';
|
||||
import type { RulesClient } from '@kbn/alerting-plugin/server';
|
||||
import type { SavedObjectReference, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import { withSecuritySpan } from '../../../../../utils/with_security_span';
|
||||
import type { RuleAlertType } from '../../../rule_schema';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyRuleActionsSavedObjectType } from '../../../rule_actions_legacy';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type {
|
||||
LegacyIRuleActionsAttributes,
|
||||
LegacyRuleAlertSavedObjectAction,
|
||||
} from '../../../rule_actions_legacy';
|
||||
|
||||
import { transformToAlertThrottle, transformToNotifyWhen } from '../../normalization/rule_actions';
|
||||
|
||||
export interface LegacyMigrateParams {
|
||||
rulesClient: RulesClient;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
rule: RuleAlertType | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if rule needs to be migrated from legacy actions
|
||||
* and returns necessary pieces for the updated rule
|
||||
*/
|
||||
export const legacyMigrate = async ({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
}: LegacyMigrateParams): Promise<RuleAlertType | null | undefined> =>
|
||||
withSecuritySpan('legacyMigrate', async () => {
|
||||
if (rule == null || rule.id == null) {
|
||||
return rule;
|
||||
}
|
||||
/**
|
||||
* On update / patch I'm going to take the actions as they are, better off taking rules client.find (siem.notification) result
|
||||
* and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actual value (1hr etc..)
|
||||
* Then use the rules client to delete the siem.notification
|
||||
* Then with the legacy Rule Actions saved object type, just delete it.
|
||||
*/
|
||||
// find it using the references array, not params.ruleAlertId
|
||||
const [siemNotification, legacyRuleActionsSO] = await Promise.all([
|
||||
rulesClient.find({
|
||||
options: {
|
||||
filter: 'alert.attributes.alertTypeId:(siem.notifications)',
|
||||
hasReference: {
|
||||
type: 'alert',
|
||||
id: rule.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
savedObjectsClient.find<LegacyIRuleActionsAttributes>({
|
||||
type: legacyRuleActionsSavedObjectType,
|
||||
hasReference: {
|
||||
type: 'alert',
|
||||
id: rule.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const siemNotificationsExist = siemNotification != null && siemNotification.data.length > 0;
|
||||
const legacyRuleNotificationSOsExist =
|
||||
legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0;
|
||||
|
||||
// Assumption: if no legacy sidecar SO or notification rule types exist
|
||||
// that reference the rule in question, assume rule actions are not legacy
|
||||
if (!siemNotificationsExist && !legacyRuleNotificationSOsExist) {
|
||||
return rule;
|
||||
}
|
||||
// If the legacy notification rule type ("siem.notification") exist,
|
||||
// migration and cleanup are needed
|
||||
if (siemNotificationsExist) {
|
||||
await rulesClient.delete({ id: siemNotification.data[0].id });
|
||||
}
|
||||
// If legacy notification sidecar ("siem-detection-engine-rule-actions")
|
||||
// exist, migration and cleanup are needed
|
||||
if (legacyRuleNotificationSOsExist) {
|
||||
// Delete the legacy sidecar SO
|
||||
await savedObjectsClient.delete(
|
||||
legacyRuleActionsSavedObjectType,
|
||||
legacyRuleActionsSO.saved_objects[0].id
|
||||
);
|
||||
|
||||
// If "siem-detection-engine-rule-actions" notes that `ruleThrottle` is
|
||||
// "no_actions" or "rule", rule has no actions or rule is set to run
|
||||
// action on every rule run. In these cases, sidecar deletion is the only
|
||||
// cleanup needed and updates to the "throttle" and "notifyWhen". "siem.notification" are
|
||||
// not created for these action types
|
||||
if (
|
||||
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'no_actions' ||
|
||||
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'rule'
|
||||
) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
// Use "legacyRuleActionsSO" instead of "siemNotification" as "siemNotification" is not created
|
||||
// until a rule is run and added to task manager. That means that if by chance a user has a rule
|
||||
// with actions which they have yet to enable, the actions would be lost. Instead,
|
||||
// "legacyRuleActionsSO" is created on rule creation (pre 7.15) and we can rely on it to be there
|
||||
const migratedRule = getUpdatedActionsParams({
|
||||
rule,
|
||||
ruleThrottle: legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle,
|
||||
actions: legacyRuleActionsSO.saved_objects[0].attributes.actions,
|
||||
references: legacyRuleActionsSO.saved_objects[0].references,
|
||||
});
|
||||
|
||||
await rulesClient.update({
|
||||
id: rule.id,
|
||||
data: migratedRule,
|
||||
});
|
||||
|
||||
return { id: rule.id, ...migratedRule };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Translate legacy action sidecar action to rule action
|
||||
*/
|
||||
export const getUpdatedActionsParams = ({
|
||||
rule,
|
||||
ruleThrottle,
|
||||
actions,
|
||||
references,
|
||||
}: {
|
||||
rule: RuleAlertType;
|
||||
ruleThrottle: string | null;
|
||||
actions: LegacyRuleAlertSavedObjectAction[];
|
||||
references: SavedObjectReference[];
|
||||
}): Omit<RuleAlertType, 'id'> => {
|
||||
const { id, ...restOfRule } = rule;
|
||||
|
||||
const actionReference = references.reduce<Record<string, SavedObjectReference>>(
|
||||
(acc, reference) => {
|
||||
acc[reference.name] = reference;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (isEmpty(actionReference)) {
|
||||
throw new Error(
|
||||
`An error occurred migrating legacy action for rule with id:${id}. Connector reference id not found.`
|
||||
);
|
||||
}
|
||||
// If rule has an action on any other interval (other than on every
|
||||
// rule run), need to move the action info from the sidecar/legacy action
|
||||
// into the rule itself
|
||||
return {
|
||||
...restOfRule,
|
||||
actions: actions.reduce<RuleAction[]>((acc, action) => {
|
||||
const { actionRef, action_type_id: actionTypeId, ...resOfAction } = action;
|
||||
if (!actionReference[actionRef]) {
|
||||
return acc;
|
||||
}
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...resOfAction,
|
||||
id: actionReference[actionRef].id,
|
||||
actionTypeId,
|
||||
},
|
||||
];
|
||||
}, []),
|
||||
throttle: transformToAlertThrottle(ruleThrottle),
|
||||
notifyWhen: transformToNotifyWhen(ruleThrottle),
|
||||
};
|
||||
};
|
|
@ -5,20 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { RuleAction } from '@kbn/alerting-plugin/common';
|
||||
|
||||
import {
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
NOTIFICATION_THROTTLE_RULE,
|
||||
} from '../../../../../common/constants';
|
||||
|
||||
import type { RuleResponse } from '../../../../../common/detection_engine/rule_schema';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRuleActions } from '../../rule_actions_legacy';
|
||||
import type { RuleAlertType } from '../../rule_schema';
|
||||
|
||||
import {
|
||||
transformActions,
|
||||
transformFromAlertThrottle,
|
||||
transformToAlertThrottle,
|
||||
transformToNotifyWhen,
|
||||
|
@ -88,307 +82,73 @@ describe('Rule actions normalization', () => {
|
|||
describe('transformFromAlertThrottle', () => {
|
||||
test('muteAll returns "NOTIFICATION_THROTTLE_NO_ACTIONS" even with notifyWhen set and actions has an array element', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: true,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as RuleAlertType,
|
||||
undefined
|
||||
)
|
||||
transformFromAlertThrottle({
|
||||
muteAll: true,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as RuleAlertType)
|
||||
).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
test('returns "NOTIFICATION_THROTTLE_NO_ACTIONS" if actions is an empty array and we do not have a throttle', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [],
|
||||
} as unknown as RuleAlertType,
|
||||
undefined
|
||||
)
|
||||
transformFromAlertThrottle({
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [],
|
||||
} as unknown as RuleAlertType)
|
||||
).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
test('returns "NOTIFICATION_THROTTLE_NO_ACTIONS" if actions is an empty array and we have a throttle', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: false,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
actions: [],
|
||||
throttle: '1d',
|
||||
} as unknown as RuleAlertType,
|
||||
undefined
|
||||
)
|
||||
transformFromAlertThrottle({
|
||||
muteAll: false,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
actions: [],
|
||||
throttle: '1d',
|
||||
} as unknown as RuleAlertType)
|
||||
).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
test('it returns "NOTIFICATION_THROTTLE_RULE" if "notifyWhen" is set, muteAll is false and we have an actions array', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as RuleAlertType,
|
||||
undefined
|
||||
)
|
||||
transformFromAlertThrottle({
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as RuleAlertType)
|
||||
).toEqual(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
|
||||
test('it returns "NOTIFICATION_THROTTLE_RULE" if "notifyWhen" and "throttle" are not set, but we have an actions array', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: false,
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as RuleAlertType,
|
||||
undefined
|
||||
)
|
||||
transformFromAlertThrottle({
|
||||
muteAll: false,
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as RuleAlertType)
|
||||
).toEqual(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
|
||||
test('it will use the "rule" and not the "legacyRuleActions" if the rule and actions is defined', () => {
|
||||
const legacyRuleActions: LegacyRuleActions = {
|
||||
id: 'id_1',
|
||||
ruleThrottle: '',
|
||||
alertThrottle: '',
|
||||
actions: [
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: true,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as RuleAlertType,
|
||||
legacyRuleActions
|
||||
)
|
||||
).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
test('it will use the "legacyRuleActions" and not the "rule" if the rule actions is an empty array', () => {
|
||||
const legacyRuleActions: LegacyRuleActions = {
|
||||
id: 'id_1',
|
||||
ruleThrottle: NOTIFICATION_THROTTLE_RULE,
|
||||
alertThrottle: null,
|
||||
actions: [
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: true,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [],
|
||||
} as unknown as RuleAlertType,
|
||||
legacyRuleActions
|
||||
)
|
||||
).toEqual(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
|
||||
test('it will use the "legacyRuleActions" and not the "rule" if the rule actions is a null', () => {
|
||||
const legacyRuleActions: LegacyRuleActions = {
|
||||
id: 'id_1',
|
||||
ruleThrottle: NOTIFICATION_THROTTLE_RULE,
|
||||
alertThrottle: null,
|
||||
actions: [
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
transformFromAlertThrottle(
|
||||
{
|
||||
muteAll: true,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: null,
|
||||
} as unknown as RuleAlertType,
|
||||
legacyRuleActions
|
||||
)
|
||||
).toEqual(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformActions', () => {
|
||||
test('It transforms two alert actions', () => {
|
||||
const alertAction: RuleAction[] = [
|
||||
{
|
||||
id: 'id_1',
|
||||
group: 'group',
|
||||
actionTypeId: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
actionTypeId: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
];
|
||||
|
||||
const transformed = transformActions(alertAction, null);
|
||||
expect(transformed).toEqual<RuleResponse['actions']>([
|
||||
{
|
||||
id: 'id_1',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('It transforms two alert actions but not a legacyRuleActions if this is also passed in', () => {
|
||||
const alertAction: RuleAction[] = [
|
||||
{
|
||||
id: 'id_1',
|
||||
group: 'group',
|
||||
actionTypeId: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
actionTypeId: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
];
|
||||
const legacyRuleActions: LegacyRuleActions = {
|
||||
id: 'id_1',
|
||||
ruleThrottle: '',
|
||||
alertThrottle: '',
|
||||
actions: [
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const transformed = transformActions(alertAction, legacyRuleActions);
|
||||
expect(transformed).toEqual<RuleResponse['actions']>([
|
||||
{
|
||||
id: 'id_1',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('It will transform the legacyRuleActions if the alertAction is an empty array', () => {
|
||||
const alertAction: RuleAction[] = [];
|
||||
const legacyRuleActions: LegacyRuleActions = {
|
||||
id: 'id_1',
|
||||
ruleThrottle: '',
|
||||
alertThrottle: '',
|
||||
actions: [
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const transformed = transformActions(alertAction, legacyRuleActions);
|
||||
expect(transformed).toEqual<RuleResponse['actions']>([
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('It will transform the legacyRuleActions if the alertAction is undefined', () => {
|
||||
const legacyRuleActions: LegacyRuleActions = {
|
||||
id: 'id_1',
|
||||
ruleThrottle: '',
|
||||
alertThrottle: '',
|
||||
actions: [
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const transformed = transformActions(undefined, legacyRuleActions);
|
||||
expect(transformed).toEqual<RuleResponse['actions']>([
|
||||
{
|
||||
id: 'id_2',
|
||||
group: 'group',
|
||||
action_type_id: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,17 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { RuleAction, RuleNotifyWhenType } from '@kbn/alerting-plugin/common';
|
||||
import type { RuleNotifyWhenType } from '@kbn/alerting-plugin/common';
|
||||
|
||||
import {
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
NOTIFICATION_THROTTLE_RULE,
|
||||
} from '../../../../../common/constants';
|
||||
|
||||
import type { RuleResponse } from '../../../../../common/detection_engine/rule_schema';
|
||||
import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRuleActions } from '../../rule_actions_legacy';
|
||||
import type { RuleAlertType } from '../../rule_schema';
|
||||
|
||||
/**
|
||||
|
@ -60,28 +56,18 @@ export const transformToAlertThrottle = (throttle: string | null | undefined): s
|
|||
* on it to which should not be typical but possible due to the split nature of the API's, this will prefer the
|
||||
* usage of the non-legacy version. Eventually the "legacyRuleActions" should be removed.
|
||||
* @param throttle The throttle from a "alerting" Saved Object (SO)
|
||||
* @param legacyRuleActions Legacy "side car" rule actions that if it detects it being passed it in will transform using it.
|
||||
* @returns The "security_solution" throttle
|
||||
*/
|
||||
export const transformFromAlertThrottle = (
|
||||
rule: RuleAlertType,
|
||||
legacyRuleActions: LegacyRuleActions | null | undefined
|
||||
): string => {
|
||||
if (legacyRuleActions == null || (rule.actions != null && rule.actions.length > 0)) {
|
||||
if (rule.muteAll || rule.actions.length === 0) {
|
||||
return NOTIFICATION_THROTTLE_NO_ACTIONS;
|
||||
} else if (rule.notifyWhen == null) {
|
||||
return transformFromFirstActionThrottle(rule);
|
||||
} else if (rule.notifyWhen === 'onActiveAlert') {
|
||||
return NOTIFICATION_THROTTLE_RULE;
|
||||
} else if (rule.throttle == null) {
|
||||
return NOTIFICATION_THROTTLE_NO_ACTIONS;
|
||||
} else {
|
||||
return rule.throttle;
|
||||
}
|
||||
} else {
|
||||
return legacyRuleActions.ruleThrottle;
|
||||
export const transformFromAlertThrottle = (rule: RuleAlertType): string => {
|
||||
if (rule.muteAll || rule.actions.length === 0) {
|
||||
return NOTIFICATION_THROTTLE_NO_ACTIONS;
|
||||
} else if (rule.notifyWhen == null) {
|
||||
return transformFromFirstActionThrottle(rule);
|
||||
} else if (rule.notifyWhen === 'onActiveAlert') {
|
||||
return NOTIFICATION_THROTTLE_RULE;
|
||||
}
|
||||
|
||||
return rule.throttle ?? NOTIFICATION_THROTTLE_NO_ACTIONS;
|
||||
};
|
||||
|
||||
function transformFromFirstActionThrottle(rule: RuleAlertType) {
|
||||
|
@ -90,25 +76,3 @@ function transformFromFirstActionThrottle(rule: RuleAlertType) {
|
|||
return NOTIFICATION_THROTTLE_RULE;
|
||||
return frequency.throttle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of actions from an "alerting" Saved Object (SO) this will transform it into a "security_solution" alert action.
|
||||
* If this detects any legacy rule actions it will transform it. If both are sent in which is not typical but possible due to
|
||||
* the split nature of the API's this will prefer the usage of the non-legacy version. Eventually the "legacyRuleActions" should
|
||||
* be removed.
|
||||
* @param alertAction The alert action form a "alerting" Saved Object (SO).
|
||||
* @param legacyRuleActions Legacy "side car" rule actions that if it detects it being passed it in will transform using it.
|
||||
* @returns The actions of the RuleResponse
|
||||
*/
|
||||
export const transformActions = (
|
||||
alertAction: RuleAction[] | undefined,
|
||||
legacyRuleActions: LegacyRuleActions | null | undefined
|
||||
): RuleResponse['actions'] => {
|
||||
if (alertAction != null && alertAction.length !== 0) {
|
||||
return alertAction.map((action) => transformAlertToRuleAction(action));
|
||||
} else if (legacyRuleActions != null) {
|
||||
return legacyRuleActions.actions;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
transformAlertToRuleResponseAction,
|
||||
transformRuleToAlertAction,
|
||||
transformRuleToAlertResponseAction,
|
||||
transformAlertToRuleAction,
|
||||
} from '../../../../../common/detection_engine/transform_actions';
|
||||
|
||||
import {
|
||||
|
@ -51,8 +52,6 @@ import {
|
|||
|
||||
import { assertUnreachable } from '../../../../../common/utility_types';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRuleActions } from '../../rule_actions_legacy';
|
||||
import type {
|
||||
InternalRuleCreate,
|
||||
RuleParams,
|
||||
|
@ -75,7 +74,6 @@ import type {
|
|||
NewTermsSpecificRuleParams,
|
||||
} from '../../rule_schema';
|
||||
import {
|
||||
transformActions,
|
||||
transformFromAlertThrottle,
|
||||
transformToAlertThrottle,
|
||||
transformToNotifyWhen,
|
||||
|
@ -646,8 +644,7 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => {
|
|||
};
|
||||
|
||||
export const internalRuleToAPIResponse = (
|
||||
rule: SanitizedRule<RuleParams> | ResolvedSanitizedRule<RuleParams>,
|
||||
legacyRuleActions?: LegacyRuleActions | null
|
||||
rule: SanitizedRule<RuleParams> | ResolvedSanitizedRule<RuleParams>
|
||||
): RuleResponse => {
|
||||
const executionSummary = createRuleExecutionSummary(rule);
|
||||
|
||||
|
@ -675,8 +672,8 @@ export const internalRuleToAPIResponse = (
|
|||
// Type specific security solution rule params
|
||||
...typeSpecificCamelToSnake(rule.params),
|
||||
// Actions
|
||||
throttle: transformFromAlertThrottle(rule, legacyRuleActions),
|
||||
actions: transformActions(rule.actions, legacyRuleActions),
|
||||
throttle: transformFromAlertThrottle(rule),
|
||||
actions: rule.actions.map(transformAlertToRuleAction),
|
||||
// Execution summary
|
||||
execution_summary: executionSummary ?? undefined,
|
||||
};
|
||||
|
|
|
@ -35,12 +35,6 @@ import { createBulkErrorObject } from '../../routes/utils';
|
|||
import type { RuleAlertType } from '../../rule_schema';
|
||||
import { getMlRuleParams, getQueryRuleParams, getThreatRuleParams } from '../../rule_schema/mocks';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type {
|
||||
LegacyRuleAlertAction,
|
||||
LegacyRulesActionsSavedObject,
|
||||
} from '../../rule_actions_legacy';
|
||||
|
||||
import { createRulesAndExceptionsStreamFromNdJson } from '../logic/import/create_rules_stream_from_ndjson';
|
||||
import type { RuleExceptionsPromiseFromStreams } from '../logic/import/import_rules_utils';
|
||||
import { internalRuleToAPIResponse } from '../normalization/rule_converters';
|
||||
|
@ -277,41 +271,17 @@ describe('utils', () => {
|
|||
|
||||
describe('transformFindAlerts', () => {
|
||||
test('outputs empty data set when data set is empty correct', () => {
|
||||
const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, {});
|
||||
const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 });
|
||||
expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 });
|
||||
});
|
||||
|
||||
test('outputs 200 if the data is of type siem alert', () => {
|
||||
const output = transformFindAlerts(
|
||||
{
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
total: 0,
|
||||
data: [getRuleMock(getQueryRuleParams())],
|
||||
},
|
||||
{}
|
||||
);
|
||||
const expected = getOutputRuleAlertForRest();
|
||||
expect(output).toEqual({
|
||||
const output = transformFindAlerts({
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
total: 0,
|
||||
data: [expected],
|
||||
data: [getRuleMock(getQueryRuleParams())],
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs 200 if the data is of type siem alert and has undefined for the legacyRuleActions', () => {
|
||||
const output = transformFindAlerts(
|
||||
{
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
total: 0,
|
||||
data: [getRuleMock(getQueryRuleParams())],
|
||||
},
|
||||
{
|
||||
'123': undefined,
|
||||
}
|
||||
);
|
||||
const expected = getOutputRuleAlertForRest();
|
||||
expect(output).toEqual({
|
||||
page: 1,
|
||||
|
@ -320,58 +290,18 @@ describe('utils', () => {
|
|||
data: [expected],
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs 200 if the data is of type siem alert and has a legacy rule action', () => {
|
||||
const actions: LegacyRuleAlertAction[] = [
|
||||
{
|
||||
id: '456',
|
||||
params: {},
|
||||
group: '',
|
||||
action_type_id: 'action_123',
|
||||
},
|
||||
];
|
||||
|
||||
const legacyRuleActions: Record<string, LegacyRulesActionsSavedObject | undefined> = {
|
||||
[getRuleMock(getQueryRuleParams()).id]: {
|
||||
id: '123',
|
||||
actions,
|
||||
alertThrottle: '1h',
|
||||
ruleThrottle: '1h',
|
||||
},
|
||||
};
|
||||
const output = transformFindAlerts(
|
||||
{
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
total: 0,
|
||||
data: [getRuleMock(getQueryRuleParams())],
|
||||
},
|
||||
legacyRuleActions
|
||||
);
|
||||
const expected = {
|
||||
...getOutputRuleAlertForRest(),
|
||||
throttle: '1h',
|
||||
actions,
|
||||
};
|
||||
expect(output).toEqual({
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
total: 0,
|
||||
data: [expected],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
test('outputs 200 if the data is of type siem alert', () => {
|
||||
const output = transform(getRuleMock(getQueryRuleParams()), undefined);
|
||||
const output = transform(getRuleMock(getQueryRuleParams()));
|
||||
const expected = getOutputRuleAlertForRest();
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns 500 if the data is not of type siem alert', () => {
|
||||
const unsafeCast = { data: [{ random: 1 }] } as unknown as PartialRule;
|
||||
const output = transform(unsafeCast, undefined);
|
||||
const output = transform(unsafeCast);
|
||||
expect(output).toBeNull();
|
||||
});
|
||||
});
|
||||
|
@ -462,12 +392,12 @@ describe('utils', () => {
|
|||
|
||||
describe('transformAlertsToRules', () => {
|
||||
test('given an empty array returns an empty array', () => {
|
||||
expect(transformAlertsToRules([], {})).toEqual([]);
|
||||
expect(transformAlertsToRules([])).toEqual([]);
|
||||
});
|
||||
|
||||
test('given single alert will return the alert transformed', () => {
|
||||
const result1 = getRuleMock(getQueryRuleParams());
|
||||
const transformed = transformAlertsToRules([result1], {});
|
||||
const transformed = transformAlertsToRules([result1]);
|
||||
const expected = getOutputRuleAlertForRest();
|
||||
expect(transformed).toEqual([expected]);
|
||||
});
|
||||
|
@ -478,7 +408,7 @@ describe('utils', () => {
|
|||
result2.id = 'some other id';
|
||||
result2.params.ruleId = 'some other id';
|
||||
|
||||
const transformed = transformAlertsToRules([result1, result2], {});
|
||||
const transformed = transformAlertsToRules([result1, result2]);
|
||||
const expected1 = getOutputRuleAlertForRest();
|
||||
const expected2 = getOutputRuleAlertForRest();
|
||||
expected2.id = 'some other id';
|
||||
|
|
|
@ -21,8 +21,6 @@ import type {
|
|||
AlertSuppressionCamel,
|
||||
} from '../../../../../common/detection_engine/rule_schema';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRulesActionsSavedObject } from '../../rule_actions_legacy';
|
||||
import type { RuleAlertType, RuleParams } from '../../rule_schema';
|
||||
import { isAlertType } from '../../rule_schema';
|
||||
import type { BulkError, OutputError } from '../../routes/utils';
|
||||
|
@ -91,11 +89,8 @@ export const getIdBulkError = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const transformAlertsToRules = (
|
||||
rules: RuleAlertType[],
|
||||
legacyRuleActions: Record<string, LegacyRulesActionsSavedObject>
|
||||
): RuleResponse[] => {
|
||||
return rules.map((rule) => internalRuleToAPIResponse(rule, legacyRuleActions[rule.id]));
|
||||
export const transformAlertsToRules = (rules: RuleAlertType[]): RuleResponse[] => {
|
||||
return rules.map((rule) => internalRuleToAPIResponse(rule));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -116,8 +111,7 @@ export const transformRuleToExportableFormat = (
|
|||
};
|
||||
|
||||
export const transformFindAlerts = (
|
||||
ruleFindResults: FindResult<RuleParams>,
|
||||
legacyRuleActions: Record<string, LegacyRulesActionsSavedObject | undefined>
|
||||
ruleFindResults: FindResult<RuleParams>
|
||||
): {
|
||||
page: number;
|
||||
perPage: number;
|
||||
|
@ -129,17 +123,14 @@ export const transformFindAlerts = (
|
|||
perPage: ruleFindResults.perPage,
|
||||
total: ruleFindResults.total,
|
||||
data: ruleFindResults.data.map((rule) => {
|
||||
return internalRuleToAPIResponse(rule, legacyRuleActions[rule.id]);
|
||||
return internalRuleToAPIResponse(rule);
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const transform = (
|
||||
rule: PartialRule<RuleParams>,
|
||||
legacyRuleActions?: LegacyRulesActionsSavedObject | null
|
||||
): RuleResponse | null => {
|
||||
export const transform = (rule: PartialRule<RuleParams>): RuleResponse | null => {
|
||||
if (isAlertType(rule)) {
|
||||
return internalRuleToAPIResponse(rule, legacyRuleActions);
|
||||
return internalRuleToAPIResponse(rule);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -84,7 +84,7 @@ describe('validate', () => {
|
|||
describe('transformValidate', () => {
|
||||
test('it should do a validation correctly of a partial alert', () => {
|
||||
const ruleAlert = getRuleMock(getQueryRuleParams());
|
||||
const [validated, errors] = transformValidate(ruleAlert, null);
|
||||
const [validated, errors] = transformValidate(ruleAlert);
|
||||
expect(validated).toEqual(ruleOutput());
|
||||
expect(errors).toEqual(null);
|
||||
});
|
||||
|
@ -93,7 +93,7 @@ describe('validate', () => {
|
|||
const ruleAlert = getRuleMock(getQueryRuleParams());
|
||||
// @ts-expect-error
|
||||
delete ruleAlert.name;
|
||||
const [validated, errors] = transformValidate(ruleAlert, null);
|
||||
const [validated, errors] = transformValidate(ruleAlert);
|
||||
expect(validated).toEqual(null);
|
||||
expect(errors).toEqual('Invalid value "undefined" supplied to "name"');
|
||||
});
|
||||
|
|
|
@ -14,15 +14,12 @@ import { isAlertType } from '../../rule_schema';
|
|||
import type { BulkError } from '../../routes/utils';
|
||||
import { createBulkErrorObject } from '../../routes/utils';
|
||||
import { transform } from './utils';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRulesActionsSavedObject } from '../../rule_actions_legacy';
|
||||
import { internalRuleToAPIResponse } from '../normalization/rule_converters';
|
||||
|
||||
export const transformValidate = (
|
||||
rule: PartialRule<RuleParams>,
|
||||
legacyRuleActions?: LegacyRulesActionsSavedObject | null
|
||||
rule: PartialRule<RuleParams>
|
||||
): [RuleResponse | null, string | null] => {
|
||||
const transformed = transform(rule, legacyRuleActions);
|
||||
const transformed = transform(rule);
|
||||
if (transformed == null) {
|
||||
return [null, 'Internal error transforming'];
|
||||
} else {
|
||||
|
|
|
@ -24,12 +24,14 @@ import {
|
|||
getWebHookAction,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
getLegacyActionSO,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
|
||||
describe('delete_rules', () => {
|
||||
describe('deleting rules', () => {
|
||||
|
@ -195,6 +197,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
// Add a legacy rule action to the body of the rule
|
||||
await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(1);
|
||||
expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(
|
||||
createRuleBody.id
|
||||
);
|
||||
|
||||
await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
|
@ -216,6 +225,10 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
// Expect that we have exactly 0 legacy rules after the deletion
|
||||
expect(bodyAfterDelete.total).to.eql(0);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,12 +24,14 @@ import {
|
|||
getWebHookAction,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
getLegacyActionSO,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
|
||||
describe('delete_rules_bulk', () => {
|
||||
describe('deprecations', () => {
|
||||
|
@ -387,6 +389,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
// Add a legacy rule action to the body of the rule
|
||||
await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(1);
|
||||
expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(
|
||||
createRuleBody.id
|
||||
);
|
||||
|
||||
// bulk delete the rule
|
||||
await supertest
|
||||
.delete(DETECTION_ENGINE_RULES_BULK_DELETE)
|
||||
|
@ -410,6 +419,10 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
// Expect that we have exactly 0 legacy rules after the deletion
|
||||
expect(bodyAfterDelete.total).to.eql(0);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,9 @@ import {
|
|||
getWebHookAction,
|
||||
removeServerGeneratedProperties,
|
||||
ruleToNdjson,
|
||||
createLegacyRuleAction,
|
||||
getLegacyActionSO,
|
||||
createRule,
|
||||
} from '../../utils';
|
||||
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
|
||||
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||
|
@ -178,6 +181,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const log = getService('log');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const es = getService('es');
|
||||
|
||||
describe('import_rules', () => {
|
||||
describe('importing rules with different roles', () => {
|
||||
|
@ -718,6 +722,45 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(bodyToCompare).to.eql(ruleOutput);
|
||||
});
|
||||
|
||||
it('should migrate legacy actions in existing rule if overwrite is set to true', async () => {
|
||||
const simpleRule = getSimpleRule('rule-1');
|
||||
|
||||
const [connector, createdRule] = await Promise.all([
|
||||
supertest
|
||||
.post(`/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: 'http://localhost:1234',
|
||||
},
|
||||
}),
|
||||
createRule(supertest, log, simpleRule),
|
||||
]);
|
||||
await createLegacyRuleAction(supertest, createdRule.id, connector.body.id);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(1);
|
||||
expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(
|
||||
createdRule.id
|
||||
);
|
||||
|
||||
simpleRule.name = 'some other name';
|
||||
const ndjson = ruleToNdjson(simpleRule);
|
||||
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', ndjson, 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
});
|
||||
|
||||
it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
|
|
|
@ -75,8 +75,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
expect(ruleSO?.alert.actions).to.eql([]);
|
||||
expect(ruleSO?.alert.throttle).to.eql(null);
|
||||
expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert');
|
||||
expect(ruleSO?.alert.throttle).to.eql('no_actions');
|
||||
expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval');
|
||||
});
|
||||
|
||||
it('migrates legacy actions for rule with action run on every run', async () => {
|
||||
|
@ -123,8 +123,19 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
uuid: ruleSO?.alert.actions[0].uuid,
|
||||
},
|
||||
{
|
||||
actionRef: 'action_1',
|
||||
actionTypeId: '.email',
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
uuid: ruleSO?.alert.actions[1].uuid,
|
||||
},
|
||||
]);
|
||||
expect(ruleSO?.alert.throttle).to.eql(null);
|
||||
expect(ruleSO?.alert.throttle).to.eql('rule');
|
||||
expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert');
|
||||
expect(ruleSO?.references).to.eql([
|
||||
{
|
||||
|
@ -132,6 +143,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
{
|
||||
id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf',
|
||||
name: 'action_1',
|
||||
type: 'action',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -23,12 +23,14 @@ import {
|
|||
createRule,
|
||||
getSimpleMlRule,
|
||||
createLegacyRuleAction,
|
||||
getLegacyActionSO,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
|
||||
describe('patch_rules', () => {
|
||||
describe('patch rules', () => {
|
||||
|
@ -351,6 +353,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
]);
|
||||
await createLegacyRuleAction(supertest, rule.id, connector.body.id);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(1);
|
||||
expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(rule.id);
|
||||
|
||||
// patch disable the rule
|
||||
const patchResponse = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
|
@ -373,8 +380,12 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
];
|
||||
outputRule.throttle = '1h';
|
||||
outputRule.revision = 2; // Expected revision is 2 as call to `createLegacyRuleAction()` does two separate rules updates for `notifyWhen` & `actions` field
|
||||
outputRule.revision = 1;
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
});
|
||||
|
||||
it('should give a 404 if it is given a fake id', async () => {
|
||||
|
|
|
@ -22,12 +22,14 @@ import {
|
|||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
createRule,
|
||||
createLegacyRuleAction,
|
||||
getLegacyActionSO,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
|
||||
describe('patch_rules_bulk', () => {
|
||||
describe('deprecations', () => {
|
||||
|
@ -169,6 +171,14 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
createLegacyRuleAction(supertest, rule1.id, connector.body.id),
|
||||
createLegacyRuleAction(supertest, rule2.id, connector.body.id),
|
||||
]);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(2);
|
||||
expect(
|
||||
sidecarActionsResults.hits.hits.map((hit) => hit?._source?.references[0].id).sort()
|
||||
).to.eql([rule1.id, rule2.id].sort());
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_BULK_UPDATE)
|
||||
|
@ -179,6 +189,10 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
])
|
||||
.expect(200);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
|
||||
// @ts-expect-error
|
||||
body.forEach((response) => {
|
||||
const bodyToCompare = removeServerGeneratedProperties(response);
|
||||
|
@ -196,7 +210,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
];
|
||||
outputRule.throttle = '1h';
|
||||
outputRule.revision = 2; // Expected revision is 2 as call to `createLegacyRuleAction()` does two separate rules updates for `notifyWhen` & `actions` field
|
||||
outputRule.revision = 1;
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
getWebHookAction,
|
||||
installMockPrebuiltRules,
|
||||
removeServerGeneratedProperties,
|
||||
waitForRuleSuccess,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@ -40,6 +41,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const log = getService('log');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const postBulkAction = () =>
|
||||
supertest.post(DETECTION_ENGINE_RULES_BULK_ACTION).set('kbn-xsrf', 'true');
|
||||
|
@ -72,11 +74,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
describe('perform_bulk_action', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest, log);
|
||||
await deleteAllRules(supertest, log);
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
});
|
||||
|
||||
it('should export rules', async () => {
|
||||
|
@ -329,6 +333,8 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
uuid: ruleBody.actions[0].uuid,
|
||||
},
|
||||
]);
|
||||
// we want to ensure rule is executing successfully, to prevent any AAD issues related to partial update of rule SO
|
||||
await waitForRuleSuccess({ id: rule1.id, supertest, log });
|
||||
});
|
||||
|
||||
it('should disable rules', async () => {
|
||||
|
@ -428,7 +434,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(rulesResponse.total).to.eql(2);
|
||||
});
|
||||
|
||||
it('should duplicate rule with a legacy action and migrate new rules action', async () => {
|
||||
it('should duplicate rule with a legacy action', async () => {
|
||||
const ruleId = 'ruleId';
|
||||
const [connector, ruleToDuplicate] = await Promise.all([
|
||||
supertest
|
||||
|
@ -465,10 +471,6 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
// Check that the duplicated rule is returned with the response
|
||||
expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
|
||||
// Check that the updates have been persisted
|
||||
const { body: rulesResponse } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
|
||||
|
@ -478,6 +480,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(rulesResponse.total).to.eql(2);
|
||||
|
||||
rulesResponse.data.forEach((rule: RuleResponse) => {
|
||||
const uuid = rule.actions[0].uuid;
|
||||
expect(rule.actions).to.eql([
|
||||
{
|
||||
action_type_id: '.slack',
|
||||
|
@ -487,7 +490,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
message:
|
||||
'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
},
|
||||
uuid: rule.actions[0].uuid,
|
||||
...(uuid ? { uuid } : {}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -1091,7 +1094,6 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(setTagsBody.attributes.summary).to.eql({
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
|
@ -1515,6 +1517,76 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
expect(readRule.actions).to.eql([]);
|
||||
});
|
||||
|
||||
it('should migrate legacy actions on edit when actions edited', async () => {
|
||||
const ruleId = 'ruleId';
|
||||
const [connector, createdRule] = await Promise.all([
|
||||
supertest
|
||||
.post(`/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: 'http://localhost:1234',
|
||||
},
|
||||
}),
|
||||
createRule(supertest, log, getSimpleRule(ruleId, true)),
|
||||
]);
|
||||
// create a new connector
|
||||
const webHookConnector = await createWebHookConnector();
|
||||
|
||||
await createLegacyRuleAction(supertest, createdRule.id, connector.body.id);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(1);
|
||||
expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(
|
||||
createdRule.id
|
||||
);
|
||||
|
||||
const { body } = await postBulkAction()
|
||||
.send({
|
||||
ids: [createdRule.id],
|
||||
action: BulkActionType.edit,
|
||||
[BulkActionType.edit]: [
|
||||
{
|
||||
type: BulkActionEditType.set_rule_actions,
|
||||
value: {
|
||||
throttle: '1h',
|
||||
actions: [
|
||||
{
|
||||
...webHookActionMock,
|
||||
id: webHookConnector.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const expectedRuleActions = [
|
||||
{
|
||||
...webHookActionMock,
|
||||
id: webHookConnector.id,
|
||||
action_type_id: '.webhook',
|
||||
uuid: body.attributes.results.updated[0].actions[0].uuid,
|
||||
},
|
||||
];
|
||||
|
||||
// Check that the updated rule is returned with the response
|
||||
expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions);
|
||||
|
||||
// Check that the updates have been persisted
|
||||
const { body: readRule } = await fetchRule(ruleId).expect(200);
|
||||
|
||||
expect(readRule.actions).to.eql(expectedRuleActions);
|
||||
|
||||
// Sidecar should be removed
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add_rule_actions', () => {
|
||||
|
|
|
@ -27,12 +27,14 @@ import {
|
|||
getSimpleRule,
|
||||
createLegacyRuleAction,
|
||||
getThresholdRuleForSignalTesting,
|
||||
getLegacyActionSO,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
|
||||
describe('update_rules', () => {
|
||||
describe('update rules', () => {
|
||||
|
@ -207,6 +209,13 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
]);
|
||||
await createLegacyRuleAction(supertest, createRuleBody.id, connector.body.id);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(1);
|
||||
expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(
|
||||
createRuleBody.id
|
||||
);
|
||||
|
||||
const action1 = {
|
||||
group: 'default',
|
||||
id: connector.body.id,
|
||||
|
@ -233,7 +242,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
const outputRule = getSimpleRuleOutputWithoutRuleId();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.revision = 2; // Migration of action results in additional revision increment (change to `notifyWhen`), so expected revision is 2
|
||||
outputRule.revision = 1;
|
||||
outputRule.actions = [
|
||||
{
|
||||
action_type_id: '.slack',
|
||||
|
@ -249,6 +258,10 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
outputRule.throttle = '1d';
|
||||
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
});
|
||||
|
||||
it('should update a single rule property of name using the auto-generated id', async () => {
|
||||
|
|
|
@ -24,12 +24,14 @@ import {
|
|||
createRule,
|
||||
getSimpleRule,
|
||||
createLegacyRuleAction,
|
||||
getLegacyActionSO,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
|
||||
describe('update_rules_bulk', () => {
|
||||
describe('deprecations', () => {
|
||||
|
@ -148,6 +150,13 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
createLegacyRuleAction(supertest, rule2.id, connector.body.id),
|
||||
]);
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionsResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsResults.hits.hits.length).to.eql(2);
|
||||
expect(
|
||||
sidecarActionsResults.hits.hits.map((hit) => hit?._source?.references[0].id).sort()
|
||||
).to.eql([rule1.id, rule2.id].sort());
|
||||
|
||||
const updatedRule1 = getSimpleRuleUpdate('rule-1');
|
||||
updatedRule1.name = 'some other name';
|
||||
updatedRule1.actions = [action1];
|
||||
|
@ -165,11 +174,15 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.send([updatedRule1, updatedRule2])
|
||||
.expect(200);
|
||||
|
||||
// legacy sidecar action should be gone
|
||||
const sidecarActionsPostResults = await getLegacyActionSO(es);
|
||||
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
|
||||
|
||||
body.forEach((response) => {
|
||||
const bodyToCompare = removeServerGeneratedProperties(response);
|
||||
const outputRule = getSimpleRuleOutput(response.rule_id);
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.revision = 2;
|
||||
outputRule.revision = 1;
|
||||
outputRule.actions = [
|
||||
{
|
||||
action_type_id: '.slack',
|
||||
|
@ -232,7 +245,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
body.forEach((response) => {
|
||||
const outputRule = getSimpleRuleOutput(response.rule_id);
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.revision = 2;
|
||||
outputRule.revision = 1;
|
||||
outputRule.actions = [];
|
||||
outputRule.throttle = 'no_actions';
|
||||
const bodyToCompare = removeServerGeneratedProperties(response);
|
||||
|
|
|
@ -70,3 +70,6 @@ export const waitForRuleSuccess = (params: WaitForRuleStatusParams): Promise<voi
|
|||
|
||||
export const waitForRulePartialFailure = (params: WaitForRuleStatusParams): Promise<void> =>
|
||||
waitForRuleStatus(RuleExecutionStatus['partial failure'], params);
|
||||
|
||||
export const waitForRuleFailure = (params: WaitForRuleStatusParams): Promise<void> =>
|
||||
waitForRuleStatus(RuleExecutionStatus.failed, params);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue