[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:
Vitalii Dmyterko 2023-04-19 11:48:37 +01:00 committed by GitHub
parent 1b36fb83c4
commit f8c16c159c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 3044 additions and 1976 deletions

View file

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

View file

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

View file

@ -0,0 +1,153 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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;
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import 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),
},
]);
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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