From 40f6628c220217fa5bebcc546d21730ccf754d90 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 7 Jan 2025 09:52:07 +0100 Subject: [PATCH] [Security Solution] Add Threat Match rule specific editable fields (#200308) **Partially addresses:** https://github.com/elastic/kibana/issues/171520 ## Summary This PR adds is built on top of https://github.com/elastic/kibana/pull/193828 and https://github.com/elastic/kibana/pull/196948 and adds the following editable components for Threat Match rule type - threat_index - threat_query - threat_mapping - threat_indicator_path - ~~threat_language~~ `threat_language` was merged with `threat_query` ## Details This PR make a set of changes to make existing Threat Match form fields easily reusable as editable components and type safe when used in forms. In particular the following was done - Fixes a bug blocking Threat Match rules upgrading - Existing functionality was refactored to have reusable self-contained editable components for `threat_index`, `threat_query`, `threat_mapping` and `threat_indicator_path` rule fields - `threat_language` was removed since query type is included in `threat_query` field and can be edited with Query Bar - threat mapping input was split into separate component for individual fields to be reused - `ThreatMatchComponent` was refactored to be a controlled component instead of uncontrolled `ThreatMatchComponent` has a feature preventing users removing the single last entry. Instead deleting the last entry the delete button clears inputs. That functionality didn't work properly in Prebuilt Rule Customization workflow and rule creation/editing forms after creating a reusable `ThreatMappingEdit` component. Instead of trying to find a tricky fix `ThreatMatchComponent` was refactored to remove internal state. The feature preventing users removing the single last entry was reimplemented in `ThreatMappingEdit` component. - Fixes a bug reproducible in `main` where validation errors duplicated described in a [comment](https://github.com/elastic/kibana/pull/200308#discussion_r1869385209) - Fixes a bug reproducible in `main` allowing to save unknown source indices or indicator indices fields described in a [comment](https://github.com/elastic/kibana/pull/200308#discussion_r1869412952) ## How to test - Ensure the `prebuiltRulesCustomizationEnabled` feature flag is enabled - Allow internal APIs via adding `server.restrictInternalApis: false` to `kibana.dev.yaml` - Clear Elasticsearch data - Run Elasticsearch and Kibana locally (do not open Kibana in a web browser) - Install an outdated version of the `security_detection_engine` Fleet package ```bash curl -X POST --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H "elastic-api-version: 2023-10-31" -d '{"force":true}' http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1 ``` - Install prebuilt rules ```bash curl -X POST --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H "elastic-api-version: 1" -d '{"mode":"ALL_RULES"}' http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform ``` - Open a `threat_match` rule for editing. For example `Threat Intel Hash Indicator Match` with rule_id `aab184d3-72b3-4639-b242-6597c99d8bca`. - Edit `Indicator index patterns`, `Indicator index query` and/or `Indicator filters`, `Indicator mapping` and `Indicator prefix override` fields - Open `Detection Rules (SIEM)` Page -> `Rule Updates` -> click on `Threat Intel Hash Indicator Match` rule -> expand each Threat Match rule type specific field -> press `Edit` button ## Screenshots Threat Match Query edit component image Threat Match Index edit component image Threat Match Mapping edit component image Threat Match Indicator Path edit component image Threat Match Mapping unknown field names validation warnings Screenshot 2024-12-18 at 12 45 41 Screenshot 2024-12-18 at 12 45 53 Screenshot 2024-12-18 at 12 47 05 Screenshot 2024-12-18 at 12 47 15 --- .../translations/translations/fr-FR.json | 11 - .../translations/translations/ja-JP.json | 11 - .../translations/translations/zh-CN.json | 11 - .../split_accordion/split_accordion.tsx | 2 +- .../components/threat_match/helpers.test.tsx | 163 +++++----- .../components/threat_match/helpers.tsx | 79 ++--- .../components/threat_match/index.test.tsx | 285 +++++++++++++----- .../common/components/threat_match/index.tsx | 161 ++++------ .../components/threat_match/reducer.test.ts | 111 ------- .../common/components/threat_match/reducer.ts | 53 ---- .../optional_field_label/index.test.tsx | 0 .../components/optional_field_label/index.tsx | 0 .../related_integrations.tsx | 16 +- .../related_integrations/translations.ts | 7 - .../required_fields/required_fields.tsx | 7 +- .../required_fields/translations.ts | 7 - .../threat_match_index_edit/index.ts | 8 + .../threat_match_index_edit.tsx | 38 +++ .../threat_match_index_selector_field.tsx | 65 ++++ .../threat_match_index_edit/translations.tsx | 29 ++ .../forbidden_index_pattern_validator.ts | 23 ++ ...hreat_index_patterns_required_validator.ts | 13 + .../validators/translations.ts | 22 ++ .../threat_match_indicator_path_edit/index.ts | 8 + .../threat_match_indicator_path_edit.tsx | 55 ++++ .../translations.tsx | 30 ++ .../threat_match_mapping_edit/index.tsx | 9 + .../threat_match_mapping_edit.tsx | 54 ++++ .../threat_match_mapping_field.tsx | 83 +++++ .../translations.tsx | 15 + .../validators/error_codes.ts | 11 + ...nknown_threat_match_mapping_field_names.ts | 51 ++++ .../threat_match_mapping_validator_factory.ts | 116 +++++++ .../threat_match_query_edit/index.ts | 8 + .../threat_match_query_edit.tsx | 56 ++++ .../threat_match_query_edit/translations.tsx | 15 + .../threat_match_query_required_validator.ts | 27 ++ .../validators/translations.ts | 15 + .../constants/validation_warning_codes.ts | 10 + .../components/description_step/helpers.tsx | 78 ++--- .../description_step/index.test.tsx | 52 +++- .../components/description_step/index.tsx | 31 +- .../components/description_step/types.ts | 1 + .../components/step_about_rule/index.test.tsx | 2 +- .../components/step_about_rule/index.tsx | 21 +- .../components/step_about_rule/schema.tsx | 57 +--- .../step_define_rule/index.test.tsx | 2 - .../components/step_define_rule/index.tsx | 72 +---- .../components/step_define_rule/schema.tsx | 123 +------- .../step_define_rule/translations.tsx | 21 -- .../use_persistent_threat_match_state.ts | 58 ++++ .../components/threat_match_edit.tsx | 51 ++++ .../components/threatmatch_input/index.tsx | 102 +++---- .../rule_creation_ui/pages/form.tsx | 12 +- .../pages/rule_creation/index.tsx | 10 +- .../pages/rule_editing/index.tsx | 17 +- .../threat_match_mapping_validator.ts | 50 +++ .../rule_details/rule_definition_section.tsx | 18 +- .../fields/data_source/index_pattern_edit.tsx | 11 +- .../threat_index_indicator_path/index.ts | 8 + .../indicator_path_edit_adapter.tsx | 13 + .../indicator_path_edit_form.tsx | 21 ++ .../fields/threat_match_index/index.ts | 8 + .../threat_match_index_edit_adapter.tsx | 13 + .../threat_match_index_edit_form.tsx | 21 ++ .../fields/threat_match_mapping/index.ts | 8 + .../threat_match_mapping_edit_adapter.tsx | 36 +++ .../threat_match_mapping_edit_form.tsx | 21 ++ .../fields/threat_match_query/index.ts | 8 + .../threat_match_query_edit_adapter.tsx | 34 +++ .../threat_match_query_edit_form.tsx | 73 +++++ .../threat_match_rule_field_edit.tsx | 19 +- .../pages/detection_engine/rules/utils.ts | 3 +- .../diffable_rule_fields_mappings.ts | 2 +- .../rule_creation/indicator_match_rule.cy.ts | 5 +- .../cypress/screens/create_new_rule.ts | 8 +- .../cypress/tasks/create_new_rule.ts | 4 + 77 files changed, 1812 insertions(+), 967 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.test.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.ts rename x-pack/solutions/security/plugins/security_solution/public/detection_engine/{rule_creation_ui => rule_creation}/components/optional_field_label/index.test.tsx (100%) rename x-pack/solutions/security/plugins/security_solution/public/detection_engine/{rule_creation_ui => rule_creation}/components/optional_field_label/index.tsx (100%) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_edit.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_selector_field.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/translations.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/forbidden_index_pattern_validator.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/threat_index_patterns_required_validator.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/threat_match_indicator_path_edit.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/translations.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/index.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_field.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/translations.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/error_codes.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/get_unknown_threat_match_mapping_field_names.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator_factory.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/threat_match_query_edit.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/translations.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/threat_match_query_required_validator.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threat_match_state.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threat_match_edit.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/threat_match_mapping_validator.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_adapter.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_form.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_adapter.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_form.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_adapter.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_form.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_adapter.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_form.tsx diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 10c1b068b8561..b50e86caf21ac 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -37573,8 +37573,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideLabel": "Remplacement du nom de règle", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText": "Saisissez une ou plusieurs balises d'identification personnalisées pour cette règle. Appuyez sur Entrée après chaque balise pour en ajouter une nouvelle.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsLabel": "Balises", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText": "Spécifiez le préfixe de document contenant vos champs d'indicateur. Utilisé pour l'enrichissement des alertes de correspondance d'indicateur.", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel": "Remplacement du préfixe d'indicateur", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText": "Sélectionnez les champs sur lesquels effectuer le regroupement. Les champs sont joints entre eux par \"AND\"", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel": "Regrouper par", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel": "Seuil", @@ -37592,7 +37590,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "Fournissez des instructions sur les conditions préalables à la règle, telles que les intégrations requises, les étapes de configuration et tout ce qui est nécessaire au bon fonctionnement de la règle.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "Guide de configuration", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "Une balise ne doit pas être vide", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "Le remplacement du préfixe d'indicateur ne peut pas être vide.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addCustomHighlightedFieldDescription": "Ajouter un champ en surbrillance personnalisé", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription": "Ajouter un exemple de faux positif", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "Ajouter une URL de référence", @@ -37629,9 +37626,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel": "Type de règle", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldShouldLoadQueryDynamicallyLabel": "Charger la requête enregistrée \"{savedQueryName}\" de façon dynamique dans chaque exécution de règle", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldShouldLoadQueryDynamicallyLabelWithoutName": "Charger la requête enregistrée de façon dynamique dans chaque exécution de règle", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel": "Modèles d'indexation d'indicateur", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel": "Mapping d'indicateur", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel": "Requête d'index d'indicateur", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel": "Compte", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel": "Valeurs uniques", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "Sélectionner un champ pour vérifier la cardinalité", @@ -37673,9 +37667,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "Requête enregistrée", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "Source", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.Su.perRuleExecutionWarning": "L'option d'exécution par règles n'est pas disponible pour le type de règle Seuil", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchIndexForbiddenError": "Le modèle d'indexation ne peut pas être { forbiddenString }. Veuillez choisir un modèle d'indexation plus spécifique.", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "Sélectionner des index de menaces", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "Au minimum un modèle d'indexation est requis.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "Tous les résultats", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.docsLinkText": "En savoir plus", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage": "{key} n'est pas un modèle de moustache valide", @@ -38493,7 +38484,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationsLink": "intégrations", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationVersion": "Version", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.notInstalledText": "Non installé", - "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.optionalText": "Facultatif", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationAriaLabel": "Sélecteur d'intégrations", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyAriaLabel": "Contrainte de version d'intégration associée", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyPlaceholder": "Semver", @@ -38511,7 +38501,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.generalWarningDescription": "Cela n'interdit pas l'exécution de la règle, mais cela peut indiquer qu'un champ requis n'a pas été correctement paramétré. Veuillez vérifier que les index spécifiés dans la {source} de la règle existent, et que les types et champs attendus sont dans le mapping.", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.generalWarningTitle": "Certains champs sont introuvables dans les modèles d'index spécifiés par la règle.", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.openHelpPopoverAriaLabel": "Ouvrir une fenêtre contextuelle d'aide", - "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.optionalText": "Facultatif", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.removeRequiredFieldButtonAriaLabel": "Supprimer le champ obligatoire", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.requiredFieldsLabel": "Champ requis", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.validation.fieldNameRequired": "Le nom de champ est requis", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index e1af9598ec881..199ec0b9d8054 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -37431,8 +37431,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideLabel": "ルール名無効化", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText": "このルールの1つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsLabel": "タグ", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText": "インジケーターフィールドを含むドキュメントプレフィックスを指定します。インジケーター一致アラートの強化で使用されます。", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel": "インジケータープレフィックスの無効化", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText": "グループ化するフィールドを選択します。フィールドは「AND」を使用して結合されます", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel": "グループ分けの条件", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel": "しきい値", @@ -37450,7 +37448,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "必要な統合、構成ステップ、ルールが正常に動作するために必要な他のすべての項目といった、ルール前提条件に関する指示を入力します。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "セットアップガイド", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "タグを空にすることはできません", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "インジケータープレフィックスの無効化を空にすることはできません", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addCustomHighlightedFieldDescription": "カスタムハイライトされたフィールドを追加", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription": "誤検出の例を追加します", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "参照URLを追加します", @@ -37487,9 +37484,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel": "ルールタイプ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldShouldLoadQueryDynamicallyLabel": "各ルールの実行時に、保存されたクエリー\"{savedQueryName}\"を動的に読み込みます", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldShouldLoadQueryDynamicallyLabelWithoutName": "各ルールの実行時に、保存されたクエリを動的に読み込みます", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel": "インジケーターインデックスパターン", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel": "インジケーターマッピング", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel": "インジケーターインデックスクエリ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel": "カウント", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel": "一意の値", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "カーディナリティを確認するフィールドを選択します", @@ -37531,9 +37525,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "保存されたクエリ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "送信元", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.Su.perRuleExecutionWarning": "しきい値ルールタイプでは、ルール実行単位オプションは使用できません。", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchIndexForbiddenError": "インデックスパターンを{ forbiddenString }にすることはできません。特定のインデックスパターンを選択してください。", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "脅威インデックスを選択", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "インデックスパターンが最低1つ必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "すべての結果", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.docsLinkText": "詳細", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage": "{key}は有効なmustacheテンプレートではありません", @@ -38350,7 +38341,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationsLink": "統合", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationVersion": "Version", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.notInstalledText": "未インストール", - "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.optionalText": "オプション", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationAriaLabel": "統合セレクター", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyAriaLabel": "関連する統合バージョン制約", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyPlaceholder": "Semver", @@ -38368,7 +38358,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.generalWarningDescription": "これはルール実行に影響しませんが、必須フィールドが間違って設定されていることを示している可能性があります。ルールの{source}で指定されたインデックスが存在し、マッピングで想定されたフィールドと型になっていることを確認してください。", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.generalWarningTitle": "一部のフィールドが、ルールの指定されたインデックスパターン内で見つかりません", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.openHelpPopoverAriaLabel": "ヘルプポップオーバーを開く", - "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.optionalText": "オプション", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.removeRequiredFieldButtonAriaLabel": "必須フィールドを削除", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.requiredFieldsLabel": "必須フィールド", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.validation.fieldNameRequired": "フィード名が必要です", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 320b6738ecc84..0ca08b7a1d89c 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -36866,8 +36866,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideLabel": "规则名称覆盖", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText": "为此规则键入一个或多个定制识别标签。在每个标签后按 Enter 键可开始新的标签。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsLabel": "标签", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText": "指定包含指标字段的文档前缀。用于丰富指标匹配告警。", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel": "指标前缀覆盖", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText": "选择分组要依据的字段。字段已使用'AND'联接在一起", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel": "分组依据", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel": "阈值", @@ -36885,7 +36883,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "提供有关规则先决条件的说明,如所需集成、配置步骤,以及规则正常运行所需的任何其他内容。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "设置指南", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "标签不得为空", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "指标前缀覆盖不得为空", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addCustomHighlightedFieldDescription": "添加突出显示的定制字段", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription": "添加误报示例", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "添加引用 URL", @@ -36921,9 +36918,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel": "定制查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel": "规则类型", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldShouldLoadQueryDynamicallyLabelWithoutName": "在每次执行规则时动态加载已保存查询", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel": "指标索引模式", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel": "指标映射", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel": "指标索引查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel": "计数", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel": "唯一值", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "选择字段以检查基数", @@ -36965,9 +36959,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "已保存查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "源", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.Su.perRuleExecutionWarning": "每次规则执行选项不可用于阈值规则类型", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchIndexForbiddenError": "索引模式不能是{ forbiddenString }。请选择更具体的索引模式。", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "选择威胁索引", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "至少需要一种索引模式。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "所有结果", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.docsLinkText": "了解详情", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage": "{key} 不是有效的 Mustache 模板", @@ -37785,7 +37776,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationsLink": "集成", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationVersion": "版本", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.notInstalledText": "未安装", - "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.optionalText": "可选", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationAriaLabel": "集成选择器", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyAriaLabel": "相关集成版本约束", "xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyPlaceholder": "Semver", @@ -37801,7 +37791,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.generalWarningDescription": "这不会中断规则执行,但可能表明未正确设置所需字段。请检查在该规则的 {source} 中指定的索引是否存在,以及映射中是否具有所需字段和类型。", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.generalWarningTitle": "在此规则的指定索引模式中找不到某些字段。", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.openHelpPopoverAriaLabel": "打开帮助弹出框", - "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.optionalText": "可选", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.removeRequiredFieldButtonAriaLabel": "移除必填字段", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.requiredFieldsLabel": "必填字段", "xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.validation.fieldNameRequired": "'字段名称'必填", diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/split_accordion/split_accordion.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/split_accordion/split_accordion.tsx index 668473919b97b..2bc77fe4c7f6a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/split_accordion/split_accordion.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/split_accordion/split_accordion.tsx @@ -30,7 +30,7 @@ export const SplitAccordion = ({ ({ v4: jest.fn().mockReturnValue('123'), @@ -51,67 +48,123 @@ describe('Helpers', () => { }); describe('#getFormattedEntry', () => { - test('it returns entry with a value when "item.field" is of type "text" and matching keyword field exists', () => { - const payloadIndexPattern: DataViewBase = { - ...getMockIndexPattern(), + test('returns fields found in dataViews', () => { + const dataView: DataViewBase = { + title: 'test1-*', fields: [ - ...fields, { - name: 'machine.os.raw.text', + name: 'fieldA.name', type: 'string', esTypes: ['text'], - count: 0, - scripted: false, - searchable: false, - aggregatable: false, - readFromDocValues: true, }, ], - } as DataViewBase; + }; + const threatMatchDataView: DataViewBase = { + title: 'test2-*', + fields: [ + { + name: 'fieldB.name', + type: 'string', + esTypes: ['text'], + }, + ], + }; + const payloadItem: Entry = { - field: 'machine.os.raw.text', + field: 'fieldA.name', type: 'mapping', - value: 'some os', + value: 'fieldB.name', }; - const output = getFormattedEntry(payloadIndexPattern, payloadIndexPattern, payloadItem, 0); - const expected: FormattedEntry = { - entryIndex: 0, - id: '123', + + expect(getFormattedEntry(dataView, threatMatchDataView, payloadItem, 0)).toMatchObject({ field: { - name: 'machine.os.raw.text', + name: 'fieldA.name', type: 'string', esTypes: ['text'], - count: 0, - scripted: false, - searchable: false, - aggregatable: false, - readFromDocValues: true, - } as FieldSpec, + }, + value: { + name: 'fieldB.name', + type: 'string', + esTypes: ['text'], + }, + }); + }); + + test('returns fallback values when fields not found in dataViews', () => { + const dataView: DataViewBase = { + title: 'test1-*', + fields: [], + }; + const threatMatchDataView: DataViewBase = { + title: 'test2-*', + fields: [], + }; + + const payloadItem: Entry = { + field: 'fieldA.name', type: 'mapping', - value: undefined, + value: 'fieldB.name', }; - expect(output).toEqual(expected); + + expect(getFormattedEntry(dataView, threatMatchDataView, payloadItem, 0)).toMatchObject({ + field: { + name: 'fieldA.name', + type: 'string', + }, + value: { + name: 'fieldB.name', + type: 'string', + }, + }); + }); + + test('returns entry parameters', () => { + const dataView: DataViewBase = { + title: 'test1-*', + fields: [], + }; + const threatMatchDataView: DataViewBase = { + title: 'test2-*', + fields: [], + }; + + const payloadItem: Entry = { + field: 'unknown', + type: 'mapping', + value: 'unknown', + }; + + expect(getFormattedEntry(dataView, threatMatchDataView, payloadItem, 3)).toMatchObject({ + entryIndex: 3, + type: 'mapping', + }); }); }); describe('#getFormattedEntries', () => { - test('it returns formatted entry with field and value undefined if it unable to find a matching index pattern field', () => { + test('returns formatted entry with fallback field and value if it unable to find a matching index pattern field', () => { const payloadIndexPattern = getMockIndexPattern(); - const payloadItems: Entry[] = [{ field: 'field.one', type: 'mapping', value: 'field.one' }]; + const payloadItems: Entry[] = [{ field: 'field.one', type: 'mapping', value: 'field.two' }]; const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems); const expected: FormattedEntry[] = [ { id: '123', entryIndex: 0, - field: undefined, - value: undefined, + field: { + name: 'field.one', + type: 'string', + }, + value: { + name: 'field.two', + type: 'string', + }, type: 'mapping', }, ]; expect(output).toEqual(expected); }); - test('it returns "undefined" value if cannot match a pattern field', () => { + test('it returns a fallback value if cannot match a pattern field', () => { const payloadIndexPattern = getMockIndexPattern(); const payloadItems: Entry[] = [{ field: 'machine.os', type: 'mapping', value: 'yolo' }]; const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems); @@ -129,7 +182,10 @@ describe('Helpers', () => { aggregatable: true, readFromDocValues: false, } as FieldSpec, - value: undefined, + value: { + name: 'yolo', + type: 'string', + }, type: 'mapping', }, ]; @@ -273,39 +329,4 @@ describe('Helpers', () => { expect(output).toEqual(expected); }); }); - - describe('#filterItems', () => { - test('it removes entry items with "value" of "undefined"', () => { - const entry: ThreatMapEntry = { field: 'host.name', type: 'mapping', value: 'host.name' }; - const mockEmpty: EmptyEntry = { - field: 'host.name', - type: 'mapping', - value: undefined, - }; - const items = filterItems([ - { - entries: [entry], - }, - { - entries: [mockEmpty], - }, - ]); - expect(items).toEqual([{ entries: [entry] }]); - }); - }); - - describe('customValidators.forbiddenField', () => { - const FORBIDDEN = '*'; - - test('it returns expected value when a forbidden value is passed in', () => { - expect(customValidators.forbiddenField('*', FORBIDDEN)).toEqual({ - code: 'ERR_FIELD_FORMAT', - message: 'The index pattern cannot be *. Please choose a more specific index pattern.', - }); - }); - - test('it returns undefined when a non-forbidden value is passed in', () => { - expect(customValidators.forbiddenField('.test-index', FORBIDDEN)).not.toBeDefined(); - }); - }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/helpers.tsx index 693dad3da4255..3292951ca4f86 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/helpers.tsx @@ -6,32 +6,24 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { i18n } from '@kbn/i18n'; import { addIdToItem } from '@kbn/securitysolution-utils'; -import type { ThreatMap, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; -import { threatMap } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { ThreatMap } from '@kbn/securitysolution-io-ts-alerting-types'; import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; -import type { ValidationFunc } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import type { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types'; import type { Entry, FormattedEntry, ThreatMapEntries, EmptyEntry } from './types'; /** * Formats the entry into one that is easily usable for the UI. - * - * @param patterns DataViewBase containing available fields on rule index - * @param item item entry - * @param itemIndex entry index */ export const getFormattedEntry = ( - indexPattern: DataViewBase, - threatIndexPatterns: DataViewBase, + dataView: DataViewBase, + threatDataView: DataViewBase, item: Entry, itemIndex: number, uuidGen: () => string = uuidv4 ): FormattedEntry => { - const { fields } = indexPattern; - const { fields: threatFields } = threatIndexPatterns; + const { fields } = dataView; + const { fields: threatFields } = threatDataView; const field = item.field; const threatField = item.value; const [foundField] = fields.filter(({ name }) => field != null && field === name); @@ -39,11 +31,20 @@ export const getFormattedEntry = ( ({ name }) => threatField != null && threatField === name ); const maybeId: typeof item & { id?: string } = item; + + // Fallback to a string field when field isn't found in known fields. + // It's required for showing field's value when appropriate data is missing in ES. return { id: maybeId.id ?? uuidGen(), - field: foundField, + field: foundField ?? { + name: field, + type: 'string', + }, type: 'mapping', - value: threatFoundField, + value: threatFoundField ?? { + name: threatField, + type: 'string', + }, entryIndex: itemIndex, }; }; @@ -128,7 +129,7 @@ export const getEntryOnThreatFieldChange = ( }; }; -export const getDefaultEmptyEntry = (): EmptyEntry => { +export const createAndNewEntryItem = (): EmptyEntry => { return addIdToItem({ field: '', type: 'mapping', @@ -136,7 +137,7 @@ export const getDefaultEmptyEntry = (): EmptyEntry => { }); }; -export const getNewItem = (): ThreatMap => { +export const createOrNewEntryItem = (): ThreatMap => { return addIdToItem({ entries: [ addIdToItem({ @@ -148,17 +149,6 @@ export const getNewItem = (): ThreatMap => { }); }; -export const filterItems = (items: ThreatMapEntries[]): ThreatMapping => { - return items.reduce((acc, item) => { - const newItem = { ...item, entries: item.entries }; - if (threatMap.is(newItem)) { - return [...acc, newItem]; - } else { - return acc; - } - }, []); -}; - /** * Given a list of items checks each one to see if any of them have an empty field * or an empty value. @@ -182,36 +172,3 @@ export const singleEntryThreat = (items: ThreatMapEntries[]): boolean => { items[0].entries[0].value === '' ); }; - -export const customValidators = { - forbiddenField: ( - value: unknown, - forbiddenString: string - ): ReturnType> => { - let match: boolean; - - if (typeof value === 'string') { - match = value === forbiddenString; - } else if (Array.isArray(value)) { - match = !!value.find((item) => item === forbiddenString); - } else { - match = false; - } - - if (match) { - return { - code: 'ERR_FIELD_FORMAT', - message: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchIndexForbiddenError', - { - defaultMessage: - 'The index pattern cannot be { forbiddenString }. Please choose a more specific index pattern.', - values: { - forbiddenString, - }, - } - ), - }; - } - }, -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.test.tsx index f2365cd064a02..67472d89806af 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { fields } from '@kbn/data-plugin/common/mocks'; @@ -18,6 +18,7 @@ import { ThreatMatchComponent } from '.'; import type { ThreatMapEntries } from './types'; import type { DataViewBase } from '@kbn/es-query'; import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; +import { createOrNewEntryItem } from './helpers'; const mockTheme = getMockTheme({ eui: { @@ -27,10 +28,6 @@ const mockTheme = getMockTheme({ jest.mock('../../lib/kibana'); -const getPayLoad = (): ThreatMapEntries[] => [ - { entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }, -]; - const getDoublePayLoad = (): ThreatMapEntries[] => [ { entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }, { entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }, @@ -59,7 +56,7 @@ describe('ThreatMatchComponent', () => { const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={jest.fn()} /> ); @@ -88,11 +85,46 @@ describe('ThreatMatchComponent', () => { ); }); - test('it displays "Search" for "listItems" that are passed in', async () => { + test('it displays field values for "listItems" that are passed in', async () => { + const mapping: ThreatMapEntries[] = [ + { entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }, + ]; + + render( + + ); + + expect(screen.getAllByTestId('itemEntryContainer')).toHaveLength(1); + + const comboboxes = screen.getAllByRole('combobox'); + + expect(comboboxes).toHaveLength(2); + expect(comboboxes[0]).toHaveValue('host.name'); + expect(comboboxes[1]).toHaveValue('host.name'); + }); + + test('it displays "or", "and" enabled', () => { const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={jest.fn()} /> ); - expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="entryField"] input').at(0).props().placeholder).toEqual( - 'Search' - ); - wrapper.unmount(); + expect(wrapper.find('[data-test-subj="andButton"] button').prop('disabled')).toBeFalsy(); + expect(wrapper.find('[data-test-subj="orButton"] button').prop('disabled')).toBeFalsy(); }); - test('it displays "or", "and" enabled', () => { + test('it adds an entry when "and" clicked', async () => { + const handleMappingEntriesChangeMock = jest.fn(); + const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={handleMappingEntriesChangeMock} /> ); - expect(wrapper.find('[data-test-subj="andButton"] button').prop('disabled')).toBeFalsy(); - expect(wrapper.find('[data-test-subj="orButton"] button').prop('disabled')).toBeFalsy(); + expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(1); + + wrapper.find('[data-test-subj="andButton"] button').simulate('click'); + + expect(handleMappingEntriesChangeMock).toHaveBeenCalledWith([ + expect.objectContaining({ + entries: [ + expect.objectContaining({ + field: '', + type: 'mapping', + value: '', + }), + expect.objectContaining({ + field: '', + type: 'mapping', + value: '', + }), + ], + }), + ]); }); - test('it adds an entry when "and" clicked', async () => { + test('it shows two AND entries', () => { + const mappingEntries: ThreatMapEntries[] = [ + { + entries: [ + { + field: '', + type: 'mapping', + value: '', + }, + { + field: '', + type: 'mapping', + value: '', + }, + ], + }, + ]; + const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={jest.fn()} /> ); - expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(1); - - wrapper.find('[data-test-subj="andButton"] button').simulate('click'); - - await waitFor(() => { - expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="entryField"] input').at(0).props().placeholder).toEqual( - 'Search' - ); - expect( - wrapper.find('[data-test-subj="threatEntryField"] input').at(0).props().placeholder - ).toEqual('Search'); - expect(wrapper.find('[data-test-subj="entryField"] input').at(1).props().placeholder).toEqual( - 'Search' - ); - expect( - wrapper.find('[data-test-subj="threatEntryField"] input').at(1).props().placeholder - ).toEqual('Search'); - }); + expect(wrapper.find('EuiFlexGroup[data-test-subj="itemEntryContainer"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="entryField"] input').at(0).props().placeholder).toEqual( + 'Search' + ); + expect( + wrapper.find('[data-test-subj="threatEntryField"] input').at(0).props().placeholder + ).toEqual('Search'); + expect(wrapper.find('[data-test-subj="entryField"] input').at(1).props().placeholder).toEqual( + 'Search' + ); + expect( + wrapper.find('[data-test-subj="threatEntryField"] input').at(1).props().placeholder + ).toEqual('Search'); }); test('it adds an item when "or" clicked', async () => { + const handleMappingEntriesChangeMock = jest.fn(); + const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={handleMappingEntriesChangeMock} /> ); @@ -220,28 +282,96 @@ describe('ThreatMatchComponent', () => { wrapper.find('[data-test-subj="orButton"] button').simulate('click'); - await waitFor(() => { - expect(wrapper.find('EuiFlexGroup[data-test-subj="entriesContainer"]')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="entryField"] input').at(0).props().placeholder).toEqual( - 'Search' - ); - expect( - wrapper.find('[data-test-subj="threatEntryField"] input').at(0).props().placeholder - ).toEqual('Search'); - expect(wrapper.find('[data-test-subj="entryField"] input').at(1).props().placeholder).toEqual( - 'Search' - ); - expect( - wrapper.find('[data-test-subj="threatEntryField"] input').at(1).props().placeholder - ).toEqual('Search'); - }); + expect(handleMappingEntriesChangeMock).toHaveBeenCalledWith([ + expect.objectContaining({ + entries: [ + expect.objectContaining({ + field: '', + type: 'mapping', + value: '', + }), + ], + }), + expect.objectContaining({ + entries: [ + expect.objectContaining({ + field: '', + type: 'mapping', + value: '', + }), + ], + }), + ]); + }); + + test('it shows two OR entries', () => { + const mappingEntries: ThreatMapEntries[] = [ + { + entries: [ + { + field: '', + type: 'mapping', + value: '', + }, + ], + }, + { + entries: [ + { + field: '', + type: 'mapping', + value: '', + }, + ], + }, + ]; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="entriesContainer"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="entryField"] input').at(0).props().placeholder).toEqual( + 'Search' + ); + expect( + wrapper.find('[data-test-subj="threatEntryField"] input').at(0).props().placeholder + ).toEqual('Search'); + expect(wrapper.find('[data-test-subj="entryField"] input').at(1).props().placeholder).toEqual( + 'Search' + ); + expect( + wrapper.find('[data-test-subj="threatEntryField"] input').at(1).props().placeholder + ).toEqual('Search'); }); test('it removes one row if user deletes a row', () => { + const mappingEntries = getDoublePayLoad(); + const handleMappingEntriesChangeMock = jest.fn(); + const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={handleMappingEntriesChangeMock} /> ); expect(wrapper.find('div[data-test-subj="entriesContainer"]').length).toEqual(2); wrapper.find('[data-test-subj="firstRowDeleteButton"] button').simulate('click'); - expect(wrapper.find('div[data-test-subj="entriesContainer"]').length).toEqual(1); - wrapper.unmount(); + + expect(handleMappingEntriesChangeMock).toHaveBeenCalledWith([mappingEntries[1]]); }); test('it displays "and" badge if at least one item includes more than one entry', () => { + const mappingEntries: ThreatMapEntries[] = [ + { + entries: [ + { + field: '', + type: 'mapping', + value: '', + }, + { + field: '', + type: 'mapping', + value: '', + }, + ], + }, + ]; + const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={jest.fn()} /> ); - expect(wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()).toBeFalsy(); - - wrapper.find('[data-test-subj="andButton"] button').simulate('click'); - expect(wrapper.find('[data-test-subj="entryItemEntryFirstRowAndBadge"]').exists()).toBeTruthy(); }); @@ -302,7 +445,7 @@ describe('ThreatMatchComponent', () => { const wrapper = mount( { fields, } as DataViewBase } - onChange={jest.fn()} + onMappingEntriesChange={jest.fn()} /> ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.tsx index 425977aa175c6..25b78d9024d6d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/index.tsx @@ -5,18 +5,15 @@ * 2.0. */ -import React, { useCallback, useEffect, useReducer } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import type { DataViewBase } from '@kbn/es-query'; -import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import { ListItemComponent } from './list_item'; import { AndOrBadge } from '../and_or_badge'; import { LogicButtons } from './logic_buttons'; import type { ThreatMapEntries } from './types'; -import type { State } from './reducer'; -import { reducer } from './reducer'; -import { getDefaultEmptyEntry, getNewItem, filterItems } from './helpers'; +import { createAndNewEntryItem, createOrNewEntryItem } from './helpers'; const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; @@ -32,135 +29,81 @@ const MyButtonsContainer = styled(EuiFlexItem)` margin: 16px 0; `; -const initialState: State = { - andLogicIncluded: false, - entries: [], - entriesToDelete: [], -}; - -interface OnChangeProps { - entryItems: ThreatMapping; - entriesToDelete: ThreatMapEntries[]; -} - interface ThreatMatchComponentProps { - listItems: ThreatMapEntries[]; + mappingEntries: ThreatMapEntries[]; indexPatterns: DataViewBase; threatIndexPatterns: DataViewBase; - onChange: (arg: OnChangeProps) => void; + 'id-aria'?: string; + 'data-test-subj'?: string; + onMappingEntriesChange: (newValue: ThreatMapEntries[]) => void; } export const ThreatMatchComponent = ({ - listItems, + mappingEntries, indexPatterns, threatIndexPatterns, - onChange, + 'id-aria': idAria, + 'data-test-subj': dataTestSubj, + onMappingEntriesChange, }: ThreatMatchComponentProps) => { - const [{ entries, entriesToDelete, andLogicIncluded }, dispatch] = useReducer(reducer(), { - ...initialState, - }); - - const setUpdateEntries = useCallback( - (items: ThreatMapEntries[]): void => { - dispatch({ - type: 'setEntries', - entries: items, - }); - }, - [dispatch] - ); - - const setDefaultEntries = useCallback( - (item: ThreatMapEntries): void => { - dispatch({ - type: 'setDefault', - initialState, - lastEntry: item, - }); - }, - [dispatch] - ); - const handleEntryItemChange = useCallback( (item: ThreatMapEntries, index: number): void => { - const updatedEntries = [ - ...entries.slice(0, index), - { - ...item, - }, - ...entries.slice(index + 1), - ]; - - setUpdateEntries(updatedEntries); + const updatedEntries = mappingEntries.slice(); + + updatedEntries.splice(index, 1, item); + + onMappingEntriesChange(updatedEntries); }, - [setUpdateEntries, entries] + [mappingEntries, onMappingEntriesChange] ); const handleDeleteEntryItem = useCallback( - (item: ThreatMapEntries, itemIndex: number): void => { + (item: ThreatMapEntries, index: number): void => { if (item.entries.length === 0) { - const updatedEntries = [...entries.slice(0, itemIndex), ...entries.slice(itemIndex + 1)]; - // if it's the only item left, don't delete it just add a default entry to it - if (updatedEntries.length === 0) { - setDefaultEntries(item); - } else { - setUpdateEntries([...entries.slice(0, itemIndex), ...entries.slice(itemIndex + 1)]); - } + const updatedEntries = mappingEntries.slice(); + + updatedEntries.splice(index, 1); + + onMappingEntriesChange(updatedEntries); } else { - handleEntryItemChange(item, itemIndex); + handleEntryItemChange(item, index); } }, - [handleEntryItemChange, setUpdateEntries, entries, setDefaultEntries] + [mappingEntries, onMappingEntriesChange, handleEntryItemChange] ); - const handleAddNewEntryItemEntry = useCallback((): void => { - const lastEntry = entries[entries.length - 1]; - const { entries: innerEntries } = lastEntry; + const handleOrClick = useCallback((): void => { + // There is a case where there are numerous list items, all with + // empty `entries` array. + + onMappingEntriesChange([...mappingEntries, createOrNewEntryItem()]); + }, [mappingEntries, onMappingEntriesChange]); + + const handleAndClick = useCallback((): void => { + const lastEntry = mappingEntries.at(-1); + + if (!lastEntry) { + onMappingEntriesChange([createOrNewEntryItem()]); + return; + } + const { entries: innerEntries } = lastEntry; const updatedEntry: ThreatMapEntries = { ...lastEntry, - entries: [...innerEntries, getDefaultEmptyEntry()], + entries: [...innerEntries, createAndNewEntryItem()], }; - setUpdateEntries([...entries.slice(0, entries.length - 1), { ...updatedEntry }]); - }, [setUpdateEntries, entries]); + onMappingEntriesChange([...mappingEntries.slice(0, -1), updatedEntry]); + }, [mappingEntries, onMappingEntriesChange]); - const handleAddNewEntryItem = useCallback((): void => { - // There is a case where there are numerous list items, all with - // empty `entries` array. - const newItem = getNewItem(); - setUpdateEntries([...entries, { ...newItem }]); - }, [setUpdateEntries, entries]); - - const handleAddClick = useCallback((): void => { - handleAddNewEntryItemEntry(); - }, [handleAddNewEntryItemEntry]); - - // Bubble up changes to parent - useEffect(() => { - onChange({ entryItems: filterItems(entries), entriesToDelete }); - }, [onChange, entriesToDelete, entries]); - - // Defaults to never be sans entry, instead - // always falls back to an empty entry if user deletes all - useEffect(() => { - if ( - entries.length === 0 || - (entries.length === 1 && entries[0].entries != null && entries[0].entries.length === 0) - ) { - handleAddNewEntryItem(); - } - }, [entries, handleAddNewEntryItem]); + const andLogicIncluded = useMemo( + () => mappingEntries.some(({ entries }) => entries.length > 1), + [mappingEntries] + ); - useEffect(() => { - if (listItems.length > 0) { - setUpdateEntries(listItems); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); return ( - - {entries.map((entryListItem, index) => { + + {mappingEntries.map((entryListItem, index) => { const key = (entryListItem as typeof entryListItem & { id?: string }).id ?? `${index}`; return ( @@ -190,7 +133,7 @@ export const ThreatMatchComponent = ({ threatIndexPatterns={threatIndexPatterns} listItemIndex={index} andLogicIncluded={andLogicIncluded} - isOnlyItem={entries.length === 1} + isOnlyItem={mappingEntries.length === 1} onDeleteEntryItem={handleDeleteEntryItem} onChangeEntryItem={handleEntryItemChange} /> @@ -200,7 +143,7 @@ export const ThreatMatchComponent = ({ ); })} - + {andLogicIncluded && ( @@ -211,8 +154,8 @@ export const ThreatMatchComponent = ({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.test.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.test.ts deleted file mode 100644 index 3cd980bd9ae95..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ThreatMapEntries } from './types'; -import type { State } from './reducer'; -import { reducer } from './reducer'; -import { getDefaultEmptyEntry } from './helpers'; -import type { ThreatMapEntry } from '@kbn/securitysolution-io-ts-alerting-types'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); - -const initialState: State = { - andLogicIncluded: false, - entries: [], - entriesToDelete: [], -}; - -const getEntry = (): ThreatMapEntry => ({ - field: 'host.name', - type: 'mapping', - value: 'host.name', -}); - -describe('reducer', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('#setEntries', () => { - test('should return "andLogicIncluded" ', () => { - const update = reducer()(initialState, { - type: 'setEntries', - entries: [], - }); - const expected: State = { - andLogicIncluded: false, - entries: [], - entriesToDelete: [], - }; - expect(update).toEqual(expected); - }); - - test('should set "andLogicIncluded" to true if any of the entries include entries with length greater than 1 ', () => { - const entries: ThreatMapEntries[] = [ - { - entries: [getEntry(), getEntry()], - }, - ]; - const { andLogicIncluded } = reducer()(initialState, { - type: 'setEntries', - entries, - }); - - expect(andLogicIncluded).toBeTruthy(); - }); - - test('should set "andLogicIncluded" to false if any of the entries include entries with length greater than 1 ', () => { - const entries: ThreatMapEntries[] = [ - { - entries: [getEntry()], - }, - ]; - const { andLogicIncluded } = reducer()(initialState, { - type: 'setEntries', - entries, - }); - - expect(andLogicIncluded).toBeFalsy(); - }); - }); - - describe('#setDefault', () => { - test('should restore initial state and add default empty entry to item" ', () => { - const entries: ThreatMapEntries[] = [ - { - entries: [getEntry()], - }, - ]; - - const update = reducer()( - { - andLogicIncluded: true, - entries, - entriesToDelete: [], - }, - { - type: 'setDefault', - initialState, - lastEntry: { - entries: [], - }, - } - ); - - expect(update).toEqual({ - ...initialState, - entries: [ - { - entries: [getDefaultEmptyEntry()], - }, - ], - }); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.ts deleted file mode 100644 index 3371cfe0e3317..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/threat_match/reducer.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ThreatMapEntries } from './types'; -import { getDefaultEmptyEntry } from './helpers'; - -export interface State { - andLogicIncluded: boolean; - entries: ThreatMapEntries[]; - entriesToDelete: ThreatMapEntries[]; -} - -export type Action = - | { - type: 'setEntries'; - entries: ThreatMapEntries[]; - } - | { - type: 'setDefault'; - initialState: State; - lastEntry: ThreatMapEntries; - }; - -export const reducer = - () => - (state: State, action: Action): State => { - switch (action.type) { - case 'setEntries': { - const isAndLogicIncluded = - action.entries.filter(({ entries }) => entries.length > 1).length > 0; - - const returnState = { - ...state, - andLogicIncluded: isAndLogicIncluded, - entries: action.entries, - }; - return returnState; - } - case 'setDefault': { - return { - ...state, - ...action.initialState, - entries: [{ ...action.lastEntry, entries: [getDefaultEmptyEntry()] }], - }; - } - default: - return state; - } - }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/optional_field_label/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/optional_field_label/index.test.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/optional_field_label/index.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/optional_field_label/index.test.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/optional_field_label/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/optional_field_label/index.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/optional_field_label/index.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/optional_field_label/index.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx index a57ce5fe8cd7b..8b24429407719 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx @@ -6,18 +6,12 @@ */ import React from 'react'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSpacer, - EuiText, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { UseArray, useFormData } from '../../../../shared_imports'; import { RelatedIntegrationsHelpInfo } from './related_integrations_help_info'; import { RelatedIntegrationFieldRow } from './related_integration_field_row'; import * as i18n from './translations'; +import { OptionalFieldLabel } from '../optional_field_label'; interface RelatedIntegrationsProps { path: string; @@ -38,11 +32,7 @@ export function RelatedIntegrations({ path, dataTestSubj }: RelatedIntegrationsP {({ items, addItem, removeItem }) => ( - {i18n.OPTIONAL} - - } + labelAppend={OptionalFieldLabel} labelType="legend" fullWidth data-test-subj={dataTestSubj} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/translations.ts index 2645298783f5c..de8b1a599fcad 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/translations.ts @@ -14,13 +14,6 @@ export const RELATED_INTEGRATIONS_LABEL = i18n.translate( } ); -export const OPTIONAL = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.optionalText', - { - defaultMessage: 'Optional', - } -); - export const RELATED_INTEGRATION_FIELDS_HELP_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.helpText', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx index f2f4c08a35813..27387909d3307 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx @@ -13,10 +13,11 @@ import type { RequiredFieldInput } from '../../../../../common/api/detection_eng import { UseArray, useFormData } from '../../../../shared_imports'; import type { FormHook, ArrayItem } from '../../../../shared_imports'; import { RequiredFieldsHelpInfo } from './required_fields_help_info'; -import { RequiredFieldRow } from './required_fields_row'; import * as defineRuleI18n from '../../../rule_creation_ui/components/step_define_rule/translations'; -import * as i18n from './translations'; +import { OptionalFieldLabel } from '../optional_field_label'; +import { RequiredFieldRow } from './required_fields_row'; import { getFlattenedArrayFieldNames } from './utils'; +import * as i18n from './translations'; interface RequiredFieldsComponentProps { path: string; @@ -173,7 +174,7 @@ const RequiredFieldsList = ({ } labelAppend={ - {i18n.OPTIONAL} + {OptionalFieldLabel} } hasChildLabel={false} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/translations.ts index bed9a4ea0024c..dacc7dfb9ff6f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/translations.ts @@ -42,13 +42,6 @@ export const REQUIRED_FIELDS_GENERAL_WARNING_TITLE = i18n.translate( } ); -export const OPTIONAL = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.optionalText', - { - defaultMessage: 'Optional', - } -); - export const REMOVE_REQUIRED_FIELD_BUTTON_ARIA_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDescription.requiredFields.removeRequiredFieldButtonAriaLabel', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/index.ts new file mode 100644 index 0000000000000..3c192f575fdd8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './threat_match_index_edit'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_edit.tsx new file mode 100644 index 0000000000000..277721f7bd975 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_edit.tsx @@ -0,0 +1,38 @@ +/* + * 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 React from 'react'; +import type { FieldConfig } from '../../../../shared_imports'; +import { UseField } from '../../../../shared_imports'; +import { ThreatMatchIndexSelectorField } from './threat_match_index_selector_field'; +import { threatIndexPatternsRequiredValidator } from './validators/threat_index_patterns_required_validator'; +import { forbiddenIndexPatternValidator } from './validators/forbidden_index_pattern_validator'; + +interface ThreatMatchIndexEditProps { + path: string; +} + +export function ThreatMatchIndexEdit({ path }: ThreatMatchIndexEditProps): JSX.Element { + return ( + + ); +} + +const THREAT_MATCH_INDEX_FIELD_CONFIG: FieldConfig = { + validations: [ + { + validator: threatIndexPatternsRequiredValidator, + }, + { + validator: forbiddenIndexPatternValidator, + }, + ], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_selector_field.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_selector_field.tsx new file mode 100644 index 0000000000000..ad694b106248d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/threat_match_index_selector_field.tsx @@ -0,0 +1,65 @@ +/* + * 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 React, { memo, useCallback } from 'react'; +import { isEqual } from 'lodash'; +import { css } from '@emotion/css'; +import { EuiText, EuiButtonEmpty, useEuiTheme } from '@elastic/eui'; +import { ComboBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { DEFAULT_THREAT_INDEX_KEY } from '../../../../../common/constants'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; +import type { FieldHook } from '../../../../shared_imports'; +import * as i18n from './translations'; + +interface ThreatMatchIndexSelectorFieldProps { + field: FieldHook; +} + +export const ThreatMatchIndexSelectorField = memo(function ThreatIndexField({ + field, +}: ThreatMatchIndexSelectorFieldProps): JSX.Element { + const { euiTheme } = useEuiTheme(); + const [defaultThreatIndices] = useUiSetting$(DEFAULT_THREAT_INDEX_KEY); + const isIndexModified = !isEqual(field.value, defaultThreatIndices); + + const handleResetIndices = useCallback( + () => field.setValue(defaultThreatIndices), + [field, defaultThreatIndices] + ); + + return ( + } + idAria="ruleThreatMatchIndicesField" + data-test-subj="ruleThreatMatchIndicesField" + euiFieldProps={EUI_COMBOBOX_PROPS} + label={i18n.THREAT_MATCH_INDEX_FIELD_LABEL} + labelAppend={ + isIndexModified ? ( + + {i18n.RESET_TO_DEFAULT_THREAT_MATCH_INDEX} + + ) : undefined + } + helpText={helpText} + /> + ); +}); + +const EUI_COMBOBOX_PROPS = { + fullWidth: true, + placeholder: '', +}; + +const helpText = {i18n.THREAT_MATCH_INDEX_FIELD_HELP_TEXT}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/translations.tsx new file mode 100644 index 0000000000000..f4ae40c6ffdc1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/translations.tsx @@ -0,0 +1,29 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const THREAT_MATCH_INDEX_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatMatchIndex.label', + { + defaultMessage: 'Indicator index patterns', + } +); + +export const THREAT_MATCH_INDEX_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatMatchIndex.helpText', + { + defaultMessage: 'Select threat indices', + } +); + +export const RESET_TO_DEFAULT_THREAT_MATCH_INDEX = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatMatchIndex.resetToDefaultIndices', + { + defaultMessage: 'Reset to default index patterns', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/forbidden_index_pattern_validator.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/forbidden_index_pattern_validator.ts new file mode 100644 index 0000000000000..115858b863a4e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/forbidden_index_pattern_validator.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 { type FormData, type ValidationFunc } from '../../../../../shared_imports'; +import * as i18n from './translations'; + +export const forbiddenIndexPatternValidator: ValidationFunc = ( + ...args +) => { + const [{ value, path }] = args; + + if (Array.isArray(value) && value.includes('*')) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: i18n.THREAT_MATCH_INDEX_FIELD_VALIDATION_FORBIDDEN_PATTERN_ERROR, + }; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/threat_index_patterns_required_validator.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/threat_index_patterns_required_validator.ts new file mode 100644 index 0000000000000..fbbc777413208 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/threat_index_patterns_required_validator.ts @@ -0,0 +1,13 @@ +/* + * 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 { fieldValidators } from '../../../../../shared_imports'; +import * as i18n from './translations'; + +export const threatIndexPatternsRequiredValidator = fieldValidators.emptyField( + i18n.THREAT_MATCH_INDEX_FIELD_VALIDATION_REQUIRED_ERROR +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/translations.ts new file mode 100644 index 0000000000000..0b4b153d26501 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_index_edit/validators/translations.ts @@ -0,0 +1,22 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const THREAT_MATCH_INDEX_FIELD_VALIDATION_REQUIRED_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.ruleFields.threatMatchIndex.validation.requiredError', + { + defaultMessage: 'A minimum of one index pattern is required.', + } +); + +export const THREAT_MATCH_INDEX_FIELD_VALIDATION_FORBIDDEN_PATTERN_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.ruleFields.threatMatchIndexForbiddenError', + { + defaultMessage: 'The index pattern cannot be "*". Please choose a more specific index pattern.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/index.ts new file mode 100644 index 0000000000000..a018c50647303 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './threat_match_indicator_path_edit'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/threat_match_indicator_path_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/threat_match_indicator_path_edit.tsx new file mode 100644 index 0000000000000..02c1aa95cb255 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/threat_match_indicator_path_edit.tsx @@ -0,0 +1,55 @@ +/* + * 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 React, { memo } from 'react'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import type { FieldConfig } from '../../../../shared_imports'; +import { fieldValidators, UseField, VALIDATION_TYPES } from '../../../../shared_imports'; +import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; +import { OptionalFieldLabel } from '../optional_field_label'; +import * as i18n from './translations'; + +interface ThreatMatchIndicatorPathEditProps { + path: string; + disabled?: boolean; +} + +export const ThreatMatchIndicatorPathEdit = memo(function ThreatMatchIndicatorPathEdit({ + path, + disabled, +}: ThreatMatchIndicatorPathEditProps): JSX.Element { + return ( + + ); +}); + +const THREAT_MATCH_INDICATOR_PATH_FIELD_CONFIG: FieldConfig = { + label: i18n.THREAT_MATCH_INDICATOR_PATH_FIELD_LABEL, + helpText: i18n.THREAT_MATCH_INDICATOR_PATH_FIELD_HELP_TEXT, + labelAppend: OptionalFieldLabel, + validations: [ + { + validator: fieldValidators.emptyField( + i18n.THREAT_MATCH_INDICATOR_PATH_FIELD_VALIDATION_REQUIRED_ERROR + ), + type: VALIDATION_TYPES.FIELD, + }, + ], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/translations.tsx new file mode 100644 index 0000000000000..c39ca7456a53f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_indicator_path_edit/translations.tsx @@ -0,0 +1,30 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const THREAT_MATCH_INDICATOR_PATH_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatMatchIndicatorPath.label', + { + defaultMessage: 'Indicator prefix override', + } +); + +export const THREAT_MATCH_INDICATOR_PATH_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatMatchIndicatorPath.helpText', + { + defaultMessage: + 'Specify the document prefix containing your indicator fields. Used for enrichment of indicator match alerts.', + } +); + +export const THREAT_MATCH_INDICATOR_PATH_FIELD_VALIDATION_REQUIRED_ERROR = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatMatchIndicatorPath.validator.requiredError', + { + defaultMessage: 'Indicator prefix override must not be empty', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/index.tsx new file mode 100644 index 0000000000000..3f54b9798e73b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/index.tsx @@ -0,0 +1,9 @@ +/* + * 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 './threat_match_mapping_edit'; +export { DEFAULT_THREAT_MAPPING_VALUE } from './threat_match_mapping_field'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx new file mode 100644 index 0000000000000..3e4bb5d686419 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx @@ -0,0 +1,54 @@ +/* + * 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 React, { memo, useMemo } from 'react'; +import type { DataViewBase } from '@kbn/es-query'; +import type { ThreatMapEntries } from '../../../../common/components/threat_match/types'; +import type { FieldConfig } from '../../../../shared_imports'; +import { UseField } from '../../../../shared_imports'; +import { threatMatchMappingValidatorFactory } from './validators/threat_match_mapping_validator_factory'; +import { ThreatMatchMappingField } from './threat_match_mapping_field'; +import * as i18n from './translations'; + +interface ThreatMatchMappingEditProps { + path: string; + threatIndexPatterns: DataViewBase; + indexPatterns: DataViewBase; +} + +export const ThreatMatchMappingEdit = memo(function ThreatMatchMappingEdit({ + path, + indexPatterns, + threatIndexPatterns, +}: ThreatMatchMappingEditProps): JSX.Element { + const fieldConfig: FieldConfig = useMemo( + () => ({ + label: i18n.THREAT_MATCH_MAPPING_FIELD_LABEL, + validations: [ + { + validator: threatMatchMappingValidatorFactory({ + indexPatterns, + threatIndexPatterns, + }), + }, + ], + }), + [indexPatterns, threatIndexPatterns] + ); + + return ( + + ); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_field.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_field.tsx new file mode 100644 index 0000000000000..7495e202ee452 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_field.tsx @@ -0,0 +1,83 @@ +/* + * 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 React, { useCallback, useEffect } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import type { DataViewBase } from '@kbn/es-query'; +import usePrevious from 'react-use/lib/usePrevious'; +import { createOrNewEntryItem } from '../../../../common/components/threat_match/helpers'; +import type { ThreatMapEntries } from '../../../../common/components/threat_match/types'; +import { ThreatMatchComponent } from '../../../../common/components/threat_match'; +import type { FieldHook } from '../../../../shared_imports'; +import { getFieldValidityAndErrorMessage } from '../../../../shared_imports'; + +export const DEFAULT_THREAT_MAPPING_VALUE = [createOrNewEntryItem()]; + +interface ThreatMatchMappingFieldProps { + field: FieldHook; + indexPatterns: DataViewBase; + threatIndexPatterns: DataViewBase; +} + +export function ThreatMatchMappingField({ + field, + indexPatterns, + threatIndexPatterns, +}: ThreatMatchMappingFieldProps): JSX.Element { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const { setValue, validate } = field; + const prevIndexTitle = usePrevious(indexPatterns.title); + const prevThreatTitle = usePrevious(threatIndexPatterns.title); + + // We have to make sure validation runs against the latest source events index patterns + // and threat match index patterns. + // Form lib's `fieldsToValidateOnChange` on the corresponding index patterns edit fields + // doesn't help here. It leads to running threat match mapping validation before render + // of this component happens. In the end validation runs against previous index patterns + // producing invalid validation results. + // + // Validating the field imperatively here fixes this issue. + useEffect(() => { + if (indexPatterns.title === prevIndexTitle && threatIndexPatterns.title === prevThreatTitle) { + return; + } + + validate(); + }, [indexPatterns.title, threatIndexPatterns.title, prevIndexTitle, prevThreatTitle, validate]); + + const handleMappingChange = useCallback( + (entryItems: ThreatMapEntries[]): void => { + if (entryItems.length === 0) { + setValue(DEFAULT_THREAT_MAPPING_VALUE); + return; + } + + setValue(entryItems); + }, + [setValue] + ); + + return ( + + + + ); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/translations.tsx new file mode 100644 index 0000000000000..47b016680b6ca --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/translations.tsx @@ -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 { i18n } from '@kbn/i18n'; + +export const THREAT_MATCH_MAPPING_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatMatchMapping.label', + { + defaultMessage: 'Indicator mapping', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/error_codes.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/error_codes.ts new file mode 100644 index 0000000000000..24a4096107be1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/error_codes.ts @@ -0,0 +1,11 @@ +/* + * 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 enum THREAT_MATCH_MAPPING_ERROR_CODES { + ERR_FIELD_MISSING = 'ERR_FIELD_MISSING', + ERR_FIELDS_UNKNOWN = 'ERR_FIELDS_UNKNOWN', +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/get_unknown_threat_match_mapping_field_names.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/get_unknown_threat_match_mapping_field_names.ts new file mode 100644 index 0000000000000..eceae001bbd16 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/get_unknown_threat_match_mapping_field_names.ts @@ -0,0 +1,51 @@ +/* + * 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 { DataViewBase } from '@kbn/es-query'; +import type { ThreatMapEntries } from '../../../../../common/components/threat_match/types'; + +interface GetUnknownThreatMatchMappingFieldNamesParams { + entries: ThreatMapEntries[]; + indexPatterns: DataViewBase; + threatIndexPatterns: DataViewBase; +} + +interface GetUnknownThreatMatchMappingFieldNamesResult { + unknownSourceIndicesFields: string[]; + unknownThreatMatchIndicesFields: string[]; +} + +export function getUnknownThreatMatchMappingFieldNames({ + entries, + indexPatterns, + threatIndexPatterns, +}: GetUnknownThreatMatchMappingFieldNamesParams): GetUnknownThreatMatchMappingFieldNamesResult { + const knownIndexPatternsFields = new Set(indexPatterns.fields.map(({ name }) => name)); + const knownThreatMatchIndexPatternsFields = new Set( + threatIndexPatterns.fields.map(({ name }) => name) + ); + + const unknownSourceIndicesFields: string[] = []; + const unknownThreatMatchIndicesFields: string[] = []; + + for (const { entries: subEntries } of entries) { + for (const subEntry of subEntries) { + if (subEntry.field && !knownIndexPatternsFields.has(subEntry.field)) { + unknownSourceIndicesFields.push(subEntry.field); + } + + if (subEntry.value && !knownThreatMatchIndexPatternsFields.has(subEntry.value)) { + unknownThreatMatchIndicesFields.push(subEntry.value); + } + } + } + + return { + unknownSourceIndicesFields, + unknownThreatMatchIndicesFields, + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator_factory.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator_factory.ts new file mode 100644 index 0000000000000..591f727f9f2f6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator_factory.ts @@ -0,0 +1,116 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { DataViewBase } from '@kbn/es-query'; +import { + containsInvalidItems, + singleEntryThreat, +} from '../../../../../common/components/threat_match/helpers'; +import type { FormData, ValidationFunc } from '../../../../../shared_imports'; +import type { ThreatMapEntries } from '../../../../../common/components/threat_match/types'; +import { THREAT_MATCH_MAPPING_ERROR_CODES } from './error_codes'; +import { getUnknownThreatMatchMappingFieldNames } from './get_unknown_threat_match_mapping_field_names'; + +interface ThreatMatchMappingValidatorFactoryParams { + indexPatterns: DataViewBase; + threatIndexPatterns: DataViewBase; +} + +export function threatMatchMappingValidatorFactory({ + indexPatterns, + threatIndexPatterns, +}: ThreatMatchMappingValidatorFactoryParams): ValidationFunc { + return (...args) => { + const [{ path, value }] = args; + + if (singleEntryThreat(value)) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELD_MISSING, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.createRule.threatMappingField.requiredError', + { + defaultMessage: 'At least one indicator match is required.', + } + ), + }; + } + + if (containsInvalidItems(value)) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELD_MISSING, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.bothFieldNamesRequiredError', + { + defaultMessage: 'All matches require both a field and threat index field.', + } + ), + }; + } + + const { unknownSourceIndicesFields, unknownThreatMatchIndicesFields } = + getUnknownThreatMatchMappingFieldNames({ + entries: value, + indexPatterns, + threatIndexPatterns, + }); + + if (unknownSourceIndicesFields.length > 0 && unknownThreatMatchIndicesFields.length > 0) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.unknownFields', + { + defaultMessage: + 'Indicator mapping has unknown fields. {unknownSourceIndicesFields} fields not found in the source events indices and {unknownThreatMatchIndicesFields} fields not found in the indicator indices.', + values: { + unknownSourceIndicesFields: `"${unknownSourceIndicesFields.join('", "')}"`, + unknownThreatMatchIndicesFields: `"${unknownThreatMatchIndicesFields.join('", "')}"`, + }, + } + ), + }; + } + + if (unknownSourceIndicesFields.length > 0) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.unknownSourceIndicesFields', + { + defaultMessage: + 'Indicator mapping has unknown fields. {unknownSourceIndicesFields} fields not found in the source events indices.', + values: { + unknownSourceIndicesFields: `"${unknownSourceIndicesFields.join('", "')}"`, + }, + } + ), + }; + } + + if (unknownThreatMatchIndicesFields.length > 0) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.unknownIndicatorIndicesFields', + { + defaultMessage: + 'Indicator mapping has unknown fields. {unknownThreatMatchIndicesFields} fields not found in the indicator indices.', + values: { + unknownThreatMatchIndicesFields: `"${unknownThreatMatchIndicesFields.join('", "')}"`, + }, + } + ), + }; + } + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/index.ts new file mode 100644 index 0000000000000..9656660986222 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './threat_match_query_edit'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/threat_match_query_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/threat_match_query_edit.tsx new file mode 100644 index 0000000000000..8d2cea12de2f0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/threat_match_query_edit.tsx @@ -0,0 +1,56 @@ +/* + * 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 React, { memo } from 'react'; +import type { DataViewBase } from '@kbn/es-query'; +import type { FieldConfig } from '../../../../shared_imports'; +import { UseField } from '../../../../shared_imports'; +import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field'; +import { QueryBarField } from '../../../rule_creation_ui/components/query_bar_field'; +import { threatMatchQueryRequiredValidator } from './validators/threat_match_query_required_validator'; +import { kueryValidatorFactory } from '../../../rule_creation_ui/validators/kuery_validator_factory'; +import * as i18n from './translations'; + +interface ThreatMatchQueryEditProps { + path: string; + threatIndexPatterns: DataViewBase; + loading?: boolean; +} + +export const ThreatMatchQueryEdit = memo(function ThreatMatchQueryEdit({ + path, + threatIndexPatterns, + loading, +}: ThreatMatchQueryEditProps): JSX.Element { + return ( + + ); +}); + +const THREAT_MATCH_QUERY_FIELD_CONFIG: FieldConfig = { + label: i18n.THREAT_MATCH_QUERY_FIELD_LABEL, + validations: [ + { + validator: threatMatchQueryRequiredValidator, + }, + { + validator: kueryValidatorFactory(), + }, + ], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/translations.tsx new file mode 100644 index 0000000000000..114f24a0298eb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/translations.tsx @@ -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 { i18n } from '@kbn/i18n'; + +export const THREAT_MATCH_QUERY_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.threatQuery.label', + { + defaultMessage: 'Indicator index query', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/threat_match_query_required_validator.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/threat_match_query_required_validator.ts new file mode 100644 index 0000000000000..f45d7926fc6e0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/threat_match_query_required_validator.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 { isEmpty } from 'lodash'; +import type { FieldValueQueryBar } from '../../../../rule_creation_ui/components/query_bar_field'; +import type { FormData, ValidationFunc } from '../../../../../shared_imports'; +import * as i18n from './translations'; + +export const threatMatchQueryRequiredValidator: ValidationFunc< + FormData, + string, + FieldValueQueryBar +> = (...args) => { + const [{ path, value }] = args; + + if (isEmpty(value.query.query as string) && isEmpty(value.filters)) { + return { + code: 'ERR_FIELD_MISSING', + path, + message: i18n.THREAT_MATCH_QUERY_REQUIRED_ERROR, + }; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/translations.ts new file mode 100644 index 0000000000000..f13ddbe5f0d15 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_query_edit/validators/translations.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 { i18n } from '@kbn/i18n'; + +export const THREAT_MATCH_QUERY_REQUIRED_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchQueryBar.requiredError', + { + defaultMessage: 'An indicator index query is required.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts index 9593324a9c224..7990007fae580 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { EQL_ERROR_CODES } from '../../../common/hooks/eql/api'; import { ESQL_ERROR_CODES } from '../components/esql_query_edit'; +import { THREAT_MATCH_MAPPING_ERROR_CODES } from '../components/threat_match_mapping_edit/validators/error_codes'; const ESQL_FIELD_NAME = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.nonBlockingErrorCodes.esqlFieldName', @@ -23,11 +24,19 @@ const EQL_FIELD_NAME = i18n.translate( } ); +const THREAT_MATCH_MAPPING_FIELD_NAME = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.nonBlockingErrorCodes.threatMatchMapping', + { + defaultMessage: 'Indicator mapping', + } +); + export const VALIDATION_WARNING_CODES = [ ESQL_ERROR_CODES.INVALID_ESQL, EQL_ERROR_CODES.FAILED_REQUEST, EQL_ERROR_CODES.INVALID_EQL, EQL_ERROR_CODES.MISSING_DATA_SOURCE, + THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, ] as const; export const VALIDATION_WARNING_CODE_FIELD_NAME_MAP: Readonly> = { @@ -35,4 +44,5 @@ export const VALIDATION_WARNING_CODE_FIELD_NAME_MAP: Readonly { - let items: ListItems[] = []; + const items: ListItems[] = []; const isLoadedFromSavedQuery = !isEmpty(savedId) && !isEmpty(savedQueryName); + if (isLoadedFromSavedQuery) { - items = [ - ...items, - { - title: <>{i18n.SAVED_QUERY_NAME_LABEL} , - description: <>{savedQueryName} , - }, - ]; + items.push({ + title: <>{i18n.SAVED_QUERY_NAME_LABEL} , + description: <>{savedQueryName} , + }); } if (!isEmpty(filters)) { filterManager.setFilters(filters); - items = [ - ...items, - { - title: <>{isLoadedFromSavedQuery ? i18n.SAVED_QUERY_FILTERS_LABEL : i18n.FILTERS_LABEL} , - description: ( - - {filterManager.getFilters().map((filter, index) => ( - - - {indexPatterns != null ? ( - - ) : ( - - )} - - - ))} - - ), - }, - ]; + items.push({ + title: <>{isLoadedFromSavedQuery ? i18n.SAVED_QUERY_FILTERS_LABEL : i18n.FILTERS_LABEL} , + description: ( + + {filterManager.getFilters().map((filter, index) => ( + + + {indexPatterns != null ? ( + + ) : ( + + )} + + + ))} + + ), + }); } if (!isEmpty(query)) { - items = [ - ...items, - { - title: ( - <>{isLoadedFromSavedQuery ? i18n.SAVED_QUERY_LABEL : queryLabel ?? i18n.QUERY_LABEL} - ), - description: {query}, - }, - ]; + items.push({ + title: ( + <>{isLoadedFromSavedQuery ? i18n.SAVED_QUERY_LABEL : queryLabel ?? i18n.QUERY_LABEL} + ), + description: {query}, + }); + } + + if (queryLanguage) { + items.push({ + title: THREAT_QUERY_LANGUAGE_LABEL, + description: getQueryLanguageLabel(queryLanguage), + }); } return items; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx index 90afe8d95fa37..bfcd8368d9675 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; +import { render } from '@testing-library/react'; import { StepRuleDescription, @@ -271,7 +272,7 @@ describe('description_step', () => { mockLicenseService ); - expect(result.length).toEqual(14); + expect(result.length).toEqual(15); }); }); @@ -838,5 +839,54 @@ describe('description_step', () => { }); }); }); + + describe('threatIndex', () => { + test('returns no data when there are no index patterns selected', () => { + const result: ListItems[] = getDescriptionItem( + 'threatIndex', + 'Threat index patterns', + { + threatIndex: [], + }, + mockFilterManager, + mockLicenseService + ); + + expect(result).toHaveLength(0); + }); + + test('returns the correct title', () => { + const result: ListItems[] = getDescriptionItem( + 'threatIndex', + 'Threat index patterns', + { + threatIndex: ['test-*'], + }, + mockFilterManager, + mockLicenseService + ); + + expect(result).toHaveLength(1); + expect(result[0].title).toBe('Indicator index patterns'); + }); + + test('returns the correct description', () => { + const result: ListItems[] = getDescriptionItem( + 'threatIndex', + 'Threat index patterns', + { + threatIndex: ['test1-*', 'test2-*'], + }, + mockFilterManager, + mockLicenseService + ); + + expect(result).toHaveLength(1); + + const { container } = render(result[0].description); + + expect(container).toHaveTextContent('test1-*test2-*'); + }); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx index 46ed1f18f70ac..3d72aaf109ec7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx @@ -55,7 +55,6 @@ import * as i18n from './translations'; import { buildMlJobsDescription } from './build_ml_jobs_description'; import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; -import { THREAT_QUERY_LABEL } from './translations'; import { filterEmptyThreats } from '../../pages/rule_creation/helpers'; import { useLicense } from '../../../../common/hooks/use_license'; import type { LicenseService } from '../../../../../common/license'; @@ -77,6 +76,10 @@ import { NEW_TERMS_FIELDS_LABEL } from '../../../rule_creation/components/new_te import { HISTORY_WINDOW_START_LABEL } from '../../../rule_creation/components/history_window_start_edit/translations'; import { MACHINE_LEARNING_JOB_ID_LABEL } from '../../../rule_creation/components/machine_learning_job_id_edit/translations'; import { ANOMALY_THRESHOLD_LABEL } from '../../../rule_creation/components/anomaly_threshold_edit/translations'; +import { THREAT_MATCH_MAPPING_FIELD_LABEL } from '../../../rule_creation/components/threat_match_mapping_edit/translations'; +import { THREAT_MATCH_QUERY_FIELD_LABEL } from '../../../rule_creation/components/threat_match_query_edit/translations'; +import { THREAT_MATCH_INDEX_FIELD_LABEL } from '../../../rule_creation/components/threat_match_index_edit/translations'; +import { THREAT_MATCH_INDICATOR_PATH_FIELD_LABEL } from '../../../rule_creation/components/threat_match_indicator_path_edit/translations'; import type { FieldValueQueryBar } from '../query_bar_field'; const DescriptionListContainer = styled(EuiDescriptionList)` @@ -327,22 +330,32 @@ export const getDescriptionItem = ( return buildRuleTypeDescription(label, ruleType); } else if (field === 'kibanaSiemAppUrl') { return []; + } else if (field === 'threatIndex') { + const values: string[] = get(field, data); + return buildStringArrayDescription(THREAT_MATCH_INDEX_FIELD_LABEL, field, values); } else if (field === 'threatQueryBar') { - const filters = addFilterStateIfNotThere(get('threatQueryBar.filters', data) ?? []); - const query = get('threatQueryBar.query.query', data); - const savedId = get('threatQueryBar.saved_id', data); + const threatQueryBar = get('threatQueryBar', data) as FieldValueQueryBar; + return buildQueryBarDescription({ field, - filters, + filters: addFilterStateIfNotThere(threatQueryBar.filters ?? []), filterManager, - query, - savedId, + query: threatQueryBar.query.query as string, + queryLanguage: threatQueryBar.query.language, + savedId: threatQueryBar.saved_id ?? '', indexPatterns, - queryLabel: THREAT_QUERY_LABEL, + queryLabel: THREAT_MATCH_QUERY_FIELD_LABEL, }); } else if (field === 'threatMapping') { const threatMap: ThreatMapping = get(field, data); - return buildThreatMappingDescription(label, threatMap); + return buildThreatMappingDescription(THREAT_MATCH_MAPPING_FIELD_LABEL, threatMap); + } else if (field === 'threatIndicatorPath') { + return [ + { + title: THREAT_MATCH_INDICATOR_PATH_FIELD_LABEL, + description: get(field, data), + }, + ]; } else if (field === 'newTermsFields') { const values: string[] = get(field, data); return buildStringArrayDescription(NEW_TERMS_FIELDS_LABEL, field, values); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/types.ts index 8fdb8288a9392..39a8c61cc7564 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/types.ts @@ -20,6 +20,7 @@ export interface BuildQueryBarDescription { filters: Filter[]; filterManager: FilterManager; query: string; + queryLanguage?: string; savedId: string; indexPatterns?: DataViewBase; queryLabel?: string; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx index cc303731b26e3..94e230c59569c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx @@ -191,7 +191,7 @@ describe('StepAboutRuleComponent', () => { await act(async () => { wrapper - .find('[data-test-subj="detectionEngineStepAboutThreatIndicatorPath"] input') + .find('[data-test-subj="ruleThreatMatchIndicatorPath"] input') .first() .simulate('change', { target: { value: '' } }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx index 6e20138256d7f..64b666781ce3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx @@ -35,16 +35,14 @@ import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; import { EsFieldSelectorField } from '../es_field_selector_field'; import { useFetchIndex } from '../../../../common/containers/source'; -import { - DEFAULT_INDICATOR_SOURCE_PATH, - DEFAULT_MAX_SIGNALS, -} from '../../../../../common/constants'; +import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; import { useKibana } from '../../../../common/lib/kibana'; import { useRuleIndices } from '../../../rule_management/logic/use_rule_indices'; import { EsqlAutocomplete } from '../esql_autocomplete'; import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; import { useAllEsqlRuleFields } from '../../hooks'; import { MaxSignals } from '../max_signals'; +import { ThreatMatchIndicatorPathEdit } from '../../../rule_creation/components/threat_match_indicator_path_edit'; const CommonUseField = getUseField({ component: Field }); @@ -367,20 +365,7 @@ const StepAboutRuleComponent: FC = ({ /> {isThreatMatchRuleValue && ( - <> - - + )} {isEsqlRuleValue ? ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx index 66c599d0dc721..a748782e2fe3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx @@ -18,11 +18,11 @@ import type { AboutStepRiskScore, AboutStepRule, } from '../../../../detections/pages/detection_engine/rules/types'; -import { OptionalFieldLabel } from '../optional_field_label'; +import { OptionalFieldLabel } from '../../../rule_creation/components/optional_field_label'; import { isUrlInvalid } from '../../../../common/utils/validators'; -import * as I18n from './translations'; import { defaultRiskScoreValidator } from '../../validators/default_risk_score_validator'; import { maxSignalsValidatorFactory } from '../../validators/max_signals_validator_factory'; +import * as I18n from './translations'; const { emptyField } = fieldValidators; @@ -249,23 +249,7 @@ export const schema: FormSchema = { ), labelAppend: OptionalFieldLabel, }, - threatIndicatorPath: { - type: FIELD_TYPES.TEXT, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel', - { - defaultMessage: 'Indicator prefix override', - } - ), - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText', - { - defaultMessage: - 'Specify the document prefix containing your indicator fields. Used for enrichment of indicator match alerts.', - } - ), - labelAppend: OptionalFieldLabel, - }, + threatIndicatorPath: {}, timestampOverride: { type: FIELD_TYPES.TEXT, label: i18n.translate( @@ -360,38 +344,3 @@ export const schema: FormSchema = { labelAppend: OptionalFieldLabel, }, }; - -const threatIndicatorPathRequiredSchemaValue = { - type: FIELD_TYPES.TEXT, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel', - { - defaultMessage: 'Indicator prefix override', - } - ), - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText', - { - defaultMessage: - 'Specify the document prefix containing your indicator fields. Used for enrichment of indicator match alerts.', - } - ), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError', - { - defaultMessage: 'Indicator prefix override must not be empty', - } - ) - ), - type: VALIDATION_TYPES.FIELD, - }, - ], -}; - -export const threatMatchAboutSchema = { - ...schema, - threatIndicatorPath: threatIndicatorPathRequiredSchemaValue, -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index 65384f838ac9b..6a31f9bee8fd7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -817,12 +817,10 @@ function TestForm({ isLoading={false} form={form} indicesConfig={[]} - threatIndicesConfig={[]} indexPattern={indexPattern} isIndexPatternLoading={false} isQueryBarValid={true} setIsQueryBarValid={jest.fn()} - setIsThreatQueryBarValid={jest.fn()} index={stepDefineDefaultValue.index} threatIndex={stepDefineDefaultValue.threatIndex} alertSuppressionFields={stepDefineDefaultValue[ALERT_SUPPRESSION_FIELDS_FIELD_NAME]} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index 4c9461c966ced..88423bb64cf05 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -47,9 +47,8 @@ import { HiddenField, UseField, useFormData, - UseMultiFields, } from '../../../../shared_imports'; -import type { FormHook, FieldHook } from '../../../../shared_imports'; +import type { FormHook } from '../../../../shared_imports'; import { schema } from './schema'; import { useExperimentalFeatureFieldsTransform } from './use_experimental_feature_fields_transform'; import * as i18n from './translations'; @@ -65,7 +64,6 @@ import { } from '../../../../../common/detection_engine/utils'; import { EqlQueryEdit } from '../../../rule_creation/components/eql_query_edit'; import { DataViewSelectorField } from '../data_view_selector_field'; -import { ThreatMatchInput } from '../threatmatch_input'; import { useFetchIndex } from '../../../../common/containers/source'; import { RequiredFields } from '../../../rule_creation/components/required_fields'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; @@ -89,12 +87,14 @@ import { AnomalyThresholdEdit } from '../../../rule_creation/components/anomaly_ import { HistoryWindowStartEdit } from '../../../rule_creation/components/history_window_start_edit'; import { NewTermsFieldsEdit } from '../../../rule_creation/components/new_terms_fields_edit'; import { EsqlQueryEdit } from '../../../rule_creation/components/esql_query_edit'; +import { CreateCustomMlJobButton } from '../../../rule_creation/components/create_ml_job_button/create_ml_job_button'; +import { ThreatMatchEdit } from '../threat_match_edit'; import { usePersistentNewTermsState } from './use_persistent_new_terms_state'; import { usePersistentAlertSuppressionState } from './use_persistent_alert_suppression_state'; import { usePersistentThresholdState } from './use_persistent_threshold_state'; import { usePersistentQuery } from './use_persistent_query'; import { usePersistentMachineLearningState } from './use_persistent_machine_learning_state'; -import { CreateCustomMlJobButton } from '../../../rule_creation/components/create_ml_job_button/create_ml_job_button'; +import { usePersistentThreatMatchState } from './use_persistent_threat_match_state'; const CommonUseField = getUseField({ component: Field }); @@ -103,14 +103,12 @@ const StyledVisibleContainer = styled.div<{ isVisible: boolean }>` `; export interface StepDefineRuleProps extends RuleStepProps { indicesConfig: string[]; - threatIndicesConfig: string[]; defaultSavedQuery?: SavedQuery; form: FormHook; indexPattern: DataViewBase; isIndexPatternLoading: boolean; isQueryBarValid: boolean; setIsQueryBarValid: (valid: boolean) => void; - setIsThreatQueryBarValid: (valid: boolean) => void; index: string[]; threatIndex: string[]; alertSuppressionFields?: string[]; @@ -162,10 +160,8 @@ const StepDefineRuleComponent: FC = ({ queryBarSavedId, queryBarTitle, setIsQueryBarValid, - setIsThreatQueryBarValid, shouldLoadQueryDynamically, threatIndex, - threatIndicesConfig, }) => { const [{ ruleType, queryBar, machineLearningJobId, threshold }] = useFormData({ form, @@ -174,7 +170,6 @@ const StepDefineRuleComponent: FC = ({ const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [indexModified, setIndexModified] = useState(false); - const [threatIndexModified, setThreatIndexModified] = useState(false); const license = useLicense(); const { @@ -229,10 +224,6 @@ const StepDefineRuleComponent: FC = ({ setIndexModified(!isEqual(index, indicesConfig)); }, [index, indicesConfig]); - useEffect(() => { - setThreatIndexModified(!isEqual(threatIndex, threatIndicesConfig)); - }, [threatIndex, threatIndicesConfig]); - const { setPersistentEqlQuery, setPersistentEqlOptions } = usePersistentQuery({ form, }); @@ -250,6 +241,7 @@ const StepDefineRuleComponent: FC = ({ newTermsFieldsPath: 'newTermsFields', historyWindowStartPath: 'historyWindowSize', }); + usePersistentThreatMatchState({ form }); const handleSetRuleFromTimeline = useCallback( ({ index: timelineIndex, queryBar: timelineQueryBar, eqlOptions }) => { @@ -296,11 +288,6 @@ const StepDefineRuleComponent: FC = ({ indexField.setValue(indicesConfig); }, [getFields, indicesConfig]); - const handleResetThreatIndices = useCallback(() => { - const threatIndexField = getFields().threatIndex; - threatIndexField.setValue(threatIndicesConfig); - }, [getFields, threatIndicesConfig]); - const handleOpenTimelineSearch = useCallback(() => { setOpenTimelineSearch(true); }, []); @@ -309,28 +296,6 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); - const ThreatMatchInputChildren = useCallback( - ({ threatMapping }: Record) => ( - - ), - [ - handleResetThreatIndices, - indexPattern, - setIsThreatQueryBarValid, - threatIndexModified, - threatIndexPatterns, - threatIndexPatternsLoading, - ] - ); - const { fields: esqlSuppressionFields, isLoading: isEsqlSuppressionLoading } = useAllEsqlRuleFields({ esqlQuery: isEsqlRule(ruleType) ? (queryBar?.query?.query as string) : undefined, @@ -678,23 +643,16 @@ const StepDefineRuleComponent: FC = ({ )} - - <> - - {ThreatMatchInputChildren} - - - + {isThreatMatchRule(ruleType) && ( + + )} {isNewTermsRule(ruleType) && ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx index d453378f03824..666cbecb07207 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx @@ -9,19 +9,13 @@ import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import React from 'react'; -import { - singleEntryThreat, - containsInvalidItems, - customValidators, -} from '../../../../common/components/threat_match/helpers'; import { isEsqlRule, - isThreatMatchRule, isSuppressionRuleConfiguredWithGroupBy, } from '../../../../../common/detection_engine/utils'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; -import type { ERROR_CODE, FormSchema, ValidationFunc } from '../../../../shared_imports'; -import { FIELD_TYPES, fieldValidators } from '../../../../shared_imports'; +import type { FormSchema, ValidationFunc } from '../../../../shared_imports'; +import { FIELD_TYPES } from '../../../../shared_imports'; import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types'; import { DataSourceType } from '../../../../detections/pages/detection_engine/rules/types'; import { dataViewIdValidatorFactory } from '../../validators/data_view_id_validator_factory'; @@ -33,12 +27,7 @@ import { ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, } from '../../../rule_creation/components/alert_suppression_edit'; import * as alertSuppressionEditI81n from '../../../rule_creation/components/alert_suppression_edit/components/translations'; -import { - INDEX_HELPER_TEXT, - THREAT_MATCH_INDEX_HELPER_TEXT, - THREAT_MATCH_REQUIRED, - THREAT_MATCH_EMPTIES, -} from './translations'; +import { INDEX_HELPER_TEXT } from './translations'; import { queryRequiredValidatorFactory } from '../../validators/query_required_validator_factory'; import { kueryValidatorFactory } from '../../validators/kuery_validator_factory'; @@ -177,109 +166,9 @@ export const schema: FormSchema = { ), }, threshold: {}, - threatIndex: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel', - { - defaultMessage: 'Indicator index patterns', - } - ), - helpText: {THREAT_MATCH_INDEX_HELPER_TEXT}, - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData }] = args; - const needsValidation = isThreatMatchRule(formData.ruleType); - if (!needsValidation) { - return; - } - return fieldValidators.emptyField( - i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError', - { - defaultMessage: 'A minimum of one index pattern is required.', - } - ) - )(...args); - }, - }, - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData, value }] = args; - const needsValidation = isThreatMatchRule(formData.ruleType); - if (!needsValidation) { - return; - } - - return customValidators.forbiddenField(value, '*'); - }, - }, - ], - }, - threatMapping: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel', - { - defaultMessage: 'Indicator mapping', - } - ), - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ path, formData }] = args; - const needsValidation = isThreatMatchRule(formData.ruleType); - if (!needsValidation) { - return; - } - if (singleEntryThreat(formData.threatMapping)) { - return { - code: 'ERR_FIELD_MISSING', - path, - message: THREAT_MATCH_REQUIRED, - }; - } else if (containsInvalidItems(formData.threatMapping)) { - return { - code: 'ERR_FIELD_MISSING', - path, - message: THREAT_MATCH_EMPTIES, - }; - } else { - return undefined; - } - }, - }, - ], - }, - threatQueryBar: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel', - { - defaultMessage: 'Indicator index query', - } - ), - validations: [ - { - validator: (...args) => { - const [{ formData }] = args; - if (!isThreatMatchRule(formData.ruleType)) { - return; - } - - return queryRequiredValidatorFactory(formData.ruleType)(...args); - }, - }, - { - validator: kueryValidatorFactory(), - }, - ], - }, + threatIndex: {}, + threatMapping: {}, + threatQueryBar: {}, newTermsFields: {}, historyWindowSize: {}, [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx index 362f586dc5abd..e5c397f55c829 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx @@ -62,27 +62,6 @@ export const getSavedQueryCheckboxLabelWithoutName = () => } ); -export const THREAT_MATCH_INDEX_HELPER_TEXT = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription', - { - defaultMessage: 'Select threat indices', - } -); - -export const THREAT_MATCH_REQUIRED = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError', - { - defaultMessage: 'At least one indicator match is required.', - } -); - -export const THREAT_MATCH_EMPTIES = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError', - { - defaultMessage: 'All matches require both a field and threat index field.', - } -); - export const SOURCE = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threat_match_state.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threat_match_state.ts new file mode 100644 index 0000000000000..d4e1b4915c787 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threat_match_state.ts @@ -0,0 +1,58 @@ +/* + * 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 { useEffect, useRef } from 'react'; +import usePrevious from 'react-use/lib/usePrevious'; +import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types'; +import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; +import type { FormHook } from '../../../../shared_imports'; +import { useFormData } from '../../../../shared_imports'; +import type { FieldValueQueryBar } from '../query_bar_field'; + +interface UsePersistentThreatMatchStateParams { + form: FormHook; +} + +interface LastThreatMatchState { + threatIndexPatterns: string[] | undefined; + threatQueryBar: FieldValueQueryBar | undefined; + threatMapping: ThreatMapping | undefined; +} + +/** + * Persists threat match form data when switching between threat match and the other rule types. + */ +export function usePersistentThreatMatchState({ form }: UsePersistentThreatMatchStateParams): void { + const lastThreatMatchState = useRef(); + const [{ ruleType, threatIndex: threatIndexPatterns, threatQueryBar, threatMapping }] = + useFormData({ + form, + watch: ['ruleType', 'threatIndex', 'threatQueryBar', 'threatMapping'], + }); + const previousRuleType = usePrevious(ruleType); + + useEffect(() => { + if ( + isThreatMatchRule(ruleType) && + !isThreatMatchRule(previousRuleType) && + lastThreatMatchState.current + ) { + form.updateFieldValues({ + threatIndex: lastThreatMatchState.current.threatIndexPatterns, + threatQueryBar: lastThreatMatchState.current.threatQueryBar, + threatMapping: lastThreatMatchState.current.threatMapping, + }); + + return; + } + + if (isThreatMatchRule(ruleType)) { + lastThreatMatchState.current = { threatIndexPatterns, threatQueryBar, threatMapping }; + } + }, [form, ruleType, previousRuleType, threatIndexPatterns, threatQueryBar, threatMapping]); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threat_match_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threat_match_edit.tsx new file mode 100644 index 0000000000000..8a42e69a62ad4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threat_match_edit.tsx @@ -0,0 +1,51 @@ +/* + * 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 React, { memo } from 'react'; +import type { DataViewBase } from '@kbn/es-query'; +import { EuiSpacer } from '@elastic/eui'; +import { ThreatMatchIndexEdit } from '../../rule_creation/components/threat_match_index_edit'; +import { ThreatMatchQueryEdit } from '../../rule_creation/components/threat_match_query_edit'; +import { ThreatMatchMappingEdit } from '../../rule_creation/components/threat_match_mapping_edit'; + +interface ThreatMatchEditProps { + indexPatternPath: string; + queryPath: string; + mappingPath: string; + threatIndexPatterns: DataViewBase; + indexPatterns: DataViewBase; + loading?: boolean; +} + +export const ThreatMatchEdit = memo(function ThreatMatchEdit({ + indexPatternPath, + queryPath, + mappingPath, + indexPatterns, + threatIndexPatterns, + loading, +}: ThreatMatchEditProps): JSX.Element { + return ( + <> + + + + + + + + + ); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx index b2c9cab59e399..a8a0c80b37f5b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx @@ -5,49 +5,58 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { memo, useCallback, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import type { DataViewBase } from '@kbn/es-query'; import type { ThreatMapEntries } from '../../../../common/components/threat_match/types'; import { ThreatMatchComponent } from '../../../../common/components/threat_match'; import type { FieldHook } from '../../../../shared_imports'; -import { - Field, - getUseField, - UseField, - getFieldValidityAndErrorMessage, -} from '../../../../shared_imports'; -import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types'; +import { UseField, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import { schema } from '../step_define_rule/schema'; import { QueryBarField } from '../query_bar_field'; -import * as i18n from '../step_define_rule/translations'; -import { MyLabelButton } from '../step_define_rule'; -const CommonUseField = getUseField({ component: Field }); - -interface ThreatMatchInputProps { - threatMapping: FieldHook; +interface ThreatMatchEditProps { + path: string; threatIndexPatterns: DataViewBase; indexPatterns: DataViewBase; threatIndexPatternsLoading: boolean; - threatIndexModified: boolean; - handleResetThreatIndices: () => void; onValidityChange?: (isValid: boolean) => void; } -const ThreatMatchInputComponent: React.FC = ({ - threatIndexModified, - handleResetThreatIndices, - threatMapping, +export const ThreatMatchEdit = memo(function ThreatMatchEdit({ + path, indexPatterns, threatIndexPatterns, threatIndexPatternsLoading, onValidityChange, -}: ThreatMatchInputProps) => { - const { setValue, value: threatItems } = threatMapping; +}: ThreatMatchEditProps): JSX.Element { + const componentProps = { + indexPatterns, + threatIndexPatterns, + threatIndexPatternsLoading, + onValidityChange, + }; + + return ; +}); +interface ThreatMatchFieldProps { + field: FieldHook; + threatIndexPatterns: DataViewBase; + indexPatterns: DataViewBase; + threatIndexPatternsLoading: boolean; + onValidityChange?: (isValid: boolean) => void; +} + +function ThreatMatchField({ + field, + threatIndexPatterns, + indexPatterns, + threatIndexPatternsLoading, + onValidityChange, +}: ThreatMatchFieldProps): JSX.Element { const { isInvalid: isThreatMappingInvalid, errorMessage } = - getFieldValidityAndErrorMessage(threatMapping); + getFieldValidityAndErrorMessage(field); const [isThreatIndexPatternValid, setIsThreatIndexPatternValid] = useState(false); useEffect(() => { @@ -56,40 +65,17 @@ const ThreatMatchInputComponent: React.FC = ({ } }, [isThreatIndexPatternValid, isThreatMappingInvalid, onValidityChange]); - const handleBuilderOnChange = useCallback( - ({ entryItems }: { entryItems: ThreatMapEntries[] }): void => { - setValue(entryItems); + const handleMappingsEntryChange = useCallback( + (newEntryItems: ThreatMapEntries[]): void => { + field.setValue(newEntryItems); }, - [setValue] + [field] ); return ( <> - - - - path="threatIndex" - config={{ - ...schema.threatIndex, - labelAppend: threatIndexModified ? ( - - {i18n.RESET_DEFAULT_INDEX} - - ) : null, - }} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleThreatMatchIndices', - 'data-test-subj': 'detectionEngineStepDefineRuleThreatMatchIndices', - euiFieldProps: { - fullWidth: true, - isDisabled: false, - placeholder: '', - }, - }} - /> - - + = ({ ); -}; - -export const ThreatMatchInput = React.memo(ThreatMatchInputComponent); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx index 65ff07924c70f..0256c90d72efa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx @@ -8,7 +8,6 @@ import { useState, useMemo, useEffect } from 'react'; import type { DataViewBase } from '@kbn/es-query'; import { useFormWithWarnings } from '../../../common/hooks/use_form_with_warnings'; -import { isThreatMatchRule } from '../../../../common/detection_engine/utils'; import type { AboutStepRule, ActionsStepRule, @@ -20,10 +19,7 @@ import { useKibana } from '../../../common/lib/kibana'; import type { FormHook } from '../../../shared_imports'; import { useFormData } from '../../../shared_imports'; import { schema as defineRuleSchema } from '../components/step_define_rule/schema'; -import { - schema as aboutRuleSchema, - threatMatchAboutSchema, -} from '../components/step_about_rule/schema'; +import { schema as aboutRuleSchema } from '../components/step_about_rule/schema'; import { schema as scheduleRuleSchema } from '../components/step_schedule_rule/schema'; import { getSchema as getActionsRuleSchema } from '../../rule_creation/components/step_rule_actions/get_schema'; import { useFetchIndex } from '../../../common/containers/source'; @@ -62,14 +58,10 @@ export const useRuleForms = ({ ); // ABOUT STEP FORM - const typeDependentAboutRuleSchema = useMemo( - () => (isThreatMatchRule(defineStepData.ruleType) ? threatMatchAboutSchema : aboutRuleSchema), - [defineStepData.ruleType] - ); const { form: aboutStepForm } = useFormWithWarnings({ defaultValue: aboutStepDefault, options: { stripEmptyFields: false, warningValidationCodes: VALIDATION_WARNING_CODES }, - schema: typeDependentAboutRuleSchema, + schema: aboutRuleSchema, }); const [aboutStepFormData] = useFormData({ form: aboutStepForm, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx index ac70dabc1006d..e145edd29c9e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx @@ -206,7 +206,6 @@ const CreateRulePageComponent: React.FC = () => { const collapseFn = useRef<() => void | undefined>(); const [prevRuleType, setPrevRuleType] = useState(); const [isQueryBarValid, setIsQueryBarValid] = useState(false); - const [isThreatQueryBarValid, setIsThreatQueryBarValid] = useState(false); const esqlQueryForAboutStep = useEsqlQueryForAboutStep({ defineStepData, activeStep }); @@ -219,10 +218,14 @@ const CreateRulePageComponent: React.FC = () => { const defineFieldsTransform = useExperimentalFeatureFieldsTransform(); + const defineStepFormFields = defineStepForm.getFields(); const isPreviewDisabled = getIsRulePreviewDisabled({ ruleType, isQueryBarValid, - isThreatQueryBarValid, + isThreatQueryBarValid: + defineStepFormFields.threatIndex?.isValid && + defineStepFormFields.threatQueryBar?.isValid && + defineStepFormFields.threatMapping?.isValid, index: memoizedIndex, dataViewId: defineStepData.dataViewId, dataSourceType: defineStepData.dataSourceType, @@ -536,13 +539,11 @@ const CreateRulePageComponent: React.FC = () => { { isQueryBarValid, loading, memoDefineStepReadOnly, - threatIndicesConfig, ] ); const memoDefineStepExtraAction = useMemo( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index dc0c9ac6409f1..601eb852d010e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -60,11 +60,7 @@ import * as i18n from './translations'; import { SecurityPageName } from '../../../../app/types'; import { ruleStepsOrder } from '../../../../detections/pages/detection_engine/rules/utils'; import { useKibana, useUiSetting$ } from '../../../../common/lib/kibana'; -import { - APP_UI_ID, - DEFAULT_INDEX_KEY, - DEFAULT_THREAT_INDEX_KEY, -} from '../../../../../common/constants'; +import { APP_UI_ID, DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useGetSavedQuery } from '../../../../detections/pages/detection_engine/rules/use_get_saved_query'; @@ -104,7 +100,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { const [isRulePreviewVisible, setIsRulePreviewVisible] = useState(true); const collapseFn = useRef<() => void | undefined>(); const [isQueryBarValid, setIsQueryBarValid] = useState(false); - const [isThreatQueryBarValid, setIsThreatQueryBarValid] = useState(false); const backOptions = useMemo( () => ({ @@ -117,7 +112,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { ); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); - const [threatIndicesConfig] = useUiSetting$(DEFAULT_THREAT_INDEX_KEY); const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ rule, @@ -151,10 +145,14 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { [defineStepData.index, esqlIndex, defineStepData.ruleType] ); + const defineStepFormFields = defineStepForm.getFields(); const isPreviewDisabled = getIsRulePreviewDisabled({ ruleType: defineStepData.ruleType, isQueryBarValid, - isThreatQueryBarValid, + isThreatQueryBarValid: + defineStepFormFields.threatIndex?.isValid && + defineStepFormFields.threatQueryBar?.isValid && + defineStepFormFields.threatMapping?.isValid, index: memoizedIndex, dataViewId: defineStepData.dataViewId, dataSourceType: defineStepData.dataSourceType, @@ -229,7 +227,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { isLoading={loading || isLoading || isSavedQueryLoading} isUpdateView indicesConfig={indicesConfig} - threatIndicesConfig={threatIndicesConfig} defaultSavedQuery={savedQuery} form={defineStepForm} key="defineStep" @@ -237,7 +234,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { isIndexPatternLoading={isIndexPatternLoading} isQueryBarValid={isQueryBarValid} setIsQueryBarValid={setIsQueryBarValid} - setIsThreatQueryBarValid={setIsThreatQueryBarValid} index={memoizedIndex} threatIndex={defineStepData.threatIndex} alertSuppressionFields={defineStepData[ALERT_SUPPRESSION_FIELDS_FIELD_NAME]} @@ -350,7 +346,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { isSavedQueryLoading, isLoading, indicesConfig, - threatIndicesConfig, savedQuery, defineStepForm, indexPattern, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/threat_match_mapping_validator.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/threat_match_mapping_validator.ts new file mode 100644 index 0000000000000..b458959cf32d6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/threat_match_mapping_validator.ts @@ -0,0 +1,50 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + containsInvalidItems, + singleEntryThreat, +} from '../../../common/components/threat_match/helpers'; +import type { FormData, ValidationFunc } from '../../../shared_imports'; +import type { ThreatMapEntries } from '../../../common/components/threat_match/types'; + +export function threatMatchMappingValidatorFactory(): ValidationFunc< + FormData, + string, + ThreatMapEntries[] +> { + return (...args) => { + const [{ path, value }] = args; + + if (singleEntryThreat(value)) { + return { + code: 'ERR_FIELD_MISSING', + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError', + { + defaultMessage: 'At least one indicator match is required.', + } + ), + }; + } + + if (containsInvalidItems(value)) { + return { + code: 'ERR_FIELD_MISSING', + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError', + { + defaultMessage: 'All matches require both a field and threat index field.', + } + ), + }; + } + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx index 047e9e5bf1977..0de99d72c2849 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx @@ -771,15 +771,6 @@ const prepareDefinitionSectionListItems = ( }); } - if ('threat_mapping' in rule && rule.threat_mapping) { - definitionSectionListItems.push({ - title: ( - {i18n.THREAT_MAPPING_FIELD_LABEL} - ), - description: , - }); - } - if ('threat_filters' in rule && rule.threat_filters && rule.threat_filters.length > 0) { definitionSectionListItems.push({ title: ( @@ -822,6 +813,15 @@ const prepareDefinitionSectionListItems = ( }); } + if ('threat_mapping' in rule && rule.threat_mapping) { + definitionSectionListItems.push({ + title: ( + {i18n.THREAT_MAPPING_FIELD_LABEL} + ), + description: , + }); + } + if ('new_terms_fields' in rule && rule.new_terms_fields && rule.new_terms_fields.length > 0) { definitionSectionListItems.push({ title: ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/data_source/index_pattern_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/data_source/index_pattern_edit.tsx index 81844af5f778f..d8e9cddd9dfca 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/data_source/index_pattern_edit.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/data_source/index_pattern_edit.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { isEqual } from 'lodash'; import { css } from '@emotion/css'; -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty, useEuiTheme } from '@elastic/eui'; import { useDefaultIndexPattern } from '../../../../../../hooks/use_default_index_pattern'; import type { FieldHook } from '../../../../../../../../shared_imports'; import { Field } from '../../../../../../../../shared_imports'; @@ -19,6 +19,7 @@ interface IndexPatternFieldProps { } export function IndexPatternField({ field }: IndexPatternFieldProps): JSX.Element { + const { euiTheme } = useEuiTheme(); const defaultIndexPattern = useDefaultIndexPattern(); const isIndexModified = !isEqual(field.value, defaultIndexPattern); @@ -39,7 +40,9 @@ export function IndexPatternField({ field }: IndexPatternFieldProps): JSX.Elemen labelAppend={ isIndexModified ? ( ); } - -const xxsHeight = css` - height: 16px; -`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/index.ts new file mode 100644 index 0000000000000..0991eb0e6c8a0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './indicator_path_edit_form'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_adapter.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_adapter.tsx new file mode 100644 index 0000000000000..08194971791a5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_adapter.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; +import { ThreatMatchIndicatorPathEdit } from '../../../../../../../rule_creation/components/threat_match_indicator_path_edit'; + +export function ThreatMatchIndicatorPathEditAdapter(): JSX.Element { + return ; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_form.tsx new file mode 100644 index 0000000000000..d5ced106cd909 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_index_indicator_path/indicator_path_edit_form.tsx @@ -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 React from 'react'; +import { RuleFieldEditFormWrapper } from '../../../field_final_side'; +import { ThreatMatchIndicatorPathEditAdapter } from './indicator_path_edit_adapter'; + +export function ThreatMatchIndicatorPathEditForm(): JSX.Element { + return ( + + ); +} + +const schema = {}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/index.ts new file mode 100644 index 0000000000000..1488e68bc266e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './threat_match_index_edit_form'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_adapter.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_adapter.tsx new file mode 100644 index 0000000000000..ea2a9a8c63d88 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_adapter.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; +import { ThreatMatchIndexEdit } from '../../../../../../../rule_creation/components/threat_match_index_edit'; + +export function ThreatMatchIndexEditAdapter(): JSX.Element { + return ; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_form.tsx new file mode 100644 index 0000000000000..2761113410770 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_index/threat_match_index_edit_form.tsx @@ -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 React from 'react'; +import { RuleFieldEditFormWrapper } from '../../../field_final_side'; +import { ThreatMatchIndexEditAdapter } from './threat_match_index_edit_adapter'; + +export function ThreatMatchIndexEditForm(): JSX.Element { + return ( + + ); +} + +const schema = {}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/index.ts new file mode 100644 index 0000000000000..afa440eba5100 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './threat_match_mapping_edit_form'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_adapter.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_adapter.tsx new file mode 100644 index 0000000000000..6738e5668137e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_adapter.tsx @@ -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 React from 'react'; +import type { DataViewBase } from '@kbn/es-query'; +import { ThreatMatchMappingEdit } from '../../../../../../../rule_creation/components/threat_match_mapping_edit'; +import type { ThreatIndex } from '../../../../../../../../../common/api/detection_engine'; +import type { RuleFieldEditComponentProps } from '../../../field_final_side'; +import { useDataView } from '../hooks/use_data_view'; +import { useDiffableRuleDataView } from '../hooks/use_diffable_rule_data_view'; + +export function ThreatMatchMappingEditAdapter({ + finalDiffableRule, +}: RuleFieldEditComponentProps): JSX.Element | null { + const { dataView: ruleDataView } = useDiffableRuleDataView(finalDiffableRule); + const { dataView: threatIndexPatterns } = useDataView({ + indexPatterns: (finalDiffableRule as { threat_index: ThreatIndex }).threat_index ?? [], + }); + + return ( + + ); +} + +const DEFAULT_DATA_VIEW: DataViewBase = { + fields: [], + title: '', +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_form.tsx new file mode 100644 index 0000000000000..d11629bf6a8d8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_mapping/threat_match_mapping_edit_form.tsx @@ -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 React from 'react'; +import { RuleFieldEditFormWrapper } from '../../../field_final_side'; +import { ThreatMatchMappingEditAdapter } from './threat_match_mapping_edit_adapter'; + +export function ThreatMatchMappingEditForm(): JSX.Element { + return ( + + ); +} + +const schema = {}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/index.ts new file mode 100644 index 0000000000000..a9ac475a6ac95 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './threat_match_query_edit_form'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_adapter.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_adapter.tsx new file mode 100644 index 0000000000000..bfab9b5d6b04a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_adapter.tsx @@ -0,0 +1,34 @@ +/* + * 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 React from 'react'; +import type { DataViewBase } from '@kbn/es-query'; +import { ThreatMatchQueryEdit } from '../../../../../../../rule_creation/components/threat_match_query_edit'; +import type { ThreatIndex } from '../../../../../../../../../common/api/detection_engine'; +import type { RuleFieldEditComponentProps } from '../../../field_final_side'; +import { useDataView } from '../hooks/use_data_view'; + +export function ThreatMatchQueryEditAdapter({ + finalDiffableRule, +}: RuleFieldEditComponentProps): JSX.Element | null { + const { dataView, isLoading } = useDataView({ + indexPatterns: (finalDiffableRule as { threat_index: ThreatIndex }).threat_index ?? [], + }); + + return ( + + ); +} + +const DEFAULT_DATA_VIEW: DataViewBase = { + fields: [], + title: '', +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_form.tsx new file mode 100644 index 0000000000000..40e95bedcbb5e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat_match_query/threat_match_query_edit_form.tsx @@ -0,0 +1,73 @@ +/* + * 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 React from 'react'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { + InlineKqlQuery, + KqlQueryLanguage, + KqlQueryType, + RuleQuery, +} from '../../../../../../../../../common/api/detection_engine'; +import type { FormData } from '../../../../../../../../shared_imports'; +import type { FieldValueQueryBar } from '../../../../../../../rule_creation_ui/components/query_bar_field'; +import { RuleFieldEditFormWrapper } from '../../../field_final_side'; +import { ThreatMatchQueryEditAdapter } from './threat_match_query_edit_adapter'; +import { isFilters } from '../../../../helpers'; + +export function ThreatMatchQueryEditForm(): JSX.Element { + return ( + + ); +} + +const schema = {}; + +function deserializer( + _: FormData, + finalDiffableRule: DiffableRule +): { + threatQuery: FieldValueQueryBar; +} { + const parsedQuery = InlineKqlQuery.parse( + (finalDiffableRule as { threat_query: InlineKqlQuery }).threat_query + ); + + return { + threatQuery: { + query: { + query: parsedQuery.query, + language: parsedQuery.language, + }, + filters: isFilters(parsedQuery.filters) ? parsedQuery.filters : [], + saved_id: null, + }, + }; +} + +function serializer(formData: FormData): { + threat_query: InlineKqlQuery; +} { + const threatQuery = (formData as { threatQuery: FieldValueQueryBar }).threatQuery; + + const query = RuleQuery.parse(threatQuery.query.query); + const language = KqlQueryLanguage.parse(threatQuery.query.language); + + return { + threat_query: { + type: KqlQueryType.inline_query, + query, + language, + filters: threatQuery.filters, + }, + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threat_match_rule_field_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threat_match_rule_field_edit.tsx index 2e7d52ae015fc..be96cca3f1868 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threat_match_rule_field_edit.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threat_match_rule_field_edit.tsx @@ -6,16 +6,23 @@ */ import React from 'react'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; import type { UpgradeableThreatMatchFields } from '../../../../model/prebuilt_rule_upgrade/fields'; import { KqlQueryEditForm } from './fields/kql_query'; import { DataSourceEditForm } from './fields/data_source'; import { AlertSuppressionEditForm } from './fields/alert_suppression'; +import { ThreatMatchIndexEditForm } from './fields/threat_match_index'; +import { ThreatMatchQueryEditForm } from './fields/threat_match_query'; +import { ThreatMatchMappingEditForm } from './fields/threat_match_mapping'; +import { ThreatMatchIndicatorPathEditForm } from './fields/threat_index_indicator_path'; interface ThreatMatchRuleFieldEditProps { fieldName: UpgradeableThreatMatchFields; } -export function ThreatMatchRuleFieldEdit({ fieldName }: ThreatMatchRuleFieldEditProps) { +export function ThreatMatchRuleFieldEdit({ + fieldName, +}: ThreatMatchRuleFieldEditProps): JSX.Element | null { switch (fieldName) { case 'kql_query': return ; @@ -23,7 +30,15 @@ export function ThreatMatchRuleFieldEdit({ fieldName }: ThreatMatchRuleFieldEdit return ; case 'alert_suppression': return ; + case 'threat_index': + return ; + case 'threat_query': + return ; + case 'threat_mapping': + return ; + case 'threat_indicator_path': + return ; default: - return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 88d2fb3d6eee7..f0529e22114a7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -6,6 +6,7 @@ */ import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { DEFAULT_THREAT_MAPPING_VALUE } from '../../../../detection_engine/rule_creation/components/threat_match_mapping_edit'; import { ALERT_SUPPRESSION_DURATION_FIELD_NAME, ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, @@ -53,7 +54,7 @@ export const stepDefineDefaultValue: DefineStepRule = { }, requiredFields: [], relatedIntegrations: [], - threatMapping: [], + threatMapping: DEFAULT_THREAT_MAPPING_VALUE, threshold: { field: [], value: '200', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts index dd36fcb6e74aa..86ba3e5dd6bee 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts @@ -132,10 +132,10 @@ const SUBFIELD_MAPPING: Record = { event_category_override: 'event_category_override', tiebreaker_field: 'tiebreaker_field', timestamp_field: 'timestamp_field', - building_block_type: 'type', threat_query: 'query', threat_language: 'language', threat_filters: 'filters', + building_block_type: 'type', rule_name_override: 'field_name', timestamp_override: 'field_name', timestamp_override_fallback_disabled: 'fallback_disabled', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule.cy.ts index 8d44be4dc3aaf..deccc1a205f61 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule.cy.ts @@ -93,6 +93,7 @@ import { getIndicatorOrButton, selectIndicatorMatchType, waitForAlertsToPopulate, + getThreatMatchQueryInvalidationText, } from '../../../../tasks/create_new_rule'; import { SCHEDULE_INTERVAL_AMOUNT_INPUT, @@ -195,7 +196,7 @@ describe('indicator match', { tags: ['@ess', '@serverless', '@skipInServerlessMK }); }); - describe('custom indicator query input', () => { + describe('indicator query input', () => { beforeEach(() => { visit(CREATE_RULE_URL); selectIndicatorMatchType(); @@ -207,7 +208,7 @@ describe('indicator match', { tags: ['@ess', '@serverless', '@skipInServerlessMK it('Shows invalidation text if text is removed', () => { getCustomIndicatorQueryInput().type('{selectall}{del}'); - getCustomQueryInvalidationText().should('exist'); + getThreatMatchQueryInvalidationText().should('exist'); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts index 483cbf06b6256..ad66955a6e244 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts @@ -73,19 +73,19 @@ export const QUERY_BAR_ADD_FILTER = '[data-test-subj="detectionEngineStepDefineRuleQueryBar"] [data-test-subj="addFilter"]'; export const THREAT_MAPPING_COMBO_BOX_INPUT = - '[data-test-subj="threatMatchInput"] [data-test-subj="fieldAutocompleteComboBox"]'; + '[data-test-subj="ruleThreatMatchMappingField"] [data-test-subj="fieldAutocompleteComboBox"]'; export const THREAT_MATCH_CUSTOM_QUERY_INPUT = '[data-test-subj="detectionEngineStepDefineRuleQueryBar"] [data-test-subj="queryInput"]'; export const THREAT_MATCH_QUERY_INPUT = - '[data-test-subj="detectionEngineStepDefineThreatRuleQueryBar"] [data-test-subj="queryInput"]'; + '[data-test-subj="ruleThreatMatchQueryField"] [data-test-subj="queryInput"]'; export const CUSTOM_INDEX_PATTERN_INPUT = '[data-test-subj="detectionEngineStepDefineRuleIndices"] [data-test-subj="comboBoxInput"]'; export const THREAT_MATCH_INDICATOR_INDICATOR_INDEX = - '[data-test-subj="detectionEngineStepDefineRuleThreatMatchIndices"] [data-test-subj="comboBoxInput"]'; + '[data-test-subj="ruleThreatMatchIndicesField"] [data-test-subj="comboBoxInput"]'; export const THREAT_MATCH_AND_BUTTON = '[data-test-subj="andButton"]'; @@ -104,6 +104,8 @@ export const AT_LEAST_ONE_INDEX_PATTERN = 'A minimum of one index pattern is req export const CUSTOM_QUERY_REQUIRED = 'A custom query is required.'; +export const THREAT_MATCH_QUERY_REQUIRED = 'An indicator index query is required.'; + export const DATA_VIEW_COMBO_BOX = '[data-test-subj="pick-rule-data-source"] [data-test-subj="comboBoxInput"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index 5890dba3a9e68..d7fbb5e13255e 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -134,6 +134,7 @@ import { PREVIEW_LOGGED_REQUESTS_CHECKBOX, ALERT_SUPPRESSION_DURATION_VALUE_INPUT, ALERT_SUPPRESSION_DURATION_UNIT_INPUT, + THREAT_MATCH_QUERY_REQUIRED, } from '../screens/create_new_rule'; import { INDEX_SELECTOR, @@ -784,6 +785,9 @@ export const getCustomIndicatorQueryInput = () => cy.get(THREAT_MATCH_QUERY_INPU /** Returns custom query required content */ export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQUIRED); +/** Returns threat match query required content */ +export const getThreatMatchQueryInvalidationText = () => cy.contains(THREAT_MATCH_QUERY_REQUIRED); + /** * Fills in the define indicator match rules and then presses the continue button * @param rule The rule to use to fill in everything