From a4f7ca037a84f1a757b047d65b4032466db9891b Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 1 Feb 2024 16:31:14 +0100 Subject: [PATCH 01/10] fix rule overriding during import --- .../logic/crud/update_rules.ts | 3 + .../logic/import/import_rules_utils.test.ts | 20 +-- .../logic/import/import_rules_utils.ts | 156 +++++++++--------- 3 files changed, 90 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts index cfbf6a639bb5e..4ffea7d55c9a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts @@ -19,12 +19,14 @@ export interface UpdateRulesOptions { rulesClient: RulesClient; existingRule: RuleAlertType | null | undefined; ruleUpdate: RuleUpdateProps; + allowMissingConnectorSecrets?: boolean; } export const updateRules = async ({ rulesClient, existingRule, ruleUpdate, + allowMissingConnectorSecrets, }: UpdateRulesOptions): Promise | null> => { if (existingRule == null) { return null; @@ -81,6 +83,7 @@ export const updateRules = async ({ const update = await rulesClient.update({ id: existingRule.id, data: newInternalRule, + allowMissingConnectorSecrets, }); if (existingRule.enabled && enabled === false) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts index 5b097bacf2d9c..259f9a9f1a717 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts @@ -16,11 +16,11 @@ import { } from '../../../routes/__mocks__/request_responses'; import { createRules } from '../crud/create_rules'; -import { patchRules } from '../crud/patch_rules'; +import { updateRules } from '../crud/update_rules'; import { importRules } from './import_rules_utils'; jest.mock('../crud/create_rules'); -jest.mock('../crud/patch_rules'); +jest.mock('../crud/update_rules'); describe('importRules', () => { const mlAuthz = { @@ -84,7 +84,7 @@ describe('importRules', () => { expect(result).toEqual([{ rule_id: 'rule-1', status_code: 200 }]); expect(createRules).toHaveBeenCalled(); - expect(patchRules).not.toHaveBeenCalled(); + expect(updateRules).not.toHaveBeenCalled(); }); it('reports error if "overwriteRules" is "false" and matching rule found', async () => { @@ -106,10 +106,10 @@ describe('importRules', () => { }, ]); expect(createRules).not.toHaveBeenCalled(); - expect(patchRules).not.toHaveBeenCalled(); + expect(updateRules).not.toHaveBeenCalled(); }); - it('patches rule if "overwriteRules" is "true" and matching rule found', async () => { + it('updates rule if "overwriteRules" is "true" and matching rule found', async () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const result = await importRules({ @@ -129,7 +129,7 @@ describe('importRules', () => { expect(result).toEqual([{ rule_id: 'rule-1', status_code: 200 }]); expect(createRules).not.toHaveBeenCalled(); - expect(patchRules).toHaveBeenCalled(); + expect(updateRules).toHaveBeenCalled(); }); it('reports error if rulesClient throws', async () => { @@ -154,7 +154,7 @@ describe('importRules', () => { }, ]); expect(createRules).not.toHaveBeenCalled(); - expect(patchRules).not.toHaveBeenCalled(); + expect(updateRules).not.toHaveBeenCalled(); }); it('reports error if "createRules" throws', async () => { @@ -180,8 +180,8 @@ describe('importRules', () => { ]); }); - it('reports error if "patchRules" throws', async () => { - (patchRules as jest.Mock).mockRejectedValue(new Error('error patching rule')); + it('reports error if "updateRules" throws', async () => { + (updateRules as jest.Mock).mockRejectedValue(new Error('import rule error')); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const result = await importRules({ @@ -196,7 +196,7 @@ describe('importRules', () => { expect(result).toEqual([ { error: { - message: 'error patching rule', + message: 'import rule error', status_code: 400, }, rule_id: 'rule-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.ts index 81940848b7dc4..179c4069596e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.ts @@ -19,7 +19,7 @@ import type { ImportRuleResponse } from '../../../routes/utils'; import { createBulkErrorObject } from '../../../routes/utils'; import { createRules } from '../crud/create_rules'; import { readRules } from '../crud/read_rules'; -import { patchRules } from '../crud/patch_rules'; +import { updateRules } from '../crud/update_rules'; import type { MlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { checkRuleExceptionReferences } from './check_rule_exception_references'; @@ -68,96 +68,94 @@ export const importRules = async ({ // otherwise we would output we are success importing 0 rules. if (ruleChunks.length === 0) { return importRuleResponse; - } else { - while (ruleChunks.length) { - const batchParseObjects = ruleChunks.shift() ?? []; - const newImportRuleResponse = await Promise.all( - batchParseObjects.reduce>>((accum, parsedRule) => { - const importsWorkerPromise = new Promise(async (resolve, reject) => { + } + + while (ruleChunks.length) { + const batchParseObjects = ruleChunks.shift() ?? []; + const newImportRuleResponse = await Promise.all( + batchParseObjects.reduce>>((accum, parsedRule) => { + const importsWorkerPromise = new Promise(async (resolve, reject) => { + try { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } + try { - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedRule.message, - }) - ); - return null; - } + const [exceptionErrors, exceptions] = checkRuleExceptionReferences({ + rule: parsedRule, + existingLists, + }); - try { - const [exceptionErrors, exceptions] = checkRuleExceptionReferences({ - rule: parsedRule, - existingLists, - }); + importRuleResponse = [...importRuleResponse, ...exceptionErrors]; - importRuleResponse = [...importRuleResponse, ...exceptionErrors]; + throwAuthzError(await mlAuthz.validateRuleType(parsedRule.type)); + const rule = await readRules({ + rulesClient, + ruleId: parsedRule.rule_id, + id: undefined, + }); - throwAuthzError(await mlAuthz.validateRuleType(parsedRule.type)); - const rule = await readRules({ + if (rule == null) { + await createRules({ rulesClient, - ruleId: parsedRule.rule_id, - id: undefined, + params: { + ...parsedRule, + exceptions_list: [...exceptions], + }, + allowMissingConnectorSecrets, }); - - if (rule == null) { - await createRules({ - rulesClient, - params: { - ...parsedRule, - exceptions_list: [...exceptions], - }, - allowMissingConnectorSecrets, - }); - resolve({ - rule_id: parsedRule.rule_id, - status_code: 200, - }); - } else if (rule != null && overwriteRules) { - await patchRules({ - rulesClient, - existingRule: rule, - nextParams: { - ...parsedRule, - exceptions_list: [...exceptions], - }, - allowMissingConnectorSecrets, - shouldIncrementRevision: false, - }); - resolve({ - rule_id: parsedRule.rule_id, - status_code: 200, - }); - } else if (rule != null) { - resolve( - createBulkErrorObject({ - ruleId: parsedRule.rule_id, - statusCode: 409, - message: `rule_id: "${parsedRule.rule_id}" already exists`, - }) - ); - } - } catch (err) { + resolve({ + rule_id: parsedRule.rule_id, + status_code: 200, + }); + } else if (rule != null && overwriteRules) { + await updateRules({ + rulesClient, + existingRule: rule, + ruleUpdate: { + ...parsedRule, + exceptions_list: [...exceptions], + }, + }); + resolve({ + rule_id: parsedRule.rule_id, + status_code: 200, + }); + } else if (rule != null) { resolve( createBulkErrorObject({ ruleId: parsedRule.rule_id, - statusCode: err.statusCode ?? 400, - message: err.message, + statusCode: 409, + message: `rule_id: "${parsedRule.rule_id}" already exists`, }) ); } - } catch (error) { - reject(error); + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId: parsedRule.rule_id, + statusCode: err.statusCode ?? 400, + message: err.message, + }) + ); } - }); - return [...accum, importsWorkerPromise]; - }, []) - ); - importRuleResponse = [...importRuleResponse, ...newImportRuleResponse]; - } - - return importRuleResponse; + } catch (error) { + reject(error); + } + }); + return [...accum, importsWorkerPromise]; + }, []) + ); + importRuleResponse = [...importRuleResponse, ...newImportRuleResponse]; } + + return importRuleResponse; }; From bdc7042756c91ceefc5060897546520e8f975e16 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Fri, 2 Feb 2024 11:41:27 +0100 Subject: [PATCH 02/10] refactor perform_bulk_action --- .../logic/import/import_rules_utils.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts index 259f9a9f1a717..9e159672b48d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts @@ -13,6 +13,7 @@ import { getRuleMock, getEmptyFindResult, getFindResultWithSingleHit, + getFindResultWithMultiHits, } from '../../../routes/__mocks__/request_responses'; import { createRules } from '../crud/create_rules'; @@ -132,6 +133,52 @@ describe('importRules', () => { expect(updateRules).toHaveBeenCalled(); }); + /** + * Existing rule may have nullable fields set to a value (e.g. `timestamp_override` is set to `some.value`) but + * a rule to import doesn't have these fields set (e.g. `timestamp_override` is presented at al in ndjson). + * We expect the updated rule won't have such fields preserved (e.g. `timestamp_override` will be removed). + * + * Unit test is only able to check `updateRules()` receives a proper update object. + */ + it('ensures overwritten rule DOES NOT preserve missed in the imported rule fields when "overwriteRules" is "true" and matching rule found', async () => { + const existingRule = getRuleMock( + getQueryRuleParams({ + timestampOverride: 'some.value', + }) + ); + + clients.rulesClient.find.mockResolvedValue( + getFindResultWithMultiHits({ data: [existingRule] }) + ); + + const result = await importRules({ + ruleChunks: [ + [ + { + ...getImportRulesSchemaMock(), + rule_id: 'rule-1', + }, + ], + ], + rulesResponseAcc: [], + mlAuthz, + overwriteRules: true, + rulesClient: context.alerting.getRulesClient(), + existingLists: {}, + }); + + expect(result).toEqual([{ rule_id: 'rule-1', status_code: 200 }]); + expect(createRules).not.toHaveBeenCalled(); + expect(updateRules).toHaveBeenCalledWith( + expect.objectContaining({ + ruleUpdate: expect.not.objectContaining({ + timestamp_override: expect.anything(), + timestampOverride: expect.anything(), + }), + }) + ); + }); + it('reports error if rulesClient throws', async () => { clients.rulesClient.find.mockRejectedValue(new Error('error reading rule')); From 2bdca6ab646e9753342d2d3329d0331b5d614c3d Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Fri, 2 Feb 2024 16:40:59 +0100 Subject: [PATCH 03/10] add an integration test --- .../combine_to_ndjson.ts | 10 + .../import_rules.ts | 99 +-------- .../import_rules_with_overwrite.ts | 191 ++++++++++++++++++ .../utils/rules/get_rules_as_ndjson.ts | 18 -- .../detections_response/utils/rules/index.ts | 1 - 5 files changed, 208 insertions(+), 111 deletions(-) create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts delete mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rules_as_ndjson.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.ts new file mode 100644 index 0000000000000..fc2baff9c365f --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.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. + */ + +export function combineToNdJson(...parts: unknown[]): string { + return parts.map((p) => JSON.stringify(p)).join('\n'); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts index 58720b601fd37..13cabdafceb67 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts @@ -26,13 +26,13 @@ import { getSimpleRuleOutput, getThresholdRuleForAlertTesting, getWebHookAction, - getRulesAsNdjson, removeServerGeneratedProperties, ruleToNdjson, } from '../../../utils'; import { deleteAllRules } from '../../../../../../common/utils/security_solution'; import { deleteAllExceptions } from '../../../../lists_and_exception_lists/utils'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { combineToNdJson } from './combine_to_ndjson'; const getImportRuleBuffer = (connectorId: string) => { const rule1 = { @@ -483,29 +483,6 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should NOT report a conflict if there is an attempt to import two rules with the same rule_id and overwrite is set to true', async () => { - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') - .expect(200); - - expect(body).to.eql({ - errors: [], - success: true, - success_count: 1, - rules_count: 2, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], - }); - }); - it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists', async () => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) @@ -544,71 +521,6 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should NOT report a conflict if there is an attempt to import a rule with a rule_id that already exists and overwrite is set to true', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); - - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); - - expect(body).to.eql({ - errors: [], - success: true, - success_count: 1, - rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], - }); - }); - - it('should overwrite an existing rule if overwrite is set to true', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); - - const simpleRule = getSimpleRule('rule-1'); - simpleRule.name = 'some other name'; - const ndjson = ruleToNdjson(simpleRule); - - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', ndjson, 'rules.ndjson') - .expect(200); - - const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - - const bodyToCompare = removeServerGeneratedProperties(body); - const ruleOutput = { - ...getSimpleRuleOutput('rule-1'), - output_index: '', - }; - ruleOutput.name = 'some other name'; - ruleOutput.revision = 0; - expect(bodyToCompare).to.eql(ruleOutput); - }); - it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) @@ -1699,9 +1611,12 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .attach( 'file', - getRulesAsNdjson([ - { ...getSimpleRule(), investigation_fields: { field_names: ['foo'] } }, - ]), + Buffer.from( + combineToNdJson({ + ...getSimpleRule(), + investigation_fields: { field_names: ['foo'] }, + }) + ), 'rules.ndjson' ) .expect('Content-Type', 'application/json; charset=utf-8') diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts new file mode 100644 index 0000000000000..8acc444a09082 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; + +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { createRule, deleteAllRules, getCustomQueryRuleParams, fetchRule } from '../../../utils'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { combineToNdJson } from './combine_to_ndjson'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @brokenInServerless @skipInQA import_rules with rule overwrite set to "true"', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + }); + + it('DOES NOT report a conflict if there is an attempt to import two rules with the same rule_id', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ rule_id: 'rule-1', name: 'Rule 1' }), + getCustomQueryRuleParams({ rule_id: 'rule-1', name: 'Rule 2' }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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: 2, + }); + + const importedRule = await fetchRule(supertest, { ruleId: 'rule-1' }); + + expect(importedRule.name).toBe('Rule 2'); + }); + + it('DOES NOT report a conflict if there is an attempt to import a rule twice', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'imported-rule', + name: 'Imported rule', + }) + ); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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, + }); + }); + + it('overwrites an existing rule', async () => { + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + name: 'Existing rule', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + name: 'Imported rule', + }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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, + }); + + const importedRule = await fetchRule(supertest, { ruleId: 'existing-rule' }); + + expect(importedRule.name).toBe('Imported rule'); + }); + + /** + * Existing rule may have nullable fields set to a value (e.g. `timestamp_override` is set to `some.value`) but + * a rule to import doesn't have these fields set (e.g. `timestamp_override` is presented at al in ndjson). + * We expect the updated rule won't have such fields preserved (e.g. `timestamp_override` will be removed). + */ + it('ensures overwritten rule DOES NOT preserve missed in the imported rule fields', async () => { + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + rule_name_override: 'some name', + timestamp_override: 'some.value', + timeline_id: 'some id', + timeline_title: 'some title', + outcome: 'exactMatch', + alias_target_id: 'some id', + license: 'some license', + note: 'some notes', + building_block_type: 'some type', + output_index: 'some-index', + namespace: 'some-namespace', + meta: { + some: 'field', + }, + investigation_fields: { field_names: ['a', 'b', 'c'] }, + throttle: 'no_actions', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + namespace: 'abc', + }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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, + }); + + const importedRule = await fetchRule(supertest, { ruleId: 'existing-rule' }); + + expect(importedRule).toMatchObject({ + rule_id: 'existing-rule', + output_index: '', + }); + expect(importedRule).toEqual( + expect.not.objectContaining({ + rule_name_override: expect.anything(), + timestamp_override: expect.anything(), + timeline_id: expect.anything(), + timeline_title: expect.anything(), + outcome: expect.anything(), + alias_target_id: expect.anything(), + license: expect.anything(), + note: expect.anything(), + building_block_type: expect.anything(), + namespace: expect.anything(), + meta: expect.anything(), + investigation_fields: expect.anything(), + throttle: expect.anything(), + }) + ); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rules_as_ndjson.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rules_as_ndjson.ts deleted file mode 100644 index 24d448af21f1b..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rules_as_ndjson.ts +++ /dev/null @@ -1,18 +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. - */ - -/** - * Given an array of objects (assuming rules) this will return a ndjson buffer which is useful - * for testing uploads. - * @param rules Array of rules - */ -export const getRulesAsNdjson = (rules: unknown[]): Buffer => { - const stringOfRules = rules.map((rule) => { - return JSON.stringify(rule); - }); - return Buffer.from(stringOfRules.join('\n')); -}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts index 7888dd6e9ef33..e69d515ce2e5d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts @@ -48,7 +48,6 @@ export * from './remove_server_generated_properties'; export * from './remove_server_generated_properties_including_rule_id'; export * from './rule_to_update_schema'; export * from './update_rule'; -export * from './get_rules_as_ndjson'; export * from './get_simple_rule_as_ndjson'; export * from './prebuilt_rules'; From a1de4885aa66a63d1b141eddbca8a37647fccf5d Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 13 Feb 2024 18:18:15 +0100 Subject: [PATCH 04/10] update FTR tests --- .../export_rules.ts | 65 +-- .../import_rules.ts | 459 +++++++++--------- 2 files changed, 260 insertions(+), 264 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts index 67d48610d4e8f..f355e9ed61fc4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts @@ -5,17 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; -import { - binaryToString, - getSimpleRule, - getSimpleRuleOutput, - removeServerGeneratedProperties, - updateUsername, -} from '../../../utils'; +import { binaryToString, getCustomQueryRuleParams } from '../../../utils'; import { createRule, createAlertsIndex, @@ -26,8 +20,6 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); const es = getService('es'); - const config = getService('config'); - const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username'); describe('@ess @serverless export_rules', () => { describe('exporting rules', () => { @@ -41,7 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should set the response content types to be expected', async () => { - await createRule(supertest, log, getSimpleRule()); + await createRule(supertest, log, getCustomQueryRuleParams()); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -54,7 +46,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should export a single rule with a rule_id', async () => { - await createRule(supertest, log, getSimpleRule()); + const ruleToExport = getCustomQueryRuleParams(); + + await createRule(supertest, log, ruleToExport); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -64,15 +58,13 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[0]); - const bodyToTest = removeServerGeneratedProperties(bodySplitAndParsed); - const expectedRule = updateUsername(getSimpleRuleOutput(), ELASTICSEARCH_USERNAME); + const exportedRule = JSON.parse(body.toString().split(/\n/)[0]); - expect(bodyToTest).to.eql(expectedRule); + expect(exportedRule).toMatchObject(ruleToExport); }); - it('should export a exported count with a single rule_id', async () => { - await createRule(supertest, log, getSimpleRule()); + it('should have export summary reflecting a number of rules', async () => { + await createRule(supertest, log, getCustomQueryRuleParams()); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -82,30 +74,22 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]); + const exportSummary = JSON.parse(body.toString().split(/\n/)[1]); - expect(bodySplitAndParsed).to.eql({ + expect(exportSummary).toMatchObject({ 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: [], }); }); it('should export exactly two rules given two rules', async () => { - await createRule(supertest, log, getSimpleRule('rule-1')); - await createRule(supertest, log, getSimpleRule('rule-2')); + const ruleToExport1 = getCustomQueryRuleParams({ rule_id: 'rule-1' }); + const ruleToExport2 = getCustomQueryRuleParams({ rule_id: 'rule-2' }); + + await createRule(supertest, log, ruleToExport1); + await createRule(supertest, log, ruleToExport2); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -115,14 +99,15 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); - const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); - const firstRule = removeServerGeneratedProperties(firstRuleParsed); - const secondRule = removeServerGeneratedProperties(secondRuleParsed); - const expectedRule = updateUsername(getSimpleRuleOutput('rule-2'), ELASTICSEARCH_USERNAME); - const expectedRule2 = updateUsername(getSimpleRuleOutput('rule-1'), ELASTICSEARCH_USERNAME); + const exportedRule1 = JSON.parse(body.toString().split(/\n/)[0]); + const exportedRule2 = JSON.parse(body.toString().split(/\n/)[1]); - expect([firstRule, secondRule]).to.eql([expectedRule, expectedRule2]); + expect([exportedRule1, exportedRule2]).toEqual( + expect.arrayContaining([ + expect.objectContaining(ruleToExport1), + expect.objectContaining(ruleToExport2), + ]) + ); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts index 5bb8fd306bd04..c0431f5690256 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts @@ -5,22 +5,16 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; -import { - getSimpleRule, - getSimpleRuleAsNdjson, - getSimpleRuleOutput, - removeServerGeneratedProperties, - ruleToNdjson, - updateUsername, -} from '../../../utils'; +import { getCustomQueryRuleParams, combineToNdJson, fetchRule } from '../../../utils'; import { createAlertsIndex, deleteAllRules, deleteAllAlerts, + createRule, } from '../../../../../../common/utils/security_solution'; export default ({ getService }: FtrProviderContext): void => { @@ -28,7 +22,6 @@ export default ({ getService }: FtrProviderContext): void => { const log = getService('log'); const es = getService('es'); const config = getService('config'); - const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username'); describe('@ess @serverless import_rules', () => { describe('importing rules with an index', () => { @@ -42,11 +35,13 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should set the response content types to be expected', async () => { + const ndjson = combineToNdJson(getCustomQueryRuleParams()); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); }); @@ -56,123 +51,113 @@ export default ({ getService }: FtrProviderContext): void => { .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.txt') + .attach('file', Buffer.from(''), 'rules.txt') .expect(400); - expect(body).to.eql({ + expect(body).toEqual({ status_code: 400, message: 'Invalid file extension .txt', }); }); it('should report that it imported a simple rule successfully', async () => { + const ndjson = combineToNdJson(getCustomQueryRuleParams()); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 1, rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should be able to read an imported rule back out correctly', async () => { + const ruleToImport = getCustomQueryRuleParams({ rule_id: 'rule-to-import' }); + const ndjson = combineToNdJson(ruleToImport); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); + const importedRule = await fetchRule(supertest, { ruleId: 'rule-to-import' }); - const bodyToCompare = removeServerGeneratedProperties(body); - const expectedRule = updateUsername( - { - ...getSimpleRuleOutput('rule-1', false), - output_index: '', - }, - ELASTICSEARCH_USERNAME - ); - - expect(bodyToCompare).to.eql(expectedRule); + expect(importedRule).toMatchObject(ruleToImport); }); it('should fail validation when importing a rule with malformed "from" params on the rules', async () => { - const stringifiedRule = JSON.stringify({ - from: 'now-3755555555555555.67s', - interval: '5m', - ...getSimpleRule('rule-1'), - }); - const fileNdJson = Buffer.from(stringifiedRule + '\n'); + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + from: 'now-3755555555555555.67s', + interval: '5m', + }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', fileNdJson, 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body.errors[0].error.message).to.eql('from: Failed to parse date-math expression'); + expect(body.errors[0].error.message).toBe('from: Failed to parse date-math expression'); }); it('should fail validation when importing two rules and one has a malformed "from" params', async () => { - const stringifiedRule = JSON.stringify({ - from: 'now-3755555555555555.67s', - interval: '5m', - ...getSimpleRule('rule-1'), - }); - const stringifiedRule2 = JSON.stringify({ - ...getSimpleRule('rule-2'), - }); - const fileNdJson = Buffer.from([stringifiedRule, stringifiedRule2].join('\n')); + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'malformed-rule', + from: 'now-3755555555555555.67s', + interval: '5m', + }), + getCustomQueryRuleParams({ + rule_id: 'good-rule', + }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', fileNdJson, 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); // should result in one success and a failure message - expect(body.success_count).to.eql(1); - expect(body.errors[0].error.message).to.eql('from: Failed to parse date-math expression'); + expect(body.success_count).toBe(1); + expect(body.errors[0].error.message).toBe('from: Failed to parse date-math expression'); }); it('should be able to import two rules', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + }), + getCustomQueryRuleParams({ + rule_id: 'rule-2', + }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 2, rules_count: 2, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); @@ -181,26 +166,25 @@ export default ({ getService }: FtrProviderContext): void => { // test to complete so at 10 rules completing in about 10 seconds // I figured this is enough to make sure the import route is doing its job. it('should be able to import 10 rules', async () => { - const ruleIds = new Array(10).fill(undefined).map((_, index) => `rule-${index}`); + const ndjson = combineToNdJson( + ...new Array(10).fill(0).map((_, i) => + getCustomQueryRuleParams({ + rule_id: `rule-${i}`, + }) + ) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 10, rules_count: 10, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); @@ -222,29 +206,45 @@ export default ({ getService }: FtrProviderContext): void => { // }); it('should NOT be able to import more than 10,000 rules', async () => { - const ruleIds = new Array(10001).fill(undefined).map((_, index) => `rule-${index}`); + const ndjson = combineToNdJson( + ...new Array(10001).fill(0).map((_, i) => + getCustomQueryRuleParams({ + rule_id: `rule-${i}`, + }) + ) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(500); - expect(body).to.eql({ + expect(body).toEqual({ status_code: 500, message: "Can't import more than 10000 rules", }); }); it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + }), + getCustomQueryRuleParams({ + rule_id: 'rule-1', + }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { @@ -257,55 +257,51 @@ export default ({ getService }: FtrProviderContext): void => { success: false, success_count: 1, rules_count: 2, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should NOT report a conflict if there is an attempt to import two rules with the same rule_id and overwrite is set to true', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + }), + getCustomQueryRuleParams({ + rule_id: 'rule-1', + }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 1, rules_count: 2, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); + const ruleToImport = getCustomQueryRuleParams({ + rule_id: 'rule-1', + }); + + await createRule(supertest, log, ruleToImport); + + const ndjson = combineToNdJson(ruleToImport); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { @@ -318,215 +314,230 @@ export default ({ getService }: FtrProviderContext): void => { success: false, rules_count: 1, success_count: 0, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should NOT report a conflict if there is an attempt to import a rule with a rule_id that already exists and overwrite is set to true', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); + const ruleToImport = getCustomQueryRuleParams({ + rule_id: 'rule-1', + }); + + await createRule(supertest, log, ruleToImport); + + const ndjson = combineToNdJson(ruleToImport); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 1, rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should overwrite an existing rule if overwrite is set to true', async () => { + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'rule-to-overwrite', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-to-overwrite', + name: 'some other name', + }) + ); + await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const simpleRule = getSimpleRule('rule-1'); - simpleRule.name = 'some other name'; - const ndjson = ruleToNdjson(simpleRule); + const importedRule = await fetchRule(supertest, { ruleId: 'rule-to-overwrite' }); + + expect(importedRule).toMatchObject({ + name: 'some other name', + }); + }); + + it('should bump a revision when overwriting a rule', async () => { + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'rule-to-overwrite', + }) + ); + + const ruleBeforeOverwriting = await fetchRule(supertest, { ruleId: 'rule-to-overwrite' }); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-to-overwrite', + name: 'some other name', + }) + ); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ndjson, 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - - const bodyToCompare = removeServerGeneratedProperties(body); - const ruleOutput = { - ...getSimpleRuleOutput('rule-1'), - output_index: '', - }; - ruleOutput.name = 'some other name'; - ruleOutput.revision = 0; - const expectedRule = updateUsername(ruleOutput, ELASTICSEARCH_USERNAME); + const ruleAfterOverwriting = await fetchRule(supertest, { ruleId: 'rule-to-overwrite' }); - expect(bodyToCompare).to.eql(expectedRule); + expect(ruleBeforeOverwriting).toMatchObject({ + revision: 0, + }); + expect(ruleAfterOverwriting).toMatchObject({ + revision: 1, + }); }); it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + }), + getCustomQueryRuleParams({ + rule_id: 'non-existing-rule-1', + }), + getCustomQueryRuleParams({ + rule_id: 'non-existing-rule-2', + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { - message: 'rule_id: "rule-1" already exists', + message: 'rule_id: "existing-rule" already exists', status_code: 409, }, - rule_id: 'rule-1', + rule_id: 'existing-rule', }, ], success: false, success_count: 2, rules_count: 3, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should report a mix of conflicts and a mix of successes', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') - .expect(200); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule-1', + }) + ); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule-2', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule-1', + }), + getCustomQueryRuleParams({ + rule_id: 'existing-rule-2', + }), + getCustomQueryRuleParams({ + rule_id: 'non-existing-rule', + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { - message: 'rule_id: "rule-1" already exists', + message: 'rule_id: "existing-rule-1" already exists', status_code: 409, }, - rule_id: 'rule-1', + rule_id: 'existing-rule-1', }, { error: { - message: 'rule_id: "rule-2" already exists', + message: 'rule_id: "existing-rule-2" already exists', status_code: 409, }, - rule_id: 'rule-2', + rule_id: 'existing-rule-2', }, ], success: false, success_count: 1, rules_count: 3, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should be able to correctly read back a mixed import of different rules even if some cause conflicts', async () => { - const getRuleOutput = (name: string) => ({ - ...getSimpleRuleOutput(name), - output_index: '', + const existingRule1 = getCustomQueryRuleParams({ + rule_id: 'existing-rule-1', }); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') - .expect(200); + const existingRule2 = getCustomQueryRuleParams({ + rule_id: 'existing-rule-2', + }); + const ruleToImportSuccessfully = getCustomQueryRuleParams({ + rule_id: 'non-existing-rule', + }); + + await createRule(supertest, log, existingRule1); + await createRule(supertest, log, existingRule2); + + const ndjson = combineToNdJson(existingRule1, existingRule2, ruleToImportSuccessfully); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') - .expect(200); - - const { body: bodyOfRule1 } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - - const { body: bodyOfRule2 } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-2`) - .set('elastic-api-version', '2023-10-31') - .send() + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body: bodyOfRule3 } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-3`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); + const rule1 = await fetchRule(supertest, { ruleId: 'existing-rule-1' }); + const rule2 = await fetchRule(supertest, { ruleId: 'existing-rule-2' }); + const rule3 = await fetchRule(supertest, { ruleId: 'non-existing-rule' }); - const bodyToCompareOfRule1 = removeServerGeneratedProperties(bodyOfRule1); - const bodyToCompareOfRule2 = removeServerGeneratedProperties(bodyOfRule2); - const bodyToCompareOfRule3 = removeServerGeneratedProperties(bodyOfRule3); - const expectedRule = updateUsername(getRuleOutput('rule-1'), ELASTICSEARCH_USERNAME); - const expectedRule2 = updateUsername(getRuleOutput('rule-2'), ELASTICSEARCH_USERNAME); - const expectedRule3 = updateUsername(getRuleOutput('rule-3'), ELASTICSEARCH_USERNAME); - - expect([bodyToCompareOfRule1, bodyToCompareOfRule2, bodyToCompareOfRule3]).to.eql([ - expectedRule, - expectedRule2, - expectedRule3, - ]); + expect(rule1).toMatchObject(existingRule1); + expect(rule2).toMatchObject(existingRule2); + expect(rule3).toMatchObject(ruleToImportSuccessfully); }); }); }); From 589671b124bcecf0f4bfce161e4def6e4863aea8 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 13 Feb 2024 22:59:54 +0100 Subject: [PATCH 05/10] update functional tests --- .../export_rules.ts | 4 +- .../import_rules.ts | 1 - .../combine_to_ndjson.ts | 10 - .../export_rules.ts | 274 ++-- .../export_rules_ess.ts | 196 ++- .../import_connectors.ts | 3 +- .../import_export_rules.ts | 120 +- .../import_rules.ts | 1190 ++++++++--------- .../import_rules_ess.ts | 315 ++--- .../import_rules_with_overwrite.ts | 4 +- .../utils/connectors/create_connector.ts | 8 +- .../get_web_hook_connector_params.ts | 23 + 12 files changed, 977 insertions(+), 1171 deletions(-) delete mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_web_hook_connector_params.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts index f355e9ed61fc4..dc992cde0dc1c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts @@ -104,8 +104,8 @@ export default ({ getService }: FtrProviderContext): void => { expect([exportedRule1, exportedRule2]).toEqual( expect.arrayContaining([ - expect.objectContaining(ruleToExport1), - expect.objectContaining(ruleToExport2), + expect.objectContaining(expect.objectContaining(ruleToExport1)), + expect.objectContaining(expect.objectContaining(ruleToExport2)), ]) ); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts index c0431f5690256..d1b2fb041f4bc 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts @@ -21,7 +21,6 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); const es = getService('es'); - const config = getService('config'); describe('@ess @serverless import_rules', () => { describe('importing rules with an index', () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.ts deleted file mode 100644 index fc2baff9c365f..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/combine_to_ndjson.ts +++ /dev/null @@ -1,10 +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. - */ - -export function combineToNdJson(...parts: unknown[]): string { - return parts.map((p) => JSON.stringify(p)).join('\n'); -} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts index 08ef7c6d7f28a..d30f2137f2151 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts @@ -10,14 +10,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID } from '../../../../../config/shared'; -import { - binaryToString, - getSimpleRule, - getSimpleRuleOutput, - getWebHookAction, - removeServerGeneratedProperties, - updateUsername, -} from '../../../utils'; +import { binaryToString, getCustomQueryRuleParams } from '../../../utils'; import { createRule, createAlertsIndex, @@ -26,6 +19,8 @@ import { waitForRulePartialFailure, } from '../../../../../../common/utils/security_solution'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { getWebHookConnectorParams } from '../../../utils/connectors/get_web_hook_connector_params'; +import { createConnector } from '../../../utils/connectors'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); @@ -33,7 +28,6 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); // TODO: add a new service for pulling kibana username, similar to getService('es') const config = getService('config'); - const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username'); describe('@ess @brokenInServerless @skipInQA export_rules', () => { describe('exporting rules', () => { @@ -47,7 +41,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should set the response content types to be expected', async () => { - await createRule(supertest, log, getSimpleRule()); + await createRule(supertest, log, getCustomQueryRuleParams()); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -59,10 +53,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect('Content-Disposition', 'attachment; filename="export.ndjson"'); }); - it('should validate exported rule schema when its exported by its rule_id', async () => { + it('should validate exported rule schema when it is exported by its rule_id', async () => { const ruleId = 'rule-1'; - await createRule(supertest, log, getSimpleRule(ruleId, true)); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId, + enabled: true, + }) + ); await waitForRulePartialFailure({ supertest, @@ -89,8 +90,22 @@ export default ({ getService }: FtrProviderContext): void => { const ruleId1 = 'rule-1'; const ruleId2 = 'rule-2'; - await createRule(supertest, log, getSimpleRule(ruleId1, true)); - await createRule(supertest, log, getSimpleRule(ruleId2, true)); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId1, + enabled: true, + }) + ); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId2, + enabled: true, + }) + ); await waitForRulePartialFailure({ supertest, @@ -119,7 +134,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should export a exported count with a single rule_id', async () => { - await createRule(supertest, log, getSimpleRule()); + await createRule(supertest, log, getCustomQueryRuleParams()); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -151,8 +166,8 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should export exactly two rules given two rules', async () => { - await createRule(supertest, log, getSimpleRule('rule-1')); - await createRule(supertest, log, getSimpleRule('rule-2')); + await createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'rule-1' })); + await createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'rule-2' })); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -162,57 +177,42 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); - const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); - const firstRule = removeServerGeneratedProperties(firstRuleParsed); - const secondRule = removeServerGeneratedProperties(secondRuleParsed); - const expectedRule1 = updateUsername( - getSimpleRuleOutput(firstRule.rule_id), - ELASTICSEARCH_USERNAME - ); - const expectedRule2 = updateUsername( - getSimpleRuleOutput(secondRule.rule_id), - ELASTICSEARCH_USERNAME - ); + const exportedRule1 = JSON.parse(body.toString().split(/\n/)[0]); + const exportedRule2 = JSON.parse(body.toString().split(/\n/)[1]); - expect(firstRule).toEqual(expectedRule1); - expect(secondRule).toEqual(expectedRule2); + expect([exportedRule1, exportedRule2]).toEqual( + expect.arrayContaining([ + expect.objectContaining(getCustomQueryRuleParams({ rule_id: 'rule-1' })), + expect.objectContaining(getCustomQueryRuleParams({ rule_id: 'rule-2' })), + ]) + ); }); it('should export multiple actions attached to 1 rule', async () => { - // 1st action - const { body: hookAction1 } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - - // 2nd action - const { body: hookAction2 } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); + const webHookConnectorParams = getWebHookConnectorParams(); + const webHookConnectorId1 = await createConnector(supertest, webHookConnectorParams); + const webHookConnectorId2 = await createConnector(supertest, webHookConnectorParams); const action1 = { group: 'default', - id: hookAction1.id, - action_type_id: hookAction1.actionTypeId, + id: webHookConnectorId1, + action_type_id: webHookConnectorParams.connector_type_id, params: {}, }; const action2 = { group: 'default', - id: hookAction2.id, - action_type_id: hookAction2.actionTypeId, + id: webHookConnectorId2, + action_type_id: webHookConnectorParams.connector_type_id, params: {}, }; - const rule1: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [action1, action2], - }; - - await createRule(supertest, log, rule1); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + actions: [action1, action2], + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -222,55 +222,45 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); - const firstRule = removeServerGeneratedProperties(firstRuleParsed); - const expectedRule = updateUsername(getSimpleRuleOutput('rule-1'), ELASTICSEARCH_USERNAME); + const exportedRule = JSON.parse(body.toString().split(/\n/)[0]); - const outputRule1: ReturnType = { - ...expectedRule, + expect(exportedRule).toMatchObject({ actions: [ { ...action1, - uuid: firstRule.actions[0].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, { ...action2, - uuid: firstRule.actions[1].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], - }; - expect(firstRule).toEqual(outputRule1); + }); }); it('should export actions attached to 2 rules', async () => { - // create a new action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); + const webHookConnectorParams = getWebHookConnectorParams(); + const webHookConnectorId = await createConnector(supertest, webHookConnectorParams); const action = { group: 'default', - id: hookAction.id, - action_type_id: hookAction.actionTypeId, + id: webHookConnectorId, + action_type_id: webHookConnectorParams.connector_type_id, params: {}, }; - const rule1: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [action], - }; - - const rule2: ReturnType = { - ...getSimpleRule('rule-2'), - actions: [action], - }; - - await createRule(supertest, log, rule1); - await createRule(supertest, log, rule2); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ rule_id: 'rule-1', actions: [action] }) + ); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ rule_id: 'rule-2', actions: [action] }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -280,59 +270,47 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); - const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); - const firstRule = removeServerGeneratedProperties(firstRuleParsed); - const secondRule = removeServerGeneratedProperties(secondRuleParsed); - const expectedRule2 = updateUsername(getSimpleRuleOutput('rule-2'), ELASTICSEARCH_USERNAME); + const exportedRule1 = JSON.parse(body.toString().split(/\n/)[0]); + const exportedRule2 = JSON.parse(body.toString().split(/\n/)[1]); - const outputRule1: ReturnType = { - ...expectedRule2, + expect(exportedRule1).toMatchObject({ actions: [ { ...action, - uuid: firstRule.actions[0].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], - }; - const expectedRule1 = updateUsername(getSimpleRuleOutput('rule-1'), ELASTICSEARCH_USERNAME); - - const outputRule2: ReturnType = { - ...expectedRule1, + }); + expect(exportedRule2).toMatchObject({ actions: [ { ...action, - uuid: secondRule.actions[0].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], - }; - expect(firstRule).toEqual(outputRule1); - expect(secondRule).toEqual(outputRule2); + }); }); - it('should export actions connectors with the rule', async () => { - // create a new action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); + it('should export action connectors with the rule', async () => { + const webHookConnectorParams = getWebHookConnectorParams(); + const webHookConnectorId = await createConnector(supertest, webHookConnectorParams); const action = { group: 'default', - id: hookAction.id, - action_type_id: hookAction.actionTypeId, + id: webHookConnectorId, + action_type_id: webHookConnectorParams.connector_type_id, params: {}, }; - const rule1: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [action], - }; - - await createRule(supertest, log, rule1); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + actions: [action], + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -342,36 +320,28 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const connectorsObjectParsed = JSON.parse(body.toString().split(/\n/)[1]); - const exportDetailsParsed = JSON.parse(body.toString().split(/\n/)[2]); - - expect(connectorsObjectParsed).toEqual( - expect.objectContaining({ - attributes: { - actionTypeId: '.webhook', - config: { - hasAuth: true, - headers: null, - method: 'post', - url: 'http://localhost', - }, - isMissingSecrets: true, - name: 'Some connector', - secrets: {}, + const exportedConnectors = JSON.parse(body.toString().split(/\n/)[1]); + const exportedSummary = JSON.parse(body.toString().split(/\n/)[2]); + + expect(exportedConnectors).toMatchObject({ + attributes: { + actionTypeId: '.webhook', + config: { + hasAuth: true, + headers: null, + method: 'post', + url: 'http://localhost', }, - references: [], - type: 'action', - }) - ); - expect(exportDetailsParsed).toEqual({ - exported_exception_list_count: 0, - exported_exception_list_item_count: 0, + isMissingSecrets: true, + name: 'Webhook connector', + secrets: {}, + }, + references: [], + type: 'action', + }); + expect(exportedSummary).toMatchObject({ 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, @@ -381,7 +351,8 @@ export default ({ getService }: FtrProviderContext): void => { missing_action_connections: [], }); }); - it('should export rule without the action connector if it is Preconfigured Connector', async () => { + + it('should NOT export preconfigured actions connectors', async () => { const action = { group: 'default', id: PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID, @@ -389,12 +360,7 @@ export default ({ getService }: FtrProviderContext): void => { params: {}, }; - const rule1: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [action], - }; - - await createRule(supertest, log, rule1); + await createRule(supertest, log, getCustomQueryRuleParams({ actions: [action] })); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -404,17 +370,11 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const exportDetailsParsed = JSON.parse(body.toString().split(/\n/)[1]); + const exportedSummary = JSON.parse(body.toString().split(/\n/)[1]); - expect(exportDetailsParsed).toEqual({ - exported_exception_list_count: 0, - exported_exception_list_item_count: 0, + expect(exportedSummary).toMatchObject({ 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, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules_ess.ts index fd2d2374c1d56..b977137096891 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules_ess.ts @@ -16,15 +16,11 @@ import { } from '@kbn/security-solution-plugin/common/constants'; import { binaryToString, - getSimpleRule, - getSimpleRuleOutput, - getWebHookAction, - removeServerGeneratedProperties, getRuleSavedObjectWithLegacyInvestigationFields, getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray, - updateUsername, createRuleThroughAlertingEndpoint, checkInvestigationFieldSoValue, + getCustomQueryRuleParams, } from '../../../utils'; import { createRule, @@ -33,14 +29,13 @@ import { deleteAllAlerts, } from '../../../../../../common/utils/security_solution'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { getWebHookConnectorParams } from '../../../utils/connectors/get_web_hook_connector_params'; +import { createConnector } from '../../../utils/connectors'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); const es = getService('es'); - // TODO: add a new service for pulling kibana username, similar to getService('es') - const config = getService('config'); - const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username'); describe('@ess export_rules - ESS specific logic', () => { describe('exporting rules', () => { @@ -59,15 +54,11 @@ export default ({ getService }: FtrProviderContext): void => { */ describe('legacy_notification_system', () => { it('should be able to export 1 legacy action on 1 rule', async () => { - // create an action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); + const webHookConnectorParams = getWebHookConnectorParams(); + const webHookConnectorId = await createConnector(supertest, webHookConnectorParams); // create a rule without actions - const rule = await createRule(supertest, log, getSimpleRule('rule-1')); + const rule = await createRule(supertest, log, getCustomQueryRuleParams()); // attach the legacy notification await supertest @@ -79,13 +70,13 @@ export default ({ getService }: FtrProviderContext): void => { interval: '1h', actions: [ { - id: hookAction.id, + id: webHookConnectorId, group: 'default', params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - actionTypeId: hookAction.actionTypeId, + actionTypeId: webHookConnectorParams.connector_type_id, }, ], }) @@ -99,18 +90,15 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200) .parse(binaryToString); - const expectedRule1 = updateUsername( - getSimpleRuleOutput('rule-1'), - ELASTICSEARCH_USERNAME - ); - const outputRule1: ReturnType = { - ...expectedRule1, + const exportedRule = JSON.parse(body.toString().split(/\n/)[0]); + + expect(exportedRule).toMatchObject({ actions: [ { group: 'default', - id: hookAction.id, - action_type_id: hookAction.actionTypeId, + id: webHookConnectorId, + action_type_id: webHookConnectorParams.connector_type_id, params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', @@ -118,30 +106,16 @@ export default ({ getService }: FtrProviderContext): void => { frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - }; - const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); - const firstRule = removeServerGeneratedProperties(firstRuleParsed); - - expect(firstRule).toEqual(outputRule1); + }); }); it('should be able to export 2 legacy actions on 1 rule', async () => { - // create 1st action/connector - const { body: hookAction1 } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - - // create 2nd action/connector - const { body: hookAction2 } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); + const webHookConnectorParams = getWebHookConnectorParams(); + const webHookConnectorId1 = await createConnector(supertest, webHookConnectorParams); + const webHookConnectorId2 = await createConnector(supertest, webHookConnectorParams); // create a rule without actions - const rule = await createRule(supertest, log, getSimpleRule('rule-1')); + const rule = await createRule(supertest, log, getCustomQueryRuleParams()); // attach the legacy notification with actions await supertest @@ -153,22 +127,22 @@ export default ({ getService }: FtrProviderContext): void => { interval: '1h', actions: [ { - id: hookAction1.id, + id: webHookConnectorId1, group: 'default', params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - actionTypeId: hookAction1.actionTypeId, + actionTypeId: webHookConnectorParams.connector_type_id, }, { - id: hookAction2.id, + id: webHookConnectorId2, group: 'default', params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - actionTypeId: hookAction2.actionTypeId, + actionTypeId: webHookConnectorParams.connector_type_id, }, ], }) @@ -182,18 +156,15 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200) .parse(binaryToString); - const expectedRule1 = updateUsername( - getSimpleRuleOutput('rule-1'), - ELASTICSEARCH_USERNAME - ); - const outputRule1: ReturnType = { - ...expectedRule1, + const exportedRule = JSON.parse(body.toString().split(/\n/)[0]); + + expect(exportedRule).toMatchObject({ actions: [ { group: 'default', - id: hookAction1.id, - action_type_id: hookAction1.actionTypeId, + id: webHookConnectorId1, + action_type_id: webHookConnectorParams.connector_type_id, params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', @@ -202,8 +173,8 @@ export default ({ getService }: FtrProviderContext): void => { }, { group: 'default', - id: hookAction2.id, - action_type_id: hookAction2.actionTypeId, + id: webHookConnectorId2, + action_type_id: webHookConnectorParams.connector_type_id, params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', @@ -211,31 +182,25 @@ export default ({ getService }: FtrProviderContext): void => { frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - }; - const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); - const firstRule = removeServerGeneratedProperties(firstRuleParsed); - - expect(firstRule).toEqual(outputRule1); + }); }); it('should be able to export 2 legacy actions on 2 rules', async () => { - // create 1st action/connector - const { body: hookAction1 } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - - // create 2nd action/connector - const { body: hookAction2 } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); + const webHookConnectorParams = getWebHookConnectorParams(); + const webHookConnectorId1 = await createConnector(supertest, webHookConnectorParams); + const webHookConnectorId2 = await createConnector(supertest, webHookConnectorParams); // create 2 rules without actions - const rule1 = await createRule(supertest, log, getSimpleRule('rule-1')); - const rule2 = await createRule(supertest, log, getSimpleRule('rule-2')); + const rule1 = await createRule( + supertest, + log, + getCustomQueryRuleParams({ rule_id: 'rule-1' }) + ); + const rule2 = await createRule( + supertest, + log, + getCustomQueryRuleParams({ rule_id: 'rule-2' }) + ); // attach the legacy notification with actions to the first rule await supertest @@ -247,22 +212,22 @@ export default ({ getService }: FtrProviderContext): void => { interval: '1h', actions: [ { - id: hookAction1.id, + id: webHookConnectorId1, group: 'default', params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - actionTypeId: hookAction1.actionTypeId, + actionTypeId: webHookConnectorParams.connector_type_id, }, { - id: hookAction2.id, + id: webHookConnectorId2, group: 'default', params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - actionTypeId: hookAction2.actionTypeId, + actionTypeId: webHookConnectorParams.connector_type_id, }, ], }) @@ -278,22 +243,22 @@ export default ({ getService }: FtrProviderContext): void => { interval: '1h', actions: [ { - id: hookAction1.id, + id: webHookConnectorId1, group: 'default', params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - actionTypeId: hookAction1.actionTypeId, + actionTypeId: webHookConnectorParams.connector_type_id, }, { - id: hookAction2.id, + id: webHookConnectorId2, group: 'default', params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - actionTypeId: hookAction2.actionTypeId, + actionTypeId: webHookConnectorParams.connector_type_id, }, ], }) @@ -308,17 +273,15 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200) .parse(binaryToString); - const expectedRule1 = updateUsername( - getSimpleRuleOutput('rule-1'), - ELASTICSEARCH_USERNAME - ); - const outputRule1: ReturnType = { - ...expectedRule1, + const exportedRule1 = JSON.parse(body.toString().split(/\n/)[0]); + const exportedRule2 = JSON.parse(body.toString().split(/\n/)[1]); + + expect(exportedRule1).toMatchObject({ actions: [ { group: 'default', - id: hookAction1.id, - action_type_id: hookAction1.actionTypeId, + id: webHookConnectorId1, + action_type_id: webHookConnectorParams.connector_type_id, params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', @@ -327,8 +290,8 @@ export default ({ getService }: FtrProviderContext): void => { }, { group: 'default', - id: hookAction2.id, - action_type_id: hookAction2.actionTypeId, + id: webHookConnectorId2, + action_type_id: webHookConnectorParams.connector_type_id, params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', @@ -336,19 +299,13 @@ export default ({ getService }: FtrProviderContext): void => { frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - }; - - const expectedRule2 = updateUsername( - getSimpleRuleOutput('rule-2'), - ELASTICSEARCH_USERNAME - ); - const outputRule2: ReturnType = { - ...expectedRule2, + }); + expect(exportedRule2).toMatchObject({ actions: [ { group: 'default', - id: hookAction1.id, - action_type_id: hookAction1.actionTypeId, + id: webHookConnectorId1, + action_type_id: webHookConnectorParams.connector_type_id, params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', @@ -357,8 +314,8 @@ export default ({ getService }: FtrProviderContext): void => { }, { group: 'default', - id: hookAction2.id, - action_type_id: hookAction2.actionTypeId, + id: webHookConnectorId2, + action_type_id: webHookConnectorParams.connector_type_id, params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', @@ -366,14 +323,7 @@ export default ({ getService }: FtrProviderContext): void => { frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - }; - const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); - const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); - const firstRule = removeServerGeneratedProperties(firstRuleParsed); - const secondRule = removeServerGeneratedProperties(secondRuleParsed); - - expect(firstRule).toEqual(outputRule2); - expect(secondRule).toEqual(outputRule1); + }); }); }); }); @@ -392,11 +342,15 @@ export default ({ getService }: FtrProviderContext): void => { supertest, getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray() ); - await createRule(supertest, log, { - ...getSimpleRule('rule-with-investigation-field'), - name: 'Test investigation fields object', - investigation_fields: { field_names: ['host.name'] }, - }); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'rule-with-investigation-field', + name: 'Test investigation fields object', + investigation_fields: { field_names: ['host.name'] }, + }) + ); }); afterEach(async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts index d6c7b3f8ede86..ad3b11f73053f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts @@ -6,10 +6,9 @@ */ import expect from 'expect'; - import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; -import { combineToNdJson, getCustomQueryRuleParams } from '../../../utils'; import { deleteAllRules } from '../../../../../../common/utils/security_solution'; +import { combineToNdJson, getCustomQueryRuleParams } from '../../../utils'; import { createConnector, deleteConnector, getConnector } from '../../../utils/connectors'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_export_rules.ts index 3be3740dda80a..3ab680b38d835 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_export_rules.ts @@ -5,7 +5,7 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { @@ -18,7 +18,7 @@ import { } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; -import { binaryToString, getSimpleRule } from '../../../utils'; +import { binaryToString, getCustomQueryRuleParams } from '../../../utils'; import { createRule, createAlertsIndex, @@ -88,7 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(exceptionItem.comments).to.eql([ + expect(exceptionItem.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItem.comments[0].created_at}`, @@ -97,17 +97,20 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); - await createRule(supertest, log, { - ...getSimpleRule(), - exceptions_list: [ - { - id: exceptionBody.id, - list_id: exceptionBody.list_id, - type: exceptionBody.type, - namespace_type: exceptionBody.namespace_type, - }, - ], - }); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + exceptions_list: [ + { + id: exceptionBody.id, + list_id: exceptionBody.list_id, + type: exceptionBody.type, + namespace_type: exceptionBody.namespace_type, + }, + ], + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -132,7 +135,7 @@ export default ({ getService }: FtrProviderContext): void => { // NOTE: Existing comment is uploaded successfully // however, the meta now reflects who imported it, // not who created the initial comment - expect(exceptionItemFind2.comments).to.eql([ + expect(exceptionItemFind2.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItemFind2.comments[0].created_at}`, @@ -167,7 +170,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(exceptionItem.comments).to.eql([ + expect(exceptionItem.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItem.comments[0].created_at}`, @@ -176,17 +179,20 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); - await createRule(supertest, log, { - ...getSimpleRule(), - exceptions_list: [ - { - id: exceptionBody.id, - list_id: exceptionBody.list_id, - type: exceptionBody.type, - namespace_type: exceptionBody.namespace_type, - }, - ], - }); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + exceptions_list: [ + { + id: exceptionBody.id, + list_id: exceptionBody.list_id, + type: exceptionBody.type, + namespace_type: exceptionBody.namespace_type, + }, + ], + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -211,7 +217,7 @@ export default ({ getService }: FtrProviderContext): void => { // NOTE: Existing comment is uploaded successfully // however, the meta now reflects who imported it, // not who created the initial comment - expect(exceptionItemFind2.comments).to.eql([ + expect(exceptionItemFind2.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItemFind2.comments[0].created_at}`, @@ -255,7 +261,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(exceptionItem.comments).to.eql([ + expect(exceptionItem.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItem.comments[0].created_at}`, @@ -264,17 +270,20 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); - await createRule(supertest, log, { - ...getSimpleRule(), - exceptions_list: [ - { - id: exceptionBody.id, - list_id: exceptionBody.list_id, - type: exceptionBody.type, - namespace_type: exceptionBody.namespace_type, - }, - ], - }); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + exceptions_list: [ + { + id: exceptionBody.id, + list_id: exceptionBody.list_id, + type: exceptionBody.type, + namespace_type: exceptionBody.namespace_type, + }, + ], + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -299,7 +308,7 @@ export default ({ getService }: FtrProviderContext): void => { // NOTE: Existing comment is uploaded successfully // however, the meta now reflects who imported it, // not who created the initial comment - expect(exceptionItemFind2.comments).to.eql([ + expect(exceptionItemFind2.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItemFind2.comments[0].created_at}`, @@ -334,7 +343,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(exceptionItem.comments).to.eql([ + expect(exceptionItem.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItem.comments[0].created_at}`, @@ -343,17 +352,20 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); - await createRule(supertest, log, { - ...getSimpleRule(), - exceptions_list: [ - { - id: exceptionBody.id, - list_id: exceptionBody.list_id, - type: exceptionBody.type, - namespace_type: exceptionBody.namespace_type, - }, - ], - }); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + exceptions_list: [ + { + id: exceptionBody.id, + list_id: exceptionBody.list_id, + type: exceptionBody.type, + namespace_type: exceptionBody.namespace_type, + }, + ], + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -378,7 +390,7 @@ export default ({ getService }: FtrProviderContext): void => { // NOTE: Existing comment is uploaded successfully // however, the meta now reflects who imported it, // not who created the initial comment - expect(exceptionItemFind2.comments).to.eql([ + expect(exceptionItemFind2.comments).toEqual([ { comment: 'this exception item rocks', created_at: `${exceptionItemFind2.comments[0].created_at}`, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts index 13cabdafceb67..f47d90f5f4e83 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules.ts @@ -5,34 +5,28 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; -import { - QueryRuleCreateProps, - RuleCreateProps, -} from '@kbn/security-solution-plugin/common/api/detection_engine'; 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_URL } from '@kbn/security-solution-plugin/common/constants'; import { - toNdJsonString, getImportExceptionsListItemSchemaMock, getImportExceptionsListSchemaMock, getImportExceptionsListItemNewerVersionSchemaMock, } from '@kbn/lists-plugin/common/schemas/request/import_exceptions_schema.mock'; import { - getSimpleRule, - getSimpleRuleAsNdjson, - getSimpleRuleOutput, + combineToNdJson, + fetchRule, + getCustomQueryRuleParams, getThresholdRuleForAlertTesting, - getWebHookAction, - removeServerGeneratedProperties, - ruleToNdjson, } from '../../../utils'; +import { createRule } from '../../../../../../common/utils/security_solution'; import { deleteAllRules } from '../../../../../../common/utils/security_solution'; import { deleteAllExceptions } from '../../../../lists_and_exception_lists/utils'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; -import { combineToNdJson } from './combine_to_ndjson'; +import { getWebHookConnectorParams } from '../../../utils/connectors/get_web_hook_connector_params'; +import { createConnector } from '../../../utils/connectors'; const getImportRuleBuffer = (connectorId: string) => { const rule1 = { @@ -90,9 +84,9 @@ const getImportRuleBuffer = (connectorId: string) => { }, ], }; - const rule1String = JSON.stringify(rule1); - const buffer = Buffer.from(`${rule1String}\n`); - return buffer; + const ndjson = combineToNdJson(rule1); + + return Buffer.from(ndjson); }; const getImportRuleWithConnectorsBuffer = (connectorId: string) => { const rule1 = { @@ -167,22 +161,9 @@ const getImportRuleWithConnectorsBuffer = (connectorId: string) => { migrationVersion: { action: '8.3.0' }, coreMigrationVersion: '8.7.0', }; - const rule1String = JSON.stringify(rule1); - const connectorString = JSON.stringify(connector); - const buffer = Buffer.from(`${rule1String}\n${connectorString}`); - return buffer; -}; + const ndjson = combineToNdJson(rule1, connector); -export const getSimpleRuleAsNdjsonWithLegacyInvestigationField = ( - ruleIds: string[], - enabled = false, - overwrites: Partial -): Buffer => { - const stringOfRules = ruleIds.map((ruleId) => { - const simpleRule = { ...getSimpleRule(ruleId, enabled), ...overwrites }; - return JSON.stringify(simpleRule); - }); - return Buffer.from(stringOfRules.join('\n')); + return Buffer.from(ndjson); }; export default ({ getService }: FtrProviderContext): void => { @@ -198,14 +179,16 @@ export default ({ getService }: FtrProviderContext): void => { describe('threshold validation', () => { it('should result in partial success if no threshold-specific fields are provided', async () => { const { threshold, ...rule } = getThresholdRuleForAlertTesting(['*']); + const ndjson = combineToNdJson(rule); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ruleToNdjson(rule as RuleCreateProps), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body.errors[0]).to.eql({ + expect(body.errors[0]).toEqual({ rule_id: '(unknown id)', error: { status_code: 400, message: 'threshold: Required' }, }); @@ -220,14 +203,16 @@ export default ({ getService }: FtrProviderContext): void => { field: ['field-1', 'field-2', 'field-3', 'field-4'], }, }; + const ndjson = combineToNdJson(rule); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ruleToNdjson(rule), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body.errors[0]).to.eql({ + expect(body.errors[0]).toEqual({ rule_id: '(unknown id)', error: { message: 'Number of fields must be 3 or less', @@ -245,14 +230,16 @@ export default ({ getService }: FtrProviderContext): void => { value: 0, }, }; + const ndjson = combineToNdJson(rule); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ruleToNdjson(rule), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body.errors[0]).to.eql({ + expect(body.errors[0]).toEqual({ rule_id: '(unknown id)', error: { message: 'threshold.value: Number must be greater than or equal to 1', @@ -275,14 +262,16 @@ export default ({ getService }: FtrProviderContext): void => { ], }, }; + const ndjson = combineToNdJson(rule); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ruleToNdjson(rule), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body.errors[0]).to.eql({ + expect(body.errors[0]).toEqual({ rule_id: '(unknown id)', error: { message: 'Cardinality of a field that is being aggregated on is always 1', @@ -294,8 +283,8 @@ export default ({ getService }: FtrProviderContext): void => { describe('forward compatibility', () => { it('should remove any extra rule fields when importing', async () => { - const rule: QueryRuleCreateProps = { - ...getSimpleRule('rule-1'), + const rule = getCustomQueryRuleParams({ + rule_id: 'rule-1', extraField: true, risk_score_mapping: [ { @@ -336,37 +325,36 @@ export default ({ getService }: FtrProviderContext): void => { // @ts-expect-error extraField: true, }, - }; - const payload = Buffer.from(JSON.stringify(rule)); + }); + const ndjson = combineToNdJson(rule); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', payload, 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); + const importedRule = await fetchRule(supertest, { ruleId: 'rule-1' }); - expect(Object.hasOwn(body, 'extraField')).to.eql(false); - expect(Object.hasOwn(body.risk_score_mapping[0], 'extraField')).to.eql(false); - expect(Object.hasOwn(body.severity_mapping[0], 'extraField')).to.eql(false); - expect(Object.hasOwn(body.threat[0], 'extraField')).to.eql(false); - expect(Object.hasOwn(body.threat[0].tactic, 'extraField')).to.eql(false); - expect(Object.hasOwn(body.investigation_fields, 'extraField')).to.eql(false); + expect(Object.hasOwn(importedRule, 'extraField')).toBeFalsy(); + expect(Object.hasOwn(importedRule.risk_score_mapping[0], 'extraField')).toBeFalsy(); + expect(Object.hasOwn(importedRule.severity_mapping[0], 'extraField')).toBeFalsy(); + expect(Object.hasOwn(importedRule.threat[0], 'extraField')).toBeFalsy(); + expect(Object.hasOwn(importedRule.threat[0].tactic, 'extraField')).toBeFalsy(); + expect(Object.hasOwn(importedRule.investigation_fields!, 'extraField')).toBeFalsy(); }); }); describe('importing rules with an index', () => { it('should set the response content types to be expected', async () => { + const ndjson = combineToNdJson(getCustomQueryRuleParams()); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); }); @@ -376,91 +364,84 @@ export default ({ getService }: FtrProviderContext): void => { .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.txt') + .attach('file', Buffer.from(''), 'rules.txt') .expect(400); - expect(body).to.eql({ + expect(body).toEqual({ status_code: 400, message: 'Invalid file extension .txt', }); }); it('should report that it imported a simple rule successfully', async () => { + const ndjson = combineToNdJson(getCustomQueryRuleParams()); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 1, rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should be able to read an imported rule back out correctly', async () => { + const ruleToImport = getCustomQueryRuleParams({ rule_id: 'rule-1' }); + const ndjson = combineToNdJson(ruleToImport); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); + const importedRule = await fetchRule(supertest, { ruleId: 'rule-1' }); - const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql({ - ...getSimpleRuleOutput('rule-1', false), - output_index: '', - }); + expect(importedRule).toMatchObject(ruleToImport); }); it('should be able to import two rules', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ rule_id: 'rule-1' }), + getCustomQueryRuleParams({ rule_id: 'rule-2' }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 2, rules_count: 2, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ rule_id: 'rule-1' }), + getCustomQueryRuleParams({ rule_id: 'rule-1' }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { @@ -473,32 +454,24 @@ export default ({ getService }: FtrProviderContext): void => { success: false, success_count: 1, rules_count: 2, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); + const existingRule = getCustomQueryRuleParams({ rule_id: 'rule-1' }); + + await createRule(supertest, log, existingRule); + + const ndjson = combineToNdJson(existingRule); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { @@ -511,168 +484,165 @@ export default ({ getService }: FtrProviderContext): void => { success: false, success_count: 0, rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') - .expect(200); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + }), + getCustomQueryRuleParams({ + rule_id: 'non-existing-rule-1', + }), + getCustomQueryRuleParams({ + rule_id: 'non-existing-rule-2', + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { - message: 'rule_id: "rule-1" already exists', + message: 'rule_id: "existing-rule" already exists', status_code: 409, }, - rule_id: 'rule-1', + rule_id: 'existing-rule', }, ], success: false, success_count: 2, rules_count: 3, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should report a mix of conflicts and a mix of successes', async () => { - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') - .expect(200); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule-1', + }) + ); + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule-2', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule-1', + }), + getCustomQueryRuleParams({ + rule_id: 'existing-rule-2', + }), + getCustomQueryRuleParams({ + rule_id: 'non-existing-rule', + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { - message: 'rule_id: "rule-1" already exists', + message: 'rule_id: "existing-rule-1" already exists', status_code: 409, }, - rule_id: 'rule-1', + rule_id: 'existing-rule-1', }, { error: { - message: 'rule_id: "rule-2" already exists', + message: 'rule_id: "existing-rule-2" already exists', status_code: 409, }, - rule_id: 'rule-2', + rule_id: 'existing-rule-2', }, ], success: false, success_count: 1, rules_count: 3, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should be able to correctly read back a mixed import of different rules even if some cause conflicts', async () => { - const simpleRuleOutput = (ruleName: string) => ({ - ...getSimpleRuleOutput(ruleName), - output_index: '', + const existingRule1 = getCustomQueryRuleParams({ + rule_id: 'existing-rule-1', + }); + const existingRule2 = getCustomQueryRuleParams({ + rule_id: 'existing-rule-2', + }); + const ruleToImportSuccessfully = getCustomQueryRuleParams({ + rule_id: 'non-existing-rule', }); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') - .expect(200); + await createRule(supertest, log, existingRule1); + await createRule(supertest, log, existingRule2); + + const ndjson = combineToNdJson(existingRule1, existingRule2, ruleToImportSuccessfully); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body: bodyOfRule1 } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); + const rule1 = await fetchRule(supertest, { ruleId: 'existing-rule-1' }); + const rule2 = await fetchRule(supertest, { ruleId: 'existing-rule-2' }); + const rule3 = await fetchRule(supertest, { ruleId: 'non-existing-rule' }); - const { body: bodyOfRule2 } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-2`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - - const { body: bodyOfRule3 } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-3`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - - const bodyToCompareOfRule1 = removeServerGeneratedProperties(bodyOfRule1); - const bodyToCompareOfRule2 = removeServerGeneratedProperties(bodyOfRule2); - const bodyToCompareOfRule3 = removeServerGeneratedProperties(bodyOfRule3); - - expect([bodyToCompareOfRule1, bodyToCompareOfRule2, bodyToCompareOfRule3]).to.eql([ - simpleRuleOutput('rule-1'), - simpleRuleOutput('rule-2'), - simpleRuleOutput('rule-3'), - ]); + expect(rule1).toMatchObject(existingRule1); + expect(rule2).toMatchObject(existingRule2); + expect(rule3).toMatchObject(ruleToImportSuccessfully); }); it('should give single connector error back if we have a single connector error message', async () => { - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [ - { - group: 'default', - id: '123', - action_type_id: '456', - params: {}, - }, - ], - }; + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + actions: [ + { + group: 'default', + id: '123', + action_type_id: '456', + params: {}, + }, + ], + }) + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ruleToNdjson(simpleRule), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ success: false, success_count: 0, rules_count: 1, @@ -686,9 +656,6 @@ export default ({ getService }: FtrProviderContext): void => { }, }, ], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: false, action_connectors_success_count: 0, action_connectors_warnings: [], @@ -704,54 +671,50 @@ export default ({ getService }: FtrProviderContext): void => { ], }); }); + it('should give single connector warning back if we have a single connector missing secret', async () => { - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [ - { - group: 'default', - id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf9', - action_type_id: '.webhook', - params: {}, + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + actions: [ + { + group: 'default', + id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf9', + action_type_id: '.webhook', + params: {}, + }, + ], + }), + { + id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf9', + 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: true, + config: {}, + secrets: {}, }, - ], - }; - const ruleWithConnector = { - id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf9', - 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: true, - config: {}, - secrets: {}, - }, - references: [], - migrationVersion: { action: '8.3.0' }, - coreMigrationVersion: '8.7.0', - }; + references: [], + migrationVersion: { action: '8.3.0' }, + coreMigrationVersion: '8.7.0', + } + ); + const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - Buffer.from(toNdJsonString([simpleRule, ruleWithConnector])), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, errors: [], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: true, action_connectors_success_count: 1, action_connectors_warnings: [ @@ -767,134 +730,108 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should be able to import a rule with an action connector that exists', async () => { - // create a new action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [ - { - group: 'default', - id: hookAction.id, - action_type_id: hookAction.actionTypeId, - params: {}, - }, - ], - }; + const webHookConnectorParams = getWebHookConnectorParams(); + const connectorId = await createConnector(supertest, webHookConnectorParams); + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + actions: [ + { + group: 'default', + id: connectorId, + action_type_id: webHookConnectorParams.connector_type_id, + params: {}, + }, + ], + }) + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ruleToNdjson(simpleRule), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, errors: [], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should be able to import 2 rules with action connectors', async () => { - // create a new action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - - const rule1: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [ - { - group: 'default', - id: 'cabc78e0-9031-11ed-b076-53cc4d57abc6', - action_type_id: hookAction.actionTypeId, - params: {}, - }, - ], - }; - - const rule2: ReturnType = { - ...getSimpleRule('rule-2'), - actions: [ - { - group: 'default', - id: 'f4e74ab0-9e59-11ed-a3db-f9134a9ce951', - action_type_id: '.index', - params: {}, + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + actions: [ + { + group: 'default', + id: 'cabc78e0-9031-11ed-b076-53cc4d57abc6', + action_type_id: '.webhook', + params: {}, + }, + ], + }), + getCustomQueryRuleParams({ + rule_id: 'rule-2', + actions: [ + { + group: 'default', + id: 'f4e74ab0-9e59-11ed-a3db-f9134a9ce951', + action_type_id: '.index', + params: {}, + }, + ], + }), + { + id: 'cabc78e0-9031-11ed-b076-53cc4d57abc6', + 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: {}, }, - ], - }; - - const connector1 = { - id: 'cabc78e0-9031-11ed-b076-53cc4d57abc6', - 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', }, - references: [], - migrationVersion: { action: '8.3.0' }, - coreMigrationVersion: '8.7.0', - }; - const connector2 = { - id: 'f4e74ab0-9e59-11ed-a3db-f9134a9ce951', - type: 'action', - updated_at: '2023-01-25T14:35:52.852Z', - created_at: '2023-01-25T14:35:52.852Z', - version: 'WzUxNTksMV0=', - attributes: { - actionTypeId: '.index', - name: 'index', - isMissingSecrets: false, - config: {}, - secrets: {}, - }, - references: [], - migrationVersion: { action: '8.3.0' }, - coreMigrationVersion: '8.7.0', - }; - const rule1String = JSON.stringify(rule1); - const rule2String = JSON.stringify(rule2); - const connector12String = JSON.stringify(connector1); - const connector22String = JSON.stringify(connector2); - const buffer = Buffer.from( - `${rule1String}\n${rule2String}\n${connector12String}\n${connector22String}\n` + { + id: 'f4e74ab0-9e59-11ed-a3db-f9134a9ce951', + type: 'action', + updated_at: '2023-01-25T14:35:52.852Z', + created_at: '2023-01-25T14:35:52.852Z', + version: 'WzUxNTksMV0=', + attributes: { + actionTypeId: '.index', + name: 'index', + isMissingSecrets: false, + config: {}, + secrets: {}, + }, + references: [], + migrationVersion: { action: '8.3.0' }, + coreMigrationVersion: '8.7.0', + } ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', buffer, 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ success: true, success_count: 2, rules_count: 2, errors: [], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: true, action_connectors_success_count: 2, action_connectors_errors: [], @@ -903,69 +840,56 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should be able to import 1 rule with an action connector that exists and get 1 other error back for a second rule that does not have the connector', async () => { - // create a new action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - - const rule1: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [ - { - group: 'default', - id: 'cabc78e0-9031-11ed-b076-53cc4d57aayo', - action_type_id: hookAction.actionTypeId, - params: {}, - }, - ], - }; - - const rule2: ReturnType = { - ...getSimpleRule('rule-2'), - actions: [ - { - group: 'default', - id: 'cabc78e0-9031-11ed-b076-53cc4d57aa22', // <-- This does not exist - action_type_id: '.index', - params: {}, + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + actions: [ + { + group: 'default', + id: 'cabc78e0-9031-11ed-b076-53cc4d57aayo', + action_type_id: '.webhook', + params: {}, + }, + ], + }), + getCustomQueryRuleParams({ + rule_id: 'rule-2', + actions: [ + { + group: 'default', + id: 'cabc78e0-9031-11ed-b076-53cc4d57aa22', // <-- This does not exist + action_type_id: '.index', + params: {}, + }, + ], + }), + { + id: 'cabc78e0-9031-11ed-b076-53cc4d57aayo', + 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: {}, }, - ], - }; - - const connector = { - id: 'cabc78e0-9031-11ed-b076-53cc4d57aayo', - 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', - }; - - const rule1String = JSON.stringify(rule1); - const rule2String = JSON.stringify(rule2); - const connector2String = JSON.stringify(connector); - - const buffer = Buffer.from(`${rule1String}\n${rule2String}\n${connector2String}\n`); + references: [], + migrationVersion: { action: '8.3.0' }, + coreMigrationVersion: '8.7.0', + } + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', buffer, 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ success: false, success_count: 0, rules_count: 2, @@ -980,9 +904,6 @@ export default ({ getService }: FtrProviderContext): void => { }, }, ], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: false, action_connectors_success_count: 0, action_connectors_errors: [ @@ -1003,6 +924,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('migrate pre-8.0 action connector ids', () => { const defaultSpaceActionConnectorId = '61b17790-544e-11ec-a349-11361cc441c4'; const space714ActionConnectorId = '51b17790-544e-11ec-a349-11361cc441c4'; + beforeEach(async () => { await esArchiver.load( 'x-pack/test/functional/es_archives/security_solution/import_rule_connector' @@ -1027,9 +949,12 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body.success).to.eql(true); - expect(body.success_count).to.eql(1); - expect(body.errors.length).to.eql(0); + + expect(body).toMatchObject({ + success: true, + success_count: 1, + errors: [], + }); }); it('should import a non-default-space 7.16 rule with a connector made in the non-default space', async () => { @@ -1044,14 +969,11 @@ export default ({ getService }: FtrProviderContext): void => { .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, errors: [], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: true, action_connectors_success_count: 1, action_connectors_warnings: [], @@ -1070,20 +992,19 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body).to.eql({ + + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, errors: [], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: true, action_connectors_success_count: 1, action_connectors_warnings: [], action_connectors_errors: [], }); }); + it('importing a non-default-space 7.16 rule with a connector made in the non-default space into the default space should result in a 404 if the file does not contain connectors', async () => { // connectorId is from the 7.x connector here // x-pack/test/functional/es_archives/security_solution/import_rule_connector @@ -1095,11 +1016,18 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body.success).to.equal(false); - expect(body.errors[0].error.status_code).to.equal(404); - expect(body.errors[0].error.message).to.equal( - `1 connector is missing. Connector id missing is: ${space714ActionConnectorId}` - ); + + expect(body).toMatchObject({ + success: false, + errors: [ + expect.objectContaining({ + error: { + status_code: 404, + message: `1 connector is missing. Connector id missing is: ${space714ActionConnectorId}`, + }, + }), + ], + }); }); // When objects become share-capable we will either add / update this test it('importing a non-default-space 7.16 rule with a connector made in the non-default space into a different non-default space should result in a 404', async () => { @@ -1115,13 +1043,21 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body.success).to.equal(false); - expect(body.errors[0].error.status_code).to.equal(404); - expect(body.errors[0].error.message).to.equal( - `1 connector is missing. Connector id missing is: ${space714ActionConnectorId}` - ); + + expect(body).toMatchObject({ + success: false, + errors: [ + expect.objectContaining({ + error: { + status_code: 404, + message: `1 connector is missing. Connector id missing is: ${space714ActionConnectorId}`, + }, + }), + ], + }); }); }); + describe('should be imported into the default space', () => { it('should import a default-space 7.16 rule with a connector made in the default space into a non-default space successfully', async () => { await esArchiver.load( @@ -1141,14 +1077,12 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body).to.eql({ + + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, errors: [], - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: true, action_connectors_success_count: 1, action_connectors_warnings: [], @@ -1169,10 +1103,14 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body.success).to.equal(true); - expect(body.success_count).to.eql(1); - expect(body.errors.length).to.eql(0); + + expect(body).toMatchObject({ + success: true, + success_count: 1, + errors: [], + }); }); + it('importing a default-space 7.16 rule with a connector made in the default space into a non-default space should result in a 404', async () => { await esArchiver.load( 'x-pack/test/functional/es_archives/security_solution/import_rule_connector' @@ -1189,11 +1127,18 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .attach('file', buffer, 'rules.ndjson') .expect(200); - expect(body.success).to.equal(false); - expect(body.errors[0].error.status_code).to.equal(404); - expect(body.errors[0].error.message).to.equal( - `1 connector is missing. Connector id missing is: ${defaultSpaceActionConnectorId}` - ); + + expect(body).toMatchObject({ + success: false, + errors: [ + expect.objectContaining({ + error: { + status_code: 404, + message: `1 connector is missing. Connector id missing is: ${defaultSpaceActionConnectorId}`, + }, + }), + ], + }); }); }); }); @@ -1211,26 +1156,21 @@ export default ({ getService }: FtrProviderContext): void => { thereby enabling simulation of migration scenarios. */ it('should be able to import a rule and an old version exception list, then delete it successfully', async () => { - const simpleRule = getSimpleRule('rule-1'); + const ndjson = combineToNdJson( + getCustomQueryRuleParams(), + getImportExceptionsListSchemaMock('test_list_id'), + getImportExceptionsListItemSchemaMock('test_item_id', 'test_list_id') + ); // import old exception version const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - Buffer.from( - toNdJsonString([ - simpleRule, - getImportExceptionsListSchemaMock('test_list_id'), - getImportExceptionsListItemSchemaMock('test_item_id', 'test_list_id'), - ]) - ), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, @@ -1238,10 +1178,6 @@ export default ({ getService }: FtrProviderContext): void => { exceptions_errors: [], exceptions_success: true, exceptions_success_count: 1, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); // delete the exception list item by its item_id @@ -1253,25 +1189,20 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should be able to import a rule and an exception list', async () => { - const simpleRule = getSimpleRule('rule-1'); + const ndjson = combineToNdJson( + getCustomQueryRuleParams(), + getImportExceptionsListSchemaMock('test_list_id'), + getImportExceptionsListItemNewerVersionSchemaMock('test_item_id', 'test_list_id') + ); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - Buffer.from( - toNdJsonString([ - simpleRule, - getImportExceptionsListSchemaMock('test_list_id'), - getImportExceptionsListItemNewerVersionSchemaMock('test_item_id', 'test_list_id'), - ]) - ), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, @@ -1279,10 +1210,6 @@ export default ({ getService }: FtrProviderContext): void => { exceptions_errors: [], exceptions_success: true, exceptions_success_count: 1, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); @@ -1299,47 +1226,47 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + exceptions_list: [ + { + id: exceptionBody.id, + list_id: 'i_exist', + type: 'detection', + namespace_type: 'single', + }, + { + id: 'i_dont_exist', + list_id: '123', + type: 'detection', + namespace_type: 'single', + }, + ], + }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') + .expect(200); + + const rule = await fetchRule(supertest, { ruleId: 'rule-1' }); + + expect(rule).toMatchObject({ exceptions_list: [ { id: exceptionBody.id, list_id: 'i_exist', - type: 'detection', namespace_type: 'single', - }, - { - id: 'i_dont_exist', - list_id: '123', type: 'detection', - namespace_type: 'single', }, ], - }; - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .attach('file', ruleToNdjson(simpleRule), 'rules.ndjson') - .expect(200); - - const { body: ruleResponse } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - - const bodyToCompare = removeServerGeneratedProperties(ruleResponse); - expect(bodyToCompare.exceptions_list).to.eql([ - { - id: exceptionBody.id, - list_id: 'i_exist', - namespace_type: 'single', - type: 'detection', - }, - ]); + }); - expect(body).to.eql({ + expect(body).toMatchObject({ success: false, success_count: 1, rules_count: 1, @@ -1356,27 +1283,58 @@ export default ({ getService }: FtrProviderContext): void => { exceptions_errors: [], exceptions_success: true, exceptions_success_count: 0, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should resolve exception references when importing into a clean slate', async () => { // So importing a rule that references an exception list // Keep in mind, no exception lists or rules exist yet - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), - exceptions_list: [ - { - id: 'abc', - list_id: 'i_exist', - type: 'detection', - namespace_type: 'single', - }, - ], - }; + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + exceptions_list: [ + { + id: 'abc', + list_id: 'i_exist', + type: 'detection', + namespace_type: 'single', + }, + ], + }), + { + ...getImportExceptionsListSchemaMock('i_exist'), + id: 'abc', + type: 'detection', + namespace_type: 'single', + }, + { + description: 'some description', + entries: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + item_id: 'item_id_1', + list_id: 'i_exist', + name: 'Query with a rule id', + type: 'simple', + } + ); // Importing the "simpleRule", along with the exception list // it's referencing and the list's item @@ -1384,57 +1342,11 @@ export default ({ getService }: FtrProviderContext): void => { .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - Buffer.from( - toNdJsonString([ - simpleRule, - { - ...getImportExceptionsListSchemaMock('i_exist'), - id: 'abc', - type: 'detection', - namespace_type: 'single', - }, - { - description: 'some description', - entries: [ - { - entries: [ - { - field: 'nested.field', - operator: 'included', - type: 'match', - value: 'some value', - }, - ], - field: 'some.parentField', - type: 'nested', - }, - { - field: 'some.not.nested.field', - operator: 'included', - type: 'match', - value: 'some value', - }, - ], - item_id: 'item_id_1', - list_id: 'i_exist', - name: 'Query with a rule id', - type: 'simple', - }, - ]) - ), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body: ruleResponse } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - const bodyToCompare = removeServerGeneratedProperties(ruleResponse); - const referencedExceptionList = ruleResponse.exceptions_list[0]; + const importedRule = await fetchRule(supertest, { ruleId: 'rule-1' }); + const referencedExceptionList = importedRule.exceptions_list[0]; // create an exception list const { body: exceptionBody } = await supertest @@ -1444,7 +1356,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(bodyToCompare.exceptions_list).to.eql([ + expect(importedRule.exceptions_list).toEqual([ { id: exceptionBody.id, list_id: 'i_exist', @@ -1453,7 +1365,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); - expect(body).to.eql({ + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, @@ -1461,27 +1373,69 @@ export default ({ getService }: FtrProviderContext): void => { exceptions_errors: [], exceptions_success: true, exceptions_success_count: 1, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); it('should resolve exception references that include comments', async () => { // So importing a rule that references an exception list // Keep in mind, no exception lists or rules exist yet - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), - exceptions_list: [ - { - id: 'abc', - list_id: 'i_exist', - type: 'detection', - namespace_type: 'single', - }, - ], - }; + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + exceptions_list: [ + { + id: 'abc', + list_id: 'i_exist', + type: 'detection', + namespace_type: 'single', + }, + ], + }), + { + ...getImportExceptionsListSchemaMock('i_exist'), + id: 'abc', + type: 'detection', + namespace_type: 'single', + }, + { + comments: [ + { + comment: 'This is an exception to the rule', + created_at: '2022-02-04T02:27:40.938Z', + created_by: 'elastic', + id: '845fc456-91ff-4530-bcc1-5b7ebd2f75b5', + }, + { + comment: 'I decided to add a new comment', + }, + ], + description: 'some description', + entries: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + item_id: 'item_id_1', + list_id: 'i_exist', + name: 'Query with a rule id', + type: 'simple', + } + ); // Importing the "simpleRule", along with the exception list // it's referencing and the list's item @@ -1489,68 +1443,11 @@ export default ({ getService }: FtrProviderContext): void => { .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - Buffer.from( - toNdJsonString([ - simpleRule, - { - ...getImportExceptionsListSchemaMock('i_exist'), - id: 'abc', - type: 'detection', - namespace_type: 'single', - }, - { - comments: [ - { - comment: 'This is an exception to the rule', - created_at: '2022-02-04T02:27:40.938Z', - created_by: 'elastic', - id: '845fc456-91ff-4530-bcc1-5b7ebd2f75b5', - }, - { - comment: 'I decided to add a new comment', - }, - ], - description: 'some description', - entries: [ - { - entries: [ - { - field: 'nested.field', - operator: 'included', - type: 'match', - value: 'some value', - }, - ], - field: 'some.parentField', - type: 'nested', - }, - { - field: 'some.not.nested.field', - operator: 'included', - type: 'match', - value: 'some value', - }, - ], - item_id: 'item_id_1', - list_id: 'i_exist', - name: 'Query with a rule id', - type: 'simple', - }, - ]) - ), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const { body: ruleResponse } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) - .set('elastic-api-version', '2023-10-31') - .send() - .expect(200); - const bodyToCompare = removeServerGeneratedProperties(ruleResponse); - const referencedExceptionList = ruleResponse.exceptions_list[0]; + const importedRule = await fetchRule(supertest, { ruleId: 'rule-1' }); + const referencedExceptionList = importedRule.exceptions_list[0]; // create an exception list const { body: exceptionBody } = await supertest @@ -1560,7 +1457,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(bodyToCompare.exceptions_list).to.eql([ + expect(importedRule.exceptions_list).toEqual([ { id: exceptionBody.id, list_id: 'i_exist', @@ -1574,7 +1471,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(exceptionItemBody.comments).to.eql([ + expect(exceptionItemBody.comments).toEqual([ { comment: 'This is an exception to the rule', created_at: `${exceptionItemBody.comments[0].created_at}`, @@ -1589,7 +1486,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); - expect(body).to.eql({ + expect(body).toMatchObject({ success: true, success_count: 1, rules_count: 1, @@ -1597,28 +1494,21 @@ export default ({ getService }: FtrProviderContext): void => { exceptions_errors: [], exceptions_success: true, exceptions_success_count: 1, - action_connectors_success: true, - action_connectors_success_count: 0, - action_connectors_errors: [], - action_connectors_warnings: [], }); }); }); it('should import a rule with "investigation_fields', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + investigation_fields: { field_names: ['foo'] }, + }) + ); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach( - 'file', - Buffer.from( - combineToNdJson({ - ...getSimpleRule(), - investigation_fields: { field_names: ['foo'] }, - }) - ), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_ess.ts index 69466edf7a1be..c78af6078133e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_ess.ts @@ -5,24 +5,17 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; -import { - InvestigationFields, - QueryRuleCreateProps, -} from '@kbn/security-solution-plugin/common/api/detection_engine'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; -import { toNdJsonString } from '@kbn/lists-plugin/common/schemas/request/import_exceptions_schema.mock'; import { - getSimpleRule, - ruleToNdjson, createLegacyRuleAction, getLegacyActionSO, fetchRule, - getWebHookAction, - getSimpleRuleAsNdjson, checkInvestigationFieldSoValue, + combineToNdJson, + getCustomQueryRuleParams, } from '../../../utils'; import { deleteAllRules, createRule } from '../../../../../../common/utils/security_solution'; import { @@ -30,18 +23,8 @@ import { deleteUserAndRole, } from '../../../../../../common/services/security_solution'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; - -export const getSimpleRuleAsNdjsonWithLegacyInvestigationField = ( - ruleIds: string[], - enabled = false, - overwrites: Partial -): Buffer => { - const stringOfRules = ruleIds.map((ruleId) => { - const simpleRule = { ...getSimpleRule(ruleId, enabled), ...overwrites }; - return JSON.stringify(simpleRule); - }); - return Buffer.from(stringOfRules.join('\n')); -}; +import { createConnector } from '../../../utils/connectors'; +import { getWebHookConnectorParams } from '../../../utils/connectors/get_web_hook_connector_params'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); @@ -55,41 +38,45 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should migrate legacy actions in existing rule if overwrite is set to true', async () => { - const simpleRule = getSimpleRule('rule-1'); - - const [connector, createdRule] = await Promise.all([ - supertest - .post(`/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - connector_type_id: '.slack', - secrets: { - webhookUrl: 'http://localhost:1234', - }, - }), - createRule(supertest, log, simpleRule), + const ruleToOverwrite = getCustomQueryRuleParams({ + rule_id: 'rule-1', + interval: '1h', // action frequency can't be shorter than the schedule interval + }); + + const [connectorId, createdRule] = await Promise.all([ + createConnector(supertest, { + name: 'My action', + connector_type_id: '.slack', + config: {}, + secrets: { + webhookUrl: 'http://localhost:1234', + }, + }), + createRule(supertest, log, ruleToOverwrite), ]); - await createLegacyRuleAction(supertest, createdRule.id, connector.body.id); + await createLegacyRuleAction(supertest, createdRule.id, connectorId); // check for legacy sidecar action const sidecarActionsResults = await getLegacyActionSO(es); - expect(sidecarActionsResults.hits.hits.length).to.eql(1); - expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(createdRule.id); - simpleRule.name = 'some other name'; - const ndjson = ruleToNdjson(simpleRule); + expect(sidecarActionsResults.hits.hits.length).toBe(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).toBe(createdRule.id); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ rule_id: 'rule-1', name: 'some other name' }) + ); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', ndjson, 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); // legacy sidecar action should be gone const sidecarActionsPostResults = await getLegacyActionSO(es); - expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + + expect(sidecarActionsPostResults.hits.hits.length).toBe(0); }); describe('importing rules with different roles', () => { @@ -101,23 +88,23 @@ export default ({ getService }: FtrProviderContext): void => { await deleteUserAndRole(getService, ROLES.hunter_no_actions); await deleteUserAndRole(getService, ROLES.hunter); }); + it('should successfully import rules without actions when user has no actions privileges', async () => { + const ndjson = combineToNdJson(getCustomQueryRuleParams()); + const { body } = await supertestWithoutAuth .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .auth(ROLES.hunter_no_actions, 'changeme') .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [], success: true, success_count: 1, rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: true, action_connectors_success_count: 0, action_connectors_errors: [], @@ -125,55 +112,48 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should not import rules with actions when user has "read" actions privileges', async () => { - // create a new action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [ - { - group: 'default', - id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', - action_type_id: hookAction.actionTypeId, - params: {}, + it('should NOT import rules with actions when user has "read" actions privileges', async () => { + const connectorId = await createConnector(supertest, getWebHookConnectorParams()); + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-with-actions', + actions: [ + { + group: 'default', + id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', + action_type_id: connectorId, + params: {}, + }, + ], + }), + { + id: 'cabc78e0-9031-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: true, + config: {}, + secrets: {}, }, - ], - }; - const ruleWithConnector = { - id: 'cabc78e0-9031-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: true, - config: {}, - secrets: {}, - }, - references: [], - migrationVersion: { action: '8.3.0' }, - coreMigrationVersion: '8.7.0', - }; + references: [], + migrationVersion: { action: '8.3.0' }, + coreMigrationVersion: '8.7.0', + } + ); const { body } = await supertestWithoutAuth .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .auth(ROLES.hunter, 'changeme') .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - Buffer.from(toNdJsonString([simpleRule, ruleWithConnector])), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + expect(body).toMatchObject({ errors: [ { error: { @@ -187,9 +167,6 @@ export default ({ getService }: FtrProviderContext): void => { success: false, success_count: 0, rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: false, action_connectors_success_count: 0, action_connectors_errors: [ @@ -205,54 +182,49 @@ export default ({ getService }: FtrProviderContext): void => { action_connectors_warnings: [], }); }); - it('should not import rules with actions when a user has no actions privileges', async () => { - // create a new action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - const simpleRule: ReturnType = { - ...getSimpleRule('rule-1'), - actions: [ - { - group: 'default', - id: 'cabc78e0-9031-11ed-b076-53cc4d57axy1', - action_type_id: hookAction.actionTypeId, - params: {}, + + it('should NOT import rules with actions when a user has no actions privileges', async () => { + const connectorId = await createConnector(supertest, getWebHookConnectorParams()); + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-with-actions', + actions: [ + { + group: 'default', + id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', + action_type_id: connectorId, + params: {}, + }, + ], + }), + { + id: 'cabc78e0-9031-11ed-b076-53cc4d57axy1', + 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: true, + config: {}, + secrets: {}, }, - ], - }; - const ruleWithConnector = { - id: 'cabc78e0-9031-11ed-b076-53cc4d57axy1', - 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: true, - config: {}, - secrets: {}, - }, - references: [], - migrationVersion: { action: '8.3.0' }, - coreMigrationVersion: '8.7.0', - }; + references: [], + migrationVersion: { action: '8.3.0' }, + coreMigrationVersion: '8.7.0', + } + ); const { body } = await supertestWithoutAuth .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .auth(ROLES.hunter_no_actions, 'changeme') .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - Buffer.from(toNdJsonString([simpleRule, ruleWithConnector])), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - expect(body).to.eql({ + + expect(body).toMatchObject({ success: false, success_count: 0, errors: [ @@ -266,9 +238,6 @@ export default ({ getService }: FtrProviderContext): void => { }, ], rules_count: 1, - exceptions_errors: [], - exceptions_success: true, - exceptions_success_count: 0, action_connectors_success: false, action_connectors_success_count: 0, action_connectors_errors: [ @@ -288,25 +257,28 @@ export default ({ getService }: FtrProviderContext): void => { describe('legacy investigation fields', () => { it('imports rule with investigation fields as array', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + ruleId: 'rule-1', + // mimicking what an 8.10 rule would look like + // we don't want to support this type in our APIs any longer, but do + // want to allow users to import rules from 8.10 + // @ts-expect-error + investigation_fields: ['foo', 'bar'], + }) + ); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - getSimpleRuleAsNdjsonWithLegacyInvestigationField(['rule-1'], false, { - // mimicking what an 8.10 rule would look like - // we don't want to support this type in our APIs any longer, but do - // want to allow users to import rules from 8.10 - investigation_fields: ['foo', 'bar'] as unknown as InvestigationFields, - }), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); const rule = await fetchRule(supertest, { ruleId: 'rule-1' }); - expect(rule.investigation_fields).to.eql({ field_names: ['foo', 'bar'] }); + + expect(rule.investigation_fields).toEqual({ field_names: ['foo', 'bar'] }); /** * Confirm type on SO so that it's clear in the tests whether it's expected that @@ -320,29 +292,32 @@ export default ({ getService }: FtrProviderContext): void => { es, rule.id ); - expect(isInvestigationFieldMigratedInSo).to.eql(true); + expect(isInvestigationFieldMigratedInSo).toBeTruthy(); }); it('imports rule with investigation fields as empty array', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + ruleId: 'rule-1', + // mimicking what an 8.10 rule would look like + // we don't want to support this type in our APIs any longer, but do + // want to allow users to import rules from 8.10 + // @ts-expect-error + investigation_fields: [], + }) + ); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - getSimpleRuleAsNdjsonWithLegacyInvestigationField(['rule-1'], false, { - // mimicking what an 8.10 rule would look like - // we don't want to support this type in our APIs any longer, but do - // want to allow users to import rules from 8.10 - investigation_fields: [] as unknown as InvestigationFields, - }), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); const rule = await fetchRule(supertest, { ruleId: 'rule-1' }); - expect(rule.investigation_fields).to.eql(undefined); + + expect(rule.investigation_fields).toBeUndefined(); /** * Confirm type on SO so that it's clear in the tests whether it's expected that @@ -356,28 +331,30 @@ export default ({ getService }: FtrProviderContext): void => { es, rule.id ); - expect(isInvestigationFieldMigratedInSo).to.eql(true); + expect(isInvestigationFieldMigratedInSo).toBeTruthy(); }); it('imports rule with investigation fields as intended object type', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + investigation_fields: { + field_names: ['foo'], + }, + }) + ); + await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') - .attach( - 'file', - getSimpleRuleAsNdjsonWithLegacyInvestigationField(['rule-1'], false, { - investigation_fields: { - field_names: ['foo'], - }, - }), - 'rules.ndjson' - ) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); const rule = await fetchRule(supertest, { ruleId: 'rule-1' }); - expect(rule.investigation_fields).to.eql({ field_names: ['foo'] }); + + expect(rule.investigation_fields).toEqual({ field_names: ['foo'] }); /** * Confirm type on SO so that it's clear in the tests whether it's expected that * the SO itself is migrated to the inteded object type, or if the transformation is @@ -390,7 +367,7 @@ export default ({ getService }: FtrProviderContext): void => { es, rule.id ); - expect(isInvestigationFieldIntendedTypeInSo).to.eql(true); + expect(isInvestigationFieldIntendedTypeInSo).toBeTruthy(); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts index 8acc444a09082..0d96e727a69ea 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts @@ -8,9 +8,9 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; -import { createRule, deleteAllRules, getCustomQueryRuleParams, fetchRule } from '../../../utils'; +import { createRule, deleteAllRules } from '../../../../../../common/utils/security_solution'; +import { combineToNdJson, getCustomQueryRuleParams, fetchRule } from '../../../utils'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; -import { combineToNdJson } from './combine_to_ndjson'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts index 9c3f54e019653..0ca8f88166c89 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts @@ -7,7 +7,7 @@ import type SuperTest from 'supertest'; -interface CreateConnectorBody { +export interface CreateConnectorBody { readonly name: string; readonly config: Record; readonly connector_type_id: string; @@ -18,10 +18,12 @@ export async function createConnector( supertest: SuperTest.SuperTest, connector: CreateConnectorBody, id = '' -): Promise { - await supertest +): Promise { + const { body } = await supertest .post(`/api/actions/connector/${id}`) .set('kbn-xsrf', 'foo') .send(connector) .expect(200); + + return body.id; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_web_hook_connector_params.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_web_hook_connector_params.ts new file mode 100644 index 0000000000000..636e4c8ce5627 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_web_hook_connector_params.ts @@ -0,0 +1,23 @@ +/* + * 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 { CreateConnectorBody } from './create_connector'; + +export function getWebHookConnectorParams(): CreateConnectorBody { + return { + name: 'Webhook connector', + connector_type_id: '.webhook', + config: { + method: 'post', + url: 'http://localhost', + }, + secrets: { + user: 'example', + password: 'example', + }, + }; +} From 98589975be0520fe50bb314969f6bef079d2d445 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 13 Feb 2024 23:05:46 +0100 Subject: [PATCH 06/10] unskip a fixed test --- .../trial_license_complete_tier/import_connectors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts index ad3b11f73053f..60576ff5cabdf 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_connectors.ts @@ -16,8 +16,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/176836 - describe.skip('@ess @brokenInServerless @skipInQA import action connectors', () => { + describe('@ess @brokenInServerless @skipInQA import action connectors', () => { const CONNECTOR_ID = '1be16246-642a-4ed8-bfd3-b47f8c7d7055'; const ANOTHER_CONNECTOR_ID = 'abc16246-642a-4ed8-bfd3-b47f8c7d7055'; const CUSTOM_ACTION_CONNECTOR = { From 9b3d62472ca080ea98136ac0cb38b2c84d469084 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Wed, 14 Feb 2024 08:40:14 +0100 Subject: [PATCH 07/10] remove unused constant --- .../trial_license_complete_tier/export_rules.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts index d30f2137f2151..3169bf580b8ec 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts @@ -26,8 +26,6 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); const es = getService('es'); - // TODO: add a new service for pulling kibana username, similar to getService('es') - const config = getService('config'); describe('@ess @brokenInServerless @skipInQA export_rules', () => { describe('exporting rules', () => { From 857e9c9e5886cefc6bbd66a0ba98905af88a0bb7 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Wed, 14 Feb 2024 08:47:16 +0100 Subject: [PATCH 08/10] fix a test --- .../basic_license_essentials_tier/export_rules.ts | 4 ++-- .../trial_license_complete_tier/export_rules.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts index dc992cde0dc1c..f355e9ed61fc4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts @@ -104,8 +104,8 @@ export default ({ getService }: FtrProviderContext): void => { expect([exportedRule1, exportedRule2]).toEqual( expect.arrayContaining([ - expect.objectContaining(expect.objectContaining(ruleToExport1)), - expect.objectContaining(expect.objectContaining(ruleToExport2)), + expect.objectContaining(ruleToExport1), + expect.objectContaining(ruleToExport2), ]) ); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts index 3169bf580b8ec..10421b88a9dde 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts @@ -164,8 +164,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should export exactly two rules given two rules', async () => { - await createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'rule-1' })); - await createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'rule-2' })); + const ruleToExport1 = getCustomQueryRuleParams({ rule_id: 'rule-1' }); + const ruleToExport2 = getCustomQueryRuleParams({ rule_id: 'rule-2' }); + + await createRule(supertest, log, ruleToExport1); + await createRule(supertest, log, ruleToExport2); const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_export`) @@ -180,8 +183,8 @@ export default ({ getService }: FtrProviderContext): void => { expect([exportedRule1, exportedRule2]).toEqual( expect.arrayContaining([ - expect.objectContaining(getCustomQueryRuleParams({ rule_id: 'rule-1' })), - expect.objectContaining(getCustomQueryRuleParams({ rule_id: 'rule-2' })), + expect.objectContaining(ruleToExport1), + expect.objectContaining(ruleToExport2), ]) ); }); From 2d112068cb5d43a3068c35f725ca7297a7be8cf3 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 15 Feb 2024 13:49:05 +0100 Subject: [PATCH 09/10] improve texts readability --- .../rule_management/logic/import/import_rules_utils.test.ts | 4 ++-- .../import_rules_with_overwrite.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts index 9e159672b48d2..7842febda6821 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts @@ -135,12 +135,12 @@ describe('importRules', () => { /** * Existing rule may have nullable fields set to a value (e.g. `timestamp_override` is set to `some.value`) but - * a rule to import doesn't have these fields set (e.g. `timestamp_override` is presented at al in ndjson). + * a rule to import doesn't have these fields set (e.g. `timestamp_override` is NOT present at all in the ndjson file). * We expect the updated rule won't have such fields preserved (e.g. `timestamp_override` will be removed). * * Unit test is only able to check `updateRules()` receives a proper update object. */ - it('ensures overwritten rule DOES NOT preserve missed in the imported rule fields when "overwriteRules" is "true" and matching rule found', async () => { + it('ensures overwritten rule DOES NOT preserve fields missed in the imported rule when "overwriteRules" is "true" and matching rule found', async () => { const existingRule = getRuleMock( getQueryRuleParams({ timestampOverride: 'some.value', diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts index 0d96e727a69ea..2a0e543a57af9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts @@ -114,10 +114,10 @@ export default ({ getService }: FtrProviderContext): void => { /** * Existing rule may have nullable fields set to a value (e.g. `timestamp_override` is set to `some.value`) but - * a rule to import doesn't have these fields set (e.g. `timestamp_override` is presented at al in ndjson). + * a rule to import doesn't have these fields set (e.g. `timestamp_override` is NOT present at all in the ndjson file). * We expect the updated rule won't have such fields preserved (e.g. `timestamp_override` will be removed). */ - it('ensures overwritten rule DOES NOT preserve missed in the imported rule fields', async () => { + it('ensures overwritten rule DOES NOT preserve fields missed in the imported rule', async () => { await createRule( supertest, log, From d6c556664174f6c64f3d27fe91a899c1d45cd763 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 15 Feb 2024 13:53:00 +0100 Subject: [PATCH 10/10] unskip serverless tests --- .../import_rules_with_overwrite.ts | 191 ++++++++++++++++++ .../basic_license_essentials_tier/index.ts | 1 + .../import_rules_with_overwrite.ts | 2 +- .../trial_license_complete_tier/index.ts | 1 + 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules_with_overwrite.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules_with_overwrite.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules_with_overwrite.ts new file mode 100644 index 0000000000000..3f2318208bb16 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules_with_overwrite.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; + +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { createRule, deleteAllRules } from '../../../../../../common/utils/security_solution'; +import { combineToNdJson, getCustomQueryRuleParams, fetchRule } from '../../../utils'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInQA import_rules with rule overwrite set to "true"', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + }); + + it('DOES NOT report a conflict if there is an attempt to import two rules with the same rule_id', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ rule_id: 'rule-1', name: 'Rule 1' }), + getCustomQueryRuleParams({ rule_id: 'rule-1', name: 'Rule 2' }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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: 2, + }); + + const importedRule = await fetchRule(supertest, { ruleId: 'rule-1' }); + + expect(importedRule.name).toBe('Rule 2'); + }); + + it('DOES NOT report a conflict if there is an attempt to import a rule twice', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'imported-rule', + name: 'Imported rule', + }) + ); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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, + }); + }); + + it('overwrites an existing rule', async () => { + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + name: 'Existing rule', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + name: 'Imported rule', + }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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, + }); + + const importedRule = await fetchRule(supertest, { ruleId: 'existing-rule' }); + + expect(importedRule.name).toBe('Imported rule'); + }); + + /** + * Existing rule may have nullable fields set to a value (e.g. `timestamp_override` is set to `some.value`) but + * a rule to import doesn't have these fields set (e.g. `timestamp_override` is NOT present at all in the ndjson file). + * We expect the updated rule won't have such fields preserved (e.g. `timestamp_override` will be removed). + */ + it('ensures overwritten rule DOES NOT preserve fields missed in the imported rule', async () => { + await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + rule_name_override: 'some name', + timestamp_override: 'some.value', + timeline_id: 'some id', + timeline_title: 'some title', + outcome: 'exactMatch', + alias_target_id: 'some id', + license: 'some license', + note: 'some notes', + building_block_type: 'some type', + output_index: 'some-index', + namespace: 'some-namespace', + meta: { + some: 'field', + }, + investigation_fields: { field_names: ['a', 'b', 'c'] }, + throttle: 'no_actions', + }) + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'existing-rule', + namespace: 'abc', + }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .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, + }); + + const importedRule = await fetchRule(supertest, { ruleId: 'existing-rule' }); + + expect(importedRule).toMatchObject({ + rule_id: 'existing-rule', + output_index: '', + }); + expect(importedRule).toEqual( + expect.not.objectContaining({ + rule_name_override: expect.anything(), + timestamp_override: expect.anything(), + timeline_id: expect.anything(), + timeline_title: expect.anything(), + outcome: expect.anything(), + alias_target_id: expect.anything(), + license: expect.anything(), + note: expect.anything(), + building_block_type: expect.anything(), + namespace: expect.anything(), + meta: expect.anything(), + investigation_fields: expect.anything(), + throttle: expect.anything(), + }) + ); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/index.ts index 7d91e7c455b58..aed66b44e21fc 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Rules Management - Rule Import & Export APIs', function () { loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./import_rules_with_overwrite')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts index 2a0e543a57af9..3f2318208bb16 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/import_rules_with_overwrite.ts @@ -16,7 +16,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); - describe('@ess @brokenInServerless @skipInQA import_rules with rule overwrite set to "true"', () => { + describe('@ess @serverless @skipInQA import_rules with rule overwrite set to "true"', () => { beforeEach(async () => { await deleteAllRules(supertest, log); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/index.ts index 2fc4754a3f230..9a6af4d80070d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/index.ts @@ -13,6 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./export_rules_ess')); loadTestFile(require.resolve('./import_export_rules')); loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./import_rules_with_overwrite')); loadTestFile(require.resolve('./import_rules_ess')); loadTestFile(require.resolve('./import_connectors')); });