diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 2f6ea38b40b81..1420d70152f73 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -352,7 +352,10 @@ export const nonRuleAlert = () => ({ alertTypeId: 'something', }); -export const getRuleMock = (params: T): SanitizedRule => ({ +export const getRuleMock = ( + params: T, + rewrites?: Partial> +): SanitizedRule => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [], @@ -377,6 +380,7 @@ export const getRuleMock = (params: T): SanitizedRule = lastExecutionDate: new Date('2020-08-20T19:23:38Z'), }, revision: 0, + ...rewrites, }); export const resolveRuleMock = (params: T): ResolvedSanitizedRule => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 8af5eeaa1a021..d09c11d1289bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -492,9 +492,7 @@ export const performBulkActionRoute = ( const exported = await getExportByObjectIds( rulesClient, exceptionsClient, - savedObjectsClient, - rules.map(({ params }) => ({ rule_id: params.ruleId })), - logger, + rules.map(({ params }) => params.ruleId), exporter, request, actionsClient diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts index 97aaff3373e01..5b190a1065018 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts @@ -51,11 +51,7 @@ export const exportRulesRoute = ( const exceptionsClient = (await context.lists)?.getExceptionListClient(); const actionsClient = (await context.actions)?.getActionsClient(); - const { - getExporter, - getClient, - client: savedObjectsClient, - } = (await context.core).savedObjects; + const { getExporter, getClient } = (await context.core).savedObjects; const client = getClient({ includedHiddenTypes: ['action'] }); const actionsExporter = getExporter(client); @@ -83,9 +79,7 @@ export const exportRulesRoute = ( ? await getExportByObjectIds( rulesClient, exceptionsClient, - savedObjectsClient, - request.body.objects, - logger, + request.body.objects.map((obj) => obj.rule_id), actionsExporter, request, actionsClient @@ -93,8 +87,6 @@ export const exportRulesRoute = ( : await getExportAll( rulesClient, exceptionsClient, - savedObjectsClient, - logger, actionsExporter, request, actionsClient diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/exportable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/exportable_rule.ts new file mode 100644 index 0000000000000..42773537219fe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/exportable_rule.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; + +export type ExportableRule = Omit; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 16167acdf51ac..0ba0afbce715a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -9,7 +9,6 @@ import type { FindHit } from '../../../routes/__mocks__/request_responses'; import { getRuleMock, getFindResultWithSingleHit, - getEmptySavedObjectsResponse, } from '../../../routes/__mocks__/request_responses'; import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; import { getExportAll } from './get_export_all'; @@ -22,8 +21,6 @@ import { import { getQueryRuleParams } from '../../../rule_schema/mocks'; import { getExceptionListClientMock } from '@kbn/lists-plugin/server/services/exception_lists/exception_list_client.mock'; -import type { loggingSystemMock } from '@kbn/core/server/mocks'; -import { requestContextMock } from '../../../routes/__mocks__/request_context'; import { savedObjectsExporterMock } from '@kbn/core-saved-objects-import-export-server-mocks'; import { mockRouter } from '@kbn/core-http-router-server-mocks'; import { Readable } from 'stream'; @@ -54,13 +51,11 @@ const connectors = [ }, ]; describe('getExportAll', () => { - let logger: ReturnType; - const { clients } = requestContextMock.createTools(); const exporterMock = savedObjectsExporterMock.create(); const requestMock = mockRouter.createKibanaRequest(); const actionsClient = actionsClientMock.create(); + beforeEach(async () => { - clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); actionsClient.getAll.mockImplementation(async () => { return connectors; }); @@ -85,8 +80,6 @@ describe('getExportAll', () => { const exports = await getExportAll( rulesClient, exceptionsClient, - clients.savedObjectsClient, - logger, exporterMock, requestMock, actionsClient @@ -172,8 +165,6 @@ describe('getExportAll', () => { const exports = await getExportAll( rulesClient, exceptionsClient, - clients.savedObjectsClient, - logger, exporterMock, requestMock, actionsClient @@ -258,8 +249,6 @@ describe('getExportAll', () => { const exports = await getExportAll( rulesClient, exceptionsClient, - clients.savedObjectsClient, - logger, exporterMockWithConnector as never, requestMock, actionsClient @@ -401,8 +390,6 @@ describe('getExportAll', () => { const exports = await getExportAll( rulesClient, exceptionsClient, - clients.savedObjectsClient, - logger, exporterMockWithConnector as never, requestMock, actionsClient diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.ts index 0962e9bf2f313..cdf8c6333e595 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.ts @@ -7,21 +7,20 @@ import { transformDataToNdjson } from '@kbn/securitysolution-utils'; -import type { ISavedObjectsExporter, KibanaRequest, Logger } from '@kbn/core/server'; +import type { ISavedObjectsExporter, KibanaRequest } from '@kbn/core/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; -import type { RulesClient, RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { ActionsClient } from '@kbn/actions-plugin/server'; import { getNonPackagedRules } from '../search/get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; -import { transformAlertsToRules, transformRuleToExportableFormat } from '../../utils/utils'; +import { transformAlertsToRules } from '../../utils/utils'; import { getRuleExceptionsForExport } from './get_export_rule_exceptions'; import { getRuleActionConnectorsForExport } from './get_export_rule_action_connectors'; +import { transformRuleToExportableFormat } from './transform_rule_to_exportable_format'; export const getExportAll = async ( rulesClient: RulesClient, exceptionsClient: ExceptionListClient | undefined, - savedObjectsClient: RuleExecutorServices['savedObjectsClient'], - logger: Logger, actionsExporter: ISavedObjectsExporter, request: KibanaRequest, actionsClient: ActionsClient diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index c9d7f82588c52..08794143ce161 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -5,34 +5,24 @@ * 2.0. */ import { Readable } from 'stream'; -import type { RulesErrors } from './get_export_by_object_ids'; -import { getExportByObjectIds, getRulesFromObjects } from './get_export_by_object_ids'; -import type { FindHit } from '../../../routes/__mocks__/request_responses'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; +import { getExceptionListClientMock } from '@kbn/lists-plugin/server/services/exception_lists/exception_list_client.mock'; +import { savedObjectsExporterMock } from '@kbn/core-saved-objects-import-export-server-mocks'; +import { mockRouter } from '@kbn/core-http-router-server-mocks'; +import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; import { + getFindResultWithMultiHits, getRuleMock, - getFindResultWithSingleHit, - getEmptySavedObjectsResponse, } from '../../../routes/__mocks__/request_responses'; -import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; -import { getListArrayMock } from '../../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock'; -import { - getSampleDetailsAsNdjson, - getOutputDetailsSampleWithExceptions, -} from '../../../../../../common/api/detection_engine/rule_management/mocks'; -import { getQueryRuleParams } from '../../../rule_schema/mocks'; -import { getExceptionListClientMock } from '@kbn/lists-plugin/server/services/exception_lists/exception_list_client.mock'; -import { savedObjectsExporterMock } from '@kbn/core-saved-objects-import-export-server-mocks'; -import { mockRouter } from '@kbn/core-http-router-server-mocks'; +import { internalRuleToAPIResponse } from '../../normalization/rule_converters'; +import { getEqlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks'; +import { getExportByObjectIds } from './get_export_by_object_ids'; const exceptionsClient = getExceptionListClientMock(); -import type { loggingSystemMock } from '@kbn/core/server/mocks'; -import { requestContextMock } from '../../../routes/__mocks__/request_context'; -import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; - const connectors = [ { - id: '123', + id: 'non-preconfigured-connector', actionTypeId: '.slack', name: 'slack', config: {}, @@ -42,7 +32,7 @@ const connectors = [ referencedByCount: 1, }, { - id: '456', + id: 'preconfigured-connector', actionTypeId: '.email', name: 'Email (preconfigured)', config: {}, @@ -52,542 +42,314 @@ const connectors = [ referencedByCount: 1, }, ]; -describe('get_export_by_object_ids', () => { - let logger: ReturnType; - const { clients } = requestContextMock.createTools(); + +describe('getExportByObjectIds', () => { const exporterMock = savedObjectsExporterMock.create(); const requestMock = mockRouter.createKibanaRequest(); const actionsClient = actionsClientMock.create(); + beforeEach(() => { jest.resetAllMocks(); jest.clearAllMocks(); - clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); - actionsClient.getAll.mockImplementation(async () => { - return connectors; - }); + actionsClient.getAll.mockResolvedValue(connectors); }); - describe('getExportByObjectIds', () => { - test('it exports object ids into an expected string with new line characters', async () => { - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds( - rulesClient, - exceptionsClient, - clients.savedObjectsClient, - objects, - logger, - exporterMock, - requestMock, - actionsClient - ); - const exportsObj = { - rulesNdjson: JSON.parse(exports.rulesNdjson), - exportDetails: JSON.parse(exports.exportDetails), - }; - expect(exportsObj).toEqual({ - rulesNdjson: { - author: ['Elastic'], - actions: [], - building_block_type: 'default', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - language: 'kuery', - license: 'Elastic License', - output_index: '.siem-signals', - max_signals: 10000, - risk_score: 50, - risk_score_mapping: [], - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://example.com', 'https://example.com'], - related_integrations: [], - required_fields: [], - revision: 0, - setup: '', - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - meta: { someMeta: 'someField' }, - severity: 'high', - severity_mapping: [], - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: getThreatMock(), - note: '# Investigative notes', - version: 1, - exceptions_list: getListArrayMock(), - investigation_fields: undefined, - }, - exportDetails: { - exported_exception_list_count: 0, - exported_exception_list_item_count: 0, - exported_count: 1, - exported_rules_count: 1, - missing_exception_list_item_count: 0, - missing_exception_list_items: [], - missing_exception_lists: [], - missing_exception_lists_count: 0, - missing_rules: [], - missing_rules_count: 0, - excluded_action_connection_count: 0, - excluded_action_connections: [], - exported_action_connector_count: 0, - missing_action_connection_count: 0, - missing_action_connections: [], - }, - }); - }); + test('it exports rules into an expected format', async () => { + const rulesClient = rulesClientMock.create(); + const rule1 = getRuleMock(getQueryRuleParams({ ruleId: 'rule-1' })); + const rule2 = getRuleMock(getEqlRuleParams({ ruleId: 'rule-2' })); - test('it does not export immutable rules', async () => { - const rulesClient = rulesClientMock.create(); - const result = getRuleMock(getQueryRuleParams()); - result.params.immutable = true; - - const findResult: FindHit = { - page: 1, - perPage: 1, - total: 0, - data: [result], - }; - - rulesClient.get.mockResolvedValue(getRuleMock(getQueryRuleParams())); - rulesClient.find.mockResolvedValue(findResult); - - const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds( - rulesClient, - exceptionsClient, - clients.savedObjectsClient, - objects, - logger, - exporterMock, - requestMock, - actionsClient - ); - const details = getOutputDetailsSampleWithExceptions({ - missingRules: [{ rule_id: 'rule-1' }], - missingCount: 1, - }); - expect(exports).toEqual({ - rulesNdjson: '', - exportDetails: getSampleDetailsAsNdjson(details), - exceptionLists: '', - actionConnectors: '', - }); - }); + rulesClient.find.mockResolvedValue( + getFindResultWithMultiHits({ + data: [rule1, rule2], + }) + ); - test('it will export with rule and action connectors', async () => { - const rulesClient = rulesClientMock.create(); - const result = getFindResultWithSingleHit(); - const alert = { - ...getRuleMock(getQueryRuleParams()), - actions: [ - { - group: 'default', - id: '123', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - }, - actionTypeId: '.slack', - }, - ], - }; + const ruleIds = ['rule-1', 'rule-2']; + const exports = await getExportByObjectIds( + rulesClient, + exceptionsClient, + ruleIds, + exporterMock, + requestMock, + actionsClient + ); + + const [rule1Json, rule2Json, emptyString] = exports.rulesNdjson.split('\n'); + + // ndjson ends with a new line symbol + expect(emptyString).toBe(''); + expect(JSON.parse(rule1Json)).toEqual(internalRuleToAPIResponse(rule1)); + expect(JSON.parse(rule2Json)).toEqual(internalRuleToAPIResponse(rule2)); + expect(JSON.parse(exports.exportDetails)).toEqual(expect.any(Object)); + expect(exports.exceptionLists).toBe(''); + expect(exports.actionConnectors).toBe(''); + }); + + test('it DOES NOT export immutable rules', async () => { + const rulesClient = rulesClientMock.create(); + const immutableRule = getRuleMock(getQueryRuleParams({ ruleId: 'rule-1', immutable: true })); - alert.params = { - ...alert.params, + rulesClient.get.mockResolvedValue(immutableRule); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [immutableRule] })); + + const ruleIds = ['rule-1']; + const exports = await getExportByObjectIds( + rulesClient, + exceptionsClient, + ruleIds, + exporterMock, + requestMock, + actionsClient + ); + + expect(JSON.parse(exports.exportDetails)).toMatchObject({ + exported_count: 0, + exported_rules_count: 0, + missing_rules: [{ rule_id: 'rule-1' }], + missing_rules_count: 1, + }); + expect(exports).toMatchObject({ + rulesNdjson: '', + exceptionLists: '', + actionConnectors: '', + }); + }); + + test('it exports a rule with action connectors', async () => { + const rulesClient = rulesClientMock.create(); + const ruleWithActions = getRuleMock( + getQueryRuleParams({ filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], threat: getThreatMock(), meta: { someMeta: 'someField' }, timelineId: 'some-timeline-id', timelineTitle: 'some-timeline-title', - }; - result.data = [alert]; - rulesClient.find.mockResolvedValue(result); - let eventCount = 0; - const readable = new Readable({ - objectMode: true, - read() { - if (eventCount === 0) { - eventCount += 1; - return this.push({ - id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', - type: 'action', - updated_at: '2023-01-11T11:30:31.683Z', - created_at: '2023-01-11T11:30:31.683Z', - version: 'WzE2MDYsMV0=', - attributes: { - actionTypeId: '.slack', - name: 'slack', - isMissingSecrets: true, - config: {}, - secrets: {}, - }, - references: [], - migrationVersion: { action: '8.3.0' }, - coreMigrationVersion: '8.7.0', - }); - } - if (eventCount === 1) { - eventCount += 1; - return this.push({ - exportedCount: 1, - missingRefCount: 0, - missingReferences: [], - excludedObjectsCount: 0, - excludedObjects: [], - }); - } - return this.push(null); - }, - }); - const objects = [{ rule_id: 'rule-1' }]; - const exporterMockWithConnector = { - exportByObjects: () => jest.fn().mockReturnValueOnce(readable), - - exportByTypes: jest.fn(), - }; - const exports = await getExportByObjectIds( - rulesClient, - exceptionsClient, - clients.savedObjectsClient, - objects, - logger, - exporterMockWithConnector as never, - requestMock, - actionsClient - ); - const rulesJson = JSON.parse(exports.rulesNdjson); - const detailsJson = JSON.parse(exports.exportDetails); - const actionConnectorsJSON = JSON.parse(exports.actionConnectors); - expect(rulesJson).toEqual({ - author: ['Elastic'], + }), + { actions: [ { group: 'default', - id: '123', + id: 'non-preconfigured-connector', params: { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - action_type_id: '.slack', - frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + actionTypeId: '.slack', }, ], - building_block_type: 'default', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], + } + ); + + rulesClient.find.mockResolvedValue( + getFindResultWithMultiHits({ + data: [ruleWithActions], + }) + ); + + const actionConnector = { + id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', + type: 'action', + updated_at: '2023-01-11T11:30:31.683Z', + created_at: '2023-01-11T11:30:31.683Z', + version: 'WzE2MDYsMV0=', + attributes: { + actionTypeId: '.slack', + name: 'slack', + isMissingSecrets: true, + config: {}, + secrets: {}, + }, + references: [], + migrationVersion: { action: '8.3.0' }, + coreMigrationVersion: '8.7.0', + }; + + const actionsConnectorsStreamMock = new Readable({ + objectMode: true, + }); + actionsConnectorsStreamMock.push(actionConnector); + actionsConnectorsStreamMock.push({ + exportedCount: 1, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 0, + excludedObjects: [], + }); + actionsConnectorsStreamMock.push(null); + + const ruleIds = ['rule-1']; + const actionsExporterMock = { + exportByObjects: jest.fn().mockReturnValueOnce(actionsConnectorsStreamMock), + exportByTypes: jest.fn(), + }; + const exports = await getExportByObjectIds( + rulesClient, + exceptionsClient, + ruleIds, + actionsExporterMock, + requestMock, + actionsClient + ); + + expect(JSON.parse(exports.rulesNdjson)).toEqual(internalRuleToAPIResponse(ruleWithActions)); + expect(JSON.parse(exports.exportDetails)).toMatchObject({ + exported_count: 2, + exported_rules_count: 1, + exported_action_connector_count: 1, + missing_action_connection_count: 0, + missing_action_connections: [], + }); + expect(JSON.parse(exports.actionConnectors)).toEqual(actionConnector); + }); + + test('it DOES NOT export preconfigured action connectors', async () => { + const rulesClient = rulesClientMock.create(); + const ruleWithActions = getRuleMock( + getQueryRuleParams({ filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - language: 'kuery', - license: 'Elastic License', - output_index: '.siem-signals', - max_signals: 10000, - risk_score: 50, - risk_score_mapping: [], - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://example.com', 'https://example.com'], - related_integrations: [], - required_fields: [], - setup: '', - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - meta: { someMeta: 'someField' }, - severity: 'high', - severity_mapping: [], - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', threat: getThreatMock(), - note: '# Investigative notes', - version: 1, - revision: 0, - exceptions_list: getListArrayMock(), - investigation_fields: undefined, - }); - expect(detailsJson).toEqual({ - exported_exception_list_count: 0, - exported_exception_list_item_count: 0, - exported_count: 2, - exported_rules_count: 1, - missing_exception_list_item_count: 0, - missing_exception_list_items: [], - missing_exception_lists: [], - missing_exception_lists_count: 0, - missing_rules: [], - missing_rules_count: 0, - excluded_action_connection_count: 0, - excluded_action_connections: [], - exported_action_connector_count: 1, - missing_action_connection_count: 0, - missing_action_connections: [], - }); - expect(actionConnectorsJSON).toEqual({ - attributes: { - actionTypeId: '.slack', - config: {}, - isMissingSecrets: true, - name: 'slack', - secrets: {}, - }, - coreMigrationVersion: '8.7.0', - created_at: '2023-01-11T11:30:31.683Z', - id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', - migrationVersion: { - action: '8.3.0', - }, - references: [], - type: 'action', - updated_at: '2023-01-11T11:30:31.683Z', - version: 'WzE2MDYsMV0=', - }); - }); - test('it will export rule without its action connectors as they are Preconfigured', async () => { - const rulesClient = rulesClientMock.create(); - const result = getFindResultWithSingleHit(); - const alert = { - ...getRuleMock(getQueryRuleParams()), + meta: { someMeta: 'someField' }, + timelineId: 'some-timeline-id', + timelineTitle: 'some-timeline-title', + }), + { actions: [ { group: 'default', - id: '456', + id: 'preconfigured-connector', params: { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, actionTypeId: '.email', }, ], - }; + } + ); - alert.params = { - ...alert.params, - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - threat: getThreatMock(), - meta: { someMeta: 'someField' }, - timelineId: 'some-timeline-id', - timelineTitle: 'some-timeline-title', - }; - result.data = [alert]; - rulesClient.find.mockResolvedValue(result); - const readable = new Readable({ - objectMode: true, - read() { - return null; + rulesClient.find.mockResolvedValue( + getFindResultWithMultiHits({ + data: [ruleWithActions], + }) + ); + + const readable = new Readable({ + objectMode: true, + read() { + return null; + }, + }); + + const ruleIds = ['rule-1']; + const exporterMockWithConnector = { + exportByObjects: jest.fn().mockReturnValueOnce(readable), + exportByTypes: jest.fn(), + }; + const exports = await getExportByObjectIds( + rulesClient, + exceptionsClient, + ruleIds, + exporterMockWithConnector, + requestMock, + actionsClient + ); + + expect(JSON.parse(exports.rulesNdjson)).toMatchObject({ + actions: [ + { + group: 'default', + id: 'preconfigured-connector', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + action_type_id: '.email', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, - }); - const objects = [{ rule_id: 'rule-1' }]; - const exporterMockWithConnector = { - exportByObjects: () => jest.fn().mockReturnValueOnce(readable), - - exportByTypes: jest.fn(), - }; - const exports = await getExportByObjectIds( - rulesClient, - exceptionsClient, - clients.savedObjectsClient, - objects, - logger, - exporterMockWithConnector as never, - requestMock, - actionsClient - ); - const rulesJson = JSON.parse(exports.rulesNdjson); - const detailsJson = JSON.parse(exports.exportDetails); - expect(rulesJson).toEqual( - expect.objectContaining({ - actions: [ - { - group: 'default', - id: '456', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - }, - action_type_id: '.email', - frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, - }, - ], - }) - ); - expect(detailsJson).toEqual({ - exported_exception_list_count: 0, - exported_exception_list_item_count: 0, - exported_count: 1, - exported_rules_count: 1, - missing_exception_list_item_count: 0, - missing_exception_list_items: [], - missing_exception_lists: [], - missing_exception_lists_count: 0, - missing_rules: [], - missing_rules_count: 0, - excluded_action_connection_count: 0, - excluded_action_connections: [], - exported_action_connector_count: 0, - missing_action_connection_count: 0, - missing_action_connections: [], - }); + ], + }); + expect(JSON.parse(exports.exportDetails)).toMatchObject({ + exported_count: 1, + exported_rules_count: 1, + exported_action_connector_count: 0, + missing_action_connection_count: 0, + missing_action_connections: [], }); + expect(exports.actionConnectors).toBe(''); }); - describe('getRulesFromObjects', () => { - test('it returns transformed rules from objects sent in', async () => { - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects( - rulesClient, - clients.savedObjectsClient, - objects, - logger - ); - const expected: RulesErrors = { - exportedCount: 1, - missingRules: [], - rules: [ - { - actions: [], - author: ['Elastic'], - building_block_type: 'default', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - language: 'kuery', - license: 'Elastic License', - output_index: '.siem-signals', - max_signals: 10000, - risk_score: 50, - risk_score_mapping: [], - rule_name_override: undefined, - saved_id: undefined, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://example.com', 'https://example.com'], - related_integrations: [], - required_fields: [], - response_actions: undefined, - setup: '', - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - meta: { someMeta: 'someField' }, - severity: 'high', - severity_mapping: [], - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: getThreatMock(), - throttle: undefined, - note: '# Investigative notes', - version: 1, - revision: 0, - exceptions_list: getListArrayMock(), - execution_summary: undefined, - outcome: undefined, - alias_target_id: undefined, - alias_purpose: undefined, - timestamp_override: undefined, - timestamp_override_fallback_disabled: undefined, - namespace: undefined, - data_view_id: undefined, - alert_suppression: undefined, - investigation_fields: undefined, - }, - ], - }; - expect(exports).toEqual(expected); + test('it processes large exports in chunks to avoid "too_many_clauses" error', async () => { + const EXPECTED_CHUNK_SIZE = 1024; + // Let's have 3 chunks, two chunks by 1024 rules and the third chunk containing just one rule + const RULES_COUNT = 2 * EXPECTED_CHUNK_SIZE + 1; + const rules = new Array(RULES_COUNT) + .fill(0) + .map((_, i) => getRuleMock(getQueryRuleParams({ ruleId: `rule-${i}` }))); + + const rulesClient = rulesClientMock.create(); + const chunk1 = getFindResultWithMultiHits({ + data: rules.slice(0, EXPECTED_CHUNK_SIZE), + }); + const chunk2 = getFindResultWithMultiHits({ + data: rules.slice(EXPECTED_CHUNK_SIZE, 2 * EXPECTED_CHUNK_SIZE), + }); + const chunk3 = getFindResultWithMultiHits({ + data: rules.slice(2 * EXPECTED_CHUNK_SIZE), + }); + + rulesClient.find + .mockResolvedValueOnce(chunk1) + .mockResolvedValueOnce(chunk2) + .mockResolvedValueOnce(chunk3); + + const ruleIds = rules.map((rule) => rule.params.ruleId); + const exports = await getExportByObjectIds( + rulesClient, + exceptionsClient, + ruleIds, + exporterMock, + requestMock, + actionsClient + ); + + expect(rulesClient.find).toHaveBeenCalledTimes(3); + expect(rulesClient.find).toHaveBeenCalledWith({ + options: expect.objectContaining({ perPage: EXPECTED_CHUNK_SIZE }), }); - test('it does not transform the rule if the rule is an immutable rule and designates it as a missing rule', async () => { - const rulesClient = rulesClientMock.create(); - const result = getRuleMock(getQueryRuleParams()); - result.params.immutable = true; - - const findResult: FindHit = { - page: 1, - perPage: 1, - total: 0, - data: [result], - }; - - rulesClient.get.mockResolvedValue(result); - rulesClient.find.mockResolvedValue(findResult); - - const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects( - rulesClient, - clients.savedObjectsClient, - objects, - logger - ); - const expected: RulesErrors = { - exportedCount: 0, - missingRules: [{ rule_id: 'rule-1' }], - rules: [], - }; - expect(exports).toEqual(expected); + expect(JSON.parse(exports.exportDetails)).toMatchObject({ + exported_count: RULES_COUNT, + exported_rules_count: RULES_COUNT, + missing_rules: [], + missing_rules_count: 0, }); + }); + + test('it DOES NOT fail when a rule is not found (rulesClient returns 404)', async () => { + const rulesClient = rulesClientMock.create(); + + rulesClient.get.mockRejectedValue({ output: { statusCode: 404 } }); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [] })); - test('it exports missing rules', async () => { - const rulesClient = rulesClientMock.create(); - - const findResult: FindHit = { - page: 1, - perPage: 1, - total: 0, - data: [], - }; - - rulesClient.get.mockRejectedValue({ output: { statusCode: 404 } }); - rulesClient.find.mockResolvedValue(findResult); - - const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects( - rulesClient, - clients.savedObjectsClient, - objects, - logger - ); - const expected: RulesErrors = { - exportedCount: 0, - missingRules: [{ rule_id: 'rule-1' }], - rules: [], - }; - expect(exports).toEqual(expected); + const ruleIds = ['rule-1']; + const exports = await getExportByObjectIds( + rulesClient, + exceptionsClient, + ruleIds, + exporterMock, + requestMock, + actionsClient + ); + + expect(JSON.parse(exports.exportDetails)).toMatchObject({ + exported_count: 0, + exported_rules_count: 0, + missing_rules: [{ rule_id: 'rule-1' }], + missing_rules_count: 1, + }); + expect(exports).toMatchObject({ + rulesNdjson: '', + exceptionLists: '', + actionConnectors: '', }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts index c92b37d7710db..ce57b33227ca4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts @@ -5,48 +5,28 @@ * 2.0. */ +import pMap from 'p-map'; import { chunk } from 'lodash'; - import { transformDataToNdjson } from '@kbn/securitysolution-utils'; - -import type { ISavedObjectsExporter, KibanaRequest, Logger } from '@kbn/core/server'; +import type { ISavedObjectsExporter, KibanaRequest } from '@kbn/core/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; -import type { RulesClient, RuleExecutorServices } from '@kbn/alerting-plugin/server'; - +import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; import type { ActionsClient } from '@kbn/actions-plugin/server'; -import { getExportDetailsNdjson } from './get_export_details_ndjson'; - +import { withSecuritySpan } from '../../../../../utils/with_security_span'; +import { internalRuleToAPIResponse } from '../../normalization/rule_converters'; +import type { RuleParams } from '../../../rule_schema'; import { hasValidRuleType } from '../../../rule_schema'; import { findRules } from '../search/find_rules'; -import { transformRuleToExportableFormat } from '../../utils/utils'; import { getRuleExceptionsForExport } from './get_export_rule_exceptions'; +import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { getRuleActionConnectorsForExport } from './get_export_rule_action_connectors'; - -import { internalRuleToAPIResponse } from '../../normalization/rule_converters'; -import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; - -interface ExportSuccessRule { - statusCode: 200; - rule: RuleResponse; -} - -interface ExportFailedRule { - statusCode: 404; - missingRuleId: { rule_id: string }; -} - -export interface RulesErrors { - exportedCount: number; - missingRules: Array<{ rule_id: string }>; - rules: RuleResponse[]; -} +import type { ExportableRule } from './exportable_rule'; +import { transformRuleToExportableFormat } from './transform_rule_to_exportable_format'; export const getExportByObjectIds = async ( rulesClient: RulesClient, exceptionsClient: ExceptionListClient | undefined, - savedObjectsClient: RuleExecutorServices['savedObjectsClient'], - objects: Array<{ rule_id: string }>, - logger: Logger, + ruleIds: string[], actionsExporter: ISavedObjectsExporter, request: KibanaRequest, actionsClient: ActionsClient @@ -55,102 +35,117 @@ export const getExportByObjectIds = async ( exportDetails: string; exceptionLists: string | null; actionConnectors: string; -}> => { - const rulesAndErrors = await getRulesFromObjects( - rulesClient, - savedObjectsClient, - objects, - logger - ); - const { rules, missingRules } = rulesAndErrors; - - // Retrieve exceptions - const exceptions = rules.flatMap((rule) => rule.exceptions_list ?? []); - const { exportData: exceptionLists, exportDetails: exceptionDetails } = - await getRuleExceptionsForExport(exceptions, exceptionsClient); - - // Retrieve Action-Connectors - const { actionConnectors, actionConnectorDetails } = await getRuleActionConnectorsForExport( - rules, - actionsExporter, - request, - actionsClient - ); - - const rulesNdjson = transformDataToNdjson(rules); - const exportDetails = getExportDetailsNdjson( - rules, - missingRules, - exceptionDetails, - actionConnectorDetails - ); +}> => + withSecuritySpan('getExportByObjectIds', async () => { + const rulesAndErrors = await fetchRulesByIds(rulesClient, ruleIds); + const { rules, missingRuleIds } = rulesAndErrors; + + // Retrieve exceptions + const exceptions = rules.flatMap((rule) => rule.exceptions_list ?? []); + const { exportData: exceptionLists, exportDetails: exceptionDetails } = + await getRuleExceptionsForExport(exceptions, exceptionsClient); + + // Retrieve Action-Connectors + const { actionConnectors, actionConnectorDetails } = await getRuleActionConnectorsForExport( + rules, + actionsExporter, + request, + actionsClient + ); + + const rulesNdjson = transformDataToNdjson(rules); + const exportDetails = getExportDetailsNdjson( + rules, + missingRuleIds, + exceptionDetails, + actionConnectorDetails + ); + + return { + rulesNdjson, + exportDetails, + exceptionLists, + actionConnectors, + }; + }); - return { - rulesNdjson, - exportDetails, - exceptionLists, - actionConnectors, - }; -}; +interface FetchRulesResult { + rules: ExportableRule[]; + missingRuleIds: string[]; +} -export const getRulesFromObjects = async ( +const fetchRulesByIds = async ( rulesClient: RulesClient, - savedObjectsClient: RuleExecutorServices['savedObjectsClient'], - objects: Array<{ rule_id: string }>, - logger: Logger -): Promise => { - // If we put more than 1024 ids in one block like "alert.attributes.tags: (id1 OR id2 OR ... OR id1100)" - // then the KQL -> ES DSL query generator still puts them all in the same "should" array, but ES defaults - // to limiting the length of "should" arrays to 1024. By chunking the array into blocks of 1024 ids, - // we can force the KQL -> ES DSL query generator into grouping them in blocks of 1024. - // The generated KQL query here looks like - // "alert.attributes.tags: (id1 OR id2 OR ... OR id1024) OR alert.attributes.tags: (...) ..." - const chunkedObjects = chunk(objects, 1024); - const filter = chunkedObjects - .map((chunkedArray) => { - const joinedIds = chunkedArray.map((object) => object.rule_id).join(' OR '); - return `alert.attributes.params.ruleId: (${joinedIds})`; - }) - .join(' OR '); - const rules = await findRules({ - rulesClient, - filter, - page: 1, - fields: undefined, - perPage: 10000, - sortField: undefined, - sortOrder: undefined, + ruleIds: string[] +): Promise => { + // It's important to avoid too many clauses in the request otherwise ES will fail to process the request + // with `too_many_clauses` error (see https://github.com/elastic/kibana/issues/170015). The clauses limit + // used to be set via `indices.query.bool.max_clause_count` but it's an option anymore. The limit is [calculated + // dynamically](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-settings.html) based + // on available CPU and memory but the minimum value is 1024. + // 1024 chunk size helps to solve the problem and use the maximum safe number of clauses. + const CHUNK_SIZE = 1024; + // We need to decouple from the number of existing rules and let the complexity/load depend only on the number of users + // exporting rules simultaneously. By using limited parallelization via p-map we trade speed for stability and scalability. + const CHUNKS_PROCESSED_IN_PARALLEL = 2; + const processChunk = async (ids: string[]) => + withSecuritySpan('processChunk', async () => { + const rulesResult = await findRules({ + rulesClient, + filter: `alert.attributes.params.ruleId: (${ids.join(' OR ')})`, + page: 1, + fields: undefined, + perPage: CHUNK_SIZE, + sortField: undefined, + sortOrder: undefined, + }); + const rulesMap = new Map>(); + + for (const rule of rulesResult.data) { + rulesMap.set(rule.params.ruleId, rule); + } + + const rulesAndErrors = ids.map((ruleId) => { + const matchingRule = rulesMap.get(ruleId); + + return matchingRule != null && + hasValidRuleType(matchingRule) && + matchingRule.params.immutable !== true + ? { + rule: transformRuleToExportableFormat(internalRuleToAPIResponse(matchingRule)), + } + : { + missingRuleId: ruleId, + }; + }); + + return rulesAndErrors; + }); + + const ruleIdChunks = chunk(ruleIds, CHUNK_SIZE); + // We expect all rules to be processed here to avoid any situation when export of some rules failed silently. + // If some error happens it just bubbles up as is and processed in the upstream code. + const rulesAndErrorsChunks = await pMap(ruleIdChunks, processChunk, { + concurrency: CHUNKS_PROCESSED_IN_PARALLEL, }); - const alertsAndErrors = objects.map(({ rule_id: ruleId }) => { - const matchingRule = rules.data.find((rule) => rule.params.ruleId === ruleId); - if ( - matchingRule != null && - hasValidRuleType(matchingRule) && - matchingRule.params.immutable !== true - ) { - return { - statusCode: 200, - rule: transformRuleToExportableFormat(internalRuleToAPIResponse(matchingRule)), - }; - } else { - return { - statusCode: 404, - missingRuleId: { rule_id: ruleId }, - }; - } - }); + const missingRuleIds: string[] = []; + const rules: ExportableRule[] = []; - const missingRules = alertsAndErrors.filter( - (resp) => resp.statusCode === 404 - ) as ExportFailedRule[]; - const exportedRules = alertsAndErrors.filter( - (resp) => resp.statusCode === 200 - ) as ExportSuccessRule[]; + for (const rulesAndErrors of rulesAndErrorsChunks) { + for (const response of rulesAndErrors) { + if (response.missingRuleId) { + missingRuleIds.push(response.missingRuleId); + } + + if (response.rule) { + rules.push(response.rule); + } + } + } return { - exportedCount: exportedRules.length, - missingRules: missingRules.map((mr) => mr.missingRuleId), - rules: exportedRules.map((er) => er.rule), + rules, + missingRuleIds, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.test.ts index 97d423158c7d3..ed21982bdcfaf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.test.ts @@ -28,8 +28,8 @@ describe('getExportDetailsNdjson', () => { }); test('it exports a correct count given a no rules and a single missing rule', () => { - const missingRule = { rule_id: 'rule-1' }; - const details = getExportDetailsNdjson([], [missingRule]); + const missingRuleId = 'rule-1'; + const details = getExportDetailsNdjson([], [missingRuleId]); const reParsed = JSON.parse(details); expect(reParsed).toEqual({ exported_count: 0, @@ -45,15 +45,15 @@ describe('getExportDetailsNdjson', () => { rule2.rule_id = 'some other id'; rule2.id = 'some other id'; - const missingRule1 = { rule_id: 'rule-1' }; - const missingRule2 = { rule_id: 'rule-2' }; + const missingRuleId1 = 'rule-1'; + const missingRuleId2 = 'rule-2'; - const details = getExportDetailsNdjson([rule1, rule2], [missingRule1, missingRule2]); + const details = getExportDetailsNdjson([rule1, rule2], [missingRuleId1, missingRuleId2]); const reParsed = JSON.parse(details); expect(reParsed).toEqual({ exported_count: 2, exported_rules_count: 2, - missing_rules: [missingRule1, missingRule2], + missing_rules: [{ rule_id: missingRuleId1 }, { rule_id: missingRuleId2 }], missing_rules_count: 2, }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.ts index 1341a0c853be0..54fe37c004cd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_details_ndjson.ts @@ -7,12 +7,12 @@ import type { ExportExceptionDetails } from '@kbn/securitysolution-io-ts-list-types'; import type { ExportRulesDetails } from '../../../../../../common/api/detection_engine/rule_management'; -import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; import type { DefaultActionConnectorDetails } from './get_export_rule_action_connectors'; +import type { ExportableRule } from './exportable_rule'; export const getExportDetailsNdjson = ( - rules: RuleResponse[], - missingRules: Array<{ rule_id: string }> = [], + rules: ExportableRule[], + missingRuleIds: string[] = [], exceptionDetails?: ExportExceptionDetails, actionConnectorDetails?: DefaultActionConnectorDetails ): string => { @@ -27,8 +27,8 @@ export const getExportDetailsNdjson = ( const stringified: ExportRulesDetails = { exported_count: exportedCount, exported_rules_count: rules.length, - missing_rules: missingRules, - missing_rules_count: missingRules.length, + missing_rules: missingRuleIds.map((id) => ({ rule_id: id })), + missing_rules_count: missingRuleIds.length, ...exceptionDetails, ...actionConnectorDetails, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_rule_action_connectors.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_rule_action_connectors.ts index f65d2a5b8c985..6b0057103c8ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_rule_action_connectors.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_rule_action_connectors.ts @@ -14,7 +14,7 @@ import type { SavedObject, } from '@kbn/core-saved-objects-server'; import { createConcatStream, createMapStream, createPromiseFromStreams } from '@kbn/utils'; -import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { ExportableRule } from './exportable_rule'; export interface DefaultActionConnectorDetails { exported_action_connector_count: number; @@ -63,7 +63,7 @@ const filterOutPredefinedActionConnectorsIds = async ( // to getAll actions connectors export const getRuleActionConnectorsForExport = async ( - rules: RuleResponse[], + rules: ExportableRule[], actionsExporter: ISavedObjectsExporter, request: KibanaRequest, actionsClient: ActionsClient diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/transform_rule_to_exportable_format.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/transform_rule_to_exportable_format.ts new file mode 100644 index 0000000000000..d3a102e39e421 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/transform_rule_to_exportable_format.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; + +/** + * Transforms a rule object to exportable format. Exportable format shouldn't contain runtime fields like + * `execution_summary` + */ +export const transformRuleToExportableFormat = ( + rule: RuleResponse +): Omit => { + const exportedRule = { + ...rule, + }; + + // Fields containing runtime information shouldn't be exported. It causes import failures. + delete exportedRule.execution_summary; + + return exportedRule; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts index 3e486c37e56e8..bda8cd7a688ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts @@ -97,23 +97,6 @@ export const transformAlertsToRules = (rules: RuleAlertType[]): RuleResponse[] = return rules.map((rule) => internalRuleToAPIResponse(rule)); }; -/** - * Transforms a rule object to exportable format. Exportable format shouldn't contain runtime fields like - * `execution_summary` - */ -export const transformRuleToExportableFormat = ( - rule: RuleResponse -): Omit => { - const exportedRule = { - ...rule, - }; - - // Fields containing runtime information shouldn't be exported. It causes import failures. - delete exportedRule.execution_summary; - - return exportedRule; -}; - export const transformFindAlerts = (ruleFindResults: FindResult): FindRulesResponse => { return { page: ruleFindResults.page, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts index 0618c0ff722f1..06aa99b210a27 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts @@ -61,7 +61,9 @@ const getBaseRuleParams = (): BaseRuleParams => { }; }; -export const getThresholdRuleParams = (): ThresholdRuleParams => { +export const getThresholdRuleParams = ( + rewrites?: Partial +): ThresholdRuleParams => { return { ...getBaseRuleParams(), type: 'threshold', @@ -81,10 +83,11 @@ export const getThresholdRuleParams = (): ThresholdRuleParams => { }, ], }, + ...rewrites, }; }; -export const getEqlRuleParams = (): EqlRuleParams => { +export const getEqlRuleParams = (rewrites?: Partial): EqlRuleParams => { return { ...getBaseRuleParams(), type: 'eql', @@ -96,19 +99,23 @@ export const getEqlRuleParams = (): EqlRuleParams => { eventCategoryOverride: undefined, dataViewId: undefined, tiebreakerField: undefined, + ...rewrites, }; }; -export const getMlRuleParams = (): MachineLearningRuleParams => { +export const getMlRuleParams = ( + rewrites?: Partial +): MachineLearningRuleParams => { return { ...getBaseRuleParams(), type: 'machine_learning', anomalyThreshold: 42, machineLearningJobId: ['my-job'], + ...rewrites, }; }; -export const getQueryRuleParams = (): QueryRuleParams => { +export const getQueryRuleParams = (rewrites?: Partial): QueryRuleParams => { return { ...getBaseRuleParams(), type: 'query', @@ -128,10 +135,13 @@ export const getQueryRuleParams = (): QueryRuleParams => { savedId: undefined, alertSuppression: undefined, responseActions: undefined, + ...rewrites, }; }; -export const getSavedQueryRuleParams = (): SavedQueryRuleParams => { +export const getSavedQueryRuleParams = ( + rewrites?: Partial +): SavedQueryRuleParams => { return { ...getBaseRuleParams(), type: 'saved_query', @@ -151,10 +161,13 @@ export const getSavedQueryRuleParams = (): SavedQueryRuleParams => { savedId: 'some-id', responseActions: undefined, alertSuppression: undefined, + ...rewrites, }; }; -export const getNewTermsRuleParams = (): NewTermsRuleParams => { +export const getNewTermsRuleParams = ( + rewrites?: Partial +): NewTermsRuleParams => { return { ...getBaseRuleParams(), type: 'new_terms', @@ -173,10 +186,11 @@ export const getNewTermsRuleParams = (): NewTermsRuleParams => { ], newTermsFields: ['host.name'], historyWindowStart: 'now-30d', + ...rewrites, }; }; -export const getThreatRuleParams = (): ThreatRuleParams => { +export const getThreatRuleParams = (rewrites?: Partial): ThreatRuleParams => { return { ...getBaseRuleParams(), type: 'threat_match', @@ -194,6 +208,7 @@ export const getThreatRuleParams = (): ThreatRuleParams => { threatIndicatorPath: '', concurrentSearches: undefined, itemsPerSearch: undefined, + ...rewrites, }; };