[Security Solution][Rules Management] Separate actions import logic from rules import (#216380)

## Summary

Redo of https://github.com/elastic/kibana/pull/193471
Closes https://github.com/elastic/security-team/issues/8644

> Fixes a bug where importing a rule fails with a connector into a space
where (1) the connector already exists, and (2) the existing connector
was exported and re-imported from another space. The import logic in
this scenario effectively tries to convert the action ID on the rule
import twice. The second conversion attempt tries to use the old action
ID to look up the correct new action ID in a map, however, in this test
scenario the action ID has already been updated by legacy SO ID
migration logic and there is no map entry with the new ID as a key. The
result is that the second attempt sets the action ID to undefined,
resulting in an import failure.

The root cause of the bug is that we have two different places in the
rule import logic where action IDs are migrated. The first ID migration
was done by `migrateLegacyActionsIds` prior to importing rule actions,
and the second migration was done by `importRuleActionConnectors` after
importing the actions. `importRuleActionConnectors` used a lookup table
to convert old IDs to new IDs, but if the connector already existed and
had an `originId` then the rule action would already be migrated by
`migrateLegacyActionsIds`. The lookup table used by
`importRuleActionConnectors` does not have entries for migrated IDs,
only the original IDs, so in that case the result of the lookup is
`undefined` which we assign to the action ID.

This PR reworks the logic to create a clean separation between action
and rule import. We now import the connectors first, ignoring the rules,
then migrate action IDs on the rules afterwards. This handles connectors
changing IDs in any way, either through the 7.x->8.0 migration long ago
or IDs changing on import if there are ID conflicts. Only after the
connectors are imported and rule actions are migrated do we then verify
if each rule action references a connector ID that actually exists with
the new `checkRuleActions` function, replacing
`checkIfActionsHaveMissingConnectors` and related functions that were
also buggy.

Finally, as a nice side effect this rework removes "rule action
connector missing" errors out of the `action_connector_errors` part of
the response. `action_connector_errors` is reserved for errors importing
connectors specifically. If a rule action is missing a connector and
therefore we don't import the rule, that's a rule error and it's
represented in the `errors` part of the response. Since the shape of the
response is not changing, I don't consider this a breaking change but
rather a bug fix.

## Repro Steps

Repro Steps
1. Download the export file below and change the extension back to
.ndjson from .json (github does not allow .ndjson files

[rules_export.json](https://github.com/user-attachments/files/17065272/rules_export.json)
2. Import the rule and connector into a space (default is fine)
3. Create a new space
4. Import the rule and connector into the new space
5. Import the rule and connector into the new space again, but check the
`Overwrite existing connectors with conflicting action "id"` box.
Observe the failure.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marshall Main 2025-04-16 11:47:26 -04:00 committed by GitHub
parent d58e274fc3
commit 52ecdd0ac7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 335 additions and 1873 deletions

View file

@ -72,7 +72,6 @@ export const createBulkErrorObject = ({
};
} else {
return {
rule_id: '(unknown id)',
error: {
status_code: statusCode,
message,

View file

@ -202,7 +202,6 @@ describe.skip('Import rules route', () => {
message: `Unexpected token 'h', "this is not"... is not valid JSON`,
status_code: 400,
},
rule_id: '(unknown id)',
},
],
success: false,
@ -350,14 +349,12 @@ describe.skip('Import rules route', () => {
message: 'rule_id: Required',
status_code: 400,
},
rule_id: '(unknown id)',
},
{
error: {
message: 'rule_id: Required',
status_code: 400,
},
rule_id: '(unknown id)',
},
],
success: false,

View file

@ -26,6 +26,7 @@ import {
} from '../../../../routes/utils';
import { createPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { importRuleActionConnectors } from '../../../logic/import/action_connectors/import_rule_action_connectors';
import { validateRuleActions } from '../../../logic/import/action_connectors/validate_rule_actions';
import { createRuleSourceImporter } from '../../../logic/import/rule_source_importer';
import { importRules } from '../../../logic/import/import_rules';
@ -123,13 +124,9 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
maxExceptionsImportSize: objectLimit,
});
// report on duplicate rules
const [duplicateIdErrors, parsedObjectsWithoutDuplicateErrors] =
getTupleDuplicateErrorsAndUniqueRules(rules, request.query.overwrite);
const migratedParsedObjectsWithoutDuplicateErrors = await migrateLegacyActionsIds(
parsedObjectsWithoutDuplicateErrors,
actionSOClient,
actionsClient
const [duplicateIdErrors, rulesToImportOrErrors] = getTupleDuplicateErrorsAndUniqueRules(
rules,
request.query.overwrite
);
// import actions-connectors
@ -138,20 +135,17 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
success: actionConnectorSuccess,
warnings: actionConnectorWarnings,
errors: actionConnectorErrors,
rulesWithMigratedActions,
} = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter,
rules: migratedParsedObjectsWithoutDuplicateErrors,
overwrite: request.query.overwrite_action_connectors,
});
// rulesWithMigratedActions: Is returned only in case connectors were exported from different namespace and the
// original rules actions' ids were replaced with new destinationIds
const parsedRuleStream = actionConnectorErrors.length
? []
: rulesWithMigratedActions || migratedParsedObjectsWithoutDuplicateErrors;
const migratedRulesToImportOrErrors = await migrateLegacyActionsIds(
rulesToImportOrErrors,
actionSOClient,
actionsClient
);
const ruleSourceImporter = createRuleSourceImporter({
config,
@ -160,8 +154,20 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
prebuiltRuleObjectsClient: createPrebuiltRuleObjectsClient(rulesClient),
});
const [parsedRules, parsedRuleErrors] = partition(isRuleToImport, parsedRuleStream);
const ruleChunks = chunk(CHUNK_PARSED_OBJECT_SIZE, parsedRules);
const [parsedRules, parsedRuleErrors] = partition(
isRuleToImport,
migratedRulesToImportOrErrors
);
// After importing the actions and migrating action IDs on rules to import,
// validate that all actions referenced by rules exist
// Filter out rules that reference non-existent actions
const { validatedActionRules, missingActionErrors } = await validateRuleActions({
actionsClient,
rules: parsedRules,
});
const ruleChunks = chunk(CHUNK_PARSED_OBJECT_SIZE, validatedActionRules);
const importRuleResponse = await importRules({
ruleChunks,
@ -180,9 +186,9 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
const importErrors = importRuleResponse.filter(isBulkError);
const errors = [
...parseErrors,
...actionConnectorErrors,
...duplicateIdErrors,
...importErrors,
...missingActionErrors,
];
const successes = importRuleResponse.filter((resp) => {

View file

@ -1,659 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
import {
getImportRulesSchemaMock,
webHookConnector,
} from '../../../../../../../common/api/detection_engine/rule_management/import_rules/rule_to_import.mock';
import { importRuleActionConnectors } from './import_rule_action_connectors';
import { coreMock } from '@kbn/core/server/mocks';
const rules = [
getImportRulesSchemaMock({
actions: [
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
action_type_id: '.webhook',
params: {},
},
],
}),
];
const rulesWithoutActions = [getImportRulesSchemaMock({ actions: [] })];
const actionConnectors = [webHookConnector];
const actionsClient = actionsClientMock.create();
actionsClient.getAll.mockResolvedValue([]);
const core = coreMock.createRequestHandlerContext();
describe('importRuleActionConnectors', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should show an error message when the user has a Read Actions permission and stops the importing ', async () => {
const newCore = coreMock.createRequestHandlerContext();
const error = {
output: { payload: { message: 'Unable to bulk_create action' }, statusCode: 403 },
};
newCore.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
import: jest.fn().mockImplementation(() => {
throw error;
}),
});
const actionsImporter2 = newCore.savedObjects.getImporter;
const res = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter: actionsImporter2(),
rules,
overwrite: false,
});
expect(res).toEqual({
success: false,
successCount: 0,
errors: [
{
error: {
message:
'You may not have actions privileges required to import rules with actions: Unable to bulk_create action',
status_code: 403,
},
rule_id: '(unknown id)',
},
],
warnings: [],
});
});
it('should return import 1 connector successfully', async () => {
core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
import: jest.fn().mockResolvedValue({
success: true,
successCount: 1,
errors: [],
warnings: [],
}),
});
const actionsImporter = core.savedObjects.getImporter;
const res = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter: actionsImporter(),
rules,
overwrite: false,
});
expect(res).toEqual({
success: true,
successCount: 1,
errors: [],
warnings: [],
});
});
it('should return import 1 connector successfully only if id is duplicated', async () => {
core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
import: jest.fn().mockResolvedValue({
success: true,
successCount: 1,
errors: [],
warnings: [],
}),
});
const actionsImporter = core.savedObjects.getImporter;
const ruleWith2Connectors = [
getImportRulesSchemaMock({
actions: [
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
},
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
},
],
}),
];
const res = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter: actionsImporter(),
rules: ruleWith2Connectors,
overwrite: false,
});
expect(res).toEqual({
success: true,
successCount: 1,
errors: [],
warnings: [],
});
});
it('should show an error message when the user has an old imported rule with a missing connector data', async () => {
const actionsImporter = core.savedObjects.getImporter;
const res = await importRuleActionConnectors({
actionConnectors: [],
actionsClient,
actionsImporter: actionsImporter(),
rules,
overwrite: false,
});
expect(res).toEqual({
success: false,
successCount: 0,
errors: [
{
error: {
message:
'1 connector is missing. Connector id missing is: cabc78e0-9031-11ed-b076-53cc4d57aaf1',
status_code: 404,
},
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
rule_id: 'rule-1',
},
],
warnings: [],
});
});
it('should show an error message when the user has an old imported rule with a 2 missing connectors data', async () => {
const actionsImporter = core.savedObjects.getImporter;
const res = await importRuleActionConnectors({
actionConnectors: [],
actionsClient,
actionsImporter: actionsImporter(),
rules: [
getImportRulesSchemaMock({
actions: [
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
action_type_id: '.webhook',
params: {},
},
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf2',
action_type_id: '.webhook',
params: {},
},
],
}),
],
overwrite: false,
});
expect(res).toEqual({
success: false,
successCount: 0,
errors: [
{
error: {
message:
'2 connectors are missing. Connector ids missing are: cabc78e0-9031-11ed-b076-53cc4d57aaf1, cabc78e0-9031-11ed-b076-53cc4d57aaf2',
status_code: 404,
},
rule_id: 'rule-1',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1,cabc78e0-9031-11ed-b076-53cc4d57aaf2',
},
],
warnings: [],
});
});
it('should show an error message when the user has 2 imported rules with a 2 missing connectors data', async () => {
const actionsImporter = core.savedObjects.getImporter;
const res = await importRuleActionConnectors({
actionConnectors: [],
actionsClient,
actionsImporter: actionsImporter(),
rules: [
getImportRulesSchemaMock({
rule_id: 'rule-1',
actions: [
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
action_type_id: '.webhook',
params: {},
},
],
}),
getImportRulesSchemaMock({
rule_id: 'rule-2',
actions: [
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf2',
action_type_id: '.webhook',
params: {},
},
],
}),
],
overwrite: false,
});
expect(res).toEqual({
success: false,
successCount: 0,
errors: [
{
error: {
message:
'2 connectors are missing. Connector ids missing are: cabc78e0-9031-11ed-b076-53cc4d57aaf1, cabc78e0-9031-11ed-b076-53cc4d57aaf2',
status_code: 404,
},
rule_id: 'rule-1,rule-2',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1,cabc78e0-9031-11ed-b076-53cc4d57aaf2',
},
],
warnings: [],
});
});
it('should skip importing the action-connectors if the actions array is empty, even if the user has exported-connectors in the file', async () => {
core.savedObjects.getImporter = jest.fn().mockReturnValue({
import: jest.fn().mockResolvedValue({
success: true,
successCount: 2,
errors: [],
warnings: [],
}),
});
const actionsImporter2 = core.savedObjects.getImporter;
const actionsImporter2Importer = actionsImporter2();
const res = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter: actionsImporter2Importer,
rules: rulesWithoutActions,
overwrite: false,
});
expect(res).toEqual({
success: true,
successCount: 0,
errors: [],
warnings: [],
});
expect(actionsImporter2Importer.import).not.toBeCalled();
});
it('should skip importing the action-connectors if all connectors have been imported/created before', async () => {
actionsClient.getAll.mockResolvedValue([
{
actionTypeId: '.webhook',
name: 'webhook',
isPreconfigured: true,
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
referencedByCount: 1,
isDeprecated: false,
isSystemAction: false,
},
]);
const actionsImporter2 = core.savedObjects.getImporter;
const actionsImporter2Importer = actionsImporter2();
const res = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter: actionsImporter2Importer,
rules,
overwrite: false,
});
expect(res).toEqual({
success: true,
successCount: 0,
errors: [],
warnings: [],
});
expect(actionsImporter2Importer.import).not.toBeCalled();
});
it('should import one rule with connector successfully even if it was exported from different namespaces by generating destinationId and replace the old actionId with it', async () => {
const successResults = [
{
destinationId: '72cab9bb-535f-45dd-b9c2-5bc1bc0db96b',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
meta: { title: 'Connector: [anotherSpaceSlack]', icon: undefined },
type: 'action',
},
];
core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
import: jest.fn().mockResolvedValue({
success: true,
successCount: 1,
successResults,
errors: [],
warnings: [],
}),
});
const actionsImporter = core.savedObjects.getImporter;
actionsClient.getAll.mockResolvedValue([]);
const res = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter: actionsImporter(),
rules,
overwrite: false,
});
const rulesWithMigratedActions = [
{
actions: [
{
action_type_id: '.webhook',
group: 'default',
id: '72cab9bb-535f-45dd-b9c2-5bc1bc0db96b',
params: {},
},
],
description: 'some description',
immutable: false,
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
risk_score: 55,
rule_id: 'rule-1',
severity: 'high',
type: 'query',
},
];
expect(res).toEqual({
success: true,
successCount: 1,
errors: [],
warnings: [],
rulesWithMigratedActions,
});
});
it('should import multiple rules with connectors successfully even if they were exported from different namespaces by generating destinationIds and replace the old actionIds with them', async () => {
const multipleRules = [
getImportRulesSchemaMock({
rule_id: 'rule_1',
actions: [
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
action_type_id: '.webhook',
params: {},
},
],
}),
getImportRulesSchemaMock({
rule_id: 'rule_2',
id: '0abc78e0-7031-11ed-b076-53cc4d57aaf1',
actions: [
{
group: 'default',
id: '11abc78e0-9031-11ed-b076-53cc4d57aaw',
action_type_id: '.index',
params: {},
},
],
}),
];
const successResults = [
{
destinationId: '72cab9bb-535f-45dd-b9c2-5bc1bc0db96b',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
meta: { title: 'Connector: [anotherSpaceSlack]', icon: undefined },
type: 'action',
},
{
destinationId: '892cab9bb-535f-45dd-b9c2-5bc1bc0db96',
id: '11abc78e0-9031-11ed-b076-53cc4d57aaw',
meta: { title: 'Connector: [anotherSpaceSlack]', icon: undefined },
type: 'action',
},
];
core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
import: jest.fn().mockResolvedValue({
success: true,
successCount: 1,
successResults,
errors: [],
warnings: [],
}),
});
const actionsImporter = core.savedObjects.getImporter;
const actionConnectorsWithIndex = [
...actionConnectors,
{
id: '0abc78e0-7031-11ed-b076-53cc4d57aaf1',
type: 'action',
updated_at: '2023-01-25T14:35:52.852Z',
created_at: '2023-01-25T14:35:52.852Z',
version: 'WzUxNTksMV0=',
attributes: {
actionTypeId: '.webhook',
name: 'webhook',
isMissingSecrets: false,
config: {},
secrets: {},
},
references: [],
migrationVersion: { action: '8.3.0' },
coreMigrationVersion: '8.7.0',
},
];
actionsClient.getAll.mockResolvedValue([]);
const res = await importRuleActionConnectors({
actionConnectors: actionConnectorsWithIndex,
actionsClient,
actionsImporter: actionsImporter(),
rules: multipleRules,
overwrite: false,
});
const rulesWithMigratedActions = [
{
actions: [
{
action_type_id: '.webhook',
group: 'default',
id: '72cab9bb-535f-45dd-b9c2-5bc1bc0db96b',
params: {},
},
],
description: 'some description',
immutable: false,
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
risk_score: 55,
rule_id: 'rule_1',
severity: 'high',
type: 'query',
},
{
actions: [
{
action_type_id: '.index',
group: 'default',
id: '892cab9bb-535f-45dd-b9c2-5bc1bc0db96',
params: {},
},
],
description: 'some description',
immutable: false,
name: 'Query with a rule id',
id: '0abc78e0-7031-11ed-b076-53cc4d57aaf1',
rule_id: 'rule_2',
query: 'user.name: root or user.name: admin',
risk_score: 55,
severity: 'high',
type: 'query',
},
];
expect(res).toEqual({
success: true,
successCount: 1,
errors: [],
warnings: [],
rulesWithMigratedActions,
});
});
describe('overwrite is set to "true"', () => {
it('should return an error when action connectors are missing in ndjson import file', async () => {
const rulesToImport = [
getImportRulesSchemaMock({
rule_id: 'rule-with-missed-action-connector',
actions: [
{
group: 'default',
id: 'some-connector-id',
params: {},
action_type_id: '.webhook',
},
],
}),
];
actionsClient.getAll.mockResolvedValue([]);
const res = await importRuleActionConnectors({
actionConnectors: [],
actionsClient,
actionsImporter: core.savedObjects.getImporter(),
rules: rulesToImport,
overwrite: true,
});
expect(res).toEqual({
success: false,
successCount: 0,
errors: [
{
error: {
message: '1 connector is missing. Connector id missing is: some-connector-id',
status_code: 404,
},
id: 'some-connector-id',
rule_id: 'rule-with-missed-action-connector',
},
],
warnings: [],
});
});
it('should NOT return an error when a missing action connector in ndjson import file is a preconfigured one', async () => {
const rulesToImport = [
getImportRulesSchemaMock({
rule_id: 'rule-with-missed-action-connector',
actions: [
{
group: 'default',
id: 'prebuilt-connector-id',
params: {},
action_type_id: '.webhook',
},
],
}),
];
actionsClient.getAll.mockResolvedValue([
{
actionTypeId: '.webhook',
name: 'webhook',
isPreconfigured: true,
id: 'prebuilt-connector-id',
referencedByCount: 1,
isDeprecated: false,
isSystemAction: false,
},
]);
const res = await importRuleActionConnectors({
actionConnectors: [],
actionsClient,
actionsImporter: core.savedObjects.getImporter(),
rules: rulesToImport,
overwrite: true,
});
expect(res).toEqual({
success: true,
successCount: 0,
errors: [],
warnings: [],
});
});
it('should not skip importing the action-connectors if all connectors have been imported/created before', async () => {
const rulesToImport = [
getImportRulesSchemaMock({
actions: [
{
group: 'default',
id: 'connector-id',
action_type_id: '.webhook',
params: {},
},
],
}),
];
core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
import: jest.fn().mockResolvedValue({
success: true,
successCount: 1,
errors: [],
warnings: [],
}),
});
actionsClient.getAll.mockResolvedValue([
{
actionTypeId: '.webhook',
name: 'webhook',
isPreconfigured: true,
id: 'connector-id',
referencedByCount: 1,
isDeprecated: false,
isSystemAction: false,
},
]);
const res = await importRuleActionConnectors({
actionConnectors,
actionsClient,
actionsImporter: core.savedObjects.getImporter(),
rules: rulesToImport,
overwrite: true,
});
expect(res).toEqual({
success: true,
successCount: 0,
errors: [],
warnings: [],
});
});
});
});

View file

@ -5,24 +5,14 @@
* 2.0.
*/
import { Readable } from 'stream';
import { isBoom } from '@hapi/boom';
import type { SavedObjectsImportResponse } from '@kbn/core-saved-objects-common';
import type { SavedObject } from '@kbn/core-saved-objects-server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
import type { RuleToImport } from '../../../../../../../common/api/detection_engine/rule_management';
import type { WarningSchema } from '../../../../../../../common/api/detection_engine';
import {
checkIfActionsHaveMissingConnectors,
filterExistingActionConnectors,
getActionConnectorRules,
handleActionsHaveNoConnectors,
mapSOErrorToRuleError,
returnErroredImportResult,
updateRuleActionsWithMigratedResults,
} from './utils';
import { mapSOErrorsToBulkErrors } from './utils';
import type { ImportRuleActionConnectorsParams, ImportRuleActionConnectorsResult } from './types';
import { createBulkErrorObject } from '../../../../routes/utils';
const NO_ACTION_RESULT = {
success: true,
@ -33,93 +23,43 @@ const NO_ACTION_RESULT = {
export const importRuleActionConnectors = async ({
actionConnectors,
actionsClient,
actionsImporter,
rules,
overwrite,
}: ImportRuleActionConnectorsParams): Promise<ImportRuleActionConnectorsResult> => {
try {
const connectorIdToRuleIdsMap = getActionConnectorRules(rules);
const referencedConnectorIds = await filterOutPreconfiguredConnectors(
actionsClient,
Object.keys(connectorIdToRuleIdsMap)
);
if (!referencedConnectorIds.length) {
if (!actionConnectors.length) {
return NO_ACTION_RESULT;
}
if (overwrite && !actionConnectors.length) {
return handleActionsHaveNoConnectors(referencedConnectorIds, connectorIdToRuleIdsMap);
}
let actionConnectorsToImport: SavedObject[] = actionConnectors;
if (!overwrite) {
const newIdsToAdd = await filterExistingActionConnectors(
actionsClient,
referencedConnectorIds
);
const foundMissingConnectors = checkIfActionsHaveMissingConnectors(
actionConnectors,
newIdsToAdd,
connectorIdToRuleIdsMap
);
if (foundMissingConnectors) return foundMissingConnectors;
// filter out existing connectors
actionConnectorsToImport = actionConnectors.filter(({ id }) => newIdsToAdd.includes(id));
}
if (!actionConnectorsToImport.length) {
return NO_ACTION_RESULT;
}
const readStream = Readable.from(actionConnectorsToImport);
const { success, successCount, successResults, warnings, errors }: SavedObjectsImportResponse =
const readStream = Readable.from(actionConnectors);
const { success, successCount, warnings, errors }: SavedObjectsImportResponse =
await actionsImporter.import({
readStream,
overwrite,
createNewCopies: false,
});
/*
// When a connector is exported from one namespace and imported to another, it does not result in an error, but instead a new object is created with
// new destination id and id will have the old origin id, so in order to be able to use the newly generated Connectors id, this util is used to swap the old id with the
// new destination Id
*/
let rulesWithMigratedActions: Array<RuleToImport | Error> | undefined;
if (successResults?.some((res) => res.destinationId))
rulesWithMigratedActions = updateRuleActionsWithMigratedResults(rules, successResults);
return {
success,
successCount,
errors: errors ? mapSOErrorToRuleError(errors) : [],
errors: errors ? mapSOErrorsToBulkErrors(errors) : [],
warnings: (warnings as WarningSchema[]) || [],
rulesWithMigratedActions,
};
} catch (error) {
return returnErroredImportResult(error);
} catch (exc) {
if (isBoom(exc) && exc.output.statusCode === 403) {
return {
success: false,
successCount: 0,
errors: [
createBulkErrorObject({
statusCode: 403,
message: `You may not have actions privileges required to import actions: ${exc.output.payload.message}`,
}),
],
warnings: [],
};
} else {
throw exc;
}
}
};
async function fetchPreconfiguredActionConnectors(
actionsClient: ActionsClient
): Promise<ConnectorWithExtraFindData[]> {
const knownConnectors = await actionsClient.getAll({ includeSystemActions: true });
return knownConnectors.filter((c) => c.isPreconfigured || c.isSystemAction);
}
async function filterOutPreconfiguredConnectors(
actionsClient: ActionsClient,
connectorsIds: string[]
): Promise<string[]> {
if (connectorsIds.length === 0) {
return [];
}
const preconfiguredActionConnectors = await fetchPreconfiguredActionConnectors(actionsClient);
const preconfiguredActionConnectorIds = new Set(preconfiguredActionConnectors.map((c) => c.id));
return connectorsIds.filter((id) => !preconfiguredActionConnectorIds.has(id));
}

View file

@ -5,9 +5,6 @@
* 2.0.
*/
import type { ISavedObjectsImporter, SavedObject } from '@kbn/core-saved-objects-server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { SavedObjectsImportFailure } from '@kbn/core-saved-objects-common';
import type { RuleToImport } from '../../../../../../../common/api/detection_engine/rule_management';
import type { WarningSchema } from '../../../../../../../common/api/detection_engine';
import type { BulkError } from '../../../../routes/utils';
@ -16,26 +13,10 @@ export interface ImportRuleActionConnectorsResult {
successCount: number;
errors: BulkError[] | [];
warnings: WarningSchema[] | [];
rulesWithMigratedActions?: Array<RuleToImport | Error>;
}
export interface ImportRuleActionConnectorsParams {
actionConnectors: SavedObject[];
actionsClient: ActionsClient;
actionsImporter: ISavedObjectsImporter;
rules: Array<RuleToImport | Error>;
overwrite: boolean;
}
export interface SOError {
output: { statusCode: number; payload: { message: string } };
}
export interface ConflictError {
type: string;
}
export type ErrorType = SOError | ConflictError | SavedObjectsImportFailure | Error;
export interface ActionRules {
[actionsIds: string]: string[];
}

View file

@ -4,170 +4,40 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { pick } from 'lodash';
import type {
SavedObjectsImportFailure,
SavedObjectsImportSuccess,
} from '@kbn/core-saved-objects-common';
import type { SavedObject } from '@kbn/core-saved-objects-server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { SavedObjectsImportFailure } from '@kbn/core-saved-objects-common';
import type { BulkError } from '../../../../../routes/utils';
import { createBulkErrorObject } from '../../../../../routes/utils';
import type { RuleToImport } from '../../../../../../../../common/api/detection_engine/rule_management';
import type {
ActionRules,
ConflictError,
ErrorType,
ImportRuleActionConnectorsResult,
SOError,
} from '../types';
export const returnErroredImportResult = (error: ErrorType): ImportRuleActionConnectorsResult => ({
success: false,
errors: [handleActionConnectorsErrors(error)],
successCount: 0,
warnings: [],
});
export const handleActionsHaveNoConnectors = (
actionsIds: string[],
actionConnectorRules: ActionRules
): ImportRuleActionConnectorsResult => {
const ruleIds: string = [...new Set(Object.values(actionConnectorRules).flat())].join();
if (actionsIds && actionsIds.length) {
const errors: BulkError[] = [];
const errorMessage =
actionsIds.length > 1
? 'connectors are missing. Connector ids missing are:'
: 'connector is missing. Connector id missing is:';
errors.push(
createBulkErrorObject({
id: actionsIds.join(),
statusCode: 404,
message: `${actionsIds.length} ${errorMessage} ${actionsIds.join(', ')}`,
ruleId: ruleIds,
})
);
return {
success: false,
errors,
successCount: 0,
warnings: [],
};
}
return {
success: true,
errors: [],
successCount: 0,
warnings: [],
};
export const mapSOErrorsToBulkErrors = (errors: SavedObjectsImportFailure[]): BulkError[] => {
return errors.map((error) => mapSOErrorToBulkError(error));
};
export const handleActionConnectorsErrors = (error: ErrorType, id?: string): BulkError => {
let statusCode: number | null = null;
let message: string = '';
if ('output' in error) {
statusCode = (error as SOError).output.statusCode;
message = (error as SOError).output.payload?.message;
}
switch (statusCode) {
case null:
export const mapSOErrorToBulkError = (error: SavedObjectsImportFailure): BulkError => {
switch (error.error.type) {
case 'conflict':
case 'ambiguous_conflict':
return createBulkErrorObject({
statusCode: 500,
message:
(error as ConflictError)?.type === 'conflict'
? 'There is a conflict'
: (error as Error).message
? (error as Error).message
: '',
id: error.id,
statusCode: 409,
message: `Saved Object already exists`,
});
case 403:
case 'unsupported_type':
return createBulkErrorObject({
id,
statusCode,
message: `You may not have actions privileges required to import rules with actions: ${message}`,
id: error.id,
statusCode: 400,
message: 'Unsupported SO action type',
});
default:
case 'missing_references':
return createBulkErrorObject({
id,
statusCode,
message,
id: error.id,
statusCode: 400,
message: 'Missing SO references',
});
case 'unknown':
return createBulkErrorObject({
id: error.id,
statusCode: error.error.statusCode,
message: `Unknown Saved Object import error: ${error.error.message}`,
});
}
};
export const mapSOErrorToRuleError = (errors: SavedObjectsImportFailure[]): BulkError[] => {
return errors.map(({ id, error }) => handleActionConnectorsErrors(error, id));
};
export const filterExistingActionConnectors = async (
actionsClient: ActionsClient,
actionsIds: string[]
) => {
const storedConnectors = await actionsClient.getAll();
const storedActionIds: string[] = storedConnectors.map(({ id }) => id);
return actionsIds.filter((id) => !storedActionIds.includes(id));
};
export const getActionConnectorRules = (rules: Array<RuleToImport | Error>) =>
rules.reduce((acc: { [actionsIds: string]: string[] }, rule) => {
if (rule instanceof Error) return acc;
rule.actions?.forEach(({ id }) => (acc[id] = [...(acc[id] || []), rule.rule_id]));
return acc;
}, {});
export const checkIfActionsHaveMissingConnectors = (
actionConnectors: SavedObject[],
newIdsToAdd: string[],
actionConnectorRules: ActionRules
) => {
// if new action-connectors don't have exported connectors will fail with missing connectors
if (actionConnectors.length < newIdsToAdd.length) {
const actionConnectorsIds = actionConnectors.map(({ id }) => id);
const missingActionConnector = newIdsToAdd.filter((id) => !actionConnectorsIds.includes(id));
const missingActionRules = pick(actionConnectorRules, [...missingActionConnector]);
return handleActionsHaveNoConnectors(missingActionConnector, missingActionRules);
}
return null;
};
export const mapActionIdToNewDestinationId = (
connectorsImportResult: SavedObjectsImportSuccess[]
) => {
return connectorsImportResult.reduce(
(acc: { [actionId: string]: string }, { destinationId, id }) => {
acc[id] = destinationId || id;
return acc;
},
{}
);
};
export const swapNonDefaultSpaceIdWithDestinationId = (
rule: RuleToImport,
actionIdDestinationIdLookup: { [actionId: string]: string }
) => {
return rule.actions?.map((action) => {
const destinationId = actionIdDestinationIdLookup[action.id];
return { ...action, id: destinationId };
});
};
/*
// When a connector is exported from one namespace and imported to another, it does not result in an error, but instead a new object is created with
// new destination id and id will have the old origin id, so in order to be able to use the newly generated Connectors id, this util is used to swap the old id with the
// new destination Id
*/
export const updateRuleActionsWithMigratedResults = (
rules: Array<RuleToImport | Error>,
connectorsImportResult: SavedObjectsImportSuccess[]
): Array<RuleToImport | Error> => {
const actionIdDestinationIdLookup = mapActionIdToNewDestinationId(connectorsImportResult);
return rules.map((rule) => {
if (rule instanceof Error) return rule;
return {
...rule,
actions: swapNonDefaultSpaceIdWithDestinationId(rule, actionIdDestinationIdLookup),
};
});
};

View file

@ -0,0 +1,106 @@
/*
* 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 { partition } from 'lodash';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
import { isBoom } from '@hapi/boom';
import type { RuleToImport } from '../../../../../../../common/api/detection_engine';
import { createBulkErrorObject, type BulkError } from '../../../../routes/utils';
export type ActionsOrErrors =
| { allActions: ConnectorWithExtraFindData[]; bulkError: undefined }
| { allActions: undefined; bulkError: BulkError };
export interface ValidatedRulesAndErrors {
validatedActionRules: RuleToImport[];
missingActionErrors: BulkError[];
}
const getActionsOrError = async ({
actionsClient,
}: {
actionsClient: ActionsClient;
}): Promise<ActionsOrErrors> => {
try {
return {
allActions: await actionsClient.getAll({ includeSystemActions: true }),
bulkError: undefined,
};
} catch (exc) {
if (isBoom(exc) && exc.output.statusCode === 403) {
return {
allActions: undefined,
bulkError: createBulkErrorObject({
statusCode: 403,
message: `You may not have actions privileges required to import rules with actions: ${exc.output.payload.message}`,
}),
};
} else {
throw exc;
}
}
};
export const validateRuleActions = async ({
actionsClient,
rules,
}: {
actionsClient: ActionsClient;
rules: RuleToImport[];
}): Promise<ValidatedRulesAndErrors> => {
const [rulesWithActions, rulesWithoutActions] = partition(
rules,
(rule) => rule.actions != null && rule.actions.length > 0
);
if (rulesWithActions.length === 0) {
return { validatedActionRules: rulesWithoutActions, missingActionErrors: [] };
}
const missingActionErrors: BulkError[] = [];
const actionsOrError = await getActionsOrError({ actionsClient });
if (actionsOrError.bulkError != null) {
return {
validatedActionRules: rulesWithoutActions,
missingActionErrors: rulesWithActions.map((rule) => ({
id: rule.id,
rule_id: rule.rule_id,
error: actionsOrError.bulkError.error,
})),
};
}
const allActionIdsSet = new Set(actionsOrError.allActions.map((action) => action.id));
const validatedRulesWithActions = rulesWithActions.filter((rule) => {
// We know rulesWithActions have actions, but TypeScript does not
if (rule.actions == null || rule.actions.length === 0) {
return true;
}
const missingActions = rule.actions.filter((action) => !allActionIdsSet.has(action.id));
if (missingActions.length > 0) {
missingActionErrors.push({
id: rule.id,
rule_id: rule.rule_id,
error: {
status_code: 404,
message: `Rule actions reference the following missing action IDs: ${missingActions
.map((action) => action.id)
.join(',')}`,
},
});
return false;
}
return true;
});
return {
validatedActionRules: rulesWithoutActions.concat(validatedRulesWithActions),
missingActionErrors,
};
};

View file

@ -20,17 +20,14 @@ import {
getIdError,
transformFindAlerts,
transform,
getIdBulkError,
transformAlertsToRules,
getTupleDuplicateErrorsAndUniqueRules,
getInvalidConnectors,
swapActionIds,
migrateLegacyActionsIds,
migrateLegacyInvestigationFields,
} from './utils';
import { getRuleMock } from '../../routes/__mocks__/request_responses';
import type { PartialFilter } from '../../types';
import type { BulkError } from '../../routes/utils';
import { createBulkErrorObject } from '../../routes/utils';
import type { RuleAlertType } from '../../rule_schema';
@ -309,90 +306,6 @@ describe('utils', () => {
});
});
describe('getIdBulkError', () => {
test('outputs message about id and rule_id not being found if both are not null', () => {
const error = getIdBulkError({ id: '123', ruleId: '456' });
const expected: BulkError = {
id: '123',
rule_id: '456',
error: { message: 'id: "123" and rule_id: "456" not found', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about id not being found if only id is defined and ruleId is undefined', () => {
const error = getIdBulkError({ id: '123', ruleId: undefined });
const expected: BulkError = {
id: '123',
error: { message: 'id: "123" not found', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about id not being found if only id is defined and ruleId is null', () => {
const error = getIdBulkError({ id: '123', ruleId: null });
const expected: BulkError = {
id: '123',
error: { message: 'id: "123" not found', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => {
const error = getIdBulkError({ id: undefined, ruleId: 'rule-id-123' });
const expected: BulkError = {
rule_id: 'rule-id-123',
error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => {
const error = getIdBulkError({ id: null, ruleId: 'rule-id-123' });
const expected: BulkError = {
rule_id: 'rule-id-123',
error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about both being not defined when both are undefined', () => {
const error = getIdBulkError({ id: undefined, ruleId: undefined });
const expected: BulkError = {
rule_id: '(unknown id)',
error: { message: 'id or rule_id should have been defined', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about both being not defined when both are null', () => {
const error = getIdBulkError({ id: null, ruleId: null });
const expected: BulkError = {
rule_id: '(unknown id)',
error: { message: 'id or rule_id should have been defined', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about both being not defined when id is null and ruleId is undefined', () => {
const error = getIdBulkError({ id: null, ruleId: undefined });
const expected: BulkError = {
rule_id: '(unknown id)',
error: { message: 'id or rule_id should have been defined', status_code: 404 },
};
expect(error).toEqual(expected);
});
test('outputs message about both being not defined when id is undefined and ruleId is null', () => {
const error = getIdBulkError({ id: undefined, ruleId: null });
const expected: BulkError = {
rule_id: '(unknown id)',
error: { message: 'id or rule_id should have been defined', status_code: 404 },
};
expect(error).toEqual(expected);
});
});
describe('transformAlertsToRules', () => {
test('given an empty array returns an empty array', () => {
expect(transformAlertsToRules([])).toEqual([]);
@ -806,485 +719,6 @@ describe('utils', () => {
expect(res).toEqual([{ ...rule, actions: [{ ...mockSystemAction }] }]);
});
});
describe('getInvalidConnectors', () => {
beforeEach(() => {
clients.actionsClient.getAll.mockReset();
});
test('returns empty errors array and rule array with instance of Syntax Error when imported rule contains parse error', async () => {
// This is a string because we have a double "::" below to make an error happen on purpose.
const multipartPayload =
'{"name"::"Simple Rule Query","description":"Simple Rule Query","risk_score":1,"rule_id":"rule-1","severity":"high","type":"query","query":"user.name: root or user.name: admin"}\n';
const ndJsonStream = new Readable({
read() {
this.push(multipartPayload);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
const isInstanceOfError = output[0] instanceof Error;
expect(isInstanceOfError).toEqual(true);
expect(errors.length).toEqual(0);
});
test('creates error with a rule has an action that does not exist within the actions client', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
],
};
const ndJsonStream = new Readable({
read() {
this.push(`${JSON.stringify(rule)}\n`);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
expect(output.length).toEqual(0);
expect(errors).toEqual<BulkError[]>([
{
error: {
message: '1 connector is missing. Connector id missing is: 123',
status_code: 404,
},
rule_id: 'rule-1',
},
]);
});
test('creates output with no errors if 1 rule with an action exists within the actions client', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
],
};
const ndJsonStream = new Readable({
read() {
this.push(`${JSON.stringify(rule)}\n`);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([
{
id: '123',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
expect(errors.length).toEqual(0);
expect(output.length).toEqual(1);
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule));
});
test('creates output with no errors if 1 rule with 2 actions exists within the actions client', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
{
group: 'default',
id: '789',
action_type_id: '101112',
params: {},
},
],
};
const ndJsonStream = new Readable({
read() {
this.push(`${JSON.stringify(rule)}\n`);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([
{
id: '123',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: '789',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
expect(errors.length).toEqual(0);
expect(output.length).toEqual(1);
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule));
});
test('creates output with no errors if 2 rules with 1 action each exists within the actions client', async () => {
const rule1: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
],
};
const rule2: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-2'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
],
};
const ndJsonStream = new Readable({
read() {
this.push(`${JSON.stringify(rule1)}\n`);
this.push(`${JSON.stringify(rule2)}\n`);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([
{
id: '123',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: '789',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
expect(errors.length).toEqual(0);
expect(output.length).toEqual(2);
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule1));
expect(output[1]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule2));
});
test('creates output with 1 error if 2 rules with 1 action each exists within the actions client but 1 has a nonexistent action', async () => {
const rule1: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
],
};
const rule2: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-2'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
{
group: 'default',
id: '456', // <--- Non-existent that triggers the error.
action_type_id: '456',
params: {},
},
],
};
const ndJsonStream = new Readable({
read() {
this.push(`${JSON.stringify(rule1)}\n`);
this.push(`${JSON.stringify(rule2)}\n`);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([
{
id: '123',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: '789',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
expect(errors.length).toEqual(1);
expect(output.length).toEqual(1);
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule1));
expect(errors).toEqual<BulkError[]>([
{
error: {
message: '1 connector is missing. Connector id missing is: 456',
status_code: 404,
},
rule_id: 'rule-2',
},
]);
});
test('creates output with error if 1 rule with 2 actions but 1 action does not exist within the actions client', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
{
group: 'default',
id: '789',
action_type_id: '101112',
params: {},
},
{
group: 'default',
id: '101112', // <-- Does not exist
action_type_id: '101112',
params: {},
},
],
};
const ndJsonStream = new Readable({
read() {
this.push(`${JSON.stringify(rule)}\n`);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([
{
id: '123',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: '789',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
expect(errors.length).toEqual(1);
expect(output.length).toEqual(0);
expect(errors).toEqual<BulkError[]>([
{
error: {
message: '1 connector is missing. Connector id missing is: 101112',
status_code: 404,
},
rule_id: 'rule-1',
},
]);
});
test('creates output with 2 errors if 3 rules with actions but 1 action does not exist within the actions client', async () => {
const rule1: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
{
group: 'default',
id: '789',
action_type_id: '101112',
params: {},
},
{
group: 'default',
id: '101112', // <-- Does not exist
action_type_id: '101112',
params: {},
},
],
};
const rule2: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
{
group: 'default',
id: '789',
action_type_id: '101112',
params: {},
},
],
};
const rule3: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [
{
group: 'default',
id: '123',
action_type_id: '456',
params: {},
},
{
group: 'default',
id: '789',
action_type_id: '101112',
params: {},
},
{
group: 'default',
id: '101112', // <-- Does not exist
action_type_id: '101112',
params: {},
},
],
};
const ndJsonStream = new Readable({
read() {
this.push(`${JSON.stringify(rule1)}\n`);
this.push(`${JSON.stringify(rule2)}\n`);
this.push(`${JSON.stringify(rule3)}\n`);
this.push(null);
},
});
const [{ rules }] = await createPromiseFromRuleImportStream({
stream: ndJsonStream,
objectLimit: 1000,
});
clients.actionsClient.getAll.mockResolvedValue([
{
id: '123',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: '789',
referencedByCount: 1,
actionTypeId: 'default',
name: 'name',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
]);
const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient);
expect(errors.length).toEqual(2);
expect(output.length).toEqual(1);
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule2));
expect(errors).toEqual<BulkError[]>([
{
error: {
message: '1 connector is missing. Connector id missing is: 101112',
status_code: 404,
},
rule_id: 'rule-1',
},
{
error: {
message: '1 connector is missing. Connector id missing is: 101112',
status_code: 404,
},
rule_id: 'rule-1',
},
]);
});
});
describe('migrateLegacyInvestigationFields', () => {
test('should return undefined if value not set', () => {

View file

@ -9,7 +9,7 @@ import { partition, isEmpty } from 'lodash/fp';
import pMap from 'p-map';
import { v4 as uuidv4 } from 'uuid';
import type { ActionsClient, FindActionResult } from '@kbn/actions-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { FindResult, PartialRule } from '@kbn/alerting-plugin/server';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { RuleAction } from '@kbn/securitysolution-io-ts-alerting-types';
@ -58,40 +58,6 @@ export const getIdError = ({
}
};
export const getIdBulkError = ({
id,
ruleId,
}: {
id: string | undefined | null;
ruleId: string | undefined | null;
}): BulkError => {
if (id != null && ruleId != null) {
return createBulkErrorObject({
id,
ruleId,
statusCode: 404,
message: `id: "${id}" and rule_id: "${ruleId}" not found`,
});
} else if (id != null) {
return createBulkErrorObject({
id,
statusCode: 404,
message: `id: "${id}" not found`,
});
} else if (ruleId != null) {
return createBulkErrorObject({
ruleId,
statusCode: 404,
message: `rule_id: "${ruleId}" not found`,
});
} else {
return createBulkErrorObject({
statusCode: 404,
message: `id or rule_id should have been defined`,
});
}
};
export const transformAlertsToRules = (rules: RuleAlertType[]): RuleResponse[] => {
return rules.map((rule) => internalRuleToAPIResponse(rule));
};
@ -269,84 +235,6 @@ export const migrateLegacyActionsIds = async (
return toReturn.flat();
};
/**
* Given a set of rules and an actions client this will return connectors that are invalid
* such as missing connectors and filter out the rules that have invalid connectors.
* @param rules The rules to check for invalid connectors
* @param actionsClient The actions client to get all the connectors.
* @returns An array of connector errors if it found any and then the promise stream of valid and invalid connectors.
*/
export const getInvalidConnectors = async (
rules: PromiseFromStreams[],
actionsClient: ActionsClient
): Promise<[BulkError[], PromiseFromStreams[]]> => {
let actionsFind: FindActionResult[] = [];
const reducerAccumulator = {
errors: new Map<string, BulkError>(),
rulesAcc: new Map<string, PromiseFromStreams>(),
};
try {
actionsFind = await actionsClient.getAll();
} catch (exc) {
if (exc?.output?.statusCode === 403) {
reducerAccumulator.errors.set(
uuidv4(),
createBulkErrorObject({
statusCode: exc.output.statusCode,
message: `You may not have actions privileges required to import rules with actions: ${exc.output.payload.message}`,
})
);
} else {
reducerAccumulator.errors.set(
uuidv4(),
createBulkErrorObject({
statusCode: 404,
message: JSON.stringify(exc),
})
);
}
}
const actionIds = new Set(actionsFind.map((action) => action.id));
const { errors, rulesAcc } = rules.reduce(
(acc, parsedRule) => {
if (parsedRule instanceof Error) {
acc.rulesAcc.set(uuidv4(), parsedRule);
} else {
const { rule_id: ruleId, actions } = parsedRule;
const missingActionIds = actions
? actions.flatMap((action) => {
if (!actionIds.has(action.id)) {
return [action.id];
} else {
return [];
}
})
: [];
if (missingActionIds.length === 0) {
acc.rulesAcc.set(ruleId, parsedRule);
} else {
const errorMessage =
missingActionIds.length > 1
? 'connectors are missing. Connector ids missing are:'
: 'connector is missing. Connector id missing is:';
acc.errors.set(
uuidv4(),
createBulkErrorObject({
ruleId,
statusCode: 404,
message: `${missingActionIds.length} ${errorMessage} ${missingActionIds.join(', ')}`,
})
);
}
}
return acc;
}, // using map (preserves ordering)
reducerAccumulator
);
return [Array.from(errors.values()), Array.from(rulesAcc.values())];
};
/**
* In ESS 8.10.x "investigation_fields" are mapped as string[].
* For 8.11+ logic is added on read in our endpoints to migrate

View file

@ -102,81 +102,6 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
it('DOES NOT import an action connector without rules', async () => {
const ndjson = combineToNdJson(CUSTOM_ACTION_CONNECTOR);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_IMPORT_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach('file', Buffer.from(ndjson), 'rules.ndjson')
.expect(200);
expect(body).toMatchObject({
errors: [],
success: true,
success_count: 0,
rules_count: 0,
action_connectors_success: true,
action_connectors_success_count: 0,
action_connectors_errors: [],
action_connectors_warnings: [],
});
await supertest
.get(`/api/actions/connector/${CONNECTOR_ID}`)
.set('kbn-xsrf', 'foo')
.expect(404);
});
it('DOES NOT import an action connector when there are no rules referencing it', async () => {
const ndjson = combineToNdJson(
getCustomQueryRuleParams({
rule_id: 'rule-1',
name: 'Rule 1',
actions: [
{
group: 'default',
id: ANOTHER_CONNECTOR_ID,
params: {
message: 'Some message',
to: ['test@test.com'],
subject: 'Test',
},
action_type_id: '.email',
uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
},
],
}),
{ ...CUSTOM_ACTION_CONNECTOR, id: ANOTHER_CONNECTOR_ID },
CUSTOM_ACTION_CONNECTOR
);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_IMPORT_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach('file', Buffer.from(ndjson), 'rules.ndjson')
.expect(200);
expect(body).toMatchObject({
errors: [],
success: true,
success_count: 1,
rules_count: 1,
action_connectors_success: true,
action_connectors_success_count: 1,
action_connectors_errors: [],
action_connectors_warnings: [],
});
await supertest
.get(`/api/actions/connector/${CONNECTOR_ID}`)
.set('kbn-xsrf', 'foo')
.expect(404);
});
it('DOES NOT return an error when rule actions reference a preconfigured connector', async () => {
const ndjson = combineToNdJson(
getCustomQueryRuleParams({
@ -273,9 +198,17 @@ export default ({ getService }: FtrProviderContext): void => {
success: true,
success_count: 1,
rules_count: 1,
action_connectors_success: true,
action_connectors_success: false,
action_connectors_success_count: 0,
action_connectors_errors: [],
action_connectors_errors: [
{
error: {
message: 'Saved Object already exists',
status_code: 409,
},
id: '1be16246-642a-4ed8-bfd3-b47f8c7d7055',
},
],
action_connectors_warnings: [],
});
@ -318,28 +251,18 @@ export default ({ getService }: FtrProviderContext): void => {
errors: [
{
error: {
message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
message: `Rule actions reference the following missing action IDs: ${CONNECTOR_ID}`,
status_code: 404,
},
id: CONNECTOR_ID,
rule_id: 'rule-1',
},
],
success: false,
success_count: 0,
rules_count: 1,
action_connectors_success: false,
action_connectors_success: true,
action_connectors_success_count: 0,
action_connectors_errors: [
{
error: {
message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
status_code: 404,
},
id: CONNECTOR_ID,
rule_id: 'rule-1',
},
],
action_connectors_errors: [],
action_connectors_warnings: [],
});
});
@ -445,28 +368,18 @@ export default ({ getService }: FtrProviderContext): void => {
errors: [
{
error: {
message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
message: `Rule actions reference the following missing action IDs: ${CONNECTOR_ID}`,
status_code: 404,
},
id: CONNECTOR_ID,
rule_id: 'rule-1',
},
],
success: false,
success_count: 0,
rules_count: 1,
action_connectors_success: false,
action_connectors_success: true,
action_connectors_success_count: 0,
action_connectors_errors: [
{
error: {
message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
status_code: 404,
},
id: CONNECTOR_ID,
rule_id: 'rule-1',
},
],
action_connectors_errors: [],
action_connectors_warnings: [],
});
});

View file

@ -11,6 +11,10 @@ import { range } from 'lodash';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock';
import { DETECTION_ENGINE_RULES_IMPORT_URL } from '@kbn/security-solution-plugin/common/constants';
import type {
RuleAction,
RuleToImport,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import {
getImportExceptionsListItemSchemaMock,
getImportExceptionsListSchemaMock,
@ -30,122 +34,79 @@ import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { getWebHookConnectorParams } from '../../../utils/connectors/get_web_hook_connector_params';
import { createConnector } from '../../../utils/connectors';
const getRuleImportWithActions = (actions: RuleAction[]): RuleToImport => ({
id: '53aad690-544e-11ec-a349-11361cc441c4',
updated_at: '2021-12-03T15:33:13.271Z',
updated_by: 'elastic',
created_at: '2021-12-03T15:33:13.271Z',
created_by: 'elastic',
name: '7.16 test with action',
tags: [],
interval: '5m',
enabled: true,
description: 'test',
risk_score: 21,
severity: 'low',
license: '',
output_index: '',
meta: { from: '1m', kibana_siem_app_url: 'http://0.0.0.0:5601/s/7/app/security' },
author: [],
false_positives: [],
from: 'now-360s',
rule_id: 'aa525d7c-8948-439f-b32d-27e00c750246',
max_signals: 100,
risk_score_mapping: [],
severity_mapping: [],
threat: [],
to: 'now',
references: [],
version: 1,
exceptions_list: [],
immutable: false,
type: 'query',
language: 'kuery',
index: [
'apm-*-transaction*',
'traces-apm*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
],
query: '*:*',
filters: [],
throttle: '1h',
actions,
});
const getImportRuleBuffer = (connectorId: string) => {
const rule1 = {
id: '53aad690-544e-11ec-a349-11361cc441c4',
updated_at: '2021-12-03T15:33:13.271Z',
updated_by: 'elastic',
created_at: '2021-12-03T15:33:13.271Z',
created_by: 'elastic',
name: '7.16 test with action',
tags: [],
interval: '5m',
enabled: true,
description: 'test',
risk_score: 21,
severity: 'low',
license: '',
output_index: '',
meta: { from: '1m', kibana_siem_app_url: 'http://0.0.0.0:5601/s/7/app/security' },
author: [],
false_positives: [],
from: 'now-360s',
rule_id: 'aa525d7c-8948-439f-b32d-27e00c750246',
max_signals: 100,
risk_score_mapping: [],
severity_mapping: [],
threat: [],
to: 'now',
references: [],
version: 1,
exceptions_list: [],
immutable: false,
type: 'query',
language: 'kuery',
index: [
'apm-*-transaction*',
'traces-apm*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
],
query: '*:*',
filters: [],
throttle: '1h',
actions: [
{
group: 'default',
id: connectorId,
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
const rule1 = getRuleImportWithActions([
{
group: 'default',
id: connectorId,
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
],
};
action_type_id: '.slack',
},
]);
const ndjson = combineToNdJson(rule1);
return Buffer.from(ndjson);
};
const getImportRuleWithConnectorsBuffer = (connectorId: string) => {
const rule1 = {
id: '53aad690-544e-11ec-a349-11361cc441c4',
updated_at: '2021-12-03T15:33:13.271Z',
updated_by: 'elastic',
created_at: '2021-12-03T15:33:13.271Z',
created_by: 'elastic',
name: '7.16 test with action',
tags: [],
interval: '5m',
enabled: true,
description: 'test',
risk_score: 21,
severity: 'low',
license: '',
output_index: '',
meta: { from: '1m', kibana_siem_app_url: 'http://0.0.0.0:5601/s/7/app/security' },
author: [],
false_positives: [],
from: 'now-360s',
rule_id: 'aa525d7c-8948-439f-b32d-27e00c750246',
max_signals: 100,
risk_score_mapping: [],
severity_mapping: [],
threat: [],
to: 'now',
references: [],
version: 1,
exceptions_list: [],
immutable: false,
type: 'query',
language: 'kuery',
index: [
'apm-*-transaction*',
'traces-apm*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
],
query: '*:*',
filters: [],
throttle: '1h',
actions: [
{
group: 'default',
id: connectorId,
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
const rule1 = getRuleImportWithActions([
{
group: 'default',
id: connectorId,
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
],
};
action_type_id: '.slack',
},
]);
const connector = {
id: connectorId,
type: 'action',
@ -172,6 +133,7 @@ export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const log = getService('log');
const esArchiver = getService('esArchiver');
const spacesServices = getService('spaces');
describe('@ess @serverless @skipInServerlessMKI import_rules', () => {
beforeEach(async () => {
@ -191,7 +153,6 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(200);
expect(body.errors[0]).toEqual({
rule_id: '(unknown id)',
error: { status_code: 400, message: 'threshold: Required' },
});
});
@ -215,7 +176,6 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(200);
expect(body.errors[0]).toEqual({
rule_id: '(unknown id)',
error: {
message: 'Number of fields must be 3 or less',
status_code: 400,
@ -242,7 +202,6 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(200);
expect(body.errors[0]).toEqual({
rule_id: '(unknown id)',
error: {
message: 'threshold.value: Number must be greater than or equal to 1',
status_code: 400,
@ -274,7 +233,6 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(200);
expect(body.errors[0]).toEqual({
rule_id: '(unknown id)',
error: {
message: 'Cardinality of a field that is being aggregated on is always 1',
status_code: 400,
@ -651,26 +609,16 @@ export default ({ getService }: FtrProviderContext): void => {
errors: [
{
rule_id: 'rule-1',
id: '123',
error: {
status_code: 404,
message: '1 connector is missing. Connector id missing is: 123',
message: 'Rule actions reference the following missing action IDs: 123',
},
},
],
action_connectors_success: false,
action_connectors_success: true,
action_connectors_success_count: 0,
action_connectors_warnings: [],
action_connectors_errors: [
{
rule_id: 'rule-1',
id: '123',
error: {
status_code: 404,
message: '1 connector is missing. Connector id missing is: 123',
},
},
],
action_connectors_errors: [],
});
});
@ -848,7 +796,7 @@ export default ({ getService }: FtrProviderContext): void => {
actions: [
{
group: 'default',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aayo',
id: '51b17790-544e-11ec-a349-11361cc441c4',
action_type_id: '.webhook',
params: {},
},
@ -866,7 +814,7 @@ export default ({ getService }: FtrProviderContext): void => {
],
}),
{
id: 'cabc78e0-9031-11ed-b076-53cc4d57aayo',
id: '51b17790-544e-11ec-a349-11361cc441c4',
type: 'action',
updated_at: '2023-01-25T14:35:52.852Z',
created_at: '2023-01-25T14:35:52.852Z',
@ -893,36 +841,77 @@ export default ({ getService }: FtrProviderContext): void => {
expect(body).toMatchObject({
success: false,
success_count: 0,
success_count: 1,
rules_count: 2,
errors: [
{
rule_id: 'rule-2',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aa22',
error: {
status_code: 404,
message:
'1 connector is missing. Connector id missing is: cabc78e0-9031-11ed-b076-53cc4d57aa22',
'Rule actions reference the following missing action IDs: cabc78e0-9031-11ed-b076-53cc4d57aa22',
},
},
],
action_connectors_success: false,
action_connectors_success_count: 0,
action_connectors_errors: [
{
error: {
status_code: 404,
message:
'1 connector is missing. Connector id missing is: cabc78e0-9031-11ed-b076-53cc4d57aa22',
},
rule_id: 'rule-2',
id: 'cabc78e0-9031-11ed-b076-53cc4d57aa22',
},
],
action_connectors_success: true,
action_connectors_success_count: 1,
action_connectors_errors: [],
action_connectors_warnings: [],
});
});
describe('with a space', () => {
const spaceId = '4567-space';
before(async () => {
await spacesServices.create({
id: spaceId,
name: spaceId,
});
});
after(async () => {
await spacesServices.delete(spaceId);
});
it('should import rules and update references correctly after overwriting an existing connector', async () => {
const defaultSpaceConnectorId = '8fbf6d10-a21a-11ed-84a4-a33e4c2558c9';
const buffer = getImportRuleWithConnectorsBuffer(defaultSpaceConnectorId);
await supertest
.post(`${DETECTION_ENGINE_RULES_IMPORT_URL}`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach('file', buffer, 'rules.ndjson')
.expect(200);
await supertest
.post(`/s/${spaceId}${DETECTION_ENGINE_RULES_IMPORT_URL}`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach('file', buffer, 'rules.ndjson')
.expect(200);
const { body: overwriteResponseBody } = await supertest
.post(
`/s/${spaceId}${DETECTION_ENGINE_RULES_IMPORT_URL}?overwrite=true&overwrite_action_connectors=true`
)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach('file', buffer, 'rules.ndjson')
.expect(200);
expect(overwriteResponseBody).toMatchObject({
success: true,
success_count: 1,
rules_count: 1,
errors: [],
action_connectors_success: true,
action_connectors_success_count: 1,
action_connectors_warnings: [],
action_connectors_errors: [],
});
});
});
describe('@skipInServerless migrate pre-8.0 action connector ids', () => {
const defaultSpaceActionConnectorId = '61b17790-544e-11ec-a349-11361cc441c4';
const space714ActionConnectorId = '51b17790-544e-11ec-a349-11361cc441c4';
@ -1025,7 +1014,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect.objectContaining({
error: {
status_code: 404,
message: `1 connector is missing. Connector id missing is: ${space714ActionConnectorId}`,
message: `Rule actions reference the following missing action IDs: ${space714ActionConnectorId}`,
},
}),
],
@ -1052,7 +1041,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect.objectContaining({
error: {
status_code: 404,
message: `1 connector is missing. Connector id missing is: ${space714ActionConnectorId}`,
message: `Rule actions reference the following missing action IDs: ${space714ActionConnectorId}`,
},
}),
],
@ -1136,7 +1125,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect.objectContaining({
error: {
status_code: 404,
message: `1 connector is missing. Connector id missing is: ${defaultSpaceActionConnectorId}`,
message: `Rule actions reference the following missing action IDs: ${defaultSpaceActionConnectorId}`,
},
}),
],

View file

@ -158,10 +158,10 @@ export default ({ getService }: FtrProviderContext): void => {
{
error: {
message:
'You may not have actions privileges required to import rules with actions: Unable to bulk_create action',
status_code: 403,
'Rule actions reference the following missing action IDs: cabc78e0-9031-11ed-b076-53cc4d57aaf1',
status_code: 404,
},
rule_id: '(unknown id)',
rule_id: 'rule-with-actions',
},
],
success: false,
@ -173,10 +173,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
error: {
message:
'You may not have actions privileges required to import rules with actions: Unable to bulk_create action',
'You may not have actions privileges required to import actions: Unable to bulk_create action',
status_code: 403,
},
rule_id: '(unknown id)',
},
],
action_connectors_warnings: [],
@ -234,7 +233,7 @@ export default ({ getService }: FtrProviderContext): void => {
'You may not have actions privileges required to import rules with actions: Unauthorized to get actions',
status_code: 403,
},
rule_id: '(unknown id)',
rule_id: 'rule-with-actions',
},
],
rules_count: 1,
@ -244,10 +243,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
error: {
message:
'You may not have actions privileges required to import rules with actions: Unauthorized to get actions',
'You may not have actions privileges required to import actions: Unable to bulk_create action',
status_code: 403,
},
rule_id: '(unknown id)',
},
],
action_connectors_warnings: [],