Skip to content

Commit 98159da

Browse files
[8.19] [Security Solution] Allow partial matches on rule name when searching installed rules. (#237496) (#240940)
# Backport This will backport the following commits from `main` to `8.19`: - [[Security Solution] Allow partial matches on rule name when searching installed rules. (#237496)](#237496) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Steven de Salas","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-10-28T08:25:18Z","message":"[Security Solution] Allow partial matches on rule name when searching installed rules. (#237496)\n\n**Fixes: #237278**\n**Fixes: #97094**\n**Fixes: #194066**\n\n## Summary\n\nWhen a user navigates to `Rules` > `Detection Rules (SIEM)` and wishes\nto find all rules matching a partial string on the rule name (like `win`\nto get all rules about `Windows`) they receive 0 matches. This is in\ncontrast to the behavior they experience when searching available\nprebuilt \"Elastic Rules\", where partial name searches are available.\n\nThis PR fixes this UX inconsistency by modifying the KQL output\ngenerated by the search query.\n\nWhereas previously a search for `\"win\"` would have generated the\nfollowing KQL:\n\n```sql\n(alert.attributes.name: \"win\" OR\n alert.attributes.params.index: \"win\" OR\n alert.attributes.params.threat.tactic.id: \"win\" OR\n ...\n```\n\nIt now treats the `alert.attributes.name` differently, allowing it to\nmatch on partial terms (ie `*win*` instead of `\"win\"`) and using\n`.keyword` index for better special character support.\n\n```sql\n(alert.attributes.name.keyword: *win* OR # <-- here\n alert.attributes.params.index: \"win\" OR \n alert.attributes.params.threat.tactic.id: \"win\" OR \n ...\n```\n\n### 🕵️ BUT! .. We only do this for single term searches!\n\nPlease note that this approach only applies to single term searches. For\nmultiple term searches we maintain the \"old\" way of searching with\nquotations `\"windows 10 patch\"` instead of `*windows 10 patch*` or even\n`*windows* *10* *patch*`.\n\nThe reasoning here is that since search results are NOT sorted by score,\nwe want to avoid returning too many matches (wildcard searches match on\n_any_ combination of the terms, ie those with `windows` or `10` or\n`patch`) which would confuse the user just as they are trying to narrow\ndown their search results!! (ie 🤔 why am I getting `Linux patch` rules\nwhen I'm asking for `windows 10 patch`??).\n\n## How to test:\n\nTo test this PR please checkout the relevant branch and run an instance\nof kibana/elasticsearch locally.\n\n1. `Security App` > `Rules` > `Detection Rules (SIEM)` \n2. A table of installed rules should appear. Remove all installed\nElastic rules.\n3. `Elastic Rules` (filter) > Tick to select all > `Select all X Rules`\n> `Bulk actions` > `Delete` > `Delete`\n4. Go to `Add Elastic Rules`\n5. `Search rules by name` > `win` > `Enter`\n6. You should see 5 pages of results (~84 rules)\n7. `Install All`\n8. All Elastic rules have been installed > `Go back to installed Elastic\nrules`\n9. Search by rule name > `win` > `Enter`\n10. You should see 5 pages of results (~84 rules - same as in Step 6.)\n\nPlease feel free to try some additional search queries. \n\n<details>\n<summary>\n\n### 👉 Click for additional testing ideas\n\n</summary>\n\nHere are some single term searches:\n - `goog`\n - `proc`\n - `sql` (should include `Postgresql` and `MSSQL`)\n - `shell` (should include `Powershell`)\n - `inject` (should include `injection`)\n - `git` (should include `GitHub`)\n - `.exe`\n - `pub/sub` (exact matches only)\n - `user-agent` (exact matches only)\n - `/bin`\n - `CVE-2025` (should include partial matches)\n - `-` (should return matches with dash in the name like `user-agent`)\n- `_` (should return matches with dash in the name like `CAP_SYS_ADMIN`)\n - `|` (should return no matches - no elastic rules with this)\n\nAnd behavior for multiple term searches:\n- `AWS` (check the count), then `AWS IAM` (there should be less results\nfor `AWS IAM`)\n- `pub/sub topic` (should be less than for `pub/sub`, note that `pub/sub\ntop` partial match has no matches!)\n- `root` then `root cert` then `root certificate` (the second has no\nresults, `root certificate` should only return exact matches)\n- `proc`, then `process`, then `process injection`, then `potential\nprocess injection` (each should return less results)\n\n\n</details>\n\n## Screenshots\n\n\n\n![497230260-e8badac1-85dd-41ff-b905-1a0a3d4bc53d](https://github.com/user-attachments/assets/69e39370-c31c-4f24-84d5-a84386929612)\n\n\n## Special character support\n\nNote that by switching to wildcard searches (ie `*win*`) on the\n`.keyword` index and fully escaping special characters in KQL we'll\n**_ALSO_** be allowing special character searches on single search\nterms.\n\nFor example, searching by `user-agent` will return results that only\nmatch `User-Agent` but not `user` or `agent` individually.\n\nSome other useful example of these approach are searches for: `Pub/Sub`,\n`CVE-2025`, `/bin`, `_`, `.exe`.\n\nThis support, in addition for escaping the backslash `\\` character will\nallow us to close the next TWO ISSUES in this epic 🥳 wohoo!!\n\n👉 #97094 (special chars in rule name) and,\n👉 #194066 (special chars in tags)\n\n<img width=\"800\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/5411d8e8-b3f7-4a3f-9863-c64cc2a7df2c\"\n/>\n\n<img width=\"800\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/c3589add-9f80-4646-807a-3f0c8a2ec5a7\"\n/>\n\n\n\n### Testing special character support.\n\nIn addition to the steps above (installing all elastic rules), we can:\n\n1. Create a rule with special characters: ie `Rule with special chars\n(&, *, #, $, ?, >, @, \\, /, \", ‘, {, [, ;)`\n2. Try some single term matches:\n - `@`\n - `&` (additional elastic rules with this term should appear).\n - `*` \n - `\"`\n - `:`\n - `>`\n - `{`\n - `\\` (backslash, this used to break the search under #97094)\n\n## Risks\n\nThese are some of the risks that could be identified by using this\napproach.\n\n### 1. Dependencies that reuse query logic\n\nNote that the `searchTerm -> KQL` conversion for searching for rules is\nused a few places.\n\n- Installed Rules\n- Rule Monitoring\n- Bulk actions\n\n**Note**: All these paths are tested manually, and form part of the\nautomated tests.\n\n### 2. Performance (ie `allowLeadingWildcards`)\n\nWe're using wildcard searches before and after the search term (ie\n`*win*`) in order to replicate the behavior across both 'prebuilt' and\n'installed' rules tables. The wildcard AFTER the term (`win*` =>\nmatching terms like `Windows`) is no problem but the one BEFORE (`*win`\n=> matching terms like `Darwin`) could create an issue.\n\nOur [KQL\ndocumentation](https://www.elastic.co/docs/reference/query-languages/kql#_filter_for_documents_using_wildcards)\nwarns that Kibana UI Advanced Settings have\n`query:allowLeadingWildcards` turned off by default. This is for\nperformance reasons as a leading wildcard can have a large impact when\nsearching indexes that have millions of terms associated with them.\n\n<img width=\"2784\" height=\"2120\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/9e0a754c-c30b-453e-a3ae-60a104b13fde\"\n/>\n\n> By default, leading wildcards are not allowed for performance reasons.\nYou can modify this with the\n[query:allowLeadingWildcards](https://www.elastic.co/docs/reference/kibana/advanced-settings#query-allowleadingwildcards)\nadvanced setting.\n\nPlease note that the [Query DSL docs also\nwarn](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard)\nabout avoiding this approach.\n\nThere is also more info and additional warnings under [Lucene API\ndocs](https://lucene.apache.org/core/9_12_3/core/org/apache/lucene/search/WildcardQuery.html).\n\nThe risk here is two pronged:\n\n1. Users with a lot (millions?) of detection rules will likely have a\ndegraded search experience as queries will take longer to execute.\n\n2. Leading wildcards appear to be working by default (in contrast to\nwhat is stated in the documentation). But the mere _existence_ of\nvarious settings to avoid them\n([`allowLeadingWildcards`](https://www.elastic.co/docs/reference/kibana/advanced-settings#query-allowleadingwildcards),\n[`allow_leading_wildcard`](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard),\n[`analyze_wildcards`](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard))\nis a risk, because some users may have a special setup we've not been\nable to anticipate in our testing, leading to potential issues like this\none: https://github.com/elastic/kibana/issues/57828\n\n### Mitigating factors:\n\n1. Please note that in the case of security detection rules, the\nprebuilt rules package is _only ~1500 rules_! Users do not manage\nmillions of rules, they generally manage low thousands or even hundreds.\nWe've tested 100K rules successfully and there was [_no perceivable\ndifference in terms of search\nperformance_](https://github.com/elastic/kibana/pull/237496#issuecomment-3389956730).\nAnd this seems to be a realistic test when checking actual usage stats\n(our 10 largest users in Sep'25 had between 60-160K installed). Also,\ndue to current limits on pagination and bulk action logic we would\nlikely hit different kinds of problems here _before_ search performance\nbecomes an issue.\n\n2. We've tested all the documented ways to disable leading wildcards,\nincluding disabling them manually (under `Server Management > Advanced\nSettings`) and explicitly in the `kibana.yml`. None of them seemed to\naffect searches carried out on Saved Objects. This is because our\nsolution uses the\n[`alerting`](http://github.com/elastic/kibana/tree/main/x-pack/platform/plugins/shared/alerting)\nplugin under the hood which [converts the filter to KQL without\nreferring to any UI Advanced\nSettings](https://github.com/elastic/kibana/blob/91cab0e1369473846dc2712aa7dfe38b8580a9a5/x-pack/platform/plugins/shared/alerting/server/rules_client/common/build_kuery_node_filter.ts#L23).\nAnd since there is no explicit setting for `allowLeadingWildcards` the\ndefault setting of `true` gets applied instead [inside the\n`grammar.peggy`\nfile](https://github.com/elastic/kibana/blob/91cab0e1369473846dc2712aa7dfe38b8580a9a5/src/platform/packages/shared/kbn-es-query/src/kuery/grammar/grammar.peggy#L12).\n\n### Risks that were found acceptable\n\nIn the search for any possible settings that might affect the rollout of\nchanges under this PR [we did find a\nsetting](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-wildcard-query#_allow_expensive_queries_7)\ninside `elasticsearch.yml` that causes problems with the proposed\nsolution.\n\n**TL/DR** 👉 `search.allow_expensive_queries=false` breaks the search.💥🤯\n\n<img width=\"600\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/8d523d15-f832-4488-8c78-4ed60c474d44\"\n/>\n\nHowever we also found bigger problems, _this setting also prevents the\ninstallation of prebuilt rules_ (see below), which are a cornerstone of\nour security solution.\n\n<img width=\"600\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/2b8d91e8-29b9-4f06-9f76-6b1edb999fd1\"\n/>\n\nIn other words, the setting `search.allow_expensive_queries=true` has\nbecome _a de-facto requirement of Detection Rules_, that has not yet\nbeen documented. Hence we including it to the\n[documentation](https://www.elastic.co/docs/solutions/security/detect-and-alert/detections-requirements)\nas part of this PR.\n\nNote also that the errors here are not limited to detection rules.\n\nWe found that the setting _also compromised or outright broke a lot of\nfunctionality in Kibana_ 💥🤯 (including Fleet, API Keys, Timelines, Saved\nObject search, Tags, Server Monitoring etc). More about this is\n[documented in this internal\ndocument](https://docs.google.com/document/d/1HLOXQZFcm1-KBj9DHTqwcF3wDdLOE6CcgUzqzZA2CAg/edit?tab=t.0).\nAnd in this [internal slack\nthread](https://elastic.slack.com/archives/C02HA9E8221/p1760694975799469).\nSo we're assuming that most if not all of our users will have it set to\n`true`.\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] [Flaky Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\nused on any tests changed\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Follow the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n- [x] Changes have been socialized with the PM and rest of the team.\n- [x] All identified Risks have been properly documented and\ninvestigated. ([internal\ninvestigation](https://docs.google.com/document/d/1HLOXQZFcm1-KBj9DHTqwcF3wDdLOE6CcgUzqzZA2CAg/edit?tab=t.0))\n- [x] New requirements added to the technical docs (PR\n[#3543](elastic/docs-content#3543), see\n[here](https://www.elastic.co/docs/solutions/security/detect-and-alert/detections-requirements))","sha":"433902b9b40c9fb3f4db5346008a61efaed3df31","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Team:Detections and Resp","Team: SecuritySolution","Feature:Rule Management","Team:Detection Rule Management","ci:cloud-deploy","ci:project-deploy-security","backport:version","v9.3.0","v8.19.7","v9.1.7","v9.2.1"],"title":"[Security Solution] Allow partial matches on rule name when searching installed rules.","number":237496,"url":"https://github.com/elastic/kibana/pull/237496","mergeCommit":{"message":"[Security Solution] Allow partial matches on rule name when searching installed rules. (#237496)\n\n**Fixes: #237278**\n**Fixes: #97094**\n**Fixes: #194066**\n\n## Summary\n\nWhen a user navigates to `Rules` > `Detection Rules (SIEM)` and wishes\nto find all rules matching a partial string on the rule name (like `win`\nto get all rules about `Windows`) they receive 0 matches. This is in\ncontrast to the behavior they experience when searching available\nprebuilt \"Elastic Rules\", where partial name searches are available.\n\nThis PR fixes this UX inconsistency by modifying the KQL output\ngenerated by the search query.\n\nWhereas previously a search for `\"win\"` would have generated the\nfollowing KQL:\n\n```sql\n(alert.attributes.name: \"win\" OR\n alert.attributes.params.index: \"win\" OR\n alert.attributes.params.threat.tactic.id: \"win\" OR\n ...\n```\n\nIt now treats the `alert.attributes.name` differently, allowing it to\nmatch on partial terms (ie `*win*` instead of `\"win\"`) and using\n`.keyword` index for better special character support.\n\n```sql\n(alert.attributes.name.keyword: *win* OR # <-- here\n alert.attributes.params.index: \"win\" OR \n alert.attributes.params.threat.tactic.id: \"win\" OR \n ...\n```\n\n### 🕵️ BUT! .. We only do this for single term searches!\n\nPlease note that this approach only applies to single term searches. For\nmultiple term searches we maintain the \"old\" way of searching with\nquotations `\"windows 10 patch\"` instead of `*windows 10 patch*` or even\n`*windows* *10* *patch*`.\n\nThe reasoning here is that since search results are NOT sorted by score,\nwe want to avoid returning too many matches (wildcard searches match on\n_any_ combination of the terms, ie those with `windows` or `10` or\n`patch`) which would confuse the user just as they are trying to narrow\ndown their search results!! (ie 🤔 why am I getting `Linux patch` rules\nwhen I'm asking for `windows 10 patch`??).\n\n## How to test:\n\nTo test this PR please checkout the relevant branch and run an instance\nof kibana/elasticsearch locally.\n\n1. `Security App` > `Rules` > `Detection Rules (SIEM)` \n2. A table of installed rules should appear. Remove all installed\nElastic rules.\n3. `Elastic Rules` (filter) > Tick to select all > `Select all X Rules`\n> `Bulk actions` > `Delete` > `Delete`\n4. Go to `Add Elastic Rules`\n5. `Search rules by name` > `win` > `Enter`\n6. You should see 5 pages of results (~84 rules)\n7. `Install All`\n8. All Elastic rules have been installed > `Go back to installed Elastic\nrules`\n9. Search by rule name > `win` > `Enter`\n10. You should see 5 pages of results (~84 rules - same as in Step 6.)\n\nPlease feel free to try some additional search queries. \n\n<details>\n<summary>\n\n### 👉 Click for additional testing ideas\n\n</summary>\n\nHere are some single term searches:\n - `goog`\n - `proc`\n - `sql` (should include `Postgresql` and `MSSQL`)\n - `shell` (should include `Powershell`)\n - `inject` (should include `injection`)\n - `git` (should include `GitHub`)\n - `.exe`\n - `pub/sub` (exact matches only)\n - `user-agent` (exact matches only)\n - `/bin`\n - `CVE-2025` (should include partial matches)\n - `-` (should return matches with dash in the name like `user-agent`)\n- `_` (should return matches with dash in the name like `CAP_SYS_ADMIN`)\n - `|` (should return no matches - no elastic rules with this)\n\nAnd behavior for multiple term searches:\n- `AWS` (check the count), then `AWS IAM` (there should be less results\nfor `AWS IAM`)\n- `pub/sub topic` (should be less than for `pub/sub`, note that `pub/sub\ntop` partial match has no matches!)\n- `root` then `root cert` then `root certificate` (the second has no\nresults, `root certificate` should only return exact matches)\n- `proc`, then `process`, then `process injection`, then `potential\nprocess injection` (each should return less results)\n\n\n</details>\n\n## Screenshots\n\n\n\n![497230260-e8badac1-85dd-41ff-b905-1a0a3d4bc53d](https://github.com/user-attachments/assets/69e39370-c31c-4f24-84d5-a84386929612)\n\n\n## Special character support\n\nNote that by switching to wildcard searches (ie `*win*`) on the\n`.keyword` index and fully escaping special characters in KQL we'll\n**_ALSO_** be allowing special character searches on single search\nterms.\n\nFor example, searching by `user-agent` will return results that only\nmatch `User-Agent` but not `user` or `agent` individually.\n\nSome other useful example of these approach are searches for: `Pub/Sub`,\n`CVE-2025`, `/bin`, `_`, `.exe`.\n\nThis support, in addition for escaping the backslash `\\` character will\nallow us to close the next TWO ISSUES in this epic 🥳 wohoo!!\n\n👉 #97094 (special chars in rule name) and,\n👉 #194066 (special chars in tags)\n\n<img width=\"800\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/5411d8e8-b3f7-4a3f-9863-c64cc2a7df2c\"\n/>\n\n<img width=\"800\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/c3589add-9f80-4646-807a-3f0c8a2ec5a7\"\n/>\n\n\n\n### Testing special character support.\n\nIn addition to the steps above (installing all elastic rules), we can:\n\n1. Create a rule with special characters: ie `Rule with special chars\n(&, *, #, $, ?, >, @, \\, /, \", ‘, {, [, ;)`\n2. Try some single term matches:\n - `@`\n - `&` (additional elastic rules with this term should appear).\n - `*` \n - `\"`\n - `:`\n - `>`\n - `{`\n - `\\` (backslash, this used to break the search under #97094)\n\n## Risks\n\nThese are some of the risks that could be identified by using this\napproach.\n\n### 1. Dependencies that reuse query logic\n\nNote that the `searchTerm -> KQL` conversion for searching for rules is\nused a few places.\n\n- Installed Rules\n- Rule Monitoring\n- Bulk actions\n\n**Note**: All these paths are tested manually, and form part of the\nautomated tests.\n\n### 2. Performance (ie `allowLeadingWildcards`)\n\nWe're using wildcard searches before and after the search term (ie\n`*win*`) in order to replicate the behavior across both 'prebuilt' and\n'installed' rules tables. The wildcard AFTER the term (`win*` =>\nmatching terms like `Windows`) is no problem but the one BEFORE (`*win`\n=> matching terms like `Darwin`) could create an issue.\n\nOur [KQL\ndocumentation](https://www.elastic.co/docs/reference/query-languages/kql#_filter_for_documents_using_wildcards)\nwarns that Kibana UI Advanced Settings have\n`query:allowLeadingWildcards` turned off by default. This is for\nperformance reasons as a leading wildcard can have a large impact when\nsearching indexes that have millions of terms associated with them.\n\n<img width=\"2784\" height=\"2120\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/9e0a754c-c30b-453e-a3ae-60a104b13fde\"\n/>\n\n> By default, leading wildcards are not allowed for performance reasons.\nYou can modify this with the\n[query:allowLeadingWildcards](https://www.elastic.co/docs/reference/kibana/advanced-settings#query-allowleadingwildcards)\nadvanced setting.\n\nPlease note that the [Query DSL docs also\nwarn](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard)\nabout avoiding this approach.\n\nThere is also more info and additional warnings under [Lucene API\ndocs](https://lucene.apache.org/core/9_12_3/core/org/apache/lucene/search/WildcardQuery.html).\n\nThe risk here is two pronged:\n\n1. Users with a lot (millions?) of detection rules will likely have a\ndegraded search experience as queries will take longer to execute.\n\n2. Leading wildcards appear to be working by default (in contrast to\nwhat is stated in the documentation). But the mere _existence_ of\nvarious settings to avoid them\n([`allowLeadingWildcards`](https://www.elastic.co/docs/reference/kibana/advanced-settings#query-allowleadingwildcards),\n[`allow_leading_wildcard`](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard),\n[`analyze_wildcards`](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard))\nis a risk, because some users may have a special setup we've not been\nable to anticipate in our testing, leading to potential issues like this\none: https://github.com/elastic/kibana/issues/57828\n\n### Mitigating factors:\n\n1. Please note that in the case of security detection rules, the\nprebuilt rules package is _only ~1500 rules_! Users do not manage\nmillions of rules, they generally manage low thousands or even hundreds.\nWe've tested 100K rules successfully and there was [_no perceivable\ndifference in terms of search\nperformance_](https://github.com/elastic/kibana/pull/237496#issuecomment-3389956730).\nAnd this seems to be a realistic test when checking actual usage stats\n(our 10 largest users in Sep'25 had between 60-160K installed). Also,\ndue to current limits on pagination and bulk action logic we would\nlikely hit different kinds of problems here _before_ search performance\nbecomes an issue.\n\n2. We've tested all the documented ways to disable leading wildcards,\nincluding disabling them manually (under `Server Management > Advanced\nSettings`) and explicitly in the `kibana.yml`. None of them seemed to\naffect searches carried out on Saved Objects. This is because our\nsolution uses the\n[`alerting`](http://github.com/elastic/kibana/tree/main/x-pack/platform/plugins/shared/alerting)\nplugin under the hood which [converts the filter to KQL without\nreferring to any UI Advanced\nSettings](https://github.com/elastic/kibana/blob/91cab0e1369473846dc2712aa7dfe38b8580a9a5/x-pack/platform/plugins/shared/alerting/server/rules_client/common/build_kuery_node_filter.ts#L23).\nAnd since there is no explicit setting for `allowLeadingWildcards` the\ndefault setting of `true` gets applied instead [inside the\n`grammar.peggy`\nfile](https://github.com/elastic/kibana/blob/91cab0e1369473846dc2712aa7dfe38b8580a9a5/src/platform/packages/shared/kbn-es-query/src/kuery/grammar/grammar.peggy#L12).\n\n### Risks that were found acceptable\n\nIn the search for any possible settings that might affect the rollout of\nchanges under this PR [we did find a\nsetting](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-wildcard-query#_allow_expensive_queries_7)\ninside `elasticsearch.yml` that causes problems with the proposed\nsolution.\n\n**TL/DR** 👉 `search.allow_expensive_queries=false` breaks the search.💥🤯\n\n<img width=\"600\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/8d523d15-f832-4488-8c78-4ed60c474d44\"\n/>\n\nHowever we also found bigger problems, _this setting also prevents the\ninstallation of prebuilt rules_ (see below), which are a cornerstone of\nour security solution.\n\n<img width=\"600\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/2b8d91e8-29b9-4f06-9f76-6b1edb999fd1\"\n/>\n\nIn other words, the setting `search.allow_expensive_queries=true` has\nbecome _a de-facto requirement of Detection Rules_, that has not yet\nbeen documented. Hence we including it to the\n[documentation](https://www.elastic.co/docs/solutions/security/detect-and-alert/detections-requirements)\nas part of this PR.\n\nNote also that the errors here are not limited to detection rules.\n\nWe found that the setting _also compromised or outright broke a lot of\nfunctionality in Kibana_ 💥🤯 (including Fleet, API Keys, Timelines, Saved\nObject search, Tags, Server Monitoring etc). More about this is\n[documented in this internal\ndocument](https://docs.google.com/document/d/1HLOXQZFcm1-KBj9DHTqwcF3wDdLOE6CcgUzqzZA2CAg/edit?tab=t.0).\nAnd in this [internal slack\nthread](https://elastic.slack.com/archives/C02HA9E8221/p1760694975799469).\nSo we're assuming that most if not all of our users will have it set to\n`true`.\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] [Flaky Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\nused on any tests changed\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Follow the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n- [x] Changes have been socialized with the PM and rest of the team.\n- [x] All identified Risks have been properly documented and\ninvestigated. ([internal\ninvestigation](https://docs.google.com/document/d/1HLOXQZFcm1-KBj9DHTqwcF3wDdLOE6CcgUzqzZA2CAg/edit?tab=t.0))\n- [x] New requirements added to the technical docs (PR\n[#3543](elastic/docs-content#3543), see\n[here](https://www.elastic.co/docs/solutions/security/detect-and-alert/detections-requirements))","sha":"433902b9b40c9fb3f4db5346008a61efaed3df31"}},"sourceBranch":"main","suggestedTargetBranches":["8.19","9.1","9.2"],"targetPullRequestStates":[{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/237496","number":237496,"mergeCommit":{"message":"[Security Solution] Allow partial matches on rule name when searching installed rules. (#237496)\n\n**Fixes: #237278**\n**Fixes: #97094**\n**Fixes: #194066**\n\n## Summary\n\nWhen a user navigates to `Rules` > `Detection Rules (SIEM)` and wishes\nto find all rules matching a partial string on the rule name (like `win`\nto get all rules about `Windows`) they receive 0 matches. This is in\ncontrast to the behavior they experience when searching available\nprebuilt \"Elastic Rules\", where partial name searches are available.\n\nThis PR fixes this UX inconsistency by modifying the KQL output\ngenerated by the search query.\n\nWhereas previously a search for `\"win\"` would have generated the\nfollowing KQL:\n\n```sql\n(alert.attributes.name: \"win\" OR\n alert.attributes.params.index: \"win\" OR\n alert.attributes.params.threat.tactic.id: \"win\" OR\n ...\n```\n\nIt now treats the `alert.attributes.name` differently, allowing it to\nmatch on partial terms (ie `*win*` instead of `\"win\"`) and using\n`.keyword` index for better special character support.\n\n```sql\n(alert.attributes.name.keyword: *win* OR # <-- here\n alert.attributes.params.index: \"win\" OR \n alert.attributes.params.threat.tactic.id: \"win\" OR \n ...\n```\n\n### 🕵️ BUT! .. We only do this for single term searches!\n\nPlease note that this approach only applies to single term searches. For\nmultiple term searches we maintain the \"old\" way of searching with\nquotations `\"windows 10 patch\"` instead of `*windows 10 patch*` or even\n`*windows* *10* *patch*`.\n\nThe reasoning here is that since search results are NOT sorted by score,\nwe want to avoid returning too many matches (wildcard searches match on\n_any_ combination of the terms, ie those with `windows` or `10` or\n`patch`) which would confuse the user just as they are trying to narrow\ndown their search results!! (ie 🤔 why am I getting `Linux patch` rules\nwhen I'm asking for `windows 10 patch`??).\n\n## How to test:\n\nTo test this PR please checkout the relevant branch and run an instance\nof kibana/elasticsearch locally.\n\n1. `Security App` > `Rules` > `Detection Rules (SIEM)` \n2. A table of installed rules should appear. Remove all installed\nElastic rules.\n3. `Elastic Rules` (filter) > Tick to select all > `Select all X Rules`\n> `Bulk actions` > `Delete` > `Delete`\n4. Go to `Add Elastic Rules`\n5. `Search rules by name` > `win` > `Enter`\n6. You should see 5 pages of results (~84 rules)\n7. `Install All`\n8. All Elastic rules have been installed > `Go back to installed Elastic\nrules`\n9. Search by rule name > `win` > `Enter`\n10. You should see 5 pages of results (~84 rules - same as in Step 6.)\n\nPlease feel free to try some additional search queries. \n\n<details>\n<summary>\n\n### 👉 Click for additional testing ideas\n\n</summary>\n\nHere are some single term searches:\n - `goog`\n - `proc`\n - `sql` (should include `Postgresql` and `MSSQL`)\n - `shell` (should include `Powershell`)\n - `inject` (should include `injection`)\n - `git` (should include `GitHub`)\n - `.exe`\n - `pub/sub` (exact matches only)\n - `user-agent` (exact matches only)\n - `/bin`\n - `CVE-2025` (should include partial matches)\n - `-` (should return matches with dash in the name like `user-agent`)\n- `_` (should return matches with dash in the name like `CAP_SYS_ADMIN`)\n - `|` (should return no matches - no elastic rules with this)\n\nAnd behavior for multiple term searches:\n- `AWS` (check the count), then `AWS IAM` (there should be less results\nfor `AWS IAM`)\n- `pub/sub topic` (should be less than for `pub/sub`, note that `pub/sub\ntop` partial match has no matches!)\n- `root` then `root cert` then `root certificate` (the second has no\nresults, `root certificate` should only return exact matches)\n- `proc`, then `process`, then `process injection`, then `potential\nprocess injection` (each should return less results)\n\n\n</details>\n\n## Screenshots\n\n\n\n![497230260-e8badac1-85dd-41ff-b905-1a0a3d4bc53d](https://github.com/user-attachments/assets/69e39370-c31c-4f24-84d5-a84386929612)\n\n\n## Special character support\n\nNote that by switching to wildcard searches (ie `*win*`) on the\n`.keyword` index and fully escaping special characters in KQL we'll\n**_ALSO_** be allowing special character searches on single search\nterms.\n\nFor example, searching by `user-agent` will return results that only\nmatch `User-Agent` but not `user` or `agent` individually.\n\nSome other useful example of these approach are searches for: `Pub/Sub`,\n`CVE-2025`, `/bin`, `_`, `.exe`.\n\nThis support, in addition for escaping the backslash `\\` character will\nallow us to close the next TWO ISSUES in this epic 🥳 wohoo!!\n\n👉 #97094 (special chars in rule name) and,\n👉 #194066 (special chars in tags)\n\n<img width=\"800\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/5411d8e8-b3f7-4a3f-9863-c64cc2a7df2c\"\n/>\n\n<img width=\"800\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/c3589add-9f80-4646-807a-3f0c8a2ec5a7\"\n/>\n\n\n\n### Testing special character support.\n\nIn addition to the steps above (installing all elastic rules), we can:\n\n1. Create a rule with special characters: ie `Rule with special chars\n(&, *, #, $, ?, >, @, \\, /, \", ‘, {, [, ;)`\n2. Try some single term matches:\n - `@`\n - `&` (additional elastic rules with this term should appear).\n - `*` \n - `\"`\n - `:`\n - `>`\n - `{`\n - `\\` (backslash, this used to break the search under #97094)\n\n## Risks\n\nThese are some of the risks that could be identified by using this\napproach.\n\n### 1. Dependencies that reuse query logic\n\nNote that the `searchTerm -> KQL` conversion for searching for rules is\nused a few places.\n\n- Installed Rules\n- Rule Monitoring\n- Bulk actions\n\n**Note**: All these paths are tested manually, and form part of the\nautomated tests.\n\n### 2. Performance (ie `allowLeadingWildcards`)\n\nWe're using wildcard searches before and after the search term (ie\n`*win*`) in order to replicate the behavior across both 'prebuilt' and\n'installed' rules tables. The wildcard AFTER the term (`win*` =>\nmatching terms like `Windows`) is no problem but the one BEFORE (`*win`\n=> matching terms like `Darwin`) could create an issue.\n\nOur [KQL\ndocumentation](https://www.elastic.co/docs/reference/query-languages/kql#_filter_for_documents_using_wildcards)\nwarns that Kibana UI Advanced Settings have\n`query:allowLeadingWildcards` turned off by default. This is for\nperformance reasons as a leading wildcard can have a large impact when\nsearching indexes that have millions of terms associated with them.\n\n<img width=\"2784\" height=\"2120\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/9e0a754c-c30b-453e-a3ae-60a104b13fde\"\n/>\n\n> By default, leading wildcards are not allowed for performance reasons.\nYou can modify this with the\n[query:allowLeadingWildcards](https://www.elastic.co/docs/reference/kibana/advanced-settings#query-allowleadingwildcards)\nadvanced setting.\n\nPlease note that the [Query DSL docs also\nwarn](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard)\nabout avoiding this approach.\n\nThere is also more info and additional warnings under [Lucene API\ndocs](https://lucene.apache.org/core/9_12_3/core/org/apache/lucene/search/WildcardQuery.html).\n\nThe risk here is two pronged:\n\n1. Users with a lot (millions?) of detection rules will likely have a\ndegraded search experience as queries will take longer to execute.\n\n2. Leading wildcards appear to be working by default (in contrast to\nwhat is stated in the documentation). But the mere _existence_ of\nvarious settings to avoid them\n([`allowLeadingWildcards`](https://www.elastic.co/docs/reference/kibana/advanced-settings#query-allowleadingwildcards),\n[`allow_leading_wildcard`](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard),\n[`analyze_wildcards`](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-wildcard))\nis a risk, because some users may have a special setup we've not been\nable to anticipate in our testing, leading to potential issues like this\none: https://github.com/elastic/kibana/issues/57828\n\n### Mitigating factors:\n\n1. Please note that in the case of security detection rules, the\nprebuilt rules package is _only ~1500 rules_! Users do not manage\nmillions of rules, they generally manage low thousands or even hundreds.\nWe've tested 100K rules successfully and there was [_no perceivable\ndifference in terms of search\nperformance_](https://github.com/elastic/kibana/pull/237496#issuecomment-3389956730).\nAnd this seems to be a realistic test when checking actual usage stats\n(our 10 largest users in Sep'25 had between 60-160K installed). Also,\ndue to current limits on pagination and bulk action logic we would\nlikely hit different kinds of problems here _before_ search performance\nbecomes an issue.\n\n2. We've tested all the documented ways to disable leading wildcards,\nincluding disabling them manually (under `Server Management > Advanced\nSettings`) and explicitly in the `kibana.yml`. None of them seemed to\naffect searches carried out on Saved Objects. This is because our\nsolution uses the\n[`alerting`](http://github.com/elastic/kibana/tree/main/x-pack/platform/plugins/shared/alerting)\nplugin under the hood which [converts the filter to KQL without\nreferring to any UI Advanced\nSettings](https://github.com/elastic/kibana/blob/91cab0e1369473846dc2712aa7dfe38b8580a9a5/x-pack/platform/plugins/shared/alerting/server/rules_client/common/build_kuery_node_filter.ts#L23).\nAnd since there is no explicit setting for `allowLeadingWildcards` the\ndefault setting of `true` gets applied instead [inside the\n`grammar.peggy`\nfile](https://github.com/elastic/kibana/blob/91cab0e1369473846dc2712aa7dfe38b8580a9a5/src/platform/packages/shared/kbn-es-query/src/kuery/grammar/grammar.peggy#L12).\n\n### Risks that were found acceptable\n\nIn the search for any possible settings that might affect the rollout of\nchanges under this PR [we did find a\nsetting](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-wildcard-query#_allow_expensive_queries_7)\ninside `elasticsearch.yml` that causes problems with the proposed\nsolution.\n\n**TL/DR** 👉 `search.allow_expensive_queries=false` breaks the search.💥🤯\n\n<img width=\"600\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/8d523d15-f832-4488-8c78-4ed60c474d44\"\n/>\n\nHowever we also found bigger problems, _this setting also prevents the\ninstallation of prebuilt rules_ (see below), which are a cornerstone of\nour security solution.\n\n<img width=\"600\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/2b8d91e8-29b9-4f06-9f76-6b1edb999fd1\"\n/>\n\nIn other words, the setting `search.allow_expensive_queries=true` has\nbecome _a de-facto requirement of Detection Rules_, that has not yet\nbeen documented. Hence we including it to the\n[documentation](https://www.elastic.co/docs/solutions/security/detect-and-alert/detections-requirements)\nas part of this PR.\n\nNote also that the errors here are not limited to detection rules.\n\nWe found that the setting _also compromised or outright broke a lot of\nfunctionality in Kibana_ 💥🤯 (including Fleet, API Keys, Timelines, Saved\nObject search, Tags, Server Monitoring etc). More about this is\n[documented in this internal\ndocument](https://docs.google.com/document/d/1HLOXQZFcm1-KBj9DHTqwcF3wDdLOE6CcgUzqzZA2CAg/edit?tab=t.0).\nAnd in this [internal slack\nthread](https://elastic.slack.com/archives/C02HA9E8221/p1760694975799469).\nSo we're assuming that most if not all of our users will have it set to\n`true`.\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] [Flaky Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\nused on any tests changed\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Follow the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n- [x] Changes have been socialized with the PM and rest of the team.\n- [x] All identified Risks have been properly documented and\ninvestigated. ([internal\ninvestigation](https://docs.google.com/document/d/1HLOXQZFcm1-KBj9DHTqwcF3wDdLOE6CcgUzqzZA2CAg/edit?tab=t.0))\n- [x] New requirements added to the technical docs (PR\n[#3543](elastic/docs-content#3543), see\n[here](https://www.elastic.co/docs/solutions/security/detect-and-alert/detections-requirements))","sha":"433902b9b40c9fb3f4db5346008a61efaed3df31"}},{"branch":"8.19","label":"v8.19.7","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.1","label":"v9.1.7","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.2","label":"v9.2.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Steven de Salas <[email protected]>
1 parent 9305480 commit 98159da

File tree

8 files changed

+460
-46
lines changed

8 files changed

+460
-46
lines changed

x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.test.ts

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,101 @@ describe('convertRulesFilterToKQL', () => {
1515
tags: [],
1616
};
1717

18-
it('returns empty string if filter options are empty', () => {
18+
it('returns empty string if filter is an empty string', () => {
1919
const kql = convertRulesFilterToKQL(filterOptions);
2020

2121
expect(kql).toBe('');
2222
});
2323

24+
it('returns empty string if filter contains only whitespace', () => {
25+
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: ' \n\t' });
26+
27+
expect(kql).toBe('');
28+
});
29+
30+
it('returns empty string if filter is undefined', () => {
31+
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: undefined });
32+
33+
expect(kql).toBe('');
34+
});
35+
2436
it('handles presence of "filter" properly', () => {
2537
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: 'foo' });
2638

2739
expect(kql).toBe(
28-
'(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")'
40+
'(' +
41+
'alert.attributes.name.keyword: *foo* ' +
42+
'OR alert.attributes.params.index: "foo" ' +
43+
'OR alert.attributes.params.threat.tactic.id: "foo" ' +
44+
'OR alert.attributes.params.threat.tactic.name: "foo" ' +
45+
'OR alert.attributes.params.threat.technique.id: "foo" ' +
46+
'OR alert.attributes.params.threat.technique.name: "foo" ' +
47+
'OR alert.attributes.params.threat.technique.subtechnique.id: "foo" ' +
48+
'OR alert.attributes.params.threat.technique.subtechnique.name: "foo"' +
49+
')'
2950
);
3051
});
3152

32-
it('escapes "filter" value properly', () => {
33-
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: '" OR (foo: bar)' });
53+
it('escapes "filter" value for single term searches', () => {
54+
const kql = convertRulesFilterToKQL({
55+
...filterOptions,
56+
filter: '"a<detection\\-rule*with)a<surprise:',
57+
});
3458

3559
expect(kql).toBe(
36-
'(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.name: "\\" OR (foo: bar)")'
60+
'(' +
61+
'alert.attributes.name.keyword: *\\"a\\<detection\\\\-rule\\*with\\)a\\<surprise\\:* ' +
62+
'OR alert.attributes.params.index: "\\"a<detection\\\\-rule*with)a<surprise:" ' +
63+
'OR alert.attributes.params.threat.tactic.id: "\\"a<detection\\\\-rule*with)a<surprise:" ' +
64+
'OR alert.attributes.params.threat.tactic.name: "\\"a<detection\\\\-rule*with)a<surprise:" ' +
65+
'OR alert.attributes.params.threat.technique.id: "\\"a<detection\\\\-rule*with)a<surprise:" ' +
66+
'OR alert.attributes.params.threat.technique.name: "\\"a<detection\\\\-rule*with)a<surprise:" ' +
67+
'OR alert.attributes.params.threat.technique.subtechnique.id: "\\"a<detection\\\\-rule*with)a<surprise:" ' +
68+
'OR alert.attributes.params.threat.technique.subtechnique.name: "\\"a<detection\\\\-rule*with)a<surprise:"' +
69+
')'
3770
);
3871
});
3972

73+
it('allows partial name matches for single term searches', () => {
74+
const kql = convertRulesFilterToKQL({
75+
...filterOptions,
76+
filter: 'sql',
77+
});
78+
79+
expect(kql.startsWith('(alert.attributes.name.keyword: *sql*')).toBe(true);
80+
expect(kql).not.toContain('alert.attributes.name: "sql"');
81+
});
82+
83+
it('escapes "filter" value for multiple term searches', () => {
84+
const kql = convertRulesFilterToKQL({
85+
...filterOptions,
86+
filter: '"a <detection rule with)\\a< surprise:',
87+
});
88+
89+
expect(kql).toBe(
90+
'(' +
91+
'alert.attributes.name: "\\"a <detection rule with)\\\\a< surprise:" ' +
92+
'OR alert.attributes.params.index: "\\"a <detection rule with)\\\\a< surprise:" ' +
93+
'OR alert.attributes.params.threat.tactic.id: "\\"a <detection rule with)\\\\a< surprise:" ' +
94+
'OR alert.attributes.params.threat.tactic.name: "\\"a <detection rule with)\\\\a< surprise:" ' +
95+
'OR alert.attributes.params.threat.technique.id: "\\"a <detection rule with)\\\\a< surprise:" ' +
96+
'OR alert.attributes.params.threat.technique.name: "\\"a <detection rule with)\\\\a< surprise:" ' +
97+
'OR alert.attributes.params.threat.technique.subtechnique.id: "\\"a <detection rule with)\\\\a< surprise:" ' +
98+
'OR alert.attributes.params.threat.technique.subtechnique.name: "\\"a <detection rule with)\\\\a< surprise:"' +
99+
')'
100+
);
101+
});
102+
103+
it('allows only exact matching for multi-term searches', () => {
104+
const kql = convertRulesFilterToKQL({
105+
...filterOptions,
106+
filter: 'sql server',
107+
});
108+
109+
expect(kql.startsWith('(alert.attributes.name: "sql server"')).toBe(true);
110+
expect(kql).not.toContain('alert.attributes.name.keyword: *sql server*');
111+
});
112+
40113
it('handles presence of "showCustomRules" properly', () => {
41114
const kql = convertRulesFilterToKQL({ ...filterOptions, showCustomRules: true });
42115

@@ -74,7 +147,19 @@ describe('convertRulesFilterToKQL', () => {
74147
});
75148

76149
expect(kql).toBe(
77-
`(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo") AND alert.attributes.params.immutable: true AND alert.attributes.tags:(\"tag1\" AND \"tag2\")`
150+
`(` +
151+
`alert.attributes.name.keyword: *foo* OR ` +
152+
`alert.attributes.params.index: "foo" OR ` +
153+
`alert.attributes.params.threat.tactic.id: "foo" OR ` +
154+
`alert.attributes.params.threat.tactic.name: "foo" OR ` +
155+
`alert.attributes.params.threat.technique.id: "foo" OR ` +
156+
`alert.attributes.params.threat.technique.name: "foo" OR ` +
157+
`alert.attributes.params.threat.technique.subtechnique.id: "foo" OR ` +
158+
`alert.attributes.params.threat.technique.subtechnique.name: "foo")` +
159+
` AND ` +
160+
`alert.attributes.params.immutable: true` +
161+
` AND ` +
162+
`alert.attributes.tags:(\"tag1\" AND \"tag2\")`
78163
);
79164
});
80165

x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
99
import type { RuleExecutionStatus } from '../../api/detection_engine';
1010
import { RuleCustomizationStatus, RuleExecutionStatusEnum } from '../../api/detection_engine';
11-
import { prepareKQLStringParam } from '../../utils/kql';
11+
import { fullyEscapeKQLStringParam, prepareKQLStringParam } from '../../utils/kql';
1212
import {
1313
ENABLED_FIELD,
1414
IS_CUSTOMIZED_FIELD,
@@ -60,7 +60,7 @@ export function convertRulesFilterToKQL({
6060
}: Partial<RulesFilterOptions>): string {
6161
const kql: string[] = [];
6262

63-
if (searchTerm?.length) {
63+
if (searchTerm?.trim().length) {
6464
kql.push(`(${convertRuleSearchTermToKQL(searchTerm)})`);
6565
}
6666

@@ -106,7 +106,6 @@ export function convertRulesFilterToKQL({
106106
}
107107

108108
const SEARCHABLE_RULE_ATTRIBUTES = [
109-
RULE_NAME_FIELD,
110109
RULE_PARAMS_FIELDS.INDEX,
111110
RULE_PARAMS_FIELDS.TACTIC_ID,
112111
RULE_PARAMS_FIELDS.TACTIC_NAME,
@@ -116,11 +115,36 @@ const SEARCHABLE_RULE_ATTRIBUTES = [
116115
RULE_PARAMS_FIELDS.SUBTECHNIQUE_NAME,
117116
];
118117

119-
export function convertRuleSearchTermToKQL(
120-
searchTerm: string,
121-
attributes = SEARCHABLE_RULE_ATTRIBUTES
122-
): string {
123-
return attributes.map((param) => `${param}: ${prepareKQLStringParam(searchTerm)}`).join(' OR ');
118+
/**
119+
* Build KQL search terms.
120+
*
121+
* Note that RULE_NAME_FIELD is special, for single term searches
122+
* it includes partial matches, supporting special characters.
123+
*
124+
* Ie - "sql" =KQL=> *sql* --matches--> sql, Postgreslq, SQLCMD.EXE
125+
* - "sql:" =KQL=> *sql\:* --matches--> sql:x64, but NOT sql_x64
126+
*
127+
* Whereas the rest of the fields, and multiple term searches,
128+
* we use exact term match with quotations.
129+
*
130+
* Ie - "sql" =KQL=> "sql" --matches--> sql server, but NOT mssql or SQLCMD.EXE
131+
*
132+
* @param searchTerm search term (ie from the search bar)
133+
* @returns KQL String
134+
*/
135+
export function convertRuleSearchTermToKQL(searchTerm: string): string {
136+
const searchableConditions = SEARCHABLE_RULE_ATTRIBUTES.map(
137+
(attribute) => `${attribute}: ${prepareKQLStringParam(searchTerm)}`
138+
);
139+
const escapedTerm = fullyEscapeKQLStringParam(searchTerm);
140+
const isSingleTerm = escapedTerm.split(' ').length === 1;
141+
let ruleNameCondition = '';
142+
if (isSingleTerm) {
143+
ruleNameCondition = `${RULE_NAME_FIELD}.keyword: *${escapedTerm}*`;
144+
} else {
145+
ruleNameCondition = `${RULE_NAME_FIELD}: ${prepareKQLStringParam(searchTerm)}`;
146+
}
147+
return [ruleNameCondition].concat(searchableConditions).join(' OR ');
124148
}
125149

126150
export function convertRuleTagsToKQL(tags: string[]): string {

x-pack/solutions/security/plugins/security_solution/common/utils/kql.test.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
* 2.0.
66
*/
77

8-
import { escapeKQLStringParam, prepareKQLParam, prepareKQLStringParam } from './kql';
8+
import {
9+
escapeKQLStringParam,
10+
prepareKQLParam,
11+
prepareKQLStringParam,
12+
fullyEscapeKQLStringParam,
13+
} from './kql';
914

10-
const testCases = [
15+
const partialEscapeTestCases = [
1116
['does NOT remove white spaces quotes', ' netcat', ' netcat'],
1217
['escapes quotes', 'I said, "Hello."', 'I said, \\"Hello.\\"'],
1318
[
1419
'should escape special characters',
1520
`This \\ has (a lot of) <special> characters, don't you *think*? "Yes."`,
16-
`This \\ has (a lot of) <special> characters, don't you *think*? \\"Yes.\\"`,
21+
`This \\\\ has (a lot of) <special> characters, don't you *think*? \\"Yes.\\"`,
1722
],
1823
['does NOT escape keywords', 'foo and bar or baz not qux', 'foo and bar or baz not qux'],
1924
[
@@ -42,10 +47,15 @@ const testCases = [
4247
'This\nhas\tnewlines\r\nwith\ttabs',
4348
'This\\nhas\\tnewlines\\r\\nwith\\ttabs',
4449
],
50+
[
51+
'escapes backslashes at the end of the string',
52+
'Try not to break the search\\',
53+
'Try not to break the search\\\\',
54+
],
4555
];
4656

4757
describe('prepareKQLParam', () => {
48-
it.each(testCases)('%s', (_, input, expected) => {
58+
it.each(partialEscapeTestCases)('%s', (_, input, expected) => {
4959
expect(prepareKQLParam(input)).toBe(`"${expected}"`);
5060
});
5161

@@ -65,13 +75,44 @@ describe('prepareKQLParam', () => {
6575
});
6676

6777
describe('prepareKQLStringParam', () => {
68-
it.each(testCases)('%s', (_, input, expected) => {
78+
it.each(partialEscapeTestCases)('%s', (_, input, expected) => {
6979
expect(prepareKQLStringParam(input)).toBe(`"${expected}"`);
7080
});
7181
});
7282

7383
describe('escapeKQLStringParam', () => {
74-
it.each(testCases)('%s', (_, input, expected) => {
84+
it.each(partialEscapeTestCases)('%s', (_, input, expected) => {
7585
expect(escapeKQLStringParam(input)).toBe(expected);
7686
});
7787
});
88+
89+
const fullyEscapeTestCases = [
90+
['escapes quotes, but keeps commas and dots', 'I said, "Hello."', 'I said, \\"Hello.\\"'],
91+
[
92+
'should cleanup special characters',
93+
`This \\ has (a lot of) <special> characters, don't you *think*? "Yes."`,
94+
`This \\\\ has \\(a lot of\\) \\<special\\> characters, don't you \\*think\\*? \\"Yes.\\"`,
95+
],
96+
[
97+
'should cleanup special characters and trim whitespace',
98+
`a "user-agent+ \t }with \n a *\\:(surprise{) \t`,
99+
`a \\"user-agent+ \\}with a \\*\\\\\\:\\(surprise\\{\\)`,
100+
],
101+
[
102+
"should keep certain characters that are not problematic (.,'&^%$#)",
103+
`\t some characters are ok to use .,'&^%$#-+_=|/!`,
104+
`some characters are ok to use .,'&^%$#-+_=|/!`,
105+
],
106+
['does NOT escape keywords', 'foo and bar or baz not qux', 'foo and bar or baz not qux'],
107+
[
108+
'It can also handle creepy unicode',
109+
'It can also handle c̶̛̫̜̞̜͕̼̱̘̤̔̿̽́̉̓̋͠͠r̵̨̨̳̯̬͔̰̙͕̲̭̞̈́͒́͋͛̕͝ȩ̷̨͖̻͓̭̮͙͖̬̿͛͐̀̐̄̀͆̾̀̏̓͗̇͘͜ḛ̷̲̖͚̼͇̖̖̩̤͖̪̠̍͂̆͒̂̿̐p̸̹͇̲͇̬̞̞̐̃̎̍͂͐̐́̋̂͝y̶̧̝͔̙̮͖̹̯̺͇̞̰̹͉̏͗̿͑̿͆̐̈́ unicode',
110+
'It can also handle c̶̛̫̜̞̜͕̼̱̘̤̔̿̽́̉̓̋͠͠r̵̨̨̳̯̬͔̰̙͕̲̭̞̈́͒́͋͛̕͝ȩ̷̨͖̻͓̭̮͙͖̬̿͛͐̀̐̄̀͆̾̀̏̓͗̇͘͜ḛ̷̲̖͚̼͇̖̖̩̤͖̪̠̍͂̆͒̂̿̐p̸̹͇̲͇̬̞̞̐̃̎̍͂͐̐́̋̂͝y̶̧̝͔̙̮͖̹̯̺͇̞̰̹͉̏͗̿͑̿͆̐̈́ unicode',
111+
],
112+
];
113+
114+
describe('fullyEscapeKQLStringParam', () => {
115+
it.each(fullyEscapeTestCases)('%s', (_, input, expected) => {
116+
expect(fullyEscapeKQLStringParam(input)).toBe(expected);
117+
});
118+
});

x-pack/solutions/security/plugins/security_solution/common/utils/kql.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,58 @@ export function prepareKQLStringParam(value: string): string {
3333
}
3434

3535
/**
36-
* Escapes string param intended to be passed to KQL. As official docs
37-
* [here](https://www.elastic.co/guide/en/kibana/current/kuery-query.html) say
38-
* `Certain characters must be escaped by a backslash (unless surrounded by quotes).` and
39-
* `You must escape following characters: \():<>"*`.
36+
* Partially escapes string param intended to be passed to KQL.
4037
*
41-
* This function assumes the value is surrounded by quotes so it escapes quotes, tabs and new line symbols.
38+
* This is intended to be used for KQL search terms surrounded by quotes.
39+
* It escapes quotes, backslashes, tabs and new line symbols.
4240
*
4341
* @param param a string param value intended to be passed to KQL
44-
* @returns an escaped string param value
42+
* @returns a partially escaped KQL string param
4543
*/
4644
export function escapeKQLStringParam(value = ''): string {
47-
return escapeStringValue(value);
45+
return partiallyEscapeStringValue(value);
4846
}
4947

5048
const escapeQuotes = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string
5149

50+
const escapeBackslash = (val: string) => val.replace(/\\/g, '\\$&');
51+
5252
const escapeTabs = (val: string) =>
5353
val.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n');
5454

55-
const escapeStringValue = flow(escapeQuotes, escapeTabs);
55+
const partiallyEscapeStringValue = flow(escapeBackslash, escapeQuotes, escapeTabs);
56+
57+
/**
58+
* Fully escapes special characters to improve matching on KQL.
59+
*
60+
* As per official docs [here](https://www.elastic.co/guide/en/kibana/current/kuery-query.html)
61+
* `Certain characters must be escaped by a backslash (unless surrounded by quotes).` and
62+
* `You must escape following characters: \():<>"*`.
63+
*
64+
* This is intended to be used on KQL search terms WITHOUT quotes.
65+
*
66+
* @example "a \"user-agent+ \t }with \n a *\:(surprise!) " => "a \\\\"user-agent+ \\}with a \\*\\\\:\\(surprise!\\)"
67+
*
68+
* @see https://www.elastic.co/docs/reference/query-languages/kql
69+
*
70+
* @param param a string param value intended to be passed to KQL
71+
* @returns a fully escaped KQL string value
72+
*/
73+
export function fullyEscapeKQLStringParam(value = ''): string {
74+
return fullyEscapeStringValue(value);
75+
}
76+
77+
const SPECIAL_KQL_CHARACTERS = '\\(){}:<>"*';
78+
const SPECIAL_KQL_CHARACTERS_REGEX = new RegExp(
79+
`[${SPECIAL_KQL_CHARACTERS.split('').join('\\')}]`,
80+
'g'
81+
);
82+
83+
const escapeSpecialKQLCharacters = (val: string) =>
84+
val.replace(SPECIAL_KQL_CHARACTERS_REGEX, '\\$&');
85+
86+
const simplifyWhitespace = (val: string) => val.replace(/\s+/g, ' ');
87+
88+
const trim = (val: string) => val.trim();
89+
90+
const fullyEscapeStringValue = flow(escapeSpecialKQLCharacters, simplifyWhitespace, trim);

0 commit comments

Comments
 (0)