diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts index e4b99e63cb6c6..c84dd8147ebcc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { Logger } from 'src/core/server'; import { exportRulesQuerySchema, ExportRulesQuerySchemaDecoded, @@ -24,6 +25,7 @@ import { buildSiemResponse } from '../utils'; export const exportRulesRoute = ( router: SecuritySolutionPluginRouter, config: ConfigType, + logger: Logger, isRuleRegistryEnabled: boolean ) => { router.post( @@ -44,6 +46,7 @@ export const exportRulesRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); const rulesClient = context.alerting?.getRulesClient(); + const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); @@ -71,8 +74,14 @@ export const exportRulesRoute = ( const exported = request.body?.objects != null - ? await getExportByObjectIds(rulesClient, request.body.objects, isRuleRegistryEnabled) - : await getExportAll(rulesClient, isRuleRegistryEnabled); + ? await getExportByObjectIds( + rulesClient, + savedObjectsClient, + request.body.objects, + logger, + isRuleRegistryEnabled + ) + : await getExportAll(rulesClient, savedObjectsClient, logger, isRuleRegistryEnabled); const responseBody = request.query.exclude_export_details ? exported.rulesNdjson diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index b09ef1a215747..3752128d3daa3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -194,6 +194,7 @@ export const importRulesRoute = ( throttle, version, exceptions_list: exceptionsList, + actions, } = parsedRule; try { @@ -265,7 +266,7 @@ export const importRulesRoute = ( note, version, exceptionsList, - actions: [], // Actions are not imported nor exported at this time + actions, }); resolve({ rule_id: ruleId, @@ -328,7 +329,7 @@ export const importRulesRoute = ( exceptionsList, anomalyThreshold, machineLearningJobId, - actions: undefined, + actions, }); resolve({ rule_id: ruleId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts index 41b909bd718c0..3e85b4898d01c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -17,6 +17,7 @@ import { import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { performBulkActionRoute } from './perform_bulk_action_route'; import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -27,15 +28,17 @@ describe.each([ let server: ReturnType; let { clients, context } = requestContextMock.createTools(); let ml: ReturnType; + let logger: ReturnType; beforeEach(() => { server = serverMock.create(); + logger = loggingSystemMock.createLogger(); ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); - performBulkActionRoute(server.router, ml, isRuleRegistryEnabled); + performBulkActionRoute(server.router, ml, logger, isRuleRegistryEnabled); }); describe('status codes', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index 0eba5af4e063a..fb5a2315479da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -6,6 +6,8 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { Logger } from 'src/core/server'; + import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants'; import { BulkAction } from '../../../../../common/detection_engine/schemas/common/schemas'; import { performBulkActionSchema } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; @@ -26,6 +28,7 @@ const BULK_ACTION_RULES_LIMIT = 10000; export const performBulkActionRoute = ( router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml'], + logger: Logger, isRuleRegistryEnabled: boolean ) => { router.post( @@ -133,7 +136,9 @@ export const performBulkActionRoute = ( case BulkAction.export: const exported = await getExportByObjectIds( rulesClient, + savedObjectsClient, rules.data.map(({ params }) => ({ rule_id: params.ruleId })), + logger, isRuleRegistryEnabled ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index c5a30c349d497..366ae607f0ba8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -469,12 +469,12 @@ describe.each([ describe('transformAlertsToRules', () => { test('given an empty array returns an empty array', () => { - expect(transformAlertsToRules([])).toEqual([]); + expect(transformAlertsToRules([], {})).toEqual([]); }); test('given single alert will return the alert transformed', () => { const result1 = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); - const transformed = transformAlertsToRules([result1]); + const transformed = transformAlertsToRules([result1], {}); const expected = getOutputRuleAlertForRest(); expect(transformed).toEqual([expected]); }); @@ -485,7 +485,7 @@ describe.each([ result2.id = 'some other id'; result2.params.ruleId = 'some other id'; - const transformed = transformAlertsToRules([result1, result2]); + const transformed = transformAlertsToRules([result1, result2], {}); const expected1 = getOutputRuleAlertForRest(); const expected2 = getOutputRuleAlertForRest(); expected2.id = 'some other id'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index afc48386a2986..bb2e35d189ca1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -103,8 +103,11 @@ export const transformAlertToRule = ( return internalRuleToAPIResponse(alert, ruleStatus?.attributes, legacyRuleActions); }; -export const transformAlertsToRules = (alerts: RuleAlertType[]): Array> => { - return alerts.map((alert) => transformAlertToRule(alert)); +export const transformAlertsToRules = ( + alerts: RuleAlertType[], + legacyRuleActions: Record +): Array> => { + return alerts.map((alert) => transformAlertToRule(alert, undefined, legacyRuleActions[alert.id])); }; export const transformFindAlerts = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index 3ca5960d7d4e1..92e4f0bbb4a5e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -9,21 +9,33 @@ import { getAlertMock, getFindResultWithSingleHit, FindHit, + getEmptySavedObjectsResponse, } from '../routes/__mocks__/request_responses'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getExportAll } from './get_export_all'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; + import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { requestContextMock } from '../routes/__mocks__/request_context'; describe.each([ ['Legacy', false], ['RAC', true], ])('getExportAll - %s', (_, isRuleRegistryEnabled) => { + let logger: ReturnType; + const { clients } = requestContextMock.createTools(); + + beforeEach(async () => { + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); + }); + test('it exports everything from the alerts client', async () => { const rulesClient = rulesClientMock.create(); const result = getFindResultWithSingleHit(isRuleRegistryEnabled); const alert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + alert.params = { ...alert.params, filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], @@ -35,7 +47,12 @@ describe.each([ result.data = [alert]; rulesClient.find.mockResolvedValue(result); - const exports = await getExportAll(rulesClient, isRuleRegistryEnabled); + const exports = await getExportAll( + rulesClient, + clients.savedObjectsClient, + logger, + isRuleRegistryEnabled + ); const rulesJson = JSON.parse(exports.rulesNdjson); const detailsJson = JSON.parse(exports.exportDetails); expect(rulesJson).toEqual({ @@ -97,7 +114,12 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); - const exports = await getExportAll(rulesClient, isRuleRegistryEnabled); + const exports = await getExportAll( + rulesClient, + clients.savedObjectsClient, + logger, + isRuleRegistryEnabled + ); expect(exports).toEqual({ rulesNdjson: '', exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts index 71079ccefc97a..cbbda5df7e2bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts @@ -7,20 +7,33 @@ import { transformDataToNdjson } from '@kbn/securitysolution-utils'; -import { RulesClient } from '../../../../../alerting/server'; +import { Logger } from 'src/core/server'; +import { RulesClient, AlertServices } from '../../../../../alerting/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { transformAlertsToRules } from '../routes/rules/utils'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; + export const getExportAll = async ( rulesClient: RulesClient, + savedObjectsClient: AlertServices['savedObjectsClient'], + logger: Logger, isRuleRegistryEnabled: boolean ): Promise<{ rulesNdjson: string; exportDetails: string; }> => { const ruleAlertTypes = await getNonPackagedRules({ rulesClient, isRuleRegistryEnabled }); - const rules = transformAlertsToRules(ruleAlertTypes); + const alertIds = ruleAlertTypes.map((rule) => rule.id); + const legacyActions = await legacyGetBulkRuleActionsSavedObject({ + alertIds, + savedObjectsClient, + logger, + }); + + const rules = transformAlertsToRules(ruleAlertTypes, legacyActions); // We do not support importing/exporting actions. When we do, delete this line of code const rulesWithoutActions = rules.map((rule) => ({ ...rule, actions: [] })); const rulesNdjson = transformDataToNdjson(rulesWithoutActions); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 740427e44b560..961f2c6a41866 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -10,28 +10,43 @@ import { getAlertMock, getFindResultWithSingleHit, FindHit, + getEmptySavedObjectsResponse, } from '../routes/__mocks__/request_responses'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { requestContextMock } from '../routes/__mocks__/request_context'; describe.each([ ['Legacy', false], ['RAC', true], ])('get_export_by_object_ids - %s', (_, isRuleRegistryEnabled) => { + let logger: ReturnType; + const { clients } = requestContextMock.createTools(); + beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); jest.clearAllMocks(); + + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); }); + describe('getExportByObjectIds', () => { test('it exports object ids into an expected string with new line characters', async () => { const rulesClient = rulesClientMock.create(); rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getExportByObjectIds( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const exportsObj = { rulesNdjson: JSON.parse(exports.rulesNdjson), exportDetails: JSON.parse(exports.exportDetails), @@ -102,7 +117,13 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getExportByObjectIds( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); expect(exports).toEqual({ rulesNdjson: '', exportDetails: @@ -117,7 +138,13 @@ describe.each([ rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getRulesFromObjects( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const expected: RulesErrors = { exportedCount: 1, missingRules: [], @@ -192,7 +219,13 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getRulesFromObjects( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], @@ -215,7 +248,13 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getRulesFromObjects( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 4cf3ad9133a71..8233fe6d4948c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -8,14 +8,20 @@ import { chunk } from 'lodash'; import { transformDataToNdjson } from '@kbn/securitysolution-utils'; +import { Logger } from 'src/core/server'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { RulesClient } from '../../../../../alerting/server'; +import { RulesClient, AlertServices } from '../../../../../alerting/server'; + import { getExportDetailsNdjson } from './get_export_details_ndjson'; + import { isAlertType } from '../rules/types'; import { transformAlertToRule } from '../routes/rules/utils'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; + interface ExportSuccessRule { statusCode: 200; rule: Partial; @@ -34,23 +40,32 @@ export interface RulesErrors { export const getExportByObjectIds = async ( rulesClient: RulesClient, + savedObjectsClient: AlertServices['savedObjectsClient'], objects: Array<{ rule_id: string }>, + logger: Logger, isRuleRegistryEnabled: boolean ): Promise<{ rulesNdjson: string; exportDetails: string; }> => { - const rulesAndErrors = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); - // We do not support importing/exporting actions. When we do, delete this line of code - const rulesWithoutActions = rulesAndErrors.rules.map((rule) => ({ ...rule, actions: [] })); - const rulesNdjson = transformDataToNdjson(rulesWithoutActions); - const exportDetails = getExportDetailsNdjson(rulesWithoutActions, rulesAndErrors.missingRules); + const rulesAndErrors = await getRulesFromObjects( + rulesClient, + savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); + + const rulesNdjson = transformDataToNdjson(rulesAndErrors.rules); + const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules); return { rulesNdjson, exportDetails }; }; export const getRulesFromObjects = async ( rulesClient: RulesClient, + savedObjectsClient: AlertServices['savedObjectsClient'], objects: Array<{ rule_id: string }>, + logger: Logger, isRuleRegistryEnabled: boolean ): Promise => { // If we put more than 1024 ids in one block like "alert.attributes.tags: (id1 OR id2 OR ... OR id1100)" @@ -78,6 +93,13 @@ export const getRulesFromObjects = async ( sortField: undefined, sortOrder: undefined, }); + const alertIds = rules.data.map((rule) => rule.id); + const legacyActions = await legacyGetBulkRuleActionsSavedObject({ + alertIds, + savedObjectsClient, + logger, + }); + const alertsAndErrors = objects.map(({ rule_id: ruleId }) => { const matchingRule = rules.data.find((rule) => rule.params.ruleId === ruleId); if ( @@ -87,7 +109,7 @@ export const getRulesFromObjects = async ( ) { return { statusCode: 200, - rule: transformAlertToRule(matchingRule), + rule: transformAlertToRule(matchingRule, undefined, legacyActions[matchingRule.id]), }; } else { return { diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index d045c6b129e43..148580d5c4477 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -91,12 +91,12 @@ export const initRoutes = ( updateRulesBulkRoute(router, ml, isRuleRegistryEnabled); patchRulesBulkRoute(router, ml, isRuleRegistryEnabled); deleteRulesBulkRoute(router, isRuleRegistryEnabled); - performBulkActionRoute(router, ml, isRuleRegistryEnabled); + performBulkActionRoute(router, ml, logger, isRuleRegistryEnabled); createTimelinesRoute(router, config, security); patchTimelinesRoute(router, config, security); importRulesRoute(router, config, ml, isRuleRegistryEnabled); - exportRulesRoute(router, config, isRuleRegistryEnabled); + exportRulesRoute(router, config, logger, isRuleRegistryEnabled); importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security);