From 78cb16c595e6f0066479b07deb6766426bdac3c3 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Wed, 14 Feb 2024 13:54:51 +0100 Subject: [PATCH] [8.12] [Security Solution] Fix importing rules referencing preconfigured connectors (#176284) (#176887) # Backport This will backport the following commits from `main` to `8.12`: - [[Security Solution] Fix importing rules referencing preconfigured connectors (#176284)](https://github.com/elastic/kibana/pull/176284) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../import_rules/rule_to_import.mock.ts | 95 ++-- .../import_rules/rule_to_import.test.ts | 268 ++++----- .../rule_to_import_validation.test.ts | 20 +- .../import_rule_action_connectors.test.ts | 224 +++++--- .../import_rule_action_connectors.ts | 70 ++- .../check_rule_exception_references.test.ts | 22 +- .../gather_referenced_exceptions.test.ts | 70 +-- .../logic/import/import_rules_utils.test.ts | 50 +- .../group10/import_connectors.ts | 517 ++++++++++++++++++ .../utils/combine_to_ndjson.ts | 10 + .../utils/connectors/create_connector.ts | 27 + .../utils/connectors/delete_connector.ts | 15 + .../utils/connectors/get_connector.ts | 21 + .../utils/connectors/index.ts | 10 + .../utils/get_custom_query_rule_params.ts | 36 ++ .../utils/index.ts | 2 + .../alerts/migrations/index.ts | 2 +- .../utils/combine_to_ndjson.ts | 10 + .../utils/connectors/create_connector.ts | 27 + .../utils/connectors/delete_connector.ts | 15 + .../utils/connectors/get_connector.ts | 21 + .../utils/connectors/index.ts | 10 + .../detections_response/utils/index.ts | 1 + 23 files changed, 1142 insertions(+), 401 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_connectors.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/combine_to_ndjson.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/connectors/create_connector.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/connectors/delete_connector.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/connectors/get_connector.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/connectors/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_custom_query_rule_params.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/combine_to_ndjson.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/delete_connector.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_connector.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/index.ts diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts index 6161e2a00f960..2b36645363edc 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts @@ -7,17 +7,19 @@ import type { RuleToImport } from './rule_to_import'; -export const getImportRulesSchemaMock = (ruleId = 'rule-1'): RuleToImport => ({ - description: 'some description', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - rule_id: ruleId, - immutable: false, -}); +export const getImportRulesSchemaMock = (rewrites?: Partial): RuleToImport => + ({ + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + immutable: false, + ...rewrites, + } as RuleToImport); export const getImportRulesWithIdSchemaMock = (ruleId = 'rule-1'): RuleToImport => ({ id: '6afb8ce1-ea94-4790-8653-fd0b021d2113', @@ -47,42 +49,46 @@ export const rulesToNdJsonString = (rules: RuleToImport[]) => { * @param ruleIds Array of ruleIds with which to generate rule JSON */ export const ruleIdsToNdJsonString = (ruleIds: string[]) => { - const rules = ruleIds.map((ruleId) => getImportRulesSchemaMock(ruleId)); + const rules = ruleIds.map((ruleId) => getImportRulesSchemaMock({ rule_id: ruleId })); return rulesToNdJsonString(rules); }; -export const getImportThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): RuleToImport => ({ - description: 'some description', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: ruleId, - threat_index: ['index-123'], - threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }], - threat_query: '*:*', - threat_filters: [ - { - bool: { - must: [ - { - query_string: { - query: 'host.name: linux', - analyze_wildcard: true, - time_zone: 'Zulu', +export const getImportThreatMatchRulesSchemaMock = ( + rewrites?: Partial +): RuleToImport => + ({ + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + threat_index: ['index-123'], + threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }], + threat_query: '*:*', + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, }, - }, - ], - filter: [], - should: [], - must_not: [], + ], + filter: [], + should: [], + must_not: [], + }, }, - }, - ], - immutable: false, -}); + ], + immutable: false, + ...rewrites, + } as RuleToImport); export const webHookConnector = { id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', @@ -104,8 +110,7 @@ export const webHookConnector = { export const ruleWithConnectorNdJSON = (): string => { const items = [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ actions: [ { group: 'default', @@ -114,7 +119,7 @@ export const ruleWithConnectorNdJSON = (): string => { params: {}, }, ], - }, + }), webHookConnector, ]; const stringOfExceptions = items.map((item) => JSON.stringify(item)); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts index 3f364c6619db6..2894da32593fa 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts @@ -27,10 +27,10 @@ describe('RuleToImport', () => { }); test('extra properties are removed', () => { - const payload: RuleToImportInput & { madeUp: string } = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ + // @ts-expect-error add an unknown field madeUp: 'hi', - }; + }); const result = RuleToImport.safeParse(payload); expectParseSuccess(result); @@ -241,10 +241,7 @@ describe('RuleToImport', () => { }); test('You can send in an empty array to threat', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - threat: [], - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ threat: [] }); const result = RuleToImport.safeParse(payload); @@ -289,10 +286,7 @@ describe('RuleToImport', () => { }); test('allows references to be sent as valid', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - references: ['index-1'], - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ references: ['index-1'] }); const result = RuleToImport.safeParse(payload); @@ -307,10 +301,10 @@ describe('RuleToImport', () => { }); test('references cannot be numbers', () => { - const payload: Omit & { references: number[] } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign wrong type value references: [5], - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -321,10 +315,10 @@ describe('RuleToImport', () => { }); test('indexes cannot be numbers', () => { - const payload: Omit & { index: number[] } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign wrong type value index: [5], - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -358,10 +352,9 @@ describe('RuleToImport', () => { }); test('saved_query type can have filters with it', () => { - const payload = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ filters: [], - }; + }); const result = RuleToImport.safeParse(payload); @@ -369,10 +362,10 @@ describe('RuleToImport', () => { }); test('filters cannot be a string', () => { - const payload: Omit & { filters: string } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign wrong type value filters: 'some string', - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -383,10 +376,7 @@ describe('RuleToImport', () => { }); test('language validates with kuery', () => { - const payload = { - ...getImportRulesSchemaMock(), - language: 'kuery', - }; + const payload = getImportRulesSchemaMock({ language: 'kuery' }); const result = RuleToImport.safeParse(payload); @@ -394,10 +384,7 @@ describe('RuleToImport', () => { }); test('language validates with lucene', () => { - const payload = { - ...getImportRulesSchemaMock(), - language: 'lucene', - }; + const payload = getImportRulesSchemaMock({ language: 'lucene' }); const result = RuleToImport.safeParse(payload); @@ -405,10 +392,10 @@ describe('RuleToImport', () => { }); test('language does not validate with something made up', () => { - const payload: Omit & { language: string } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value language: 'something-made-up', - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -419,10 +406,7 @@ describe('RuleToImport', () => { }); test('max_signals cannot be negative', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - max_signals: -1, - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ max_signals: -1 }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -433,10 +417,7 @@ describe('RuleToImport', () => { }); test('max_signals cannot be zero', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - max_signals: 0, - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ max_signals: 0 }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -447,10 +428,7 @@ describe('RuleToImport', () => { }); test('max_signals can be 1', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - max_signals: 1, - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ max_signals: 1 }); const result = RuleToImport.safeParse(payload); @@ -458,10 +436,7 @@ describe('RuleToImport', () => { }); test('You can optionally send in an array of tags', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - tags: ['tag_1', 'tag_2'], - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ tags: ['tag_1', 'tag_2'] }); const result = RuleToImport.safeParse(payload); @@ -469,10 +444,10 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of tags that are numbers', () => { - const payload: Omit & { tags: number[] } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value tags: [0, 1, 2], - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -483,11 +458,9 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of threat that are missing "framework"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ threat: [ + // @ts-expect-error assign unsupported value { tactic: { id: 'fakeId', @@ -503,7 +476,7 @@ describe('RuleToImport', () => { ], }, ], - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -512,11 +485,9 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of threat that are missing "tactic"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ threat: [ + // @ts-expect-error assign unsupported value { framework: 'fake', technique: [ @@ -528,7 +499,7 @@ describe('RuleToImport', () => { ], }, ], - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -537,10 +508,7 @@ describe('RuleToImport', () => { }); test('You can send in an array of threat that are missing "technique"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ threat: [ { framework: 'fake', @@ -551,7 +519,7 @@ describe('RuleToImport', () => { }, }, ], - }; + }); const result = RuleToImport.safeParse(payload); @@ -559,10 +527,9 @@ describe('RuleToImport', () => { }); test('You can optionally send in an array of false positives', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ false_positives: ['false_1', 'false_2'], - }; + }); const result = RuleToImport.safeParse(payload); @@ -570,10 +537,10 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of false positives that are numbers', () => { - const payload: Omit & { false_positives: number[] } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value false_positives: [5, 4], - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -584,10 +551,10 @@ describe('RuleToImport', () => { }); test('You cannot set the immutable to a number when trying to create a rule', () => { - const payload: Omit & { immutable: number } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value immutable: 5, - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -598,10 +565,9 @@ describe('RuleToImport', () => { }); test('You can optionally set the immutable to be false', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ immutable: false, - }; + }); const result = RuleToImport.safeParse(payload); @@ -609,10 +575,10 @@ describe('RuleToImport', () => { }); test('You cannot set the immutable to be true', () => { - const payload: Omit & { immutable: true } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value immutable: true, - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -623,10 +589,10 @@ describe('RuleToImport', () => { }); test('You cannot set the immutable to be a number', () => { - const payload: Omit & { immutable: number } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value immutable: 5, - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -637,10 +603,9 @@ describe('RuleToImport', () => { }); test('You cannot set the risk_score to 101', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ risk_score: 101, - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -651,10 +616,9 @@ describe('RuleToImport', () => { }); test('You cannot set the risk_score to -1', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ risk_score: -1, - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -665,10 +629,9 @@ describe('RuleToImport', () => { }); test('You can set the risk_score to 0', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ risk_score: 0, - }; + }); const result = RuleToImport.safeParse(payload); @@ -676,10 +639,9 @@ describe('RuleToImport', () => { }); test('You can set the risk_score to 100', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ risk_score: 100, - }; + }); const result = RuleToImport.safeParse(payload); @@ -687,12 +649,11 @@ describe('RuleToImport', () => { }); test('You can set meta to any object you want', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ meta: { somethingMadeUp: { somethingElse: true }, }, - }; + }); const result = RuleToImport.safeParse(payload); @@ -700,10 +661,10 @@ describe('RuleToImport', () => { }); test('You cannot create meta as a string', () => { - const payload: Omit & { meta: string } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value meta: 'should not work', - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -714,11 +675,10 @@ describe('RuleToImport', () => { }); test('validates with timeline_id and timeline_title', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ timeline_id: 'timeline-id', timeline_title: 'timeline-title', - }; + }); const result = RuleToImport.safeParse(payload); @@ -726,10 +686,9 @@ describe('RuleToImport', () => { }); test('rule_id is required and you cannot get by with just id', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', - }; + }); // @ts-expect-error delete payload.rule_id; @@ -740,13 +699,12 @@ describe('RuleToImport', () => { }); test('it validates with created_at, updated_at, created_by, updated_by values', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), + const payload: RuleToImportInput = getImportRulesSchemaMock({ created_at: '2020-01-09T06:15:24.749Z', updated_at: '2020-01-09T06:15:24.749Z', created_by: 'Braden Hassanabad', updated_by: 'Evan Hassanabad', - }; + }); const result = RuleToImport.safeParse(payload); @@ -754,10 +712,7 @@ describe('RuleToImport', () => { }); test('it does not validate with epoch strings for created_at', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - created_at: '1578550728650', - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ created_at: '1578550728650' }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -766,10 +721,7 @@ describe('RuleToImport', () => { }); test('it does not validate with epoch strings for updated_at', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - updated_at: '1578550728650', - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ updated_at: '1578550728650' }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -800,10 +752,10 @@ describe('RuleToImport', () => { }); test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const payload: Omit & { severity: string } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value severity: 'junk', - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -825,10 +777,12 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of actions that are missing "group"', () => { - const payload: Omit = { - ...getImportRulesSchemaMock(), - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - }; + const payload = getImportRulesSchemaMock({ + actions: [ + // @ts-expect-error assign unsupported value + { id: 'id', action_type_id: 'action_type_id', params: {} }, + ], + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -837,10 +791,12 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of actions that are missing "id"', () => { - const payload: Omit = { - ...getImportRulesSchemaMock(), - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - }; + const payload = getImportRulesSchemaMock({ + actions: [ + // @ts-expect-error assign unsupported value + { group: 'group', action_type_id: 'action_type_id', params: {} }, + ], + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -849,10 +805,12 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of actions that are missing "action_type_id"', () => { - const payload: Omit = { - ...getImportRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', params: {} }], - }; + const payload = getImportRulesSchemaMock({ + actions: [ + // @ts-expect-error assign unsupported value + { group: 'group', id: 'id', params: {} }, + ], + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -863,10 +821,12 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of actions that are missing "params"', () => { - const payload: Omit = { - ...getImportRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - }; + const payload = getImportRulesSchemaMock({ + actions: [ + // @ts-expect-error assign unsupported value + { group: 'group', id: 'id', action_type_id: 'action_type_id' }, + ], + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -875,17 +835,17 @@ describe('RuleToImport', () => { }); test('You cannot send in an array of actions that are including "actionTypeId"', () => { - const payload: Omit = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ actions: [ { group: 'group', id: 'id', + // @ts-expect-error assign unsupported value actionTypeId: 'actionTypeId', params: {}, }, ], - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -907,32 +867,28 @@ describe('RuleToImport', () => { describe('note', () => { test('You can set note to a string', () => { - const payload: RuleToImport = { - ...getImportRulesSchemaMock(), + const payload: RuleToImport = getImportRulesSchemaMock({ note: '# documentation markdown here', - }; + }); const result = RuleToImport.safeParse(payload); expectParseSuccess(result); }); test('You can set note to an empty string', () => { - const payload: RuleToImportInput = { - ...getImportRulesSchemaMock(), - note: '', - }; + const payload: RuleToImportInput = getImportRulesSchemaMock({ note: '' }); const result = RuleToImport.safeParse(payload); expectParseSuccess(result); }); test('You cannot create note as an object', () => { - const payload: Omit & { note: {} } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value note: { somethingHere: 'something else', }, - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); @@ -1102,10 +1058,10 @@ describe('RuleToImport', () => { }); test('data_view_id cannot be a number', () => { - const payload: Omit & { data_view_id: number } = { - ...getImportRulesSchemaMock(), + const payload = getImportRulesSchemaMock({ + // @ts-expect-error assign unsupported value data_view_id: 5, - }; + }); const result = RuleToImport.safeParse(payload); expectParseError(result); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts index 31ac993eb4053..597dcf0cc3bcb 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts @@ -12,40 +12,34 @@ import { validateRuleToImport } from './rule_to_import_validation'; describe('Rule to import schema, additional validation', () => { describe('validateRuleToImport', () => { test('You cannot omit timeline_title when timeline_id is present', () => { - const schema: RuleToImport = { - ...getImportRulesSchemaMock(), + const schema: RuleToImport = getImportRulesSchemaMock({ timeline_id: '123', - }; + }); delete schema.timeline_title; const errors = validateRuleToImport(schema); expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']); }); test('You cannot have empty string for timeline_title when timeline_id is present', () => { - const schema: RuleToImport = { - ...getImportRulesSchemaMock(), + const schema: RuleToImport = getImportRulesSchemaMock({ timeline_id: '123', timeline_title: '', - }; + }); const errors = validateRuleToImport(schema); expect(errors).toEqual(['"timeline_title" cannot be an empty string']); }); test('You cannot have timeline_title with an empty timeline_id', () => { - const schema: RuleToImport = { - ...getImportRulesSchemaMock(), + const schema: RuleToImport = getImportRulesSchemaMock({ timeline_id: '', timeline_title: 'some-title', - }; + }); const errors = validateRuleToImport(schema); expect(errors).toEqual(['"timeline_id" cannot be an empty string']); }); test('You cannot have timeline_title without timeline_id', () => { - const schema: RuleToImport = { - ...getImportRulesSchemaMock(), - timeline_title: 'some-title', - }; + const schema: RuleToImport = getImportRulesSchemaMock({ timeline_title: 'some-title' }); delete schema.timeline_id; const errors = validateRuleToImport(schema); expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts index 8afecd245e2a9..84352c1ea0f1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts @@ -13,8 +13,7 @@ import { importRuleActionConnectors } from './import_rule_action_connectors'; import { coreMock } from '@kbn/core/server/mocks'; const rules = [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ actions: [ { group: 'default', @@ -23,14 +22,9 @@ const rules = [ params: {}, }, ], - }, -]; -const rulesWithoutActions = [ - { - ...getImportRulesSchemaMock(), - actions: [], - }, + }), ]; +const rulesWithoutActions = [getImportRulesSchemaMock({ actions: [] })]; const actionConnectors = [webHookConnector]; const actionsClient = actionsClientMock.create(); actionsClient.getAll.mockResolvedValue([]); @@ -115,8 +109,7 @@ describe('importRuleActionConnectors', () => { const actionsImporter = core.savedObjects.getImporter; const ruleWith2Connectors = [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ actions: [ { group: 'default', @@ -135,7 +128,7 @@ describe('importRuleActionConnectors', () => { action_type_id: '.slack', }, ], - }, + }), ]; const res = await importRuleActionConnectors({ actionConnectors, @@ -189,8 +182,7 @@ describe('importRuleActionConnectors', () => { actionsClient, actionsImporter: actionsImporter(), rules: [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ actions: [ { group: 'default', @@ -205,7 +197,7 @@ describe('importRuleActionConnectors', () => { params: {}, }, ], - }, + }), ], overwrite: false, }); @@ -235,8 +227,8 @@ describe('importRuleActionConnectors', () => { actionsClient, actionsImporter: actionsImporter(), rules: [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ + rule_id: 'rule-1', actions: [ { group: 'default', @@ -245,9 +237,9 @@ describe('importRuleActionConnectors', () => { params: {}, }, ], - }, - { - ...getImportRulesSchemaMock('rule-2'), + }), + getImportRulesSchemaMock({ + rule_id: 'rule-2', actions: [ { group: 'default', @@ -256,7 +248,7 @@ describe('importRuleActionConnectors', () => { params: {}, }, ], - }, + }), ], overwrite: false, }); @@ -340,45 +332,6 @@ describe('importRuleActionConnectors', () => { expect(actionsImporter2Importer.import).not.toBeCalled(); }); - it('should not skip importing the action-connectors if all connectors have been imported/created before when overwrite is true', async () => { - core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({ - import: jest.fn().mockResolvedValue({ - success: true, - successCount: 1, - errors: [], - warnings: [], - }), - }); - const actionsImporter = core.savedObjects.getImporter; - - actionsClient.getAll.mockResolvedValue([ - { - actionTypeId: '.webhook', - name: 'webhook', - isPreconfigured: true, - id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1', - referencedByCount: 1, - isDeprecated: false, - isSystemAction: false, - }, - ]); - - const res = await importRuleActionConnectors({ - actionConnectors, - actionsClient, - actionsImporter: actionsImporter(), - rules, - overwrite: true, - }); - - expect(res).toEqual({ - success: true, - successCount: 1, - errors: [], - warnings: [], - }); - }); - it('should import one rule with connector successfully even if it was exported from different namespaces by generating destinationId and replace the old actionId with it', async () => { const successResults = [ { @@ -441,8 +394,8 @@ describe('importRuleActionConnectors', () => { it('should import multiple rules with connectors successfully even if they were exported from different namespaces by generating destinationIds and replace the old actionIds with them', async () => { const multipleRules = [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ + rule_id: 'rule_1', actions: [ { group: 'default', @@ -451,9 +404,8 @@ describe('importRuleActionConnectors', () => { params: {}, }, ], - }, - { - ...getImportRulesSchemaMock(), + }), + getImportRulesSchemaMock({ rule_id: 'rule_2', id: '0abc78e0-7031-11ed-b076-53cc4d57aaf1', actions: [ @@ -464,7 +416,7 @@ describe('importRuleActionConnectors', () => { params: {}, }, ], - }, + }), ]; const successResults = [ { @@ -535,7 +487,7 @@ describe('importRuleActionConnectors', () => { name: 'Query with a rule id', query: 'user.name: root or user.name: admin', risk_score: 55, - rule_id: 'rule-1', + rule_id: 'rule_1', severity: 'high', type: 'query', }, @@ -569,4 +521,142 @@ describe('importRuleActionConnectors', () => { rulesWithMigratedActions, }); }); + + describe('overwrite is set to "true"', () => { + it('should return an error when action connectors are missing in ndjson import file', async () => { + const rulesToImport = [ + getImportRulesSchemaMock({ + rule_id: 'rule-with-missed-action-connector', + actions: [ + { + group: 'default', + id: 'some-connector-id', + params: {}, + action_type_id: '.webhook', + }, + ], + }), + ]; + + actionsClient.getAll.mockResolvedValue([]); + + const res = await importRuleActionConnectors({ + actionConnectors: [], + actionsClient, + actionsImporter: core.savedObjects.getImporter(), + rules: rulesToImport, + overwrite: true, + }); + + expect(res).toEqual({ + success: false, + successCount: 0, + errors: [ + { + error: { + message: '1 connector is missing. Connector id missing is: some-connector-id', + status_code: 404, + }, + id: 'some-connector-id', + rule_id: 'rule-with-missed-action-connector', + }, + ], + warnings: [], + }); + }); + + it('should NOT return an error when a missing action connector in ndjson import file is a preconfigured one', async () => { + const rulesToImport = [ + getImportRulesSchemaMock({ + rule_id: 'rule-with-missed-action-connector', + actions: [ + { + group: 'default', + id: 'prebuilt-connector-id', + params: {}, + action_type_id: '.webhook', + }, + ], + }), + ]; + + actionsClient.getAll.mockResolvedValue([ + { + actionTypeId: '.webhook', + name: 'webhook', + isPreconfigured: true, + id: 'prebuilt-connector-id', + referencedByCount: 1, + isDeprecated: false, + isSystemAction: false, + }, + ]); + + const res = await importRuleActionConnectors({ + actionConnectors: [], + actionsClient, + actionsImporter: core.savedObjects.getImporter(), + rules: rulesToImport, + overwrite: true, + }); + + expect(res).toEqual({ + success: true, + successCount: 0, + errors: [], + warnings: [], + }); + }); + + it('should not skip importing the action-connectors if all connectors have been imported/created before', async () => { + const rulesToImport = [ + getImportRulesSchemaMock({ + actions: [ + { + group: 'default', + id: 'connector-id', + action_type_id: '.webhook', + params: {}, + }, + ], + }), + ]; + + core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({ + import: jest.fn().mockResolvedValue({ + success: true, + successCount: 1, + errors: [], + warnings: [], + }), + }); + + actionsClient.getAll.mockResolvedValue([ + { + actionTypeId: '.webhook', + name: 'webhook', + isPreconfigured: true, + id: 'connector-id', + referencedByCount: 1, + isDeprecated: false, + isSystemAction: false, + }, + ]); + + const res = await importRuleActionConnectors({ + actionConnectors, + actionsClient, + actionsImporter: core.savedObjects.getImporter(), + rules: rulesToImport, + overwrite: true, + }); + + expect(res).toEqual({ + success: true, + successCount: 0, + errors: [], + warnings: [], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts index dfbacdcfd8ce2..db86ad158289c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts @@ -8,6 +8,8 @@ import { Readable } from 'stream'; import type { SavedObjectsImportResponse } from '@kbn/core-saved-objects-common'; import type { SavedObject } from '@kbn/core-saved-objects-server'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types'; import type { RuleToImport } from '../../../../../../../common/api/detection_engine/rule_management'; import type { WarningSchema } from '../../../../../../../common/api/detection_engine'; @@ -22,6 +24,13 @@ import { } from './utils'; import type { ImportRuleActionConnectorsParams, ImportRuleActionConnectorsResult } from './types'; +const NO_ACTION_RESULT = { + success: true, + errors: [], + successCount: 0, + warnings: [], +}; + export const importRuleActionConnectors = async ({ actionConnectors, actionsClient, @@ -30,41 +39,40 @@ export const importRuleActionConnectors = async ({ overwrite, }: ImportRuleActionConnectorsParams): Promise => { try { - const actionConnectorRules = getActionConnectorRules(rules); - const actionsIds: string[] = Object.keys(actionConnectorRules); + const connectorIdToRuleIdsMap = getActionConnectorRules(rules); + const referencedConnectorIds = await filterOutPreconfiguredConnectors( + actionsClient, + Object.keys(connectorIdToRuleIdsMap) + ); - if (!actionsIds.length) - return { - success: true, - errors: [], - successCount: 0, - warnings: [], - }; + if (!referencedConnectorIds.length) { + return NO_ACTION_RESULT; + } - if (overwrite && !actionConnectors.length) - return handleActionsHaveNoConnectors(actionsIds, actionConnectorRules); + if (overwrite && !actionConnectors.length) { + return handleActionsHaveNoConnectors(referencedConnectorIds, connectorIdToRuleIdsMap); + } let actionConnectorsToImport: SavedObject[] = actionConnectors; if (!overwrite) { - const newIdsToAdd = await filterExistingActionConnectors(actionsClient, actionsIds); + const newIdsToAdd = await filterExistingActionConnectors( + actionsClient, + referencedConnectorIds + ); const foundMissingConnectors = checkIfActionsHaveMissingConnectors( actionConnectors, newIdsToAdd, - actionConnectorRules + connectorIdToRuleIdsMap ); if (foundMissingConnectors) return foundMissingConnectors; // filter out existing connectors actionConnectorsToImport = actionConnectors.filter(({ id }) => newIdsToAdd.includes(id)); } - if (!actionConnectorsToImport.length) - return { - success: true, - errors: [], - successCount: 0, - warnings: [], - }; + if (!actionConnectorsToImport.length) { + return NO_ACTION_RESULT; + } const readStream = Readable.from(actionConnectorsToImport); const { success, successCount, successResults, warnings, errors }: SavedObjectsImportResponse = @@ -93,3 +101,25 @@ export const importRuleActionConnectors = async ({ return returnErroredImportResult(error); } }; + +async function fetchPreconfiguredActionConnectors( + actionsClient: ActionsClient +): Promise { + const knownConnectors = await actionsClient.getAll(); + + return knownConnectors.filter((c) => c.isPreconfigured); +} + +async function filterOutPreconfiguredConnectors( + actionsClient: ActionsClient, + connectorsIds: string[] +): Promise { + if (connectorsIds.length === 0) { + return []; + } + + const preconfiguredActionConnectors = await fetchPreconfiguredActionConnectors(actionsClient); + const preconfiguredActionConnectorIds = new Set(preconfiguredActionConnectors.map((c) => c.id)); + + return connectorsIds.filter((id) => !preconfiguredActionConnectorIds.has(id)); +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts index 2b7996c5094ab..2a249e7d9383a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts @@ -13,7 +13,7 @@ describe('checkRuleExceptionReferences', () => { it('returns empty array if rule has no exception list references', () => { const result = checkRuleExceptionReferences({ existingLists: {}, - rule: { ...getImportRulesSchemaMock(), exceptions_list: [] }, + rule: getImportRulesSchemaMock({ exceptions_list: [] }), }); expect(result).toEqual([[], []]); @@ -29,12 +29,11 @@ describe('checkRuleExceptionReferences', () => { type: 'detection', }, }, - rule: { - ...getImportRulesSchemaMock(), + rule: getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), }); expect(result).toEqual([ @@ -53,12 +52,11 @@ describe('checkRuleExceptionReferences', () => { it('removes an exception reference if list not found to exist', () => { const result = checkRuleExceptionReferences({ existingLists: {}, - rule: { - ...getImportRulesSchemaMock(), + rule: getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), }); expect(result).toEqual([ @@ -86,12 +84,11 @@ describe('checkRuleExceptionReferences', () => { type: 'detection', }, }, - rule: { - ...getImportRulesSchemaMock(), + rule: getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), }); expect(result).toEqual([ [ @@ -118,12 +115,11 @@ describe('checkRuleExceptionReferences', () => { type: 'endpoint', }, }, - rule: { - ...getImportRulesSchemaMock(), + rule: getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), }); expect(result).toEqual([ [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts index df136fe6cfc8d..1605c745256b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts @@ -53,12 +53,11 @@ describe('get referenced exceptions', () => { it('returns found referenced exception lists', async () => { const result = await getReferencedExceptionLists({ rules: [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ], savedObjectsClient, }); @@ -77,16 +76,14 @@ describe('get referenced exceptions', () => { it('returns found referenced exception lists when first exceptions list is empty array and second list has a value', async () => { const result = await getReferencedExceptionLists({ rules: [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [], - }, - { - ...getImportRulesSchemaMock(), + }), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ], savedObjectsClient, }); @@ -105,18 +102,16 @@ describe('get referenced exceptions', () => { it('returns found referenced exception lists when two rules reference same list', async () => { const result = await getReferencedExceptionLists({ rules: [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, - { - ...getImportRulesSchemaMock(), + }), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ], savedObjectsClient, }); @@ -157,18 +152,16 @@ describe('get referenced exceptions', () => { const result = await getReferencedExceptionLists({ rules: [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [ { id: '456', list_id: 'other-list', namespace_type: 'single', type: 'detection' }, ], - }, - { - ...getImportRulesSchemaMock(), + }), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ], savedObjectsClient, }); @@ -207,45 +200,38 @@ describe('get referenced exceptions', () => { describe('parseReferencdedExceptionsLists', () => { it('should return parsed lists when exception lists are not empty', () => { const res = parseReferencedExceptionsLists([ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ]); expect(res).toEqual([[], [{ listId: 'my-list', namespaceType: 'single' }]]); }); it('should return parsed lists when one empty exception list and one non-empty list', () => { const res = parseReferencedExceptionsLists([ - { - ...getImportRulesSchemaMock(), - exceptions_list: [], - }, - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [] }), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ]); expect(res).toEqual([[], [{ listId: 'my-list', namespaceType: 'single' }]]); }); it('should return parsed lists when two non-empty exception lists reference same list', () => { const res = parseReferencedExceptionsLists([ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, - { - ...getImportRulesSchemaMock(), + }), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ]); expect(res).toEqual([ [], @@ -258,18 +244,16 @@ describe('get referenced exceptions', () => { it('should return parsed lists when two non-empty exception lists reference differet lists', () => { const res = parseReferencedExceptionsLists([ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ exceptions_list: [ { id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' }, ], - }, - { - ...getImportRulesSchemaMock(), + }), + getImportRulesSchemaMock({ exceptions_list: [ { id: '456', list_id: 'other-list', namespace_type: 'single', type: 'detection' }, ], - }, + }), ]); expect(res).toEqual([ [], 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 0b601be81dd62..5b097bacf2d9c 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 @@ -74,14 +74,7 @@ describe('importRules', () => { it('creates rule if no matching existing rule found', async () => { const result = await importRules({ - ruleChunks: [ - [ - { - ...getImportRulesSchemaMock(), - rule_id: 'rule-1', - }, - ], - ], + ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]], rulesResponseAcc: [], mlAuthz, overwriteRules: false, @@ -98,14 +91,7 @@ describe('importRules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const result = await importRules({ - ruleChunks: [ - [ - { - ...getImportRulesSchemaMock(), - rule_id: 'rule-1', - }, - ], - ], + ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]], rulesResponseAcc: [], mlAuthz, overwriteRules: false, @@ -129,10 +115,9 @@ describe('importRules', () => { const result = await importRules({ ruleChunks: [ [ - { - ...getImportRulesSchemaMock(), + getImportRulesSchemaMock({ rule_id: 'rule-1', - }, + }), ], ], rulesResponseAcc: [], @@ -151,14 +136,7 @@ describe('importRules', () => { clients.rulesClient.find.mockRejectedValue(new Error('error reading rule')); const result = await importRules({ - ruleChunks: [ - [ - { - ...getImportRulesSchemaMock(), - rule_id: 'rule-1', - }, - ], - ], + ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]], rulesResponseAcc: [], mlAuthz, overwriteRules: true, @@ -183,14 +161,7 @@ describe('importRules', () => { (createRules as jest.Mock).mockRejectedValue(new Error('error creating rule')); const result = await importRules({ - ruleChunks: [ - [ - { - ...getImportRulesSchemaMock(), - rule_id: 'rule-1', - }, - ], - ], + ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]], rulesResponseAcc: [], mlAuthz, overwriteRules: false, @@ -214,14 +185,7 @@ describe('importRules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const result = await importRules({ - ruleChunks: [ - [ - { - ...getImportRulesSchemaMock(), - rule_id: 'rule-1', - }, - ], - ], + ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]], rulesResponseAcc: [], mlAuthz, overwriteRules: true, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_connectors.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_connectors.ts new file mode 100644 index 0000000000000..0dcda3af45510 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_connectors.ts @@ -0,0 +1,517 @@ +/* + * 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 { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createConnector, deleteConnector, getConnector } from '../../utils/connectors'; +import { combineToNdJson, deleteAllRules, getCustomQueryRuleParams } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const log = getService('log'); + + 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 = { + id: CONNECTOR_ID, + type: 'action', + updated_at: '2024-02-05T11:52:10.692Z', + created_at: '2024-02-05T11:52:10.692Z', + version: 'WzYsMV0=', + attributes: { + actionTypeId: '.email', + name: 'test-connector', + isMissingSecrets: false, + config: { + from: 'a@test.com', + service: 'other', + host: 'example.com', + port: 123, + secure: false, + hasAuth: false, + tenantId: null, + clientId: null, + oauthTokenUrl: null, + }, + secrets: {}, + }, + references: [], + managed: false, + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.3.0', + }; + + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteConnector(supertest, CONNECTOR_ID); + await deleteConnector(supertest, ANOTHER_CONNECTOR_ID); + }); + + describe('overwrite connectors is set to "false"', () => { + it('imports a rule with an action connector', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: CONNECTOR_ID, + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }), + CUSTOM_ACTION_CONNECTOR + ); + + 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); + + expect(body).toMatchObject({ + errors: [], + success: true, + success_count: 1, + rules_count: 1, + action_connectors_success: true, + action_connectors_success_count: 1, + action_connectors_errors: [], + action_connectors_warnings: [], + }); + + expect(await getConnector(supertest, CONNECTOR_ID)).toMatchObject({ + id: CONNECTOR_ID, + name: 'test-connector', + }); + }); + + it('DOES NOT import an action connector without rules', async () => { + const ndjson = combineToNdJson(CUSTOM_ACTION_CONNECTOR); + + 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); + + expect(body).toMatchObject({ + errors: [], + success: true, + success_count: 0, + rules_count: 0, + action_connectors_success: true, + action_connectors_success_count: 0, + action_connectors_errors: [], + action_connectors_warnings: [], + }); + + await supertest + .get(`/api/actions/connector/${CONNECTOR_ID}`) + .set('kbn-xsrf', 'foo') + .expect(404); + }); + + it('DOES NOT import an action connector when there are no rules referencing it', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: ANOTHER_CONNECTOR_ID, + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }), + { ...CUSTOM_ACTION_CONNECTOR, id: ANOTHER_CONNECTOR_ID }, + CUSTOM_ACTION_CONNECTOR + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .attach('file', Buffer.from(ndjson), 'rules.ndjson') + .expect(200); + + expect(body).toMatchObject({ + errors: [], + success: true, + success_count: 1, + rules_count: 1, + action_connectors_success: true, + action_connectors_success_count: 1, + action_connectors_errors: [], + action_connectors_warnings: [], + }); + + await supertest + .get(`/api/actions/connector/${CONNECTOR_ID}`) + .set('kbn-xsrf', 'foo') + .expect(404); + }); + + it('DOES NOT return an error when rule actions reference a preconfigured connector', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: 'my-test-email', + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }) + ); + + 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); + + expect(body).toMatchObject({ + errors: [], + success: true, + success_count: 1, + rules_count: 1, + action_connectors_success: true, + action_connectors_success_count: 0, + action_connectors_errors: [], + action_connectors_warnings: [], + }); + }); + + /** + * When importing an action connector, if its `id` matches with an existing one, the type and config isn't checked. + * In fact, the connector being imported can have a different type and configuration, and its creation will be skipped. + */ + it('skips importing already existing action connectors', async () => { + await createConnector( + supertest, + { + connector_type_id: '.webhook', + name: 'test-connector', + config: { + // checkout `x-pack/test/security_solution_api_integration/config/ess/config.base.ts` for configuration + // `some.non.existent.com` must be set as an allowed host + url: 'https://some.non.existent.com', + method: 'post', + }, + secrets: {}, + }, + CONNECTOR_ID + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: CONNECTOR_ID, + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }), + CUSTOM_ACTION_CONNECTOR + ); + + 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); + + expect(body).toMatchObject({ + errors: [], + success: true, + success_count: 1, + rules_count: 1, + action_connectors_success: true, + action_connectors_success_count: 0, + action_connectors_errors: [], + action_connectors_warnings: [], + }); + + expect(await getConnector(supertest, CONNECTOR_ID)).toMatchObject({ + id: CONNECTOR_ID, + name: 'test-connector', + }); + }); + + it('returns an error when connector is missing in ndjson', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: CONNECTOR_ID, + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }) + ); + + 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); + + expect(body).toMatchObject({ + errors: [ + { + error: { + message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`, + status_code: 404, + }, + id: CONNECTOR_ID, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 0, + rules_count: 1, + action_connectors_success: false, + action_connectors_success_count: 0, + action_connectors_errors: [ + { + error: { + message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`, + status_code: 404, + }, + id: CONNECTOR_ID, + rule_id: 'rule-1', + }, + ], + action_connectors_warnings: [], + }); + }); + }); + + describe('overwrite connectors is set to "true"', () => { + it('overwrites existing connector', async () => { + await createConnector( + supertest, + { + connector_type_id: '.webhook', + name: 'existing-connector', + config: { + // checkout `x-pack/test/security_solution_api_integration/config/ess/config.base.ts` for configuration + // `some.non.existent.com` must be set as an allowed host + url: 'https://some.non.existent.com', + method: 'post', + }, + secrets: {}, + }, + CONNECTOR_ID + ); + + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: CONNECTOR_ID, + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }), + { + ...CUSTOM_ACTION_CONNECTOR, + attributes: { ...CUSTOM_ACTION_CONNECTOR.attributes, name: 'updated-connector' }, + } + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite_action_connectors=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, + action_connectors_success: true, + action_connectors_success_count: 1, + action_connectors_errors: [], + action_connectors_warnings: [], + }); + + expect(await getConnector(supertest, CONNECTOR_ID)).toMatchObject({ + id: CONNECTOR_ID, + name: 'updated-connector', + }); + }); + + it('returns an error when connector is missing in ndjson', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: CONNECTOR_ID, + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite_action_connectors=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: [ + { + error: { + message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`, + status_code: 404, + }, + id: CONNECTOR_ID, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 0, + rules_count: 1, + action_connectors_success: false, + action_connectors_success_count: 0, + action_connectors_errors: [ + { + error: { + message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`, + status_code: 404, + }, + id: CONNECTOR_ID, + rule_id: 'rule-1', + }, + ], + action_connectors_warnings: [], + }); + }); + + it('DOES NOT return an error when rule actions reference a preconfigured connector', async () => { + const ndjson = combineToNdJson( + getCustomQueryRuleParams({ + rule_id: 'rule-1', + name: 'Rule 1', + actions: [ + { + group: 'default', + id: 'my-test-email', + params: { + message: 'Some message', + to: ['test@test.com'], + subject: 'Test', + }, + action_type_id: '.email', + uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + }, + ], + }) + ); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite_action_connectors=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, + action_connectors_success: true, + action_connectors_success_count: 0, + action_connectors_errors: [], + action_connectors_warnings: [], + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/combine_to_ndjson.ts b/x-pack/test/detection_engine_api_integration/utils/combine_to_ndjson.ts new file mode 100644 index 0000000000000..fc2baff9c365f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/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/detection_engine_api_integration/utils/connectors/create_connector.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/create_connector.ts new file mode 100644 index 0000000000000..9c3f54e019653 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/connectors/create_connector.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; + +interface CreateConnectorBody { + readonly name: string; + readonly config: Record; + readonly connector_type_id: string; + readonly secrets: Record; +} + +export async function createConnector( + supertest: SuperTest.SuperTest, + connector: CreateConnectorBody, + id = '' +): Promise { + await supertest + .post(`/api/actions/connector/${id}`) + .set('kbn-xsrf', 'foo') + .send(connector) + .expect(200); +} diff --git a/x-pack/test/detection_engine_api_integration/utils/connectors/delete_connector.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/delete_connector.ts new file mode 100644 index 0000000000000..683f845fd8bf8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/connectors/delete_connector.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; + +export function deleteConnector( + supertest: SuperTest.SuperTest, + connectorId: string +): SuperTest.Test { + return supertest.delete(`/api/actions/connector/${connectorId}`).set('kbn-xsrf', 'foo'); +} diff --git a/x-pack/test/detection_engine_api_integration/utils/connectors/get_connector.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/get_connector.ts new file mode 100644 index 0000000000000..8f7e4830372f9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/connectors/get_connector.ts @@ -0,0 +1,21 @@ +/* + * 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 { Connector } from '@kbn/actions-plugin/server/application/connector/types'; +import type SuperTest from 'supertest'; + +export async function getConnector( + supertest: SuperTest.SuperTest, + connectorId: string +): Promise { + const response = await supertest + .get(`/api/actions/connector/${connectorId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + return response.body; +} diff --git a/x-pack/test/detection_engine_api_integration/utils/connectors/index.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/index.ts new file mode 100644 index 0000000000000..be89cd4a94d47 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/connectors/index.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 * from './create_connector'; +export * from './get_connector'; +export * from './delete_connector'; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_custom_query_rule_params.ts b/x-pack/test/detection_engine_api_integration/utils/get_custom_query_rule_params.ts new file mode 100644 index 0000000000000..d4773a4f7e516 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_custom_query_rule_params.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine'; + +type CreateRulePropsRewrites = Partial>; + +/** + * Returns custom query rule params that is easy for most basic testing of output of alerts. + * It starts out in an disabled state. The 'from' is set very far back to test the basics of signal + * creation and testing by getting all the signals at once. + * + * @param rewrites rule params rewrites, see QueryRuleCreateProps for possible fields + */ +export function getCustomQueryRuleParams( + rewrites?: CreateRulePropsRewrites +): QueryRuleCreateProps { + return { + type: 'query', + query: '*:*', + name: 'Custom query rule', + description: 'Custom query rule description', + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + index: ['logs-*'], + interval: '100m', + from: 'now-6m', + enabled: false, + ...rewrites, + }; +} diff --git a/x-pack/test/detection_engine_api_integration/utils/index.ts b/x-pack/test/detection_engine_api_integration/utils/index.ts index 47823f76ea6b3..7a17f692540ce 100644 --- a/x-pack/test/detection_engine_api_integration/utils/index.ts +++ b/x-pack/test/detection_engine_api_integration/utils/index.ts @@ -89,3 +89,5 @@ export * from './prebuilt_rules/install_mock_prebuilt_rules'; export * from './prebuilt_rules/install_prebuilt_rules_and_timelines'; export * from './get_legacy_action_so'; export * from './delete_all_exceptions'; +export * from './combine_to_ndjson'; +export * from './get_custom_query_rule_params'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts index 115c8dbeab1f2..f57649c8ebbb0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts @@ -11,6 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./create_alerts_migrations')); loadTestFile(require.resolve('./delete_alerts_migrations')); loadTestFile(require.resolve('./finalize_alerts_migrations')); - loadTestFile(require.resolve('./finalize_alerts_migrations')); + loadTestFile(require.resolve('./get_alerts_migration_status')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/combine_to_ndjson.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/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/utils/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/utils/connectors/create_connector.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts new file mode 100644 index 0000000000000..9c3f54e019653 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; + +interface CreateConnectorBody { + readonly name: string; + readonly config: Record; + readonly connector_type_id: string; + readonly secrets: Record; +} + +export async function createConnector( + supertest: SuperTest.SuperTest, + connector: CreateConnectorBody, + id = '' +): Promise { + await supertest + .post(`/api/actions/connector/${id}`) + .set('kbn-xsrf', 'foo') + .send(connector) + .expect(200); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/delete_connector.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/delete_connector.ts new file mode 100644 index 0000000000000..683f845fd8bf8 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/delete_connector.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; + +export function deleteConnector( + supertest: SuperTest.SuperTest, + connectorId: string +): SuperTest.Test { + return supertest.delete(`/api/actions/connector/${connectorId}`).set('kbn-xsrf', 'foo'); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_connector.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_connector.ts new file mode 100644 index 0000000000000..8f7e4830372f9 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_connector.ts @@ -0,0 +1,21 @@ +/* + * 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 { Connector } from '@kbn/actions-plugin/server/application/connector/types'; +import type SuperTest from 'supertest'; + +export async function getConnector( + supertest: SuperTest.SuperTest, + connectorId: string +): Promise { + const response = await supertest + .get(`/api/actions/connector/${connectorId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + return response.body; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/index.ts new file mode 100644 index 0000000000000..be89cd4a94d47 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/index.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 * from './create_connector'; +export * from './get_connector'; +export * from './delete_connector'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts index afcdf130392f1..c16c3dc1f3674 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts @@ -25,3 +25,4 @@ export * from './get_stats'; export * from './get_detection_metrics_from_body'; export * from './get_stats_url'; export * from './retry'; +export * from './combine_to_ndjson';