mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Legacy Actions] - Update legacy action migration to account for more edge cases (#130511)
## Summary Updates the legacy actions migration code to account for edge cases we had not initially caught. Thanks to testing from some teammates, they reported seeing the following behavior: - Rules created pre 7.16 with no actions still create the legacy action sidecar (but not a `siem.notifications` legacy actions alert) which upon migration to 7.16+ was not being deleted - Rules created pre 7.16 with actions that run on every rule run create the legacy action sidecar(but not a `siem.notifications` legacy actions alert) which upon migration to 7.16+ was not being deleted - Rules created pre 7.16 with actions that were never enabled until 8.x did not have a `siem.notifications` legacy actions alert type created Because the legacy migration code relied on checking if a corresponding `siem.notifications` SO existed to kick off the necessary cleanup/migration, the above edge cases were not being caught.
This commit is contained in:
parent
956612d071
commit
29705c01e4
22 changed files with 2584 additions and 36 deletions
|
@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
|||
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
|
||||
import { ruleTypeMappings } from '@kbn/securitysolution-rules';
|
||||
|
||||
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import { SavedObjectsFindResponse, SavedObjectsFindResult } from '@kbn/core/server';
|
||||
|
||||
import { ActionResult } from '@kbn/actions-plugin/server';
|
||||
import {
|
||||
|
@ -49,6 +49,8 @@ import {
|
|||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { LegacyRuleNotificationAlertType } from '../../notifications/legacy_types';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LegacyIRuleActionsAttributes } from '../../rule_actions/legacy_types';
|
||||
import { RuleExecutionSummariesByRuleId } from '../../rule_execution_log';
|
||||
|
||||
export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({
|
||||
|
@ -688,14 +690,20 @@ export const getSignalsMigrationStatusRequest = () =>
|
|||
/**
|
||||
* @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 legacyGetNotificationResult = (): LegacyRuleNotificationAlertType => ({
|
||||
id: '200dbf2f-b269-4bf9-aa85-11ba32ba73ba',
|
||||
export const legacyGetNotificationResult = ({
|
||||
id = '456',
|
||||
ruleId = '123',
|
||||
}: {
|
||||
id?: string;
|
||||
ruleId?: string;
|
||||
} = {}): LegacyRuleNotificationAlertType => ({
|
||||
id,
|
||||
name: 'Notification for Rule Test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: '85b64e8a-2e40-4096-86af-5ac172c10825',
|
||||
ruleAlertId: `${ruleId}`,
|
||||
},
|
||||
schedule: {
|
||||
interval: '5m',
|
||||
|
@ -732,10 +740,357 @@ export const legacyGetNotificationResult = (): LegacyRuleNotificationAlertType =
|
|||
/**
|
||||
* @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 legacyGetFindNotificationsResultWithSingleHit =
|
||||
(): FindHit<LegacyRuleNotificationAlertType> => ({
|
||||
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'),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @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'),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @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'),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @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 legacyGetFindNotificationsResultWithSingleHit = (
|
||||
ruleId = '123'
|
||||
): FindHit<LegacyRuleNotificationAlertType> => ({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
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,
|
||||
perPage: 1,
|
||||
per_page: 1,
|
||||
total: 1,
|
||||
data: [legacyGetNotificationResult()],
|
||||
});
|
||||
saved_objects: actionTypes.map((type) => actions[type]()),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -21,6 +21,15 @@ import { installPrepackagedTimelines } from '../../../timeline/routes/prepackage
|
|||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from '@kbn/core/server/elasticsearch/client/mocks';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
|
||||
jest.mock('../../rules/utils', () => {
|
||||
const actual = jest.requireActual('../../rules/utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../rules/get_prepackaged_rules', () => {
|
||||
return {
|
||||
|
@ -92,6 +101,8 @@ describe('add_prepackaged_rules_route', () => {
|
|||
errors: [],
|
||||
});
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
|
||||
);
|
||||
|
|
|
@ -13,10 +13,20 @@ import {
|
|||
getFindResultWithSingleHit,
|
||||
getDeleteRequestById,
|
||||
getEmptySavedObjectsResponse,
|
||||
getRuleMock,
|
||||
} from '../__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
|
||||
import { deleteRulesRoute } from './delete_rules_route';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
|
||||
jest.mock('../../rules/utils', () => {
|
||||
const actual = jest.requireActual('../../rules/utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('delete_rules', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
|
@ -29,6 +39,8 @@ describe('delete_rules', () => {
|
|||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse());
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
deleteRulesRoute(server.router);
|
||||
});
|
||||
|
||||
|
@ -54,6 +66,7 @@ describe('delete_rules', () => {
|
|||
|
||||
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)
|
||||
|
|
|
@ -23,9 +23,18 @@ import { patchRulesBulkRoute } from './patch_rules_bulk_route';
|
|||
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
|
||||
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
|
||||
|
||||
jest.mock('../../rules/utils', () => {
|
||||
const actual = jest.requireActual('../../rules/utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('patch_rules_bulk', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -40,6 +49,8 @@ describe('patch_rules_bulk', () => {
|
|||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
|
||||
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // update succeeds
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
patchRulesBulkRoute(server.router, ml, logger);
|
||||
});
|
||||
|
||||
|
@ -54,6 +65,7 @@ describe('patch_rules_bulk', () => {
|
|||
|
||||
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)
|
||||
|
@ -148,6 +160,8 @@ describe('patch_rules_bulk', () => {
|
|||
|
||||
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,
|
||||
|
|
|
@ -21,9 +21,18 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__';
|
|||
import { patchRulesRoute } from './patch_rules_route';
|
||||
import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
|
||||
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
|
||||
|
||||
jest.mock('../../rules/utils', () => {
|
||||
const actual = jest.requireActual('../../rules/utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('patch_rules', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -41,6 +50,8 @@ describe('patch_rules', () => {
|
|||
getRuleExecutionSummarySucceeded()
|
||||
);
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
patchRulesRoute(server.router, ml);
|
||||
});
|
||||
|
||||
|
@ -55,6 +66,7 @@ describe('patch_rules', () => {
|
|||
|
||||
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)
|
||||
|
@ -67,6 +79,7 @@ describe('patch_rules', () => {
|
|||
});
|
||||
|
||||
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(),
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
getBulkActionEditRequest,
|
||||
getFindResultWithSingleHit,
|
||||
getFindResultWithMultiHits,
|
||||
getRuleMock,
|
||||
} from '../__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
|
||||
import { performBulkActionRoute } from './perform_bulk_action_route';
|
||||
|
@ -23,10 +24,20 @@ import {
|
|||
} from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { readRules } from '../../rules/read_rules';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
|
||||
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
|
||||
jest.mock('../../rules/read_rules', () => ({ readRules: jest.fn() }));
|
||||
|
||||
jest.mock('../../rules/utils', () => {
|
||||
const actual = jest.requireActual('../../rules/utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('perform_bulk_action', () => {
|
||||
const readRulesMock = readRules as jest.Mock;
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
|
@ -40,6 +51,8 @@ describe('perform_bulk_action', () => {
|
|||
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);
|
||||
});
|
||||
|
@ -220,7 +233,10 @@ describe('perform_bulk_action', () => {
|
|||
readRulesMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ ...mockRule, params: { ...mockRule.params, type: 'machine_learning' } })
|
||||
);
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue({
|
||||
...mockRule,
|
||||
params: { ...mockRule.params, type: 'machine_learning' },
|
||||
});
|
||||
const request = requestMock.create({
|
||||
method: 'patch',
|
||||
path: DETECTION_ENGINE_RULES_BULK_ACTION,
|
||||
|
@ -271,7 +287,10 @@ describe('perform_bulk_action', () => {
|
|||
readRulesMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ ...mockRule, params: { ...mockRule.params, index: ['index-*'] } })
|
||||
);
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue({
|
||||
...mockRule,
|
||||
params: { ...mockRule.params, index: ['index-*'] },
|
||||
});
|
||||
const request = requestMock.create({
|
||||
method: 'patch',
|
||||
path: DETECTION_ENGINE_RULES_BULK_ACTION,
|
||||
|
|
|
@ -21,9 +21,18 @@ import { BulkError } from '../utils';
|
|||
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
|
||||
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
|
||||
|
||||
jest.mock('../../rules/utils', () => {
|
||||
const actual = jest.requireActual('../../rules/utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('update_rules_bulk', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -40,6 +49,8 @@ describe('update_rules_bulk', () => {
|
|||
|
||||
clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index');
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
updateRulesBulkRoute(server.router, ml, logger);
|
||||
});
|
||||
|
||||
|
@ -54,6 +65,8 @@ describe('update_rules_bulk', () => {
|
|||
|
||||
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[] = [
|
||||
{
|
||||
error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
|
||||
|
@ -116,6 +129,8 @@ describe('update_rules_bulk', () => {
|
|||
|
||||
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,
|
||||
|
|
|
@ -21,9 +21,18 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
|||
import { updateRulesRoute } from './update_rules_route';
|
||||
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
|
||||
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
|
||||
|
||||
jest.mock('../../rules/utils', () => {
|
||||
const actual = jest.requireActual('../../rules/utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('update_rules', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -42,6 +51,8 @@ describe('update_rules', () => {
|
|||
);
|
||||
clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index');
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
|
||||
updateRulesRoute(server.router, ml);
|
||||
});
|
||||
|
||||
|
@ -56,6 +67,7 @@ describe('update_rules', () => {
|
|||
|
||||
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)
|
||||
|
@ -69,6 +81,7 @@ describe('update_rules', () => {
|
|||
});
|
||||
|
||||
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(),
|
||||
|
|
|
@ -7,14 +7,24 @@
|
|||
|
||||
import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses';
|
||||
import { getRuleMock, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses';
|
||||
import { updatePrepackagedRules } from './update_prepacked_rules';
|
||||
import { patchRules } from './patch_rules';
|
||||
import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock';
|
||||
import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__';
|
||||
import { legacyMigrate } from './utils';
|
||||
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
|
||||
|
||||
jest.mock('./patch_rules');
|
||||
|
||||
jest.mock('./utils', () => {
|
||||
const actual = jest.requireActual('./utils');
|
||||
return {
|
||||
...actual,
|
||||
legacyMigrate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('updatePrepackagedRules', () => {
|
||||
let rulesClient: ReturnType<typeof rulesClientMock.create>;
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
|
@ -24,6 +34,8 @@ describe('updatePrepackagedRules', () => {
|
|||
rulesClient = rulesClientMock.create();
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
ruleExecutionLog = ruleExecutionLogMock.forRoutes.create();
|
||||
|
||||
(legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams()));
|
||||
});
|
||||
|
||||
it('should omit actions and enabled when calling patchRules', async () => {
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
transformToAlertThrottle,
|
||||
transformFromAlertThrottle,
|
||||
transformActions,
|
||||
legacyMigrate,
|
||||
getUpdatedActionsParams,
|
||||
} from './utils';
|
||||
import { RuleAction, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { RuleParams } from '../schemas/rule_schemas';
|
||||
|
@ -23,6 +25,67 @@ import {
|
|||
import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LegacyRuleActions } from '../rule_actions/legacy_types';
|
||||
import {
|
||||
getEmptyFindResult,
|
||||
legacyGetSiemNotificationRuleActionsSOResultWithSingleHit,
|
||||
legacyGetDailyNotificationResult,
|
||||
legacyGetHourlyNotificationResult,
|
||||
legacyGetWeeklyNotificationResult,
|
||||
} from '../routes/__mocks__/request_responses';
|
||||
import { requestContextMock } from '../routes/__mocks__';
|
||||
|
||||
const getRuleLegacyActions = (): SanitizedRule<RuleParams> =>
|
||||
({
|
||||
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 SanitizedRule<RuleParams>);
|
||||
|
||||
describe('utils', () => {
|
||||
describe('#calculateInterval', () => {
|
||||
|
@ -588,4 +651,415 @@ describe('utils', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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 SanitizedRule<RuleParams>;
|
||||
|
||||
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 SanitizedRule<RuleParams>;
|
||||
|
||||
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 SanitizedRule<RuleParams>;
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
|||
} from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { ListArrayOrUndefined } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import { RuleAction, RuleNotifyWhenType, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { RulesClient } from '@kbn/alerting-plugin/server';
|
||||
import {
|
||||
|
@ -63,7 +64,11 @@ import {
|
|||
NOTIFICATION_THROTTLE_RULE,
|
||||
} from '../../../../common/constants';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LegacyRuleActions } from '../rule_actions/legacy_types';
|
||||
import {
|
||||
LegacyIRuleActionsAttributes,
|
||||
LegacyRuleActions,
|
||||
LegacyRuleAlertSavedObjectAction,
|
||||
} from '../rule_actions/legacy_types';
|
||||
import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request';
|
||||
import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
|
@ -302,6 +307,59 @@ export const maybeMute = async ({
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate legacy action sidecar action to rule action
|
||||
*/
|
||||
export const getUpdatedActionsParams = ({
|
||||
rule,
|
||||
ruleThrottle,
|
||||
actions,
|
||||
references,
|
||||
}: {
|
||||
rule: SanitizedRule<RuleParams>;
|
||||
ruleThrottle: string | null;
|
||||
actions: LegacyRuleAlertSavedObjectAction[];
|
||||
references: SavedObjectReference[];
|
||||
}): Omit<SanitizedRule<RuleParams>, '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),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if rule needs to be migrated from legacy actions
|
||||
* and returns necessary pieces for the updated rule
|
||||
|
@ -332,7 +390,7 @@ export const legacyMigrate = async ({
|
|||
},
|
||||
},
|
||||
}),
|
||||
savedObjectsClient.find({
|
||||
savedObjectsClient.find<LegacyIRuleActionsAttributes>({
|
||||
type: legacyRuleActionsSavedObjectType,
|
||||
hasReference: {
|
||||
type: 'alert',
|
||||
|
@ -341,29 +399,57 @@ export const legacyMigrate = async ({
|
|||
}),
|
||||
]);
|
||||
|
||||
if (siemNotification != null && siemNotification.data.length > 0) {
|
||||
await Promise.all([
|
||||
rulesClient.delete({ id: siemNotification.data[0].id }),
|
||||
legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0
|
||||
? savedObjectsClient.delete(
|
||||
legacyRuleActionsSavedObjectType,
|
||||
legacyRuleActionsSO.saved_objects[0].id
|
||||
)
|
||||
: null,
|
||||
]);
|
||||
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,
|
||||
});
|
||||
|
||||
const { id, ...restOfRule } = rule;
|
||||
const migratedRule = {
|
||||
...restOfRule,
|
||||
actions: siemNotification.data[0].actions,
|
||||
throttle: siemNotification.data[0].schedule.interval,
|
||||
notifyWhen: transformToNotifyWhen(siemNotification.data[0].throttle),
|
||||
};
|
||||
await rulesClient.update({
|
||||
id: rule.id,
|
||||
data: migratedRule,
|
||||
});
|
||||
|
||||
return { id: rule.id, ...migratedRule };
|
||||
}
|
||||
return rule;
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
getSimpleRuleOutput,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
getSimpleRuleWithoutRuleId,
|
||||
getSlackAction,
|
||||
getWebHookAction,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
|
@ -148,7 +149,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.send(getSlackAction())
|
||||
.expect(200);
|
||||
|
||||
// create a rule without actions
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
getSimpleRuleOutput,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
getSimpleRuleWithoutRuleId,
|
||||
getSlackAction,
|
||||
getWebHookAction,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
|
@ -277,7 +278,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.send(getSlackAction())
|
||||
.expect(200);
|
||||
|
||||
// create a rule without actions
|
||||
|
@ -318,12 +319,12 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const { body: hookAction1 } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.send(getSlackAction())
|
||||
.expect(200);
|
||||
const { body: hookAction2 } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.send(getSlackAction())
|
||||
.expect(200);
|
||||
|
||||
// create 2 rules without actions
|
||||
|
|
|
@ -13,6 +13,9 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
describe('', function () {
|
||||
this.tags('ciGroup11');
|
||||
|
||||
// !!NOTE: For new routes that do any updates on a rule, please ensure that you are including the legacy
|
||||
// action migration code. We are monitoring legacy action telemetry to clean up once we see their
|
||||
// existence being near 0.
|
||||
loadTestFile(require.resolve('./aliases'));
|
||||
loadTestFile(require.resolve('./add_actions'));
|
||||
loadTestFile(require.resolve('./update_actions'));
|
||||
|
@ -33,6 +36,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./get_rule_execution_events'));
|
||||
loadTestFile(require.resolve('./import_rules'));
|
||||
loadTestFile(require.resolve('./import_export_rules'));
|
||||
loadTestFile(require.resolve('./legacy_actions_migrations'));
|
||||
loadTestFile(require.resolve('./read_rules'));
|
||||
loadTestFile(require.resolve('./resolve_read_rules'));
|
||||
loadTestFile(require.resolve('./update_rules'));
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
getLegacyActionSOById,
|
||||
getLegacyActionNotificationSOById,
|
||||
getRuleSOById,
|
||||
} from '../../utils';
|
||||
|
||||
/**
|
||||
* @deprecated Once the legacy notification system is removed, remove this test too.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
// This test suite is not meant to test a specific route, but to test the legacy action migration
|
||||
// code that lives in multiple routes. This code is also tested in each of the routes it lives in
|
||||
// but not in as much detail and relying on mocks. This test loads an es_archive containing rules
|
||||
// created in 7.15 with legacy actions.
|
||||
// For new routes that do any updates on a rule, please ensure that you are including the legacy
|
||||
// action migration code. We are monitoring legacy action telemetry to clean up once we see their
|
||||
// existence being near 0.
|
||||
describe('migrate_legacy_actions', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/legacy_actions');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/functional/es_archives/security_solution/legacy_actions'
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates legacy actions for rule with no actions', async () => {
|
||||
const soId = '9095ee90-b075-11ec-bb3f-1f063f8e06cf';
|
||||
const ruleId = '2297be91-894c-4831-830f-b424a0ec84f0';
|
||||
const legacySidecarId = '926668d0-b075-11ec-bb3f-1f063f8e06cf';
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// check for legacy notification SO
|
||||
// should not have been created for a rule with no actions
|
||||
const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSO.hits.hits.length).to.eql(0);
|
||||
|
||||
// patch enable the rule
|
||||
// any route that edits the rule should trigger the migration
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: ruleId, enabled: false })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
hits: {
|
||||
hits: [{ _source: ruleSO }],
|
||||
},
|
||||
} = await getRuleSOById(es, soId);
|
||||
|
||||
// Sidecar should be removed
|
||||
const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId);
|
||||
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');
|
||||
});
|
||||
|
||||
it('migrates legacy actions for rule with action run on every run', async () => {
|
||||
const soId = 'dc6595f0-b075-11ec-bb3f-1f063f8e06cf';
|
||||
const ruleId = '72a0d429-363b-4f70-905e-c6019a224d40';
|
||||
const legacySidecarId = 'dde13970-b075-11ec-bb3f-1f063f8e06cf';
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// check for legacy notification SO
|
||||
// should not have been created for a rule that runs on every rule run
|
||||
const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSO.hits.hits.length).to.eql(0);
|
||||
|
||||
// patch enable the rule
|
||||
// any route that edits the rule should trigger the migration
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: ruleId, enabled: false })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
hits: {
|
||||
hits: [{ _source: ruleSO }],
|
||||
},
|
||||
} = await getRuleSOById(es, soId);
|
||||
|
||||
// Sidecar should be removed
|
||||
const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
expect(ruleSO?.alert.actions).to.eql([
|
||||
{
|
||||
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'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(ruleSO?.alert.throttle).to.eql(null);
|
||||
expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert');
|
||||
expect(ruleSO?.references).to.eql([
|
||||
{
|
||||
id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('migrates legacy actions for rule with action run hourly', async () => {
|
||||
const soId = '064e3160-b076-11ec-bb3f-1f063f8e06cf';
|
||||
const ruleId = '4c056b05-75ac-4209-be32-82100f771eb4';
|
||||
const legacySidecarId = '07aa8d10-b076-11ec-bb3f-1f063f8e06cf';
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// check for legacy notification SO
|
||||
const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// patch enable the rule
|
||||
// any route that edits the rule should trigger the migration
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: ruleId, enabled: false })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
hits: {
|
||||
hits: [{ _source: ruleSO }],
|
||||
},
|
||||
} = await getRuleSOById(es, soId);
|
||||
|
||||
// Sidecar should be removed
|
||||
const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
// Legacy notification should be removed
|
||||
const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
expect(ruleSO?.alert.actions).to.eql([
|
||||
{
|
||||
actionTypeId: '.email',
|
||||
params: {
|
||||
subject: 'Rule email',
|
||||
to: ['test@test.com'],
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
},
|
||||
actionRef: 'action_0',
|
||||
group: 'default',
|
||||
},
|
||||
{
|
||||
actionTypeId: '.slack',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
},
|
||||
actionRef: 'action_1',
|
||||
group: 'default',
|
||||
},
|
||||
]);
|
||||
expect(ruleSO?.alert.throttle).to.eql('1h');
|
||||
expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval');
|
||||
expect(ruleSO?.references).to.eql([
|
||||
{
|
||||
id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
{
|
||||
id: '207fa0e0-c04e-11ec-8a52-4fb92379525a',
|
||||
name: 'action_1',
|
||||
type: 'action',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('migrates legacy actions for rule with action run daily', async () => {
|
||||
const soId = '27639570-b076-11ec-bb3f-1f063f8e06cf';
|
||||
const ruleId = '8e2c8550-f13f-4e21-be0c-92148d71a5f1';
|
||||
const legacySidecarId = '291ae260-b076-11ec-bb3f-1f063f8e06cf';
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// check for legacy notification SO
|
||||
const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// patch enable the rule
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: ruleId, enabled: false })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
hits: {
|
||||
hits: [{ _source: ruleSO }],
|
||||
},
|
||||
} = await getRuleSOById(es, soId);
|
||||
|
||||
// Sidecar should be removed
|
||||
const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
// Legacy notification should be removed
|
||||
const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
expect(ruleSO?.alert.actions).to.eql([
|
||||
{
|
||||
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'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(ruleSO?.alert.throttle).to.eql('1d');
|
||||
expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval');
|
||||
expect(ruleSO?.references).to.eql([
|
||||
{
|
||||
id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('migrates legacy actions for rule with action run weekly', async () => {
|
||||
const soId = '61ec7a40-b076-11ec-bb3f-1f063f8e06cf';
|
||||
const ruleId = '05fbdd2a-e802-420b-bdc3-95ae0acca454';
|
||||
const legacySidecarId = '63aa2fd0-b076-11ec-bb3f-1f063f8e06cf';
|
||||
|
||||
// check for legacy sidecar action
|
||||
const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// check for legacy notification SO
|
||||
const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSO.hits.hits.length).to.eql(1);
|
||||
|
||||
// patch enable the rule
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: ruleId, enabled: false })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
hits: {
|
||||
hits: [{ _source: ruleSO }],
|
||||
},
|
||||
} = await getRuleSOById(es, soId);
|
||||
|
||||
// Sidecar should be removed
|
||||
const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId);
|
||||
expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
// Legacy notification should be removed
|
||||
const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId);
|
||||
expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0);
|
||||
|
||||
expect(ruleSO?.alert.actions).to.eql([
|
||||
{
|
||||
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'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(ruleSO?.alert.throttle).to.eql('7d');
|
||||
expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval');
|
||||
expect(ruleSO?.references).to.eql([
|
||||
{
|
||||
id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { LegacyRuleNotificationAlertTypeParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/notifications/legacy_types';
|
||||
|
||||
interface LegacyActionNotificationSO extends LegacyRuleNotificationAlertTypeParams {
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all legacy action sidecar notification SOs from the .kibana index
|
||||
* @param es The ElasticSearch service
|
||||
*/
|
||||
export const getLegacyActionNotificationSO = async (
|
||||
es: Client
|
||||
): Promise<SearchResponse<LegacyActionNotificationSO>> =>
|
||||
es.search({
|
||||
index: '.kibana',
|
||||
q: 'alert.alertTypeId:siem.notifications',
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { LegacyRuleNotificationAlertTypeParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/notifications/legacy_types';
|
||||
|
||||
interface LegacyActionNotificationSO extends LegacyRuleNotificationAlertTypeParams {
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch legacy action sidecar notification SOs from the .kibana index
|
||||
* @param es The ElasticSearch service
|
||||
* @param id SO id
|
||||
*/
|
||||
export const getLegacyActionNotificationSOById = async (
|
||||
es: Client,
|
||||
id: string
|
||||
): Promise<SearchResponse<LegacyActionNotificationSO>> =>
|
||||
es.search({
|
||||
index: '.kibana',
|
||||
q: `alert.alertTypeId:siem.notifications AND alert.params.ruleAlertId:"${id}"`,
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { LegacyRuleActions } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_actions/legacy_types';
|
||||
|
||||
interface LegacyActionSO extends LegacyRuleActions {
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch legacy action sidecar SOs from the .kibana index
|
||||
* @param es The ElasticSearch service
|
||||
* @param id SO id
|
||||
*/
|
||||
export const getLegacyActionSOById = async (
|
||||
es: Client,
|
||||
id: string
|
||||
): Promise<SearchResponse<LegacyActionSO>> =>
|
||||
es.search({
|
||||
index: '.kibana',
|
||||
q: `type:siem-detection-engine-rule-actions AND _id:"siem-detection-engine-rule-actions:${id}"`,
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { Rule } from '@kbn/alerting-plugin/common';
|
||||
|
||||
interface RuleSO {
|
||||
alert: Rule;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch legacy action sidecar SOs from the .kibana index
|
||||
* @param es The ElasticSearch service
|
||||
* @param id SO id
|
||||
*/
|
||||
export const getRuleSOById = async (es: Client, id: string): Promise<SearchResponse<RuleSO>> =>
|
||||
es.search({
|
||||
index: '.kibana',
|
||||
q: `type:alert AND _id:"alert:${id}"`,
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const getSlackAction = () => ({
|
||||
actionTypeId: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: 'http://localhost:123',
|
||||
},
|
||||
name: 'Slack connector',
|
||||
});
|
|
@ -35,7 +35,10 @@ export * from './get_detection_metrics_from_body';
|
|||
export * from './get_eql_rule_for_signal_testing';
|
||||
export * from './get_event_log_execute_complete_by_id';
|
||||
export * from './get_index_name_from_load';
|
||||
export * from './get_legacy_action_notification_so';
|
||||
export * from './get_legacy_action_notifications_so_by_id';
|
||||
export * from './get_legacy_action_so';
|
||||
export * from './get_legacy_actions_so_by_id';
|
||||
export * from './get_open_signals';
|
||||
export * from './get_prepackaged_rule_status';
|
||||
export * from './get_query_all_signals';
|
||||
|
@ -44,6 +47,7 @@ export * from './get_query_signals_ids';
|
|||
export * from './get_query_signals_rule_id';
|
||||
export * from './get_rule';
|
||||
export * from './get_rule_for_signal_testing';
|
||||
export * from './get_rule_so_by_id';
|
||||
export * from './get_rule_for_signal_testing_with_timestamp_override';
|
||||
export * from './get_rule_with_web_hook_action';
|
||||
export * from './get_saved_query_rule_for_signal_testing';
|
||||
|
@ -70,6 +74,7 @@ export * from './get_stats';
|
|||
export * from './get_stats_url';
|
||||
export * from './get_threat_match_rule_for_signal_testing';
|
||||
export * from './get_threshold_rule_for_signal_testing';
|
||||
export * from './get_slack_action';
|
||||
export * from './get_web_hook_action';
|
||||
export * from './index_event_log_execution_events';
|
||||
export * from './install_prepackaged_rules';
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue