[Alerting] Remove predefined connectors from rule reference array (#109437)

* Exposing preconfigured connectors through actions setup contract

* Adding stub for migration using preconfigured connectors

* Adding isPreconfigured fn to actions client

* Updating rules client logic to not extract predefined connector ids

* Functional tests

* Adding migration

* Adding functional test for migration

* Adding functional test for migration

* Adding note to docs about referenced_by_count if is_preconfigured

* Fixing functional test

* Changing to isPreconfiguredConnector fn in actions plugin setup contract

* Update docs/api/actions-and-connectors/get_all.asciidoc

Co-authored-by: Mike Côté <mikecote@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Mike Côté <mikecote@users.noreply.github.com>
This commit is contained in:
ymao1 2021-08-26 14:50:32 -04:00 committed by GitHub
parent 3854d3a586
commit a3d03ecbdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1475 additions and 66 deletions

View file

@ -44,7 +44,7 @@ The API returns the following:
"connector_type_id": ".email",
"name": "email: preconfigured-mail-connector",
"is_preconfigured": true,
"referenced_by_count": 1
"referenced_by_count": 0 <1>
},
{
"id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad",
@ -61,3 +61,5 @@ The API returns the following:
}
]
--------------------------------------------------
<1> `referenced_by_count` - The number of saved-objects referencing this connector. This value is not calculated if `is_preconfigured: true`.

View file

@ -24,6 +24,7 @@ const createActionsClientMock = () => {
ephemeralEnqueuedExecution: jest.fn(),
listTypes: jest.fn(),
isActionTypeEnabled: jest.fn(),
isPreconfigured: jest.fn(),
};
return mocked;
};

View file

@ -1823,3 +1823,65 @@ describe('isActionTypeEnabled()', () => {
});
});
});
describe('isPreconfigured()', () => {
test('should return true if connector id is in list of preconfigured connectors', () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
request,
authorization: (authorization as unknown) as ActionsAuthorization,
preconfiguredActions: [
{
id: 'testPreconfigured',
actionTypeId: 'my-action-type',
secrets: {
test: 'test1',
},
isPreconfigured: true,
name: 'test',
config: {
foo: 'bar',
},
},
],
});
expect(actionsClient.isPreconfigured('testPreconfigured')).toEqual(true);
});
test('should return false if connector id is not in list of preconfigured connectors', () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
request,
authorization: (authorization as unknown) as ActionsAuthorization,
preconfiguredActions: [
{
id: 'testPreconfigured',
actionTypeId: 'my-action-type',
secrets: {
test: 'test1',
},
isPreconfigured: true,
name: 'test',
config: {
foo: 'bar',
},
},
],
});
expect(actionsClient.isPreconfigured(uuid.v4())).toEqual(false);
});
});

View file

@ -523,6 +523,10 @@ export class ActionsClient {
) {
return this.actionTypeRegistry.isActionTypeEnabled(actionTypeId, options);
}
public isPreconfigured(connectorId: string): boolean {
return !!this.preconfiguredActions.find((preconfigured) => preconfigured.id === connectorId);
}
}
function actionFromSavedObject(savedObject: SavedObject<RawAction>): ActionResult {

View file

@ -19,6 +19,7 @@ export { actionsClientMock };
const createSetupMock = () => {
const mock: jest.Mocked<PluginSetupContract> = {
registerType: jest.fn(),
isPreconfiguredConnector: jest.fn(),
};
return mock;
};

View file

@ -185,6 +185,62 @@ describe('Actions Plugin', () => {
});
});
});
describe('isPreconfiguredConnector', () => {
function getConfig(overrides = {}) {
return {
enabled: true,
enabledActionTypes: ['*'],
allowedHosts: ['*'],
preconfiguredAlertHistoryEsIndex: false,
preconfigured: {
preconfiguredServerLog: {
actionTypeId: '.server-log',
name: 'preconfigured-server-log',
config: {},
secrets: {},
},
},
proxyRejectUnauthorizedCertificates: true,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
rejectUnauthorized: true,
maxResponseContentLength: new ByteSizeValue(1000000),
responseTimeout: moment.duration('60s'),
cleanupFailedExecutionsTask: {
enabled: true,
cleanupInterval: schema.duration().validate('5m'),
idleInterval: schema.duration().validate('1h'),
pageSize: 100,
},
...overrides,
};
}
function setup(config: ActionsConfig) {
context = coreMock.createPluginInitializerContext<ActionsConfig>(config);
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
pluginsSetup = {
taskManager: taskManagerMock.createSetup(),
encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(),
licensing: licensingMock.createSetup(),
eventLog: eventLogMock.createSetup(),
usageCollection: usageCollectionPluginMock.createSetupContract(),
features: featuresPluginMock.createSetup(),
};
}
it('should correctly return whether connector is preconfigured', async () => {
setup(getConfig());
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
expect(pluginSetup.isPreconfiguredConnector('preconfiguredServerLog')).toEqual(true);
expect(pluginSetup.isPreconfiguredConnector('anotherConnectorId')).toEqual(false);
});
});
});
describe('start()', () => {

View file

@ -96,6 +96,7 @@ export interface PluginSetupContract {
>(
actionType: ActionType<Config, Secrets, Params, ExecutorResultData>
): void;
isPreconfiguredConnector(connectorId: string): boolean;
}
export interface PluginStartContract {
@ -289,6 +290,11 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
ensureSufficientLicense(actionType);
actionTypeRegistry.register(actionType);
},
isPreconfiguredConnector: (connectorId: string): boolean => {
return !!this.preconfiguredActions.find(
(preconfigured) => preconfigured.id === connectorId
);
},
};
}

View file

@ -228,7 +228,8 @@ export class AlertingPlugin {
core.savedObjects,
plugins.encryptedSavedObjects,
this.ruleTypeRegistry,
this.logger
this.logger,
plugins.actions.isPreconfiguredConnector
);
initializeApiKeyInvalidator(

View file

@ -189,6 +189,9 @@ export interface GetAlertInstanceSummaryParams {
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
const extractedSavedObjectParamReferenceNamePrefix = 'param:';
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
const preconfiguredConnectorActionRefPrefix = 'preconfigured:';
const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
type: AlertingAuthorizationFilterType.KQL,
fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' },
@ -1508,6 +1511,13 @@ export class RulesClient {
references: SavedObjectReference[]
) {
return actions.map((action) => {
if (action.actionRef.startsWith(preconfiguredConnectorActionRefPrefix)) {
return {
...omit(action, 'actionRef'),
id: action.actionRef.replace(preconfiguredConnectorActionRefPrefix, ''),
};
}
const reference = references.find((ref) => ref.name === action.actionRef);
if (!reference) {
throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);
@ -1700,17 +1710,25 @@ export class RulesClient {
alertActions.forEach(({ id, ...alertAction }, i) => {
const actionResultValue = actionResults.find((action) => action.id === id);
if (actionResultValue) {
const actionRef = `action_${i}`;
references.push({
id,
name: actionRef,
type: 'action',
});
actions.push({
...alertAction,
actionRef,
actionTypeId: actionResultValue.actionTypeId,
});
if (actionsClient.isPreconfigured(id)) {
actions.push({
...alertAction,
actionRef: `${preconfiguredConnectorActionRefPrefix}${id}`,
actionTypeId: actionResultValue.actionTypeId,
});
} else {
const actionRef = `action_${i}`;
references.push({
id,
name: actionRef,
type: 'action',
});
actions.push({
...alertAction,
actionRef,
actionTypeId: actionResultValue.actionTypeId,
});
}
} else {
actions.push({
...alertAction,

View file

@ -791,6 +791,253 @@ describe('create()', () => {
`);
});
test('creates a rule with some actions using preconfigured connectors', async () => {
const data = getMockData({
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
{
group: 'default',
id: 'preconfigured',
params: {
foo: true,
},
},
{
group: 'default',
id: '2',
params: {
foo: true,
},
},
],
});
actionsClient.getBulk.mockReset();
actionsClient.getBulk.mockResolvedValue([
{
id: '1',
actionTypeId: 'test',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'email connector',
isPreconfigured: false,
},
{
id: '2',
actionTypeId: 'test2',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'another email connector',
isPreconfigured: false,
},
{
id: 'preconfigured',
actionTypeId: 'test',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'preconfigured email connector',
isPreconfigured: true,
},
]);
actionsClient.isPreconfigured.mockReset();
actionsClient.isPreconfigured.mockReturnValueOnce(false);
actionsClient.isPreconfigured.mockReturnValueOnce(true);
actionsClient.isPreconfigured.mockReturnValueOnce(false);
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'preconfigured:preconfigured',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'action_2',
actionTypeId: 'test2',
params: {
foo: true,
},
},
],
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'action_2',
type: 'action',
id: '2',
},
],
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
actions: [],
scheduledTaskId: 'task-123',
},
references: [],
});
const result = await rulesClient.create({ data });
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"actionTypeId": "test",
"group": "default",
"id": "preconfigured",
"params": Object {
"foo": true,
},
},
Object {
"actionTypeId": "test2",
"group": "default",
"id": "2",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'preconfigured:preconfigured',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'action_2',
actionTypeId: 'test2',
params: {
foo: true,
},
},
],
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
legacyId: null,
executionStatus: {
error: null,
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
},
meta: { versionApiKeyLastmodified: kibanaVersion },
muteAll: false,
mutedInstanceIds: [],
name: 'abc',
notifyWhen: 'onActiveAlert',
params: { bar: true },
schedule: { interval: '10s' },
tags: ['foo'],
throttle: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
id: 'mock-saved-object-id',
references: [
{ id: '1', name: 'action_0', type: 'action' },
{ id: '2', name: 'action_2', type: 'action' },
],
}
);
expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(3);
});
test('creates a disabled alert', async () => {
const data = getMockData({ enabled: false });
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({

View file

@ -185,6 +185,106 @@ describe('find()', () => {
`);
});
test('finds rules with actions using preconfigured connectors', async () => {
unsecuredSavedObjectsClient.find.mockReset();
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'alert',
attributes: {
alertTypeId: 'myType',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'preconfigured:preconfigured',
params: {
foo: true,
},
},
],
},
score: 1,
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
},
],
});
const rulesClient = new RulesClient(rulesClientParams);
const result = await rulesClient.find({ options: {} });
expect(result).toMatchInlineSnapshot(`
Object {
"data": Array [
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"group": "default",
"id": "preconfigured",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "myType",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
},
],
"page": 1,
"perPage": 10,
"total": 1,
}
`);
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"fields": undefined,
"filter": undefined,
"sortField": undefined,
"type": "alert",
},
]
`);
});
test('calls mapSortField', async () => {
const rulesClient = new RulesClient(rulesClientParams);
await rulesClient.find({ options: { sortField: 'name' } });

View file

@ -119,6 +119,86 @@ describe('get()', () => {
`);
});
test('gets rule with actions using preconfigured connectors', async () => {
const rulesClient = new RulesClient(rulesClientParams);
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
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,
},
},
{
group: 'default',
actionRef: 'preconfigured:preconfigured',
params: {
foo: true,
},
},
],
notifyWhen: 'onActiveAlert',
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
});
const result = await rulesClient.get({ id: '1' });
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"group": "default",
"id": "preconfigured",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
]
`);
});
test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,

View file

@ -130,7 +130,10 @@ describe('update()', () => {
ruleTypeRegistry.get.mockReturnValue({
id: 'myType',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
actionGroups: [
{ id: 'default', name: 'Default' },
{ id: 'custom', name: 'Not the Default' },
],
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
@ -407,6 +410,252 @@ describe('update()', () => {
expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true });
});
test('should update a rule with some preconfigured actions', async () => {
actionsClient.getBulk.mockReset();
actionsClient.getBulk.mockResolvedValue([
{
id: '1',
actionTypeId: 'test',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'email connector',
isPreconfigured: false,
},
{
id: '2',
actionTypeId: 'test2',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'another email connector',
isPreconfigured: false,
},
{
id: 'preconfigured',
actionTypeId: 'test',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'preconfigured email connector',
isPreconfigured: true,
},
]);
actionsClient.isPreconfigured.mockReset();
actionsClient.isPreconfigured.mockReturnValueOnce(false);
actionsClient.isPreconfigured.mockReturnValueOnce(true);
actionsClient.isPreconfigured.mockReturnValueOnce(true);
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
enabled: true,
schedule: { interval: '10s' },
params: {
bar: true,
},
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'preconfigured:preconfigured',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'custom',
actionRef: 'preconfigured:preconfigured',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
notifyWhen: 'onActiveAlert',
scheduledTaskId: 'task-123',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'param:soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
const result = await rulesClient.update({
id: '1',
data: {
schedule: { interval: '10s' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
throttle: null,
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
{
group: 'default',
id: 'preconfigured',
params: {
foo: true,
},
},
{
group: 'custom',
id: 'preconfigured',
params: {
foo: true,
},
},
],
},
});
expect(unsecuredSavedObjectsClient.create).toHaveBeenNthCalledWith(
1,
'alert',
{
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'preconfigured:preconfigured',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'custom',
actionRef: 'preconfigured:preconfigured',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
consumer: 'myApp',
enabled: true,
meta: { versionApiKeyLastmodified: 'v7.10.0' },
name: 'abc',
notifyWhen: 'onActiveAlert',
params: { bar: true },
schedule: { interval: '10s' },
scheduledTaskId: 'task-123',
tags: ['foo'],
throttle: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
id: '1',
overwrite: true,
references: [{ id: '1', name: 'action_0', type: 'action' }],
version: '123',
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"actionTypeId": "test",
"group": "default",
"id": "preconfigured",
"params": Object {
"foo": true,
},
},
Object {
"actionTypeId": "test",
"group": "custom",
"id": "preconfigured",
"params": Object {
"foo": true,
},
},
],
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(3);
});
test('should call useSavedObjectReferences.extractReferences and useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const ruleParams = {
bar: true,

View file

@ -20,7 +20,6 @@ import { RawAlert } from '../types';
import { getImportWarnings } from './get_import_warnings';
import { isRuleExportable } from './is_rule_exportable';
import { RuleTypeRegistry } from '../rule_type_registry';
export { partiallyUpdateAlert } from './partially_update_alert';
export const AlertAttributesExcludedFromAAD = [
@ -48,13 +47,14 @@ export function setupSavedObjects(
savedObjects: SavedObjectsServiceSetup,
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
ruleTypeRegistry: RuleTypeRegistry,
logger: Logger
logger: Logger,
isPreconfigured: (connectorId: string) => boolean
) {
savedObjects.registerType({
name: 'alert',
hidden: true,
namespaceType: 'single',
migrations: getMigrations(encryptedSavedObjects),
migrations: getMigrations(encryptedSavedObjects, isPreconfigured),
mappings: mappings.alert as SavedObjectsTypeMappingDefinition,
management: {
importableAndExportable: true,

View file

@ -15,6 +15,8 @@ import { migrationMocks } from 'src/core/server/mocks';
const migrationContext = migrationMocks.createContext();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
const isPreconfigured = jest.fn();
describe('successful migrations', () => {
beforeEach(() => {
jest.resetAllMocks();
@ -22,7 +24,7 @@ describe('successful migrations', () => {
});
describe('7.10.0', () => {
test('marks alerts as legacy', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({});
expect(migration710(alert, migrationContext)).toMatchObject({
...alert,
@ -36,7 +38,7 @@ describe('successful migrations', () => {
});
test('migrates the consumer for metrics', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({
consumer: 'metrics',
});
@ -53,7 +55,7 @@ describe('successful migrations', () => {
});
test('migrates the consumer for siem', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({
consumer: 'securitySolution',
});
@ -70,7 +72,7 @@ describe('successful migrations', () => {
});
test('migrates the consumer for alerting', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({
consumer: 'alerting',
});
@ -87,7 +89,7 @@ describe('successful migrations', () => {
});
test('migrates PagerDuty actions to set a default dedupkey of the AlertId', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({
actions: [
{
@ -124,7 +126,7 @@ describe('successful migrations', () => {
});
test('skips PagerDuty actions with a specified dedupkey', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({
actions: [
{
@ -162,7 +164,7 @@ describe('successful migrations', () => {
});
test('skips PagerDuty actions with an eventAction of "trigger"', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({
actions: [
{
@ -201,7 +203,7 @@ describe('successful migrations', () => {
});
test('creates execution status', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData();
const dateStart = Date.now();
const migratedAlert = migration710(alert, migrationContext);
@ -229,7 +231,7 @@ describe('successful migrations', () => {
describe('7.11.0', () => {
test('add updatedAt field to alert - set to SavedObject updated_at attribute', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0'];
const alert = getMockData({}, true);
expect(migration711(alert, migrationContext)).toEqual({
...alert,
@ -242,7 +244,7 @@ describe('successful migrations', () => {
});
test('add updatedAt field to alert - set to createdAt when SavedObject updated_at is not defined', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0'];
const alert = getMockData({});
expect(migration711(alert, migrationContext)).toEqual({
...alert,
@ -255,7 +257,7 @@ describe('successful migrations', () => {
});
test('add notifyWhen=onActiveAlert when throttle is null', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0'];
const alert = getMockData({});
expect(migration711(alert, migrationContext)).toEqual({
...alert,
@ -268,7 +270,7 @@ describe('successful migrations', () => {
});
test('add notifyWhen=onActiveAlert when throttle is set', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0'];
const alert = getMockData({ throttle: '5m' });
expect(migration711(alert, migrationContext)).toEqual({
...alert,
@ -283,7 +285,7 @@ describe('successful migrations', () => {
describe('7.11.2', () => {
test('transforms connectors that support incident correctly', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2'];
const alert = getMockData({
actions: [
{
@ -425,7 +427,7 @@ describe('successful migrations', () => {
});
test('it transforms only subAction=pushToService', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2'];
const alert = getMockData({
actions: [
{
@ -444,7 +446,7 @@ describe('successful migrations', () => {
});
test('it does not transforms other connectors', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2'];
const alert = getMockData({
actions: [
{
@ -523,7 +525,7 @@ describe('successful migrations', () => {
});
test('it does not transforms alerts when the right structure connectors is already applied', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2'];
const alert = getMockData({
actions: [
{
@ -560,7 +562,7 @@ describe('successful migrations', () => {
});
test('if incident attribute is an empty object, copy back the related attributes from subActionParams back to incident', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2'];
const alert = getMockData({
actions: [
{
@ -622,7 +624,7 @@ describe('successful migrations', () => {
});
test('custom action does not get migrated/loss', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2'];
const alert = getMockData({
actions: [
{
@ -651,7 +653,7 @@ describe('successful migrations', () => {
describe('7.13.0', () => {
test('security solution alerts get migrated and remove null values', () => {
const migration713 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -747,7 +749,7 @@ describe('successful migrations', () => {
});
test('non-null values in security solution alerts are not modified', () => {
const migration713 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -815,7 +817,7 @@ describe('successful migrations', () => {
});
test('security solution threshold alert with string in threshold.field is migrated to array', () => {
const migration713 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -846,7 +848,7 @@ describe('successful migrations', () => {
});
test('security solution threshold alert with empty string in threshold.field is migrated to empty array', () => {
const migration713 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -877,7 +879,7 @@ describe('successful migrations', () => {
});
test('security solution threshold alert with array in threshold.field and cardinality is left alone', () => {
const migration713 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -919,7 +921,7 @@ describe('successful migrations', () => {
});
test('security solution ML alert with string in machineLearningJobId is converted to an array', () => {
const migration713 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -945,7 +947,7 @@ describe('successful migrations', () => {
});
test('security solution ML alert with an array in machineLearningJobId is preserved', () => {
const migration713 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -973,7 +975,7 @@ describe('successful migrations', () => {
describe('7.14.1', () => {
test('security solution author field is migrated to array if it is undefined', () => {
const migration7141 = getMigrations(encryptedSavedObjectsSetup)['7.14.1'];
const migration7141 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.14.1'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {},
@ -991,7 +993,7 @@ describe('successful migrations', () => {
});
test('security solution author field does not override existing values if they exist', () => {
const migration7141 = getMigrations(encryptedSavedObjectsSetup)['7.14.1'];
const migration7141 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.14.1'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -1015,7 +1017,7 @@ describe('successful migrations', () => {
describe('7.15.0', () => {
test('security solution is migrated to saved object references if it has 1 exceptionsList', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -1044,7 +1046,7 @@ describe('successful migrations', () => {
});
test('security solution is migrated to saved object references if it has 2 exceptionsLists', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -1084,7 +1086,7 @@ describe('successful migrations', () => {
});
test('security solution is migrated to saved object references if it has 3 exceptionsLists', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -1135,7 +1137,7 @@ describe('successful migrations', () => {
});
test('security solution does not change anything if exceptionsList is missing', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = getMockData({
alertTypeId: 'siem.signals',
params: {
@ -1147,7 +1149,7 @@ describe('successful migrations', () => {
});
test('security solution will keep existing references if we do not have an exceptionsList but we do already have references', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = {
...getMockData({
alertTypeId: 'siem.signals',
@ -1177,7 +1179,7 @@ describe('successful migrations', () => {
});
test('security solution keep any foreign references if they exist but still migrate other references', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = {
...getMockData({
alertTypeId: 'siem.signals',
@ -1242,7 +1244,7 @@ describe('successful migrations', () => {
});
test('security solution is idempotent and if re-run on the same migrated data will keep the same items', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = {
...getMockData({
alertTypeId: 'siem.signals',
@ -1282,7 +1284,7 @@ describe('successful migrations', () => {
});
test('security solution will migrate with only missing data if we have partially migrated data', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = {
...getMockData({
alertTypeId: 'siem.signals',
@ -1331,7 +1333,7 @@ describe('successful migrations', () => {
});
test('security solution will not migrate if exception list if it is invalid data', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = {
...getMockData({
alertTypeId: 'siem.signals',
@ -1345,7 +1347,7 @@ describe('successful migrations', () => {
});
test('security solution will migrate valid data if it is mixed with invalid data', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = {
...getMockData({
alertTypeId: 'siem.signals',
@ -1387,7 +1389,7 @@ describe('successful migrations', () => {
});
test('security solution will not migrate if exception list is invalid data but will keep existing references', () => {
const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0'];
const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0'];
const alert = {
...getMockData({
alertTypeId: 'siem.signals',
@ -1419,7 +1421,7 @@ describe('successful migrations', () => {
describe('7.16.0', () => {
test('add legacyId field to alert - set to SavedObject id attribute', () => {
const migration716 = getMigrations(encryptedSavedObjectsSetup)['7.16.0'];
const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0'];
const alert = getMockData({}, true);
expect(migration716(alert, migrationContext)).toEqual({
...alert,
@ -1429,6 +1431,274 @@ describe('successful migrations', () => {
},
});
});
test('removes preconfigured connectors from references array', () => {
isPreconfigured.mockReset();
isPreconfigured.mockReturnValueOnce(true);
isPreconfigured.mockReturnValueOnce(false);
const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0'];
const rule = {
...getMockData({
actions: [
{
actionRef: 'action_0',
actionTypeId: '.slack',
group: 'small',
params: {
message: 'preconfigured',
},
},
{
actionRef: 'action_1',
actionTypeId: '.server-log',
group: 'small',
params: {
level: 'info',
message: 'boo',
},
},
],
}),
references: [
{
id: 'my-slack1',
name: 'action_0',
type: 'action',
},
{
id: '997c0120-00ee-11ec-b067-2524946ba327',
name: 'action_1',
type: 'action',
},
],
};
expect(migration716(rule, migrationContext)).toEqual({
...rule,
attributes: {
...rule.attributes,
legacyId: rule.id,
actions: [
{
actionRef: 'preconfigured:my-slack1',
actionTypeId: '.slack',
group: 'small',
params: {
message: 'preconfigured',
},
},
{
actionRef: 'action_1',
actionTypeId: '.server-log',
group: 'small',
params: {
level: 'info',
message: 'boo',
},
},
],
},
references: [
{
id: '997c0120-00ee-11ec-b067-2524946ba327',
name: 'action_1',
type: 'action',
},
],
});
});
test('removes preconfigured connectors from references array and maintains non-action references', () => {
isPreconfigured.mockReset();
isPreconfigured.mockReturnValueOnce(true);
isPreconfigured.mockReturnValueOnce(false);
isPreconfigured.mockReturnValueOnce(false);
const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0'];
const rule = {
...getMockData({
actions: [
{
actionRef: 'action_0',
actionTypeId: '.slack',
group: 'small',
params: {
message: 'preconfigured',
},
},
{
actionRef: 'action_1',
actionTypeId: '.server-log',
group: 'small',
params: {
level: 'info',
message: 'boo',
},
},
],
}),
references: [
{
id: 'my-slack1',
name: 'action_0',
type: 'action',
},
{
id: '997c0120-00ee-11ec-b067-2524946ba327',
name: 'action_1',
type: 'action',
},
{
id: '3838d98c-67d3-49e8-b813-aa8404bb6b1a',
name: 'params:something-else',
type: 'some-other-type',
},
],
};
expect(migration716(rule, migrationContext)).toEqual({
...rule,
attributes: {
...rule.attributes,
legacyId: rule.id,
actions: [
{
actionRef: 'preconfigured:my-slack1',
actionTypeId: '.slack',
group: 'small',
params: {
message: 'preconfigured',
},
},
{
actionRef: 'action_1',
actionTypeId: '.server-log',
group: 'small',
params: {
level: 'info',
message: 'boo',
},
},
],
},
references: [
{
id: '997c0120-00ee-11ec-b067-2524946ba327',
name: 'action_1',
type: 'action',
},
{
id: '3838d98c-67d3-49e8-b813-aa8404bb6b1a',
name: 'params:something-else',
type: 'some-other-type',
},
],
});
});
test('does nothing to rules with no references', () => {
isPreconfigured.mockReset();
const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0'];
const rule = {
...getMockData({
actions: [
{
actionRef: 'action_0',
actionTypeId: '.slack',
group: 'small',
params: {
message: 'preconfigured',
},
},
{
actionRef: 'action_1',
actionTypeId: '.server-log',
group: 'small',
params: {
level: 'info',
message: 'boo',
},
},
],
}),
references: [],
};
expect(migration716(rule, migrationContext)).toEqual({
...rule,
attributes: {
...rule.attributes,
legacyId: rule.id,
},
});
});
test('does nothing to rules with no action references', () => {
isPreconfigured.mockReset();
const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0'];
const rule = {
...getMockData({
actions: [
{
actionRef: 'action_0',
actionTypeId: '.slack',
group: 'small',
params: {
message: 'preconfigured',
},
},
{
actionRef: 'action_1',
actionTypeId: '.server-log',
group: 'small',
params: {
level: 'info',
message: 'boo',
},
},
],
}),
references: [
{
id: '3838d98c-67d3-49e8-b813-aa8404bb6b1a',
name: 'params:something-else',
type: 'some-other-type',
},
],
};
expect(migration716(rule, migrationContext)).toEqual({
...rule,
attributes: {
...rule.attributes,
legacyId: rule.id,
},
});
});
test('does nothing to rules with references but no actions', () => {
isPreconfigured.mockReset();
const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0'];
const rule = {
...getMockData({
actions: [],
}),
references: [
{
id: 'my-slack1',
name: 'action_0',
type: 'action',
},
{
id: '997c0120-00ee-11ec-b067-2524946ba327',
name: 'action_1',
type: 'action',
},
],
};
expect(migration716(rule, migrationContext)).toEqual({
...rule,
attributes: {
...rule.attributes,
legacyId: rule.id,
},
});
});
});
});
@ -1441,7 +1711,7 @@ describe('handles errors during migrations', () => {
});
describe('7.10.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0'];
const alert = getMockData({
consumer: 'alerting',
});
@ -1466,7 +1736,7 @@ describe('handles errors during migrations', () => {
describe('7.11.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0'];
const alert = getMockData({
consumer: 'alerting',
});
@ -1491,7 +1761,7 @@ describe('handles errors during migrations', () => {
describe('7.11.2 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2'];
const alert = getMockData({
consumer: 'alerting',
});
@ -1516,7 +1786,7 @@ describe('handles errors during migrations', () => {
describe('7.13.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration7130 = getMigrations(encryptedSavedObjectsSetup)['7.13.0'];
const migration7130 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0'];
const alert = getMockData({
consumer: 'alerting',
});
@ -1538,6 +1808,29 @@ describe('handles errors during migrations', () => {
);
});
});
describe('7.16.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0'];
const rule = getMockData();
expect(() => {
migration7160(rule, migrationContext);
}).toThrowError(`Can't migrate!`);
expect(migrationContext.log.error).toHaveBeenCalledWith(
`encryptedSavedObject 7.16.0 migration failed for alert ${rule.id} with error: Can't migrate!`,
{
migrations: {
alertDocument: {
...rule,
attributes: {
...rule.attributes,
},
},
},
}
);
});
});
});
function getUpdatedAt(): string {

View file

@ -55,7 +55,8 @@ export const isSecuritySolutionRule = (doc: SavedObjectUnsanitizedDoc<RawAlert>)
doc.attributes.alertTypeId === 'siem.signals';
export function getMigrations(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
isPreconfigured: (connectorId: string) => boolean
): SavedObjectMigrationMap {
const migrationWhenRBACWasIntroduced = createEsoMigration(
encryptedSavedObjects,
@ -99,10 +100,10 @@ export function getMigrations(
pipeMigrations(addExceptionListsToReferences)
);
const migrateLegacyIds716 = createEsoMigration(
const migrateRules716 = createEsoMigration(
encryptedSavedObjects,
(doc): doc is SavedObjectUnsanitizedDoc<RawAlert> => true,
pipeMigrations(setLegacyId)
pipeMigrations(setLegacyId, getRemovePreconfiguredConnectorsFromReferencesFn(isPreconfigured))
);
return {
@ -112,7 +113,7 @@ export function getMigrations(
'7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'),
'7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'),
'7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'),
'7.16.0': executeMigrationWithErrorHandling(migrateLegacyIds716, '7.16.0'),
'7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'),
};
}
@ -587,6 +588,82 @@ function setLegacyId(
};
}
function getRemovePreconfiguredConnectorsFromReferencesFn(
isPreconfigured: (connectorId: string) => boolean
) {
return (doc: SavedObjectUnsanitizedDoc<RawAlert>) => {
return removePreconfiguredConnectorsFromReferences(doc, isPreconfigured);
};
}
function removePreconfiguredConnectorsFromReferences(
doc: SavedObjectUnsanitizedDoc<RawAlert>,
isPreconfigured: (connectorId: string) => boolean
): SavedObjectUnsanitizedDoc<RawAlert> {
const {
attributes: { actions },
references,
} = doc;
// Look for connector references
const connectorReferences = (references ?? []).filter((ref: SavedObjectReference) =>
ref.name.startsWith('action_')
);
if (connectorReferences.length > 0) {
const restReferences = (references ?? []).filter(
(ref: SavedObjectReference) => !ref.name.startsWith('action_')
);
const updatedConnectorReferences: SavedObjectReference[] = [];
const updatedActions: RawAlert['actions'] = [];
// For each connector reference, check if connector is preconfigured
// If yes, we need to remove from the references array and update
// the corresponding action so it directly references the preconfigured connector id
connectorReferences.forEach((connectorRef: SavedObjectReference) => {
// Look for the corresponding entry in the actions array
const correspondingAction = getCorrespondingAction(actions, connectorRef.name);
if (correspondingAction) {
if (isPreconfigured(connectorRef.id)) {
updatedActions.push({
...correspondingAction,
actionRef: `preconfigured:${connectorRef.id}`,
});
} else {
updatedActions.push(correspondingAction);
updatedConnectorReferences.push(connectorRef);
}
} else {
// Couldn't find the matching action, leave as is
updatedConnectorReferences.push(connectorRef);
}
});
return {
...doc,
attributes: {
...doc.attributes,
actions: [...updatedActions],
},
references: [...updatedConnectorReferences, ...restReferences],
};
}
return doc;
}
function getCorrespondingAction(
actions: SavedObjectAttribute,
connectorRef: string
): RawAlertAction | null {
if (!Array.isArray(actions)) {
return null;
} else {
return actions.find(
(action) => (action as RawAlertAction)?.actionRef === connectorRef
) as RawAlertAction;
}
}
function pipeMigrations(...migrations: AlertMigration[]): AlertMigration {
return (doc: SavedObjectUnsanitizedDoc<RawAlert>) =>
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);

View file

@ -204,7 +204,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
is_preconfigured: true,
connector_type_id: '.slack',
name: 'Slack#xyz',
referenced_by_count: 1,
referenced_by_count: 0,
},
{
id: 'custom-system-abc-connector',

View file

@ -7,6 +7,7 @@
import expect from '@kbn/expect';
import type { ApiResponse, estypes } from '@elastic/elasticsearch';
import { SavedObject } from 'kibana/server';
import { Spaces } from '../../scenarios';
import {
checkAAD,
@ -17,6 +18,7 @@ import {
TaskManagerDoc,
} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { RawAlert } from '../../../../../plugins/alerting/server/types';
// eslint-disable-next-line import/no-default-export
export default function createAlertTests({ getService }: FtrProviderContext) {
@ -115,6 +117,112 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
});
});
it('should store references correctly for actions', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'MY action',
connector_type_id: 'test.noop',
config: {},
secrets: {},
})
.expect(200);
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestAlertData({
actions: [
{
id: createdAction.id,
group: 'default',
params: {},
},
{
id: 'my-slack1',
group: 'default',
params: {
message: 'something important happened!',
},
},
],
})
);
expect(response.status).to.eql(200);
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
expect(response.body).to.eql({
id: response.body.id,
name: 'abc',
tags: ['foo'],
actions: [
{
id: createdAction.id,
connector_type_id: createdAction.connector_type_id,
group: 'default',
params: {},
},
{
id: 'my-slack1',
group: 'default',
connector_type_id: '.slack',
params: {
message: 'something important happened!',
},
},
],
enabled: true,
rule_type_id: 'test.noop',
consumer: 'alertsFixture',
params: {},
created_by: null,
schedule: { interval: '1m' },
scheduled_task_id: response.body.scheduled_task_id,
updated_by: null,
api_key_owner: null,
throttle: '1m',
notify_when: 'onThrottleInterval',
mute_all: false,
muted_alert_ids: [],
created_at: response.body.created_at,
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
});
const esResponse = await es.get<SavedObject<RawAlert>>({
index: '.kibana',
id: `${Spaces.space1.id}:alert:${response.body.id}`,
});
expect(esResponse.statusCode).to.eql(200);
const rawActions = (esResponse.body._source as any)?.alert.actions ?? [];
expect(rawActions).to.eql([
{
actionRef: 'action_0',
actionTypeId: 'test.noop',
group: 'default',
params: {},
},
{
actionRef: 'preconfigured:my-slack1',
actionTypeId: '.slack',
group: 'default',
params: {
message: 'something important happened!',
},
},
]);
const references = esResponse.body._source?.references ?? [];
expect(references.length).to.eql(1);
expect(references[0]).to.eql({
id: createdAction.id,
name: 'action_0',
type: 'action',
});
});
// see: https://github.com/elastic/kibana/issues/100607
// note this fails when the mappings for `params` does not have ignore_above
it('should handle alerts with immense params', async () => {

View file

@ -9,7 +9,7 @@ import expect from '@kbn/expect';
import type { ApiResponse, estypes } from '@elastic/elasticsearch';
import { getUrlPrefix } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import type { RawAlert } from '../../../../../plugins/alerting/server/types';
import type { RawAlert, RawAlertAction } from '../../../../../plugins/alerting/server/types';
// eslint-disable-next-line import/no-default-export
export default function createGetTests({ getService }: FtrProviderContext) {
@ -218,5 +218,44 @@ export default function createGetTests({ getService }: FtrProviderContext) {
'74f3e6d7-b7bb-477d-ac28-92ee22728e6e'
);
});
it('7.16.0 migrates existing rules so predefined connectors are not stored in references', async () => {
const searchResult: ApiResponse<estypes.SearchResponse<RawAlert>> = await es.search({
index: '.kibana',
body: {
query: {
term: {
_id: 'alert:9c003b00-00ee-11ec-b067-2524946ba327',
},
},
},
});
expect(searchResult.statusCode).to.equal(200);
expect((searchResult.body.hits.total as estypes.SearchTotalHits).value).to.equal(1);
const hit = searchResult.body.hits.hits[0];
expect((hit!._source!.alert! as RawAlert).actions! as RawAlertAction[]).to.eql([
{
actionRef: 'action_0',
actionTypeId: 'test.noop',
group: 'default',
params: {},
},
{
actionRef: 'preconfigured:my-slack1',
actionTypeId: '.slack',
group: 'default',
params: {
message: 'something happened!',
},
},
]);
expect(hit!._source!.references!).to.eql([
{
id: '66a8ab7a-35cf-445e-ade3-215a029c6969',
name: 'action_0',
type: 'action',
},
]);
});
});
}

View file

@ -387,3 +387,68 @@
}
}
}
{
"type": "doc",
"value": {
"id": "alert:9c003b00-00ee-11ec-b067-2524946ba327",
"index": ".kibana_1",
"source": {
"alert": {
"actions": [{
"actionRef": "action_0",
"actionTypeId": "test.noop",
"group": "default",
"params": {
}
},
{
"actionRef": "action_1",
"actionTypeId": ".slack",
"group": "default",
"params": {
"message": "something happened!"
}
}],
"alertTypeId": "test.noop",
"apiKey": null,
"apiKeyOwner": null,
"consumer": "alertsFixture",
"createdAt": "2020-09-22T15:16:07.451Z",
"createdBy": null,
"enabled": true,
"muteAll": false,
"mutedInstanceIds": [
],
"name": "test migration of preconfigured connector",
"params": {
},
"schedule": {
"interval": "1m"
},
"scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4",
"tags": [
],
"throttle": null,
"updatedBy": "elastic"
},
"migrationVersion": {
"alert": "7.15.0"
},
"references": [
{
"id": "66a8ab7a-35cf-445e-ade3-215a029c6969",
"name": "action_0",
"type": "action"
},
{
"id": "my-slack1",
"name": "action_1",
"type": "action"
}
],
"type": "alert",
"updated_at": "2020-06-17T15:35:39.839Z"
}
}
}