From ad665b6e207bae44e8472a96123952f78be5159a Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:39:40 +0200 Subject: [PATCH] Merge 4.6.0 into 4.7.0 (#5783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Merge 4.5.1 into 4.6.0 (#5671) * Change windows agent service name (#5538) * Change windows agent service name to Wazuh Change windows agent service name to Wazuh * Add CHANGELOG * Remove agent name in agent info ribbon (#5497) * remove: agent name in agent info ribbon * changelog: add pull request entry --------- Co-authored-by: Álex Ruiz * Fix IPV6 visualizations (#5471) * add ipv6 service * add test for service * Fix issue in agents-table * fix issue in agents-info * fix groups agents issue * Fix width in groups agents * use mapResponseItem * Add copy button to groups * Add copy button to info * fix for node list * Optimize code * Fix styles * Edit changelog * Edit changelog * Add imposter changes to test ipv6 * Replace onMouseDown with onClick * Move copy buttons to the left * fix: removed compressipv6 property of TableWzAPI * feat: add tableLayout property to some tables and remove IPv6 address compression add tableLayout=auto property to some tables: - Agents/{agent_id}/Inventory data - Management/Cluster/Nodes - Agents - Management/Configuration/Client - Management/Global configuration/Remote remove IPv6 address compression * remove: remove unused service to IPv6 compression * revert: revert changes in TableWzAPI component * add: add mocked responses to some syscollector endpoints * remove: unwanted table columns properties * changelog: add pull request entry * Fix imposter --------- Co-authored-by: Antonio David Gutiérrez Co-authored-by: Álex Ruiz Co-authored-by: yenienserrano Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Bump v4.4.4-2.6.0-rc2 * Add Apple Silicon architecture to the register Agent wizard (#5478) * Add Apple Silicon architecture * Add changelog * Change macOS environment variables * Revert "Change macOS environment variables" This reverts commit 108e86626045de6b5cd7b7053a8c6333d8bf8b89. * Change macOS architecture ids * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Bump 4.5.1 * Change the method to make the redirect (#5539) * Change the metod to make the redirect * Remove unused code * Add changelog --------- Co-authored-by: Álex Ruiz * Fix agents active coverage stat as NaN (#5490) * fix: agents active coverate stat as NaN Ensure the values used to calculate have the expected types and the total count is greater than 0. * remove: unused openRegistrationDocs method * changelog: add entry * fix: check if agents active coverage is a NaN * changelog: fix entry --------- Co-authored-by: Álex Ruiz * [Backport 4.5.1] Update test snapshots for 4.5 (#5607) * Update test snapshots for 4.5 (#5601) * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Update test snapshost * Update API data to 4.5 * Update branch patterns for GH Actions --------- Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> (cherry picked from commit 1ae5f19a9edc967187b2d946aad6e8d8f0afff14) * Fix API reference links in endpoints.json * Add kbn-dev 7.17.11 (#5628) * Merge 4.5.0 into 4.5.1 (#5670) * Update test snapshots for 4.5 (#5601) * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Update test snapshost * Update API data to 4.5 * Update branch patterns for GH Actions --------- Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Fix API reference links in endpoints.json * Merge 4.4 into 4.5.0 (#5669) Merge v4.4.5-2.6.0 into 4.4 (#5665) * Bump Wazuh and platform versions for v4.4.5 (#5639) * Update changelog * Update opensearch_dashboards.json * Update package.json * Update readme * Update tag script * Change tag.py version value * Empty tag suffix * Prepare tag.py for v4.4.5-rc1 (#5645) Add -rc1 tag suffix * Fix incompatible version of triple-beam subdependency (#5652) fix: add yarn.lock file and set version of triple-beam in yarn.lock * Update unit-test.yml (#5655) * Add support for Wazuh 4.4.5-rc2 (#5659) * Update revision of v4.4.5 in the Changelog * Bump v4.4.5-2.6.0-rc2 --------- Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez Co-authored-by: Álex Ruiz Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --------- Co-authored-by: Álex Ruiz Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez --------- Co-authored-by: Julio César Biset <43619595+jbiset@users.noreply.github.com> Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Álex Ruiz Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Antonio David Gutiérrez Co-authored-by: Federico Rodriguez * Remove unused embedded jquery-ui (#5592) * fix: remove unused embedded jquery-ui dependency * changelog: add pull request entry --------- Co-authored-by: Federico Rodriguez * Fixes redirection problem to inventory data, stats and configuration in an agent (#5685) Fix redirect to inventory data, stats and configuration * Fix conflicts in branch synchronization (#5708) * fix: fix conflicts * changelog: remove entry * Fix 4.6.0 changelog merge errors (#5692) * Update changelog * Update CHANGELOG.md * Merge 4.5.2 into 4.6.0 (#5721) * Upgrade environments to 4.6 and 4.7 (#5741) * Add wzd-dev.dockerfile * Update osd dev.sh * Redesign deploy new agent page (#5457) * parent component * Added a title to the container and updated filenames * Update register-agent.scss * [Redesign add agent] Register agent reuse common/form component (Settings > Configuration) (#5446) * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * Remove react use inside hook test file * Fix review requested changes * [Redesign add agent] Add register agent command generator (#5469) * Create reusable card for operating systems (#5462) * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * reuse of common form on the card * Card with logic * CheckboxGroup component logic update * CheckboxGroup component logic update * Adding card icons * update checkbox logic, styles, and card styles * clean code * clean code * gitignore Mac files * updating checkbox logic, styles, and card styles * Update os-card.scss * macos card update * undoing merging as it was causing checkboxes not to work * test * file ds_store * file ds_store * file ds_store * remove files DS_store * remove files DS_store --------- Co-authored-by: Maximiliano Ibarra Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> * 5518 inputs logic server address name password and group (#5554) * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * reuse of common form on the card * Card with logic * CheckboxGroup component logic update * CheckboxGroup component logic update * Adding card icons * update checkbox logic, styles, and card styles * clean code * clean code * gitignore Mac files * updating checkbox logic, styles, and card styles * step component * Passing interfaces to a separate file, updating styles, and component logic * Update interfaces and clean up code * update of folder structure and step logic * tcp, udp, protocols, password, groups, logics * input logic server address name password groups and styles * group input logic * oscards input logic * oscards input logic * styles * regex * styles and settings * styles * various adjustments * cleaning up code and changing some styles * cleaning up code * cleaning code * update password * gitignore * gitignore * correcting validation text in input agent name * correcting validation text in input agent name * corrección de validación de input de nombre del agente * cleaning code * cleaning code * regex that differentiates between FQDN and IP * Use of PLUGIN_VERSION_SHORT * Use of PLUGIN_VERSION_SHORT * link * Revert "Merge branch '4205-redesign-add-agent-page' into 5518-inputs-logic-server-address-name-password-and-group" This reverts commit a4c6fb5d24a482e80f9595a879d141ff2d7fa5bb, reversing changes made to 5a0d2cb0e71972eb8f68b16f035ebc977220379f. * link and revert * characteres valid * correction of styles when bringing changes from parent branch * change tooltip to popover * moving validations to a separate file with their tests * corrections and cleaning of comments * camel case * change in function * type * remove type * fullWidth * type * change * conditional * change label a to Euilink * change label a to Euilink * conditional * delete usePrevious * delete usePrevious * deleted files ds store * test correction and placeholder * show architecture instead of id * removing console css warnings * fixed regex fqdn * fixed regex fqdn * data * changelog * changelog --------- Co-authored-by: Maximiliano Ibarra Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> * [Redesign add agent] Integration commands generator with UI (#5593) * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * reuse of common form on the card * Card with logic * CheckboxGroup component logic update * CheckboxGroup component logic update * Adding card icons * update checkbox logic, styles, and card styles * clean code * clean code * gitignore Mac files * updating checkbox logic, styles, and card styles * step component * Passing interfaces to a separate file, updating styles, and component logic * Update interfaces and clean up code * update of folder structure and step logic * tcp, udp, protocols, password, groups, logics * input logic server address name password groups and styles * group input logic * oscards input logic * oscards input logic * styles * regex * styles and settings * styles * various adjustments * cleaning up code and changing some styles * cleaning up code * cleaning code * update password * gitignore * gitignore * correcting validation text in input agent name * correcting validation text in input agent name * corrección de validación de input de nombre del agente * cleaning code * cleaning code * regex that differentiates between FQDN and IP * Use of PLUGIN_VERSION_SHORT * Use of PLUGIN_VERSION_SHORT * link * Revert "Merge branch '4205-redesign-add-agent-page' into 5518-inputs-logic-server-address-name-password-and-group" This reverts commit a4c6fb5d24a482e80f9595a879d141ff2d7fa5bb, reversing changes made to 5a0d2cb0e71972eb8f68b16f035ebc977220379f. * link and revert * characteres valid * correction of styles when bringing changes from parent branch * change tooltip to popover * moving validations to a separate file with their tests * corrections and cleaning of comments * camel case * change in function * type * remove type * fullWidth * type * change * conditional * change label a to Euilink * change label a to Euilink * conditional * delete usePrevious * delete usePrevious * deleted files ds store * test correction and placeholder * show architecture instead of id * Add register agent form values parser * Remove extension on operating system type * Add command sections with form values * Create new components for steps inputs * Fix some types * Renamed some options * Move commands config inside core folder * Fix server address error message display * Create methods to get form steps status * Allow select more than group * Hide agent group param when is empty * Fix steps form statuses * Remove break lines in commands * Add white space in error messages * Fix steps form status * Added new command component white custom copy and language * Fixed step form status --------- Co-authored-by: chantal.kelm Co-authored-by: Chantal Belén kelm <99441266+chantal-kelm@users.noreply.github.com> * [Redesign add agent] Dark mode (#5620) * remove custom color styles to make the elastic dark mode work by default on the agent registration page * add development for images to have dark mode in the section deploy a new agent * changelog about dark mode * Cleaning console.log from assets file * Adding suggested style modifications in the agent registration section * add a style hint so that text cannot be selected on cards * add suggested changes to the styles in the register an agent section * correction added to the word wizard * added coding enhancements in the agent registration section * adding an enhancement to eliminate the console error * adding an enhancement to eliminate the console error * [Redesign add agent] Add and validate register agent commands (#5622) * Add show/hide password in command component * Add protocol and password types * Add more step status methods * Add os commands service * Resolve strings replacements in command component * Change macos packages name by arch * Add \n to the macos params * Fixed parsed macos params inside echo * Add -e in mac os install command * Remove sudo from macos install command with echo * Add sudo to linux before optional params * Fix PR review comments * Fixed imports in tests * Fix components unit tests * Fix unit test checkbox group component * Fix os card unit test with mock uiSettings * modify the fqdn regex because it interferes with an ipv4 instance * [Redesign add page] Add form status callout message (#5634) * Add form status manager and unit tests * Add empty and invalid fields messages * Hide commands code block when exists warning messages * Fix fields names in warning messages * Updated CHANGELOG * Step 2: the design triggers warnings (#5649) * changing design to remove console warnings * update changelog * Changes in the display of pop-up windows in the agents log section * semicolon is added * update changelog * update changelog * Add requested fixs on texts * Add new rpm and deb install commands * modify fqdn regex * Fix server address validation unit test * Add type in command output types --------- Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Co-authored-by: Maximiliano Ibarra * Add new search bar with WQL (#5363) * feat: add a search bar component Features: - Supports multiple query languages - Decouple the business logic of query languages of the search bar component - Ability of query language to interact with the search bar Query language implementations - AQL: custom implementation of the Wazuh Query Language. Include suggestions. - UIQL: simple implementation (as another example) * feat(search-bar): change the AQL implemenation to use the regular expression used in the Wazuh manager API - Change the implementation of AQL query language to use the regular expression decomposition defined in the Wazuh manager API - Adapt the tests for the tokenizer and getting the suggestions - Enchance documentation of search bar - Add documentation of AQL query language - Add more fields and values for the use example in Agents section - Add description to the query language select input * fix(search-bar): fixes a problem hidding the suggestion popover when using the Search suggestion in AQL - Fixes a problem hidding the suggestion popover when using the Search suggestion in AQL - Fixes a problem of input text with undefined value - Minor fixes - Remove `syntax` property of SearchBar component - Add disableFocusTrap property to the custom EuiSuggestInput component to be forwarded to the EuiInputPopover - Replace the inputRef by a reference instead of a state and pass as a parameter in the query language run function - Move the rebuiding of input text when using some suggestion that changes the input to be done when a related suggestion was clicked instead of any suggestion (exclude Search). * feat(search-bar): add the ability to update the input of example implemenation - Add the ability to update the input of the search bar in the example implementation - Enhance the component documentation * feat(search-bar): add initial suggestions to AQL - (AQL) Add the fields and an open operator group when there is no input text * feat(search-bar): add target and rel attributes to the documentation link of query language displayed in the popover * feat(search-bar): enhancements in AQL and search bar documentation - AQL enhancements: - documentation: - Enhance some descriptions - Enhance input processing - Remove intermetiate interface of EuiSuggestItem - Remove the intermediate interface of EuiSuggestItem. Now it is managed in the internal of query language instead of be built by the suggestion handler - Display suggestions when the input text is empty - Add the unifiedQuery field to the query language output - Adapt tests - Search Bar component: - Enhance documentation * feat(search-bar): Add HAQL - Remove UIQL - Add HAQL query language that is a high-level implementation of AQL - Add the query language interface - Add tests for tokenizer, get suggestions and transformSpecificQLToUnifiedQL method - Add documentation about the language - Syntax - Options - Workflow * feat(search-bar): add test to HAQL and AQL query languages - Add tests to HAQL and AQL query languages - Fix suggestions for HAQL when typing as first element a value entity. Now there are no suggestions because the field and operator_compare are missing. - Enhance documentation of HAQL and AQL - Removed unnecesary returns of suggestion handler in the example implementation of search bar on Agents section * feat(search-bar): Rename HAQL query language to WQL - Rename query language HAQL to WQL - Update tests - Remove AQL usage from the implementation in the agents section * feat(search-bar): Add more use cases to the tests of WQL query language - Add more use cases to the test of WQL query language - Replace some literals by constants in the WQL query language implementation * feat(search-bar): enhance the documenation of query languages * feat(search-bar): Add a popover title to replicate similar UI to the platform search bar * feat(search-bar): wrap the user input with group operators when there is an implicit query * feat(search-bar): add implicit query mode to WQL - WQL - add implicit query mode to WQL - enhance query language documentation - renamed transformUnifiedQuery to transformUQLToQL - now wraps the user input if this is defined and there a implicit query string - fix a problem with the value suggestions if there is a previous conjunction - add tests cases - update tests - AQL - enhance query language documentation - renamed transformUnifiedQuery to transformUQLToQL - add warning about the query language implementation is not updated to the last changes in the search bar component - update tests - Search Bar - renamed transformUnifiedQuery to transformUQLToQL * feat(search-bar): set the width of the syntax options popover * feat(search-bar): unify suggestion descriptions in WQL - Set a width for the syntax options popover - Unify the description in the suggestions of WQL example implementation - Update tests - Fix minor bugs in the WQL example implementation in Agents * feat(search-bar): add enhancements to WQL - WQL - Enhance documentation - Add partial and "expanded" input validation - Add tests * feat(search-bar): rename previousField and previousOperatorCompare in WQL * fix(tests): update snapshot * fix(search-bar): fix documentation link for WQL * fix(search-bar): remove example usage of SearchBar component in Agents * fix(search-bar): fix an error using the value suggestions in WQL Fix an error when the last token in the input was a value and used a value suggestion whose label contains whitespaces, the value was not wrapped with quotes. * feat(search-bar): add search function suggestion when the input is empty * fix(search-bar): ensure the query language output changed to trigger the onChange handler * feat(search-bar): allow the API query output can be redone when the search term fields changed - Search bar: - Add a dependency to run the query language output - Adapt search bar documentation to the changes - WQL - Create a new parameter called `options` - Moved the `implicitFilter` and `searchTerm` settings to `options` - Update tests - Update documentation * feat(search-bar): enhance the validation of value token in WQL * feat(search-bar): enhance search bar and WQL Search bar: - Add the possibility to render buttons to the right of the input - Minor changes WQL: - Add options.filterButtons to render filter buttons and component rendering - Extract the quoted value for the quoted token values - Add the `validate` parameter to validate the tokens (only available for `value` token) - Enhance language description - Add test related to value token validation - Update language documentation with this changes * feat(search-bar): enhace search bar and WQL Search bar: - rename method from `transformUQLtoQL` to `transformInput` - add a options paramenter to `transformInput` to manage when there is an implicit filter, that this methods can remove the filter from the query - update documentation WQL: - rename method from `transformUQLtoQL` to `transformInput` - add a options paramenter to `transformInput` to manage when there is an implicit filter, that this methods can remove the filter from the query - add tests * fix: enhance search bar and WQL types * fix: remove exact validation for the token value due to performance problems * fix: fix token value validation * fix: update the link to the documentation of WQL * fix(search-bar): use value of value token as the value used to get the value suggestions in the search bar instead of raw token that could include " character * fix(search-bar): fix a problem extracting value for value tokens wrapped by double quotation marks that contains the new line character and remove separation of invalid characters in the value token - Fix tests * fix(search-bar): add validation for value token in WQL * fix(search-bar): value token in message related to this is invalid --------- Co-authored-by: Federico Rodriguez * [Redesign add agent] Fix custom Eui styles in register agent wizard (#5769) Add wrapper to custom eui styles * Merge 4.5.2 into 4.6.0 (#5775) * Change windows agent service name (#5538) * Change windows agent service name to Wazuh Change windows agent service name to Wazuh * Add CHANGELOG * Remove agent name in agent info ribbon (#5497) * remove: agent name in agent info ribbon * changelog: add pull request entry --------- Co-authored-by: Álex Ruiz * Fix IPV6 visualizations (#5471) * add ipv6 service * add test for service * Fix issue in agents-table * fix issue in agents-info * fix groups agents issue * Fix width in groups agents * use mapResponseItem * Add copy button to groups * Add copy button to info * fix for node list * Optimize code * Fix styles * Edit changelog * Edit changelog * Add imposter changes to test ipv6 * Replace onMouseDown with onClick * Move copy buttons to the left * fix: removed compressipv6 property of TableWzAPI * feat: add tableLayout property to some tables and remove IPv6 address compression add tableLayout=auto property to some tables: - Agents/{agent_id}/Inventory data - Management/Cluster/Nodes - Agents - Management/Configuration/Client - Management/Global configuration/Remote remove IPv6 address compression * remove: remove unused service to IPv6 compression * revert: revert changes in TableWzAPI component * add: add mocked responses to some syscollector endpoints * remove: unwanted table columns properties * changelog: add pull request entry * Fix imposter --------- Co-authored-by: Antonio David Gutiérrez Co-authored-by: Álex Ruiz Co-authored-by: yenienserrano Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Bump v4.4.4-2.6.0-rc2 * Add Apple Silicon architecture to the register Agent wizard (#5478) * Add Apple Silicon architecture * Add changelog * Change macOS environment variables * Revert "Change macOS environment variables" This reverts commit 108e86626045de6b5cd7b7053a8c6333d8bf8b89. * Change macOS architecture ids * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Bump 4.5.1 * Change the method to make the redirect (#5539) * Change the metod to make the redirect * Remove unused code * Add changelog --------- Co-authored-by: Álex Ruiz * Fix agents active coverage stat as NaN (#5490) * fix: agents active coverate stat as NaN Ensure the values used to calculate have the expected types and the total count is greater than 0. * remove: unused openRegistrationDocs method * changelog: add entry * fix: check if agents active coverage is a NaN * changelog: fix entry --------- Co-authored-by: Álex Ruiz * [Backport 4.5.1] Update test snapshots for 4.5 (#5607) * Update test snapshots for 4.5 (#5601) * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Update test snapshost * Update API data to 4.5 * Update branch patterns for GH Actions --------- Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> (cherry picked from commit 1ae5f19a9edc967187b2d946aad6e8d8f0afff14) * Fix API reference links in endpoints.json * Add kbn-dev 7.17.11 (#5628) * Merge 4.5.0 into 4.5.1 (#5670) * Update test snapshots for 4.5 (#5601) * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Update test snapshost * Update API data to 4.5 * Update branch patterns for GH Actions --------- Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Fix API reference links in endpoints.json * Merge 4.4 into 4.5.0 (#5669) Merge v4.4.5-2.6.0 into 4.4 (#5665) * Bump Wazuh and platform versions for v4.4.5 (#5639) * Update changelog * Update opensearch_dashboards.json * Update package.json * Update readme * Update tag script * Change tag.py version value * Empty tag suffix * Prepare tag.py for v4.4.5-rc1 (#5645) Add -rc1 tag suffix * Fix incompatible version of triple-beam subdependency (#5652) fix: add yarn.lock file and set version of triple-beam in yarn.lock * Update unit-test.yml (#5655) * Add support for Wazuh 4.4.5-rc2 (#5659) * Update revision of v4.4.5 in the Changelog * Bump v4.4.5-2.6.0-rc2 --------- Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez Co-authored-by: Álex Ruiz Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --------- Co-authored-by: Álex Ruiz Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez * Bump Wazuh version 4.5.2 (#5702) Bump 4.5.2 * Merge 4.5.1 into 4.5.2 (#5720) Merge 4.5.0 into 4.5.1 (#5719) * Update test snapshots for 4.5 (#5601) * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Update test snapshost * Update API data to 4.5 * Update branch patterns for GH Actions --------- * Fix API reference links in endpoints.json * Merge 4.4 into 4.5.0 (#5669) Merge v4.4.5-2.6.0 into 4.4 (#5665) * Bump Wazuh and platform versions for v4.4.5 (#5639) * Update changelog * Update opensearch_dashboards.json * Update package.json * Update readme * Update tag script * Change tag.py version value * Empty tag suffix * Prepare tag.py for v4.4.5-rc1 (#5645) Add -rc1 tag suffix * Fix incompatible version of triple-beam subdependency (#5652) fix: add yarn.lock file and set version of triple-beam in yarn.lock * Update unit-test.yml (#5655) * Add support for Wazuh 4.4.5-rc2 (#5659) * Update revision of v4.4.5 in the Changelog * Bump v4.4.5-2.6.0-rc2 --------- * Update release utilities (#5677) * feat: update release utilities to current process - Add new bump script - Port tag.py to NodeJS and allow receive parameters from stdin - Add RELEASING.md file with information about the release process related to the usage of the included scripts - Add release:bump and release:tag package scripts to run these process * remove: remove scripts/tag.py and reference in the Makefile * fix: fix help text in bump and tag scripts * remove: remove stage and commit properties from the package.json * remove: test related to stage property in the package.json * fix: check if there are changes to commit in the tag script - Code formatting - Fix variable name --------- Co-authored-by: Álex Ruiz Co-authored-by: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez * Merge 4.5.1 into 4.5.2 (#5774) * Merge 4.5.0 into 4.5.1 (#5719) * Update test snapshots for 4.5 (#5601) * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Update test snapshost * Update API data to 4.5 * Update branch patterns for GH Actions --------- Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Fix API reference links in endpoints.json * Merge 4.4 into 4.5.0 (#5669) Merge v4.4.5-2.6.0 into 4.4 (#5665) * Bump Wazuh and platform versions for v4.4.5 (#5639) * Update changelog * Update opensearch_dashboards.json * Update package.json * Update readme * Update tag script * Change tag.py version value * Empty tag suffix * Prepare tag.py for v4.4.5-rc1 (#5645) Add -rc1 tag suffix * Fix incompatible version of triple-beam subdependency (#5652) fix: add yarn.lock file and set version of triple-beam in yarn.lock * Update unit-test.yml (#5655) * Add support for Wazuh 4.4.5-rc2 (#5659) * Update revision of v4.4.5 in the Changelog * Bump v4.4.5-2.6.0-rc2 --------- Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez Co-authored-by: Álex Ruiz Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Update release utilities (#5677) * feat: update release utilities to current process - Add new bump script - Port tag.py to NodeJS and allow receive parameters from stdin - Add RELEASING.md file with information about the release process related to the usage of the included scripts - Add release:bump and release:tag package scripts to run these process * remove: remove scripts/tag.py and reference in the Makefile * fix: fix help text in bump and tag scripts * remove: remove stage and commit properties from the package.json * remove: test related to stage property in the package.json * fix: check if there are changes to commit in the tag script - Code formatting - Fix variable name --------- Co-authored-by: Álex Ruiz Co-authored-by: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez * Fix API request to get the manager labels and broken documentation link (#5687) * fix: broken documentation link * changelog: add pull request entry * fix: changed API endpoint to get the manager labels and managing the data to render * changelog: add pull request entry * changelog: fix entry * changelog: fix entry * Add response to imposter --------- Co-authored-by: yenienserrano * Fix server side query in pdf report filter (#5714) * Add server side query * Fix reporting unit test * Remove duplicated allowed agents filter and gdpr-pci-tsc filters * Code cleaning * Added Changelog * Fix deep clone filters * Fix server side requirement query * Fix rootkit filter * Update API data for 4.5.1 (#5758) update: API data * Fix outdated year in PDF report footer (#5766) * Fix year in PDF footer * Modify changelog * Change tests to match the new value * Change md5 in reporting test * Change md5 in reporting test * Revert accidental change * Revert accidental change * Fix md5 in test * Change md5 in test * Change md5 in test * Merge 4.5 into 4.5.1 (#5773) * Update test snapshots for 4.5 (#5601) * Add missing supported versions to the Docker environments (#5584) feat(environments): add latest versions to Docker environments - Add Kibana versions: 7.17.7, 7.17.8, 7.17.9 and 7.17.10 - Add OpenSearch: 2.6.0 - Add OpenSearch Dashboards: 2.6.0 - Add Wazuh 4.4.1, 4.4.2, 4.4.3 and 4.4.4 * Update test snapshost * Update API data to 4.5 * Update branch patterns for GH Actions --------- Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Fix API reference links in endpoints.json * Merge 4.4 into 4.5.0 (#5669) Merge v4.4.5-2.6.0 into 4.4 (#5665) * Bump Wazuh and platform versions for v4.4.5 (#5639) * Update changelog * Update opensearch_dashboards.json * Update package.json * Update readme * Update tag script * Change tag.py version value * Empty tag suffix * Prepare tag.py for v4.4.5-rc1 (#5645) Add -rc1 tag suffix * Fix incompatible version of triple-beam subdependency (#5652) fix: add yarn.lock file and set version of triple-beam in yarn.lock * Update unit-test.yml (#5655) * Add support for Wazuh 4.4.5-rc2 (#5659) * Update revision of v4.4.5 in the Changelog * Bump v4.4.5-2.6.0-rc2 --------- Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez Co-authored-by: Álex Ruiz Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Update release utilities (#5677) * feat: update release utilities to current process - Add new bump script - Port tag.py to NodeJS and allow receive parameters from stdin - Add RELEASING.md file with information about the release process related to the usage of the included scripts - Add release:bump and release:tag package scripts to run these process * remove: remove scripts/tag.py and reference in the Makefile * fix: fix help text in bump and tag scripts * remove: remove stage and commit properties from the package.json * remove: test related to stage property in the package.json * fix: check if there are changes to commit in the tag script - Code formatting - Fix variable name * Bump v4.5.0-2.6.0-alpha1 * Update README.md --------- Co-authored-by: Álex Ruiz Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez --------- Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Álex Ruiz Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Federico Rodriguez * Remove files * Remove console.log --------- Co-authored-by: Julio César Biset <43619595+jbiset@users.noreply.github.com> Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Álex Ruiz Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Antonio David Gutiérrez Co-authored-by: Federico Rodriguez * Update api_info imposter --------- Co-authored-by: Julio César Biset <43619595+jbiset@users.noreply.github.com> Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Álex Ruiz Co-authored-by: Nicolas Agustin Guevara Pihen <42900763+Tostti@users.noreply.github.com> Co-authored-by: Antonio David Gutiérrez Co-authored-by: Federico Rodriguez Co-authored-by: Chantal Belén kelm <99441266+chantal-kelm@users.noreply.github.com> Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Co-authored-by: Maximiliano Ibarra --- CHANGELOG.md | 4 + docker/imposter/agents/configuration.js | 15 + .../agents/configuration/agent_labels.json | 12 + .../agents/configuration/default.json | 33 + docker/imposter/api-info/api_info.json | 2 +- .../cluster/configuration/agent_labels.json | 20 + docker/imposter/manager/configuration.js | 22 + .../manager/configuration/agent_labels.json | 20 + .../manager/configuration/default.json | 35 + .../configuration/monitor_reports.json | 16 + docker/imposter/wazuh-config.yml | 3 + plugins/main/common/api-info/endpoints.json | 8 +- plugins/main/common/constants.ts | 2 +- plugins/main/common/services/settings.test.ts | 115 +- .../table-with-search-bar.test.tsx.snap | 1 + .../components/eui-suggest/suggest_input.js | 2 + .../public/components/search-bar/README.md | 201 +++ .../__snapshots__/index.test.tsx.snap | 59 + .../components/search-bar/index.test.tsx | 57 + .../public/components/search-bar/index.tsx | 229 ++++ .../__snapshots__/aql.test.tsx.snap | 99 ++ .../__snapshots__/wql.test.tsx.snap | 99 ++ .../search-bar/query-language/aql.md | 204 +++ .../search-bar/query-language/aql.test.tsx | 205 +++ .../search-bar/query-language/aql.tsx | 523 ++++++++ .../search-bar/query-language/index.ts | 32 + .../search-bar/query-language/wql.md | 269 ++++ .../search-bar/query-language/wql.test.tsx | 476 +++++++ .../search-bar/query-language/wql.tsx | 1157 +++++++++++++++++ .../controllers/agent/wazuh-config/index.ts | 28 +- .../configuration/alerts/alerts-labels.js | 87 +- .../management/configuration/alerts/alerts.js | 22 +- .../command-output/command-output.tsx | 3 +- .../os-selector/os-card/os-card.scss | 4 - .../register-agent/register-agent.tsx | 3 - .../containers/steps/steps.scss | 76 +- .../register-agent/containers/steps/steps.tsx | 5 +- .../core/config/os-commands-definitions.ts | 44 +- .../command-generator/command-generator.ts | 32 +- .../optional-parameters-manager.ts | 64 +- .../core/register-commands/types.ts | 54 +- .../hooks/use-register-agent-commands.ts | 63 +- .../register-agent-os-commands-services.tsx | 30 +- .../main/public/react-services/reporting.js | 4 +- .../server/controllers/wazuh-reporting.ts | 24 +- .../main/server/lib/reporting/base-query.ts | 45 +- .../lib/reporting/extended-information.ts | 16 +- .../main/server/lib/reporting/gdpr-request.ts | 23 +- .../main/server/lib/reporting/pci-request.ts | 26 +- .../server/lib/reporting/rootcheck-request.ts | 25 +- .../main/server/lib/reporting/tsc-request.ts | 42 +- .../server/routes/wazuh-reporting.test.ts | 133 +- plugins/main/server/routes/wazuh-reporting.ts | 84 +- 53 files changed, 4364 insertions(+), 493 deletions(-) create mode 100644 docker/imposter/agents/configuration.js create mode 100644 docker/imposter/agents/configuration/agent_labels.json create mode 100644 docker/imposter/agents/configuration/default.json create mode 100644 docker/imposter/cluster/configuration/agent_labels.json create mode 100644 docker/imposter/manager/configuration.js create mode 100644 docker/imposter/manager/configuration/agent_labels.json create mode 100644 docker/imposter/manager/configuration/default.json create mode 100644 docker/imposter/manager/configuration/monitor_reports.json create mode 100644 plugins/main/public/components/search-bar/README.md create mode 100644 plugins/main/public/components/search-bar/__snapshots__/index.test.tsx.snap create mode 100644 plugins/main/public/components/search-bar/index.test.tsx create mode 100644 plugins/main/public/components/search-bar/index.tsx create mode 100644 plugins/main/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap create mode 100644 plugins/main/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap create mode 100644 plugins/main/public/components/search-bar/query-language/aql.md create mode 100644 plugins/main/public/components/search-bar/query-language/aql.test.tsx create mode 100644 plugins/main/public/components/search-bar/query-language/aql.tsx create mode 100644 plugins/main/public/components/search-bar/query-language/index.ts create mode 100644 plugins/main/public/components/search-bar/query-language/wql.md create mode 100644 plugins/main/public/components/search-bar/query-language/wql.test.tsx create mode 100644 plugins/main/public/components/search-bar/query-language/wql.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 70178ece80..398d33d1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed the rendering of tables that contains IPs and agent overview [#5471](https://github.com/wazuh/wazuh-kibana-app/pull/5471) - Fixed the agents active coverage stat as NaN in Details panel of Agents section [#5490](https://github.com/wazuh/wazuh-kibana-app/pull/5490) +- Fixed a broken documentation link to agent labels [#5687](https://github.com/wazuh/wazuh-kibana-app/pull/5687) +- Fixed the PDF report filters applied to tables [#5714](https://github.com/wazuh/wazuh-kibana-app/pull/5714) +- Fixed outdated year in the PDF report footer [#5766](https://github.com/wazuh/wazuh-kibana-app/pull/5766) ### Removed @@ -98,6 +101,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed method to perform redirection on agent table buttons [#5539](https://github.com/wazuh/wazuh-kibana-app/pull/5539) - Changed windows agent service name in the deploy agent wizard [#5538](https://github.com/wazuh/wazuh-kibana-app/pull/5538) +- Changed the requests to get the agent labels for the managers [#5687](https://github.com/wazuh/wazuh-kibana-app/pull/5687) ## Wazuh v4.5.0 - OpenSearch Dashboards 2.6.0 - Revision 01 diff --git a/docker/imposter/agents/configuration.js b/docker/imposter/agents/configuration.js new file mode 100644 index 0000000000..f1d3c93a34 --- /dev/null +++ b/docker/imposter/agents/configuration.js @@ -0,0 +1,15 @@ +var path = context.request.path; +var pathConfiguration = path.split('/'); +pathConfiguration.splice(0, 5); +console.log(pathConfiguration); +switch (pathConfiguration[0]) { + case 'labels': + respond() + .withStatusCode(200) + .withFile('agents/configuration/agent_labels.json'); + + break; + default: + respond().withStatusCode(200).withFile('agents/configuration/default.json'); + break; +} diff --git a/docker/imposter/agents/configuration/agent_labels.json b/docker/imposter/agents/configuration/agent_labels.json new file mode 100644 index 0000000000..a3bbe13481 --- /dev/null +++ b/docker/imposter/agents/configuration/agent_labels.json @@ -0,0 +1,12 @@ +{ + "data": { + "labels": [ + { + "value": "customLabel", + "key": "custom", + "hidden": "no" + } + ] + }, + "error": 0 +} diff --git a/docker/imposter/agents/configuration/default.json b/docker/imposter/agents/configuration/default.json new file mode 100644 index 0000000000..d97500d76f --- /dev/null +++ b/docker/imposter/agents/configuration/default.json @@ -0,0 +1,33 @@ +{ + "data": { + "client": { + "config-profile": "ubuntu, ubuntu20, ubuntu20.04", + "notify_time": 10, + "time-reconnect": 60, + "force_reconnect_interval": 0, + "ip_update_interval": 0, + "auto_restart": "yes", + "remote_conf": "yes", + "crypto_method": "aes", + "server": [ + { + "address": "nginx-lb/172.25.0.4", + "port": 1514, + "max_retries": 5, + "retry_interval": 10, + "protocol": "tcp" + } + ], + "enrollment": [ + { + "enabled": "yes", + "delay_after_enrollment": 20, + "port": 1515, + "ssl_cipher": "HIGH:!ADH:!EXP:!MD5:!RC4:!3DES:!CAMELLIA:@STRENGTH", + "auto_method": "no" + } + ] + } + }, + "error": 0 +} diff --git a/docker/imposter/api-info/api_info.json b/docker/imposter/api-info/api_info.json index 6bc67bd7fe..ff6d673fc1 100644 --- a/docker/imposter/api-info/api_info.json +++ b/docker/imposter/api-info/api_info.json @@ -1,7 +1,7 @@ { "data": { "title": "Wazuh API REST", - "api_version": "4.6.0", + "api_version": "4.7.0", "revision": 1, "license_name": "GPL 2.0", "license_url": "https://github.com/wazuh/wazuh/blob/4.5/LICENSE", diff --git a/docker/imposter/cluster/configuration/agent_labels.json b/docker/imposter/cluster/configuration/agent_labels.json new file mode 100644 index 0000000000..52edc2ea1d --- /dev/null +++ b/docker/imposter/cluster/configuration/agent_labels.json @@ -0,0 +1,20 @@ +{ + "data": { + "affected_items": [ + { + "labels": [ + { + "value": "customLabel", + "key": "custom", + "hidden": "no" + } + ] + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Active configuration was successfully read in specified node.", + "error": 0 +} diff --git a/docker/imposter/manager/configuration.js b/docker/imposter/manager/configuration.js new file mode 100644 index 0000000000..9b5a87219d --- /dev/null +++ b/docker/imposter/manager/configuration.js @@ -0,0 +1,22 @@ +var path = context.request.path; +var pathConfiguration = path.split('/'); +pathConfiguration.splice(0, 4); +switch (pathConfiguration[0]) { + case 'labels': + respond() + .withStatusCode(200) + .withFile('manager/configuration/agent_labels.json'); + + break; + case 'reports': + respond() + .withStatusCode(200) + .withFile('manager/configuration/monitor_reports.json'); + + break; + default: + respond() + .withStatusCode(200) + .withFile('manager/configuration/default.json'); + break; +} diff --git a/docker/imposter/manager/configuration/agent_labels.json b/docker/imposter/manager/configuration/agent_labels.json new file mode 100644 index 0000000000..52edc2ea1d --- /dev/null +++ b/docker/imposter/manager/configuration/agent_labels.json @@ -0,0 +1,20 @@ +{ + "data": { + "affected_items": [ + { + "labels": [ + { + "value": "customLabel", + "key": "custom", + "hidden": "no" + } + ] + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Active configuration was successfully read in specified node.", + "error": 0 +} diff --git a/docker/imposter/manager/configuration/default.json b/docker/imposter/manager/configuration/default.json new file mode 100644 index 0000000000..614c20c2f8 --- /dev/null +++ b/docker/imposter/manager/configuration/default.json @@ -0,0 +1,35 @@ +{ + "data": { + "affected_items": [ + { + "global": { + "email_notification": "no", + "logall": "no", + "logall_json": "no", + "integrity_checking": 8, + "rootkit_detection": 8, + "host_information": 8, + "prelude_output": "no", + "zeromq_output": "no", + "jsonout_output": "yes", + "alerts_log": "yes", + "stats": 4, + "memory_size": 8192, + "white_list": [ + "127.0.0.1", + "80.58.61.250", + "80.58.61.254", + "localhost.localdomain" + ], + "rotate_interval": 0, + "max_output_size": 0 + } + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Active configuration was successfully read in specified node", + "error": 0 +} diff --git a/docker/imposter/manager/configuration/monitor_reports.json b/docker/imposter/manager/configuration/monitor_reports.json new file mode 100644 index 0000000000..a611e47fbe --- /dev/null +++ b/docker/imposter/manager/configuration/monitor_reports.json @@ -0,0 +1,16 @@ +{ + "data": { + "affected_items": [{ + "reports": [{ + "category": "syscheck", + "title": "Daily report: File changes", + "email_to": "example@test.com" + }] + }], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Could not read active configuration in specified node", + "error": 0 +} \ No newline at end of file diff --git a/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index dc836bf3fa..e0e964af3b 100755 --- a/docker/imposter/wazuh-config.yml +++ b/docker/imposter/wazuh-config.yml @@ -507,6 +507,9 @@ resources: # Get active configuration - method: GET path: /manager/configuration/{component}/{configuration} + response: + statusCode: 200 + scriptFile: manager/configuration.js # ===================================================== # # MITRE diff --git a/plugins/main/common/api-info/endpoints.json b/plugins/main/common/api-info/endpoints.json index eb98743bca..94c1d72557 100644 --- a/plugins/main/common/api-info/endpoints.json +++ b/plugins/main/common/api-info/endpoints.json @@ -265,7 +265,7 @@ }, { "name": ":configuration", - "description": "

Selected agent's configuration to read. The configuration to read depends on the selected component.\nThe following table shows all available combinations of component and configuration values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ComponentConfigurationTag
agentclient<client>
agentbuffer<client_buffer>
agentlabels<labels>
agentinternal<agent>, <monitord>, <remoted>
agentlessagentless<agentless>
analysisglobal<global>
analysisactive_response<active-response>
analysisalerts<alerts>
analysiscommand<command>
analysisrules<rule>
analysisdecoders<decoder>
analysisinternal<analysisd>
analysisrule_test<rule_test>
authauth<auth>
comactive-response<active-response>
comlogging<logging>
cominternal<execd>
comcluster<cluster>
csyslogcsyslog<csyslog_output>
integratorintegration<integration>
logcollectorlocalfile<localfile>
logcollectorsocket<socket>
logcollectorinternal<logcollector>
mailglobal<global><email...>
mailalerts<email_alerts>
mailinternal<maild>
monitorglobal<global>
monitorinternal<monitord>
monitorinternal<reports>
requestglobal<global>
requestremote<remote>
requestinternal<remoted>
syschecksyscheck<syscheck>
syscheckrootcheck<rootcheck>
syscheckinternal<syscheck>, <rootcheck>
wazuh-dbinternal<wazuh_db>
wazuh-dbwdb<wdb>
wmoduleswmodules<wodle>
\n", + "description": "

Selected agent's configuration to read. The configuration to read depends on the selected component.\nThe following table shows all available combinations of component and configuration values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ComponentConfigurationTag
agentclient<client>
agentbuffer<client_buffer>
agentlabels<labels>
agentinternal<agent>, <monitord>, <remoted>
agentlessagentless<agentless>
analysisglobal<global>
analysisactive_response<active-response>
analysisalerts<alerts>
analysiscommand<command>
analysisrules<rule>
analysisdecoders<decoder>
analysisinternal<analysisd>
analysisrule_test<rule_test>
authauth<auth>
comactive-response<active-response>
comlogging<logging>
cominternal<execd>
comcluster<cluster>
csyslogcsyslog<csyslog_output>
integratorintegration<integration>
logcollectorlocalfile<localfile>
logcollectorsocket<socket>
logcollectorinternal<logcollector>
mailglobal<global><email...>
mailalerts<email_alerts>
mailinternal<maild>
monitorglobal<global>
monitorinternal<monitord>
monitorreports<reports>
requestglobal<global>
requestremote<remote>
requestinternal<remoted>
syschecksyscheck<syscheck>
syscheckrootcheck<rootcheck>
syscheckinternal<syscheck>, <rootcheck>
wazuh-dbinternal<wazuh_db>
wazuh-dbwdb<wdb>
wmoduleswmodules<wodle>
\n", "required": true, "schema": { "type": "string", @@ -1183,7 +1183,7 @@ }, { "name": ":configuration", - "description": "

Selected agent's configuration to read. The configuration to read depends on the selected component.\nThe following table shows all available combinations of component and configuration values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ComponentConfigurationTag
agentclient<client>
agentbuffer<client_buffer>
agentlabels<labels>
agentinternal<agent>, <monitord>, <remoted>
agentlessagentless<agentless>
analysisglobal<global>
analysisactive_response<active-response>
analysisalerts<alerts>
analysiscommand<command>
analysisrules<rule>
analysisdecoders<decoder>
analysisinternal<analysisd>
analysisrule_test<rule_test>
authauth<auth>
comactive-response<active-response>
comlogging<logging>
cominternal<execd>
comcluster<cluster>
csyslogcsyslog<csyslog_output>
integratorintegration<integration>
logcollectorlocalfile<localfile>
logcollectorsocket<socket>
logcollectorinternal<logcollector>
mailglobal<global><email...>
mailalerts<email_alerts>
mailinternal<maild>
monitorglobal<global>
monitorinternal<monitord>
monitorinternal<reports>
requestglobal<global>
requestremote<remote>
requestinternal<remoted>
syschecksyscheck<syscheck>
syscheckrootcheck<rootcheck>
syscheckinternal<syscheck>, <rootcheck>
wazuh-dbinternal<wazuh_db>
wazuh-dbwdb<wdb>
wmoduleswmodules<wodle>
\n", + "description": "

Selected agent's configuration to read. The configuration to read depends on the selected component.\nThe following table shows all available combinations of component and configuration values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ComponentConfigurationTag
agentclient<client>
agentbuffer<client_buffer>
agentlabels<labels>
agentinternal<agent>, <monitord>, <remoted>
agentlessagentless<agentless>
analysisglobal<global>
analysisactive_response<active-response>
analysisalerts<alerts>
analysiscommand<command>
analysisrules<rule>
analysisdecoders<decoder>
analysisinternal<analysisd>
analysisrule_test<rule_test>
authauth<auth>
comactive-response<active-response>
comlogging<logging>
cominternal<execd>
comcluster<cluster>
csyslogcsyslog<csyslog_output>
integratorintegration<integration>
logcollectorlocalfile<localfile>
logcollectorsocket<socket>
logcollectorinternal<logcollector>
mailglobal<global><email...>
mailalerts<email_alerts>
mailinternal<maild>
monitorglobal<global>
monitorinternal<monitord>
monitorreports<reports>
requestglobal<global>
requestremote<remote>
requestinternal<remoted>
syschecksyscheck<syscheck>
syscheckrootcheck<rootcheck>
syscheckinternal<syscheck>, <rootcheck>
wazuh-dbinternal<wazuh_db>
wazuh-dbwdb<wdb>
wmoduleswmodules<wodle>
\n", "required": true, "schema": { "type": "string", @@ -4572,7 +4572,7 @@ }, { "name": ":configuration", - "description": "

Selected agent's configuration to read. The configuration to read depends on the selected component.\nThe following table shows all available combinations of component and configuration values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ComponentConfigurationTag
agentclient<client>
agentbuffer<client_buffer>
agentlabels<labels>
agentinternal<agent>, <monitord>, <remoted>
agentlessagentless<agentless>
analysisglobal<global>
analysisactive_response<active-response>
analysisalerts<alerts>
analysiscommand<command>
analysisrules<rule>
analysisdecoders<decoder>
analysisinternal<analysisd>
analysisrule_test<rule_test>
authauth<auth>
comactive-response<active-response>
comlogging<logging>
cominternal<execd>
comcluster<cluster>
csyslogcsyslog<csyslog_output>
integratorintegration<integration>
logcollectorlocalfile<localfile>
logcollectorsocket<socket>
logcollectorinternal<logcollector>
mailglobal<global><email...>
mailalerts<email_alerts>
mailinternal<maild>
monitorglobal<global>
monitorinternal<monitord>
monitorinternal<reports>
requestglobal<global>
requestremote<remote>
requestinternal<remoted>
syschecksyscheck<syscheck>
syscheckrootcheck<rootcheck>
syscheckinternal<syscheck>, <rootcheck>
wazuh-dbinternal<wazuh_db>
wazuh-dbwdb<wdb>
wmoduleswmodules<wodle>
\n", + "description": "

Selected agent's configuration to read. The configuration to read depends on the selected component.\nThe following table shows all available combinations of component and configuration values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ComponentConfigurationTag
agentclient<client>
agentbuffer<client_buffer>
agentlabels<labels>
agentinternal<agent>, <monitord>, <remoted>
agentlessagentless<agentless>
analysisglobal<global>
analysisactive_response<active-response>
analysisalerts<alerts>
analysiscommand<command>
analysisrules<rule>
analysisdecoders<decoder>
analysisinternal<analysisd>
analysisrule_test<rule_test>
authauth<auth>
comactive-response<active-response>
comlogging<logging>
cominternal<execd>
comcluster<cluster>
csyslogcsyslog<csyslog_output>
integratorintegration<integration>
logcollectorlocalfile<localfile>
logcollectorsocket<socket>
logcollectorinternal<logcollector>
mailglobal<global><email...>
mailalerts<email_alerts>
mailinternal<maild>
monitorglobal<global>
monitorinternal<monitord>
monitorreports<reports>
requestglobal<global>
requestremote<remote>
requestinternal<remoted>
syschecksyscheck<syscheck>
syscheckrootcheck<rootcheck>
syscheckinternal<syscheck>, <rootcheck>
wazuh-dbinternal<wazuh_db>
wazuh-dbwdb<wdb>
wmoduleswmodules<wodle>
\n", "required": true, "schema": { "type": "string", @@ -9373,7 +9373,7 @@ "required": true, "schema": { "type": "string", - "format": "wazuh_path" + "format": "wpk_path" } }, { diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index c9fd8f23ac..94562b6a78 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -282,7 +282,7 @@ export const ASSETS_PUBLIC_URL = '/plugins/wazuh/public/assets/'; // Reports export const REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH = 'images/logo_reports.png'; export const REPORTS_PRIMARY_COLOR = '#256BD1'; -export const REPORTS_PAGE_FOOTER_TEXT = 'Copyright © 2022 Wazuh, Inc.'; +export const REPORTS_PAGE_FOOTER_TEXT = 'Copyright © 2023 Wazuh, Inc.'; export const REPORTS_PAGE_HEADER_TEXT = 'info@wazuh.com\nhttps://wazuh.com'; // Plugin platform diff --git a/plugins/main/common/services/settings.test.ts b/plugins/main/common/services/settings.test.ts index eeee05d52b..21efe9e414 100644 --- a/plugins/main/common/services/settings.test.ts +++ b/plugins/main/common/services/settings.test.ts @@ -1,60 +1,67 @@ import { - formatLabelValuePair, - formatSettingValueToFile, - getCustomizationSetting -} from "./settings"; + formatLabelValuePair, + formatSettingValueToFile, + getCustomizationSetting, +} from './settings'; describe('[settings] Methods', () => { + describe('formatLabelValuePair: Format the label-value pairs used to display the allowed values', () => { + it.each` + label | value | expected + ${'TestLabel'} | ${true} | ${'true (TestLabel)'} + ${'true'} | ${true} | ${'true'} + `( + `label: $label | value: $value | expected: $expected`, + ({ label, expected, value }) => { + expect(formatLabelValuePair(label, value)).toBe(expected); + }, + ); + }); - describe('formatLabelValuePair: Format the label-value pairs used to display the allowed values', () => { - it.each` - label | value | expected - ${'TestLabel'} | ${true} | ${'true (TestLabel)'} - ${'true'} | ${true} | ${'true'} - `(`label: $label | value: $value | expected: $expected`, ({ label, expected, value }) => { - expect(formatLabelValuePair(label, value)).toBe(expected); - }); - }); + describe('formatSettingValueToFile: Format setting values to save in the configuration file', () => { + it.each` + input | expected + ${'test'} | ${'"test"'} + ${'test space'} | ${'"test space"'} + ${'test\nnew line'} | ${'"test\\nnew line"'} + ${''} | ${'""'} + ${1} | ${1} + ${true} | ${true} + ${false} | ${false} + ${['test1']} | ${'["test1"]'} + ${['test1', 'test2']} | ${'["test1","test2"]'} + `(`input: $input | expected: $expected`, ({ input, expected }) => { + expect(formatSettingValueToFile(input)).toBe(expected); + }); + }); - describe('formatSettingValueToFile: Format setting values to save in the configuration file', () => { - it.each` - input | expected - ${'test'} | ${'\"test\"'} - ${'test space'} | ${'\"test space\"'} - ${'test\nnew line'} | ${'\"test\\nnew line\"'} - ${''} | ${'\"\"'} - ${1} | ${1} - ${true} | ${true} - ${false} | ${false} - ${['test1']} | ${'[\"test1\"]'} - ${['test1', 'test2']} | ${'[\"test1\",\"test2\"]'} - `(`input: $input | expected: $expected`, ({ input, expected }) => { - expect(formatSettingValueToFile(input)).toBe(expected); - }); - }); - - describe('getCustomizationSetting: Get the value for the "customization." settings depending on the "customization.enabled" setting', () => { - it.each` - customizationEnabled | settingKey | configValue | expected - ${true} | ${'customization.logo.app'} | ${'custom-image-app.png'} | ${'custom-image-app.png'} - ${true} | ${'customization.logo.app'} | ${''} | ${''} - ${false} | ${'customization.logo.app'} | ${'custom-image-app.png'} | ${''} - ${false} | ${'customization.logo.app'} | ${''} | ${''} - ${true} | ${'customization.reports.footer'} | ${'Custom footer'} | ${'Custom footer'} - ${true} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2022 Wazuh, Inc.'} - ${false} | ${'customization.reports.footer'} | ${'Custom footer'} | ${'Copyright © 2022 Wazuh, Inc.'} - ${false} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2022 Wazuh, Inc.'} - ${false} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2022 Wazuh, Inc.'} - ${true} | ${'customization.reports.header'} | ${'Custom header'} | ${'Custom header'} - ${true} | ${'customization.reports.header'} | ${''} | ${'info@wazuh.com\nhttps://wazuh.com'} - ${false} | ${'customization.reports.header'} | ${'Custom header'} | ${'info@wazuh.com\nhttps://wazuh.com'} - ${false} | ${'customization.reports.header'} | ${''} | ${'info@wazuh.com\nhttps://wazuh.com'} - `(`customizationEnabled: $customizationEnabled | settingKey: $settingKey | configValue: $configValue | expected: $expected`, ({ configValue, customizationEnabled, expected, settingKey }) => { - const configuration = { - 'customization.enabled': customizationEnabled, - [settingKey]: configValue - }; - expect(getCustomizationSetting(configuration, settingKey)).toBe(expected); - }); - }); + describe('getCustomizationSetting: Get the value for the "customization." settings depending on the "customization.enabled" setting', () => { + it.each` + customizationEnabled | settingKey | configValue | expected + ${true} | ${'customization.logo.app'} | ${'custom-image-app.png'} | ${'custom-image-app.png'} + ${true} | ${'customization.logo.app'} | ${''} | ${''} + ${false} | ${'customization.logo.app'} | ${'custom-image-app.png'} | ${''} + ${false} | ${'customization.logo.app'} | ${''} | ${''} + ${true} | ${'customization.reports.footer'} | ${'Custom footer'} | ${'Custom footer'} + ${true} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2023 Wazuh, Inc.'} + ${false} | ${'customization.reports.footer'} | ${'Custom footer'} | ${'Copyright © 2023 Wazuh, Inc.'} + ${false} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2023 Wazuh, Inc.'} + ${false} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2023 Wazuh, Inc.'} + ${true} | ${'customization.reports.header'} | ${'Custom header'} | ${'Custom header'} + ${true} | ${'customization.reports.header'} | ${''} | ${'info@wazuh.com\nhttps://wazuh.com'} + ${false} | ${'customization.reports.header'} | ${'Custom header'} | ${'info@wazuh.com\nhttps://wazuh.com'} + ${false} | ${'customization.reports.header'} | ${''} | ${'info@wazuh.com\nhttps://wazuh.com'} + `( + `customizationEnabled: $customizationEnabled | settingKey: $settingKey | configValue: $configValue | expected: $expected`, + ({ configValue, customizationEnabled, expected, settingKey }) => { + const configuration = { + 'customization.enabled': customizationEnabled, + [settingKey]: configValue, + }; + expect(getCustomizationSetting(configuration, settingKey)).toBe( + expected, + ); + }, + ); + }); }); diff --git a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index 43dbaa0096..0c57fd5b26 100644 --- a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -108,6 +108,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot anchorPosition="downLeft" attachToAnchor={true} closePopover={[Function]} + disableFocusTrap={false} display="block" fullWidth={true} id="popover" diff --git a/plugins/main/public/components/eui-suggest/suggest_input.js b/plugins/main/public/components/eui-suggest/suggest_input.js index 55393ba820..7a4f5df6f2 100644 --- a/plugins/main/public/components/eui-suggest/suggest_input.js +++ b/plugins/main/public/components/eui-suggest/suggest_input.js @@ -53,6 +53,7 @@ export class EuiSuggestInput extends Component { onPopoverFocus, isPopoverOpen, onClosePopover, + disableFocusTrap = false, ...rest } = this.props; @@ -108,6 +109,7 @@ export class EuiSuggestInput extends Component { panelPaddingSize="none" fullWidth closePopover={onClosePopover} + disableFocusTrap={disableFocusTrap} >
{suggestions}
diff --git a/plugins/main/public/components/search-bar/README.md b/plugins/main/public/components/search-bar/README.md new file mode 100644 index 0000000000..ce9fd0d65b --- /dev/null +++ b/plugins/main/public/components/search-bar/README.md @@ -0,0 +1,201 @@ +# Component + +The `SearchBar` component is a base component of a search bar. + +It is designed to be extensible through the self-contained query language implementations. This means +the behavior of the search bar depends on the business logic of each query language. For example, a +query language can display suggestions according to the user input or prepend some buttons to the search bar. + +It is based on a custom `EuiSuggest` component defined in `public/components/eui-suggest/suggest.js`. So the +abilities are restricted by this one. + +## Features + +- Supports multiple query languages. +- Switch the selected query language. +- Self-contained query language implementation and ability to interact with the search bar component. +- React to external changes to set the new input. This enables to change the input from external components. + +# Usage + +Basic usage: + +```tsx + { + switch (field) { + case 'configSum': + return [ + { label: 'configSum1' }, + { label: 'configSum2' }, + ]; + break; + case 'dateAdd': + return [ + { label: 'dateAdd1' }, + { label: 'dateAdd2' }, + ]; + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + label: status, + }), + ); + break; + default: + return []; + break; + } + }, + } + }, + ]} + // Handler fired when the input handler changes. Optional. + onChange={onChange} + // Handler fired when the user press the Enter key or custom implementations. Required. + onSearch={onSearch} + // Used to define the internal input. Optional. + // This could be used to change the input text from the external components. + // Use the UQL (Unified Query Language) syntax. + input='' + // Define the default mode. Optional. If not defined, it will use the first one mode. + defaultMode='' +> +``` + +# Query languages + +The built-in query languages are: + +- AQL: API Query Language. Based on https://documentation.wazuh.com/current/user-manual/api/queries.html. + +## How to add a new query language + +### Definition + +The language expects to take the interface: + +```ts +type SearchBarQueryLanguage = { + description: string; + documentationLink?: string; + id: string; + label: string; + getConfiguration?: () => any; + run: (input: string | undefined, params: any) => Promise<{ + searchBarProps: any, + output: { + language: string, + apiQuery: string, + query: string + } + }>; + transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; +}; +``` + +where: + +- `description`: is the description of the query language. This is displayed in a query language popover + on the right side of the search bar. Required. +- `documentationLink`: URL to the documentation link. Optional. +- `id`: identification of the query language. +- `label`: name +- `getConfiguration`: method that returns the configuration of the language. This allows custom behavior. +- `run`: method that returns: + - `searchBarProps`: properties to be passed to the search bar component. This allows the + customization the properties that will used by the base search bar component and the output used when searching + - `output`: + - `language`: query language ID + - `apiQuery`: API query. + - `query`: current query in the specified language +- `transformInput`: method that transforms the UQL (Unified Query Language) to the specific query + language. This is used when receives a external input in the Unified Query Language, the returned + value is converted to the specific query language to set the new input text of the search bar + component. + +Create a new file located in `public/components/search-bar/query-language` and define the expected interface; + +### Register + +Go to `public/components/search-bar/query-language/index.ts` and add the new query language: + +```ts +import { AQL } from './aql'; + +// Import the custom query language +import { CustomQL } from './custom'; + +// [...] + +// Register the query languages +export const searchBarQueryLanguages: { + [key: string]: SearchBarQueryLanguage; +} = [ + AQL, + CustomQL, // Add the new custom query language +].reduce((accum, item) => { + if (accum[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + return { + ...accum, + [item.id]: item, + }; +}, {}); +``` + +## Unified Query Language - UQL + +This is an unified syntax used by the search bar component that provides a way to communicate +with the different query language implementations. + +The input and output parameters of the search bar component must use this syntax. + +This is used in: +- input: + - `input` component property +- output: + - `onChange` component handler + - `onSearch` component handler + +Its syntax is equal to Wazuh API Query Language +https://wazuh.com/./user-manual/api/queries.html + +> The AQL query language is a implementation of this syntax. \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/__snapshots__/index.test.tsx.snap b/plugins/main/public/components/search-bar/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..5602512bd0 --- /dev/null +++ b/plugins/main/public/components/search-bar/__snapshots__/index.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly the initial render 1`] = ` +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/plugins/main/public/components/search-bar/index.test.tsx b/plugins/main/public/components/search-bar/index.test.tsx new file mode 100644 index 0000000000..31f18f6dda --- /dev/null +++ b/plugins/main/public/components/search-bar/index.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SearchBar } from './index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: 'wql', + input: '', + modes: [ + { + id: 'aql', + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { field }){ + return []; + }, + }, + }, + { + id: 'wql', + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { field }){ + return []; + }, + }, + }, + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {} + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly the initial render', async () => { + const wrapper = render( + + ); + + /* This test causes a warning about act. This is intentional, because the test pretends to get + the first rendering of the component that doesn't have the component properties coming of the + selected query language */ + expect(wrapper.container).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/index.tsx b/plugins/main/public/components/search-bar/index.tsx new file mode 100644 index 0000000000..4a82d5d360 --- /dev/null +++ b/plugins/main/public/components/search-bar/index.tsx @@ -0,0 +1,229 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFormRow, + EuiLink, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + EuiSelect, + EuiText, + EuiFlexGroup, + EuiFlexItem +} from '@elastic/eui'; +import { EuiSuggest } from '../eui-suggest'; +import { searchBarQueryLanguages } from './query-language'; +import _ from 'lodash'; +import { ISearchBarModeWQL } from './query-language/wql'; + +export interface SearchBarProps{ + defaultMode?: string; + modes: ISearchBarModeWQL[]; + onChange?: (params: any) => void; + onSearch: (params: any) => void; + buttonsRender?: () => React.ReactNode + input?: string; +}; + +export const SearchBar = ({ + defaultMode, + modes, + onChange, + onSearch, + ...rest +}: SearchBarProps) => { + // Query language ID and configuration + const [queryLanguage, setQueryLanguage] = useState<{ + id: string; + configuration: any; + }>({ + id: defaultMode || modes[0].id, + configuration: + searchBarQueryLanguages[ + defaultMode || modes[0].id + ]?.getConfiguration?.() || {}, + }); + // Popover query language is open + const [isOpenPopoverQueryLanguage, setIsOpenPopoverQueryLanguage] = + useState(false); + // Input field + const [input, setInput] = useState(rest.input || ''); + // Query language output of run method + const [queryLanguageOutputRun, setQueryLanguageOutputRun] = useState({ + searchBarProps: { suggestions: [] }, + output: undefined, + }); + // Cache the previous output + const queryLanguageOutputRunPreviousOutput = useRef(queryLanguageOutputRun.output); + // Controls when the suggestion popover is open/close + const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = + useState(false); + // Reference to the input + const inputRef = useRef(); + + // Handler when searching + const _onSearch = (output: any) => { + // TODO: fix when searching + onSearch(output); + setIsOpenSuggestionPopover(false); + }; + + // Handler on change the input field text + const onChangeInput = (event: React.ChangeEvent) => + setInput(event.target.value); + + // Handler when pressing a key + const onKeyPressHandler = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + _onSearch(queryLanguageOutputRun.output); + } + }; + + const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); + + useEffect(() => { + // React to external changes and set the internal input text. Use the `transformInput` of + // the query language in use + rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformInput && setInput( + searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( + rest.input, + { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + } + ), + ); + }, [rest.input]); + + useEffect(() => { + (async () => { + // Set the query language output + const queryLanguageOutput = await searchBarQueryLanguages[queryLanguage.id].run(input, { + onSearch: _onSearch, + setInput, + closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), + openSuggestionPopover: () => setIsOpenSuggestionPopover(true), + setQueryLanguageConfiguration: (configuration: any) => + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), + setQueryLanguageOutput: setQueryLanguageOutputRun, + inputRef, + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + }); + queryLanguageOutputRunPreviousOutput.current = { + ...queryLanguageOutputRun.output + }; + setQueryLanguageOutputRun(queryLanguageOutput); + })(); + }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); + + useEffect(() => { + onChange + // Ensure the previous output is different to the new one + && !_.isEqual(queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current) + && onChange(queryLanguageOutputRun.output); + }, [queryLanguageOutputRun.output]); + + const onQueryLanguagePopoverSwitch = () => + setIsOpenPopoverQueryLanguage(state => !state); + + const searchBar = ( + <> + {}} /* This method is run by EuiSuggest when there is a change in + a div wrapper of the input and should be defined. Defining this + property prevents an error. */ + suggestions={[]} + isPopoverOpen={ + queryLanguageOutputRun?.searchBarProps?.suggestions?.length > 0 && + isOpenSuggestionPopover + } + onClosePopover={() => setIsOpenSuggestionPopover(false)} + onPopoverFocus={() => setIsOpenSuggestionPopover(true)} + placeholder={'Search'} + append={ + + {searchBarQueryLanguages[queryLanguage.id].label} + + } + isOpen={isOpenPopoverQueryLanguage} + closePopover={onQueryLanguagePopoverSwitch} + > + SYNTAX OPTIONS +
+ + {searchBarQueryLanguages[queryLanguage.id].description} + + {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( + <> + +
+ + Documentation + +
+ + )} + {modes?.length > 1 && ( + <> + + + ({ + value: id, + text: searchBarQueryLanguages[id].label, + }))} + value={queryLanguage.id} + onChange={(event: React.ChangeEvent) => { + const queryLanguageID: string = event.target.value; + setQueryLanguage({ + id: queryLanguageID, + configuration: + searchBarQueryLanguages[ + queryLanguageID + ]?.getConfiguration?.() || {}, + }); + setInput(''); + }} + aria-label='query-language-selector' + /> + + + )} +
+
+ } + {...queryLanguageOutputRun.searchBarProps} + /> + + ); + return rest.buttonsRender || queryLanguageOutputRun.filterButtons + ? ( + + {searchBar} + {rest.buttonsRender && {rest.buttonsRender()}} + {queryLanguageOutputRun.filterButtons && {queryLanguageOutputRun.filterButtons}} + + ) + : searchBar; +}; diff --git a/plugins/main/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap b/plugins/main/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap new file mode 100644 index 0000000000..0ef68d2e9e --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/plugins/main/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap b/plugins/main/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap new file mode 100644 index 0000000000..f1bad4e5d4 --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/plugins/main/public/components/search-bar/query-language/aql.md b/plugins/main/public/components/search-bar/query-language/aql.md new file mode 100644 index 0000000000..9d144e3b15 --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/aql.md @@ -0,0 +1,204 @@ +**WARNING: The search bar was changed and this language needs some adaptations to work.** + +# Query Language - AQL + +AQL (API Query Language) is a query language based in the `q` query parameters of the Wazuh API +endpoints. + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +The implementation is adapted to work with the search bar component defined +`public/components/search-bar/index.tsx`. + +## Features +- Suggestions for `fields` (configurable), `operators` and `values` (configurable) +- Support implicit query + +# Language syntax + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +# Developer notes + +## Options + +- `implicitQuery`: add an implicit query that is added to the user input. Optional. +Use UQL (Unified Query Language). +This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + +```ts +// language options +// ID is not equal to 000 and . This is defined in UQL that is transformed internally to the specific query language. +implicitQuery: 'id!=000;' +``` + +- `suggestions`: define the suggestion handlers. This is required. + + - `field`: method that returns the suggestions for the fields + + ```ts + // language options + field(currentValue) { + return [ + { label: 'configSum', description: 'Config sum' }, + { label: 'dateAdd', description: 'Date add' }, + { label: 'id', description: 'ID' }, + { label: 'ip', description: 'IP address' }, + { label: 'group', description: 'Group' }, + { label: 'group_config_status', description: 'Synced configuration status' }, + { label: 'lastKeepAline', description: 'Date add' }, + { label: 'manager', description: 'Manager' }, + { label: 'mergedSum', description: 'Merged sum' }, + { label: 'name', description: 'Agent name' }, + { label: 'node_name', description: 'Node name' }, + { label: 'os.platform', description: 'Operating system platform' }, + { label: 'status', description: 'Status' }, + { label: 'version', description: 'Version' }, + ]; + } + ``` + + - `value`: method that returns the suggestion for the values + ```ts + // language options + value: async (currentValue, { previousField }) => { + switch (previousField) { + case 'configSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'dateAdd': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'id': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'ip': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group_config_status': + return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'lastKeepAline': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'manager': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'mergedSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'node_name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'os.platform': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'version': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + default: + return []; + break; + } + } + ``` + +## Language workflow + +```mermaid +graph TD; + user_input[User input]-->tokenizer; + subgraph tokenizer + tokenize_regex[Wazuh API `q` regular expression] + end + + tokenizer-->tokens; + + tokens-->searchBarProps; + subgraph searchBarProps; + searchBarProps_suggestions-->searchBarProps_suggestions_get_last_token_with_value[Get last token with value] + searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{implicitQuery} + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null + searchBarProps_disableFocusTrap:true[disableFocusTrap = true] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + end + + tokens-->output; + subgraph output[output]; + output_result[implicitFilter + user input] + end + + output-->output_search_bar[Output] +``` \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/query-language/aql.test.tsx b/plugins/main/public/components/search-bar/query-language/aql.test.tsx new file mode 100644 index 0000000000..a5f7c7d36c --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/aql.test.tsx @@ -0,0 +1,205 @@ +import { AQL, getSuggestions, tokenizer } from './aql'; +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { SearchBar } from '../index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: AQL.id, + input: '', + modes: [ + { + id: AQL.id, + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { previousField }){ + return []; + }, + }, + } + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {} + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly to match the snapshot of query language', async () => { + const wrapper = render( + + ); + + await waitFor(() => { + const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); + expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); + expect(wrapper.container).toMatchSnapshot(); + }); + }); +}); + +describe('Query language - AQL', () => { + // Tokenize the input + it.each` + input | tokens + ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + `(`Tokenizer API input $input`, ({input, tokens}) => { + expect(tokenizer(input)).toEqual(tokens); + }); + + // Get suggestions + it.each` + input | suggestions + ${''} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'w'} | ${[]} + ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} + ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} + ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value;'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'field=value;field2'} | ${[{ description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field=value;field2='} | ${[{ label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { label: '190.0.0.1', type: 'value' }, { label: '190.0.0.2', type: 'value' }]} + ${'field=value;field2=127'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + `('Get suggestion from the input: $input', async ({ input, suggestions }) => { + expect( + await getSuggestions(tokenizer(input), { + id: 'aql', + suggestions: { + field(currentValue) { + return [ + { label: 'field', description: 'Field' }, + { label: 'field2', description: 'Field2' }, + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); + }, + value(currentValue = '', { previousField }) { + switch (previousField) { + case 'field': + return ['value', 'value2', 'value3', 'value4'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + case 'field2': + return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + default: + return []; + break; + } + }, + }, + }), + ).toEqual(suggestions); + }); + + // When a suggestion is clicked, change the input text + it.each` + AQL | clikedSuggestion | changedInput + ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} + ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} + ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} + ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} + ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} + ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';'}} | ${'field=value;'} + ${'field=value;'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value;field2'} + ${'field=value;field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value;field2>'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field=with spaces'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field=with "spaces'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="value'} + ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} + ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} + ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} + ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} + ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} + ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} + ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ','}} | ${'(field=value,'} + ${'(field=value,'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value,field2'} + ${'(field=value,field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value,field2~'} + ${'(field=value,field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value,field2>value3'} + ${'(field=value,field2>value2'} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value,field2>value2)'} + `('click suggestion - AQL $AQL => $changedInput', async ({AQL: currentInput, clikedSuggestion, changedInput}) => { + // Mock input + let input = currentInput; + + const qlOutput = await AQL.run(input, { + setInput: (value: string): void => { input = value; }, + queryLanguage: { + parameters: { + implicitQuery: '', + suggestions: { + field: () => ([]), + value: () => ([]) + } + } + } + }); + qlOutput.searchBarProps.onItemClick(clikedSuggestion); + expect(input).toEqual(changedInput); + }); + + // Transform the external input in UQL (Unified Query Language) to QL + it.each` + UQL | AQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value;'} + ${'field=value;field2'} | ${'field=value;field2'} + ${'field="'} | ${'field="'} + ${'field=with spaces'} | ${'field=with spaces'} + ${'field=with "spaces'} | ${'field=with "spaces'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value,'} + ${'(field=value,field2'} | ${'(field=value,field2'} + ${'(field=value,field2>'} | ${'(field=value,field2>'} + ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} + `('Transform the external input UQL to QL - UQL $UQL => $AQL', async ({UQL, AQL: changedInput}) => { + expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); + }); +}); diff --git a/plugins/main/public/components/search-bar/query-language/aql.tsx b/plugins/main/public/components/search-bar/query-language/aql.tsx new file mode 100644 index 0000000000..8c898af3e2 --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/aql.tsx @@ -0,0 +1,523 @@ +import React from 'react'; +import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; +import { webDocumentationLink } from '../../../../common/services/web_documentation'; + +type ITokenType = + | 'field' + | 'operator_compare' + | 'operator_group' + | 'value' + | 'conjunction'; +type IToken = { type: ITokenType; value: string }; +type ITokens = IToken[]; + +/* API Query Language +Define the API Query Language to use in the search bar. +It is based in the language used by the q query parameter. +https://documentation.wazuh.com/current/user-manual/api/queries.html + +Use the regular expression of API with some modifications to allow the decomposition of +input in entities that doesn't compose a valid query. It allows get not-completed queries. + +API schema: +??? + +Implemented schema: +?????? +*/ + +// Language definition +export const language = { + // Tokens + tokens: { + // eslint-disable-next-line camelcase + operator_compare: { + literal: { + '=': 'equality', + '!=': 'not equality', + '>': 'bigger', + '<': 'smaller', + '~': 'like as', + }, + }, + conjunction: { + literal: { + ';': 'and', + ',': 'or', + }, + }, + // eslint-disable-next-line camelcase + operator_group: { + literal: { + '(': 'open group', + ')': 'close group', + }, + }, + }, +}; + +// Suggestion mapper by language token type +const suggestionMappingLanguageTokenType = { + field: { iconType: 'kqlField', color: 'tint4' }, + // eslint-disable-next-line camelcase + operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, + value: { iconType: 'kqlValue', color: 'tint0' }, + conjunction: { iconType: 'kqlSelector', color: 'tint3' }, + // eslint-disable-next-line camelcase + operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, + // eslint-disable-next-line camelcase + function_search: { iconType: 'search', color: 'tint5' }, +}; + +/** + * Creator of intermediate interface of EuiSuggestItem + * @param type + * @returns + */ +function mapSuggestionCreator(type: ITokenType ){ + return function({...params}){ + return { + type, + ...params + }; + }; +}; + +const mapSuggestionCreatorField = mapSuggestionCreator('field'); +const mapSuggestionCreatorValue = mapSuggestionCreator('value'); + + +/** + * Tokenize the input string. Returns an array with the tokens. + * @param input + * @returns + */ +export function tokenizer(input: string): ITokens{ + // API regular expression + // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 + // self.query_regex = re.compile( + // # A ( character. + // r"(\()?" + + // # Field name: name of the field to look on DB. + // r"([\w.]+)" + + // # Operator: looks for '=', '!=', '<', '>' or '~'. + // rf"([{''.join(self.query_operators.keys())}]{{1,2}})" + + // # Value: A string. + // r"((?:(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*" + // r"(?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]+)" + // r"(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*)+)" + + // # A ) character. + // r"(\))?" + + // # Separator: looks for ';', ',' or nothing. + // rf"([{''.join(self.query_separators.keys())}])?" + // ) + + const re = new RegExp( + // The following regular expression is based in API one but was modified to use named groups + // and added the optional operator to allow matching the entities when the query is not + // completed. This helps to tokenize the query and manage when the input is not completed. + // A ( character. + '(?\\()?' + + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find + // Value: A string. + '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find + // A ) character. + '(?\\))?' + + `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, + 'g' + ); + + return [ + ...input.matchAll(re)] + .map( + ({groups}) => Object.entries(groups) + .map(([key, value]) => ({ + type: key.startsWith('operator_group') ? 'operator_group' : key, + value}) + ) + ).flat(); +}; + +type QLOptionSuggestionEntityItem = { + description?: string + label: string +}; + +type QLOptionSuggestionEntityItemTyped = + QLOptionSuggestionEntityItem + & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; + +type SuggestItem = QLOptionSuggestionEntityItem & { + type: { iconType: string, color: string } +}; + +type QLOptionSuggestionHandler = ( + currentValue: string | undefined, + { + previousField, + previousOperatorCompare, + }: { previousField: string; previousOperatorCompare: string }, +) => Promise; + +type optionsQL = { + suggestions: { + field: QLOptionSuggestionHandler; + value: QLOptionSuggestionHandler; + }; +}; + +/** + * Get the last token with value + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenWithValue( + tokens: ITokens +): IToken | undefined { + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ value }) => value, + ); + return tokenFound; +} + +/** + * Get the last token with value by type + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenWithValueByType( + tokens: ITokens, + tokenType: ITokenType, +): IToken | undefined { + // Find the last token by type + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type === tokenType && value, + ); + return tokenFound; +} + +/** + * Get the suggestions from the tokens + * @param tokens + * @param language + * @param options + * @returns + */ +export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { + if (!tokens.length) { + return []; + } + + // Get last token + const lastToken = getLastTokenWithValue(tokens); + + // If it can't get a token with value, then returns fields and open operator group + if(!lastToken?.type){ + return [ + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + } + ]; + }; + + switch (lastToken.type) { + case 'field': + return [ + // fields that starts with the input but is not equals + ...(await options.suggestions.field()).filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ).map(mapSuggestionCreatorField), + // operators if the input field is exact + ...((await options.suggestions.field()).some( + ({ label }) => label === lastToken.value, + ) + ? [ + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] + : []), + ]; + break; + case 'operator_compare': + return [ + ...Object.keys(language.tokens.operator_compare.literal) + .filter( + operator => + operator.startsWith(lastToken.value) && + operator !== lastToken.value, + ) + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), + ...(Object.keys(language.tokens.operator_compare.literal).some( + operator => operator === lastToken.value, + ) + ? [ + ...(await options.suggestions.value(undefined, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + })).map(mapSuggestionCreatorValue), + ] + : []), + ]; + break; + case 'value': + return [ + ...(lastToken.value + ? [ + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + ] + : []), + ...(await options.suggestions.value(lastToken.value, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + })).map(mapSuggestionCreatorValue), + ...Object.entries(language.tokens.conjunction.literal).map( + ([ conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), + ), + { + type: 'operator_group', + label: ')', + description: language.tokens.operator_group.literal[')'], + }, + ]; + break; + case 'conjunction': + return [ + ...Object.keys(language.tokens.conjunction.literal) + .filter( + conjunction => + conjunction.startsWith(lastToken.value) && + conjunction !== lastToken.value, + ) + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), + // fields if the input field is exact + ...(Object.keys(language.tokens.conjunction.literal).some( + conjunction => conjunction === lastToken.value, + ) + ? [ + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + ] + : []), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + break; + case 'operator_group': + if (lastToken.value === '(') { + return [ + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + ]; + } else if (lastToken.value === ')') { + return [ + // conjunction + ...Object.keys(language.tokens.conjunction.literal).map( + conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }), + ), + ]; + } + break; + default: + return []; + break; + } + + return []; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param param0 + * @returns + */ +export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ + const { type, ...rest} = suggestion; + return { + type: { ...suggestionMappingLanguageTokenType[type] }, + ...rest + }; +}; + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param suggestions + * @returns + */ +function transformSuggestionsToEuiSuggestItem( + suggestions: QLOptionSuggestionEntityItemTyped[] +): SuggestItem[] { + return suggestions.map(transformSuggestionToEuiSuggestItem); +}; + +/** + * Get the output from the input + * @param input + * @returns + */ +function getOutput(input: string, options: {implicitQuery?: string} = {}) { + const unifiedQuery = `${options?.implicitQuery ?? ''}${options?.implicitQuery ? `(${input})` : input}`; + return { + language: AQL.id, + query: unifiedQuery, + unifiedQuery + }; +}; + +export const AQL = { + id: 'aql', + label: 'AQL', + description: 'API Query Language (AQL) allows to do queries.', + documentationLink: webDocumentationLink('user-manual/api/queries.html'), + getConfiguration() { + return { + isOpenPopoverImplicitFilter: false, + }; + }, + async run(input, params) { + // Get the tokens from the input + const tokens: ITokens = tokenizer(input); + + return { + searchBarProps: { + // Props that will be used by the EuiSuggest component + // Suggestions + suggestions: transformSuggestionsToEuiSuggestItem( + await getSuggestions(tokens, params.queryLanguage.parameters) + ), + // Handler to manage when clicking in a suggestion item + onItemClick: item => { + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + params.onSearch(getOutput(input, params.queryLanguage.parameters)); + } else { + // When the clicked item has another iconType + const lastToken: IToken = getLastTokenWithValue(tokens); + // if the clicked suggestion is of same type of last token + if ( + lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token + lastToken.value = item.label; + } else { + // add a new token of the selected type and value + tokens.push({ + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: item.label, + }); + }; + + // Change the input + params.setInput(tokens + .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join('')); + } + }, + prepend: params.queryLanguage.parameters.implicitQuery ? ( + + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: + !state.isOpenPopoverImplicitFilter, + })) + } + iconType='filter' + > + + {params.queryLanguage.parameters.implicitQuery} + + + } + isOpen={ + params.queryLanguage.configuration.isOpenPopoverImplicitFilter + } + closePopover={() => + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: false, + })) + } + > + + Implicit query:{' '} + {params.queryLanguage.parameters.implicitQuery} + + This query is added to the input. + + ) : null, + // Disable the focus trap in the EuiInputPopover. + // This causes when using the Search suggestion, the suggestion popover can be closed. + // If this is disabled, then the suggestion popover is open after a short time for this + // use case. + disableFocusTrap: true + }, + output: getOutput(input, params.queryLanguage.parameters), + }; + }, + transformUQLToQL(unifiedQuery: string): string { + return unifiedQuery; + }, +}; diff --git a/plugins/main/public/components/search-bar/query-language/index.ts b/plugins/main/public/components/search-bar/query-language/index.ts new file mode 100644 index 0000000000..5a897d1d34 --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/index.ts @@ -0,0 +1,32 @@ +import { AQL } from './aql'; +import { WQL } from './wql'; + +type SearchBarQueryLanguage = { + description: string; + documentationLink?: string; + id: string; + label: string; + getConfiguration?: () => any; + run: (input: string | undefined, params: any) => Promise<{ + searchBarProps: any, + output: { + language: string, + unifiedQuery: string, + query: string + } + }>; + transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; +}; + +// Register the query languages +export const searchBarQueryLanguages: { + [key: string]: SearchBarQueryLanguage; +} = [AQL, WQL].reduce((accum, item) => { + if (accum[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + return { + ...accum, + [item.id]: item, + }; +}, {}); diff --git a/plugins/main/public/components/search-bar/query-language/wql.md b/plugins/main/public/components/search-bar/query-language/wql.md new file mode 100644 index 0000000000..108c942d32 --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/wql.md @@ -0,0 +1,269 @@ +# Query Language - WQL + +WQL (Wazuh Query Language) is a query language based in the `q` query parameter of the Wazuh API +endpoints. + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +The implementation is adapted to work with the search bar component defined +`public/components/search-bar/index.tsx`. + +# Language syntax + +It supports 2 modes: + +- `explicit`: define the field, operator and value +- `search term`: use a term to search in the available fields + +Theses modes can not be combined. + +`explicit` mode is enabled when it finds a field and operator tokens. + +## Mode: explicit + +### Schema + +``` +???????????? +``` + +### Fields + +Regular expression: /[\\w.]+/ + +Examples: + +``` +field +field.custom +``` + +### Operators + +#### Compare + +- `=` equal to +- `!=` not equal to +- `>` bigger +- `<` smaller +- `~` like + +#### Group + +- `(` open +- `)` close + +#### Conjunction (logical) + +- `and` intersection +- `or` union + +#### Values + +- Value without spaces can be literal +- Value with spaces should be wrapped by `"`. The `"` can be escaped using `\"`. + +Examples: +``` +value_without_whitespace +"value with whitespaces" +"value with whitespaces and escaped \"quotes\"" +``` + +### Notes + +- The tokens can be separated by whitespaces. + +### Examples + +- Simple query + +``` +id=001 +id = 001 +``` + +- Complex query (logical operator) +``` +status=active and os.platform~linux +status = active and os.platform ~ linux +``` + +``` +status!=never_connected and ip~240 or os.platform~linux +status != never_connected and ip ~ 240 or os.platform ~ linux +``` + +- Complex query (logical operators and group operator) +``` +(status!=never_connected and ip~240) or id=001 +( status != never_connected and ip ~ 240 ) or id = 001 +``` + +## Mode: search term + +Search the term in the available fields. + +This mode is used when there is no a `field` and `operator` according to the regular expression +of the **explicit** mode. + +### Examples: + +``` +linux +``` + +If the available fields are `id` and `ip`, then the input will be translated under the hood to the +following UQL syntax: + +``` +id~linux,ip~linux +``` + +## Developer notes + +## Features +- Support suggestions for each token entity. `fields` and `values` are customizable. +- Support implicit query. +- Support for search term mode. It enables to search a term in multiple fields. + The query is built under the hoods. This mode requires there are `field` and `operator_compare`. + +### Implicit query + +This a query that can't be added, edited or removed by the user. It is added to the user input. + +### Search term mode + +This mode enables to search in multiple fields using a search term. The fields to use must be defined. + +Use an union expression of each field with the like as operation `~`. + +The user input is transformed to something as: +``` +field1~user_input,field2~user_input,field3~user_input +``` + +## Options + +- `options`: options + + - `implicitQuery`: add an implicit query that is added to the user input. Optional. + This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + - `query`: query string in UQL (Unified Query Language) +Use UQL (Unified Query Language). + - `conjunction`: query string of the conjunction in UQL (Unified Query Language) + - `searchTermFields`: define the fields used to build the query for the search term mode + - `filterButtons`: define a list of buttons to filter in the search bar + + +```ts +// language options +options: { + // ID is not equal to 000 and . This is defined in UQL that is transformed internally to + // the specific query language. + implicitQuery: { + query: 'id!=000', + conjunction: ';' + } + searchTermFields: ['id', 'ip'] + filterButtons: [ + {id: 'status-active', input: 'status=active', label: 'Active'} + ] +} +``` + +- `suggestions`: define the suggestion handlers. This is required. + + - `field`: method that returns the suggestions for the fields + + ```ts + // language options + field(currentValue) { + // static or async fetching is allowed + return [ + { label: 'field1', description: 'Description' }, + { label: 'field2', description: 'Description' } + ]; + } + ``` + + - `value`: method that returns the suggestion for the values + ```ts + // language options + value: async (currentValue, { field }) => { + // static or async fetching is allowed + // async fetching data + // const response = await fetchData(); + return [ + { label: 'value1' }, + { label: 'value2' } + ] + } + ``` + +- `validate`: define validation methods for the field types. Optional + - `value`: method to validate the value token + + ```ts + validate: { + value: (token, {field, operator_compare}) => { + if(field === 'field1'){ + const value = token.formattedValue || token.value + return /\d+/ ? undefined : `Invalid value for field ${field}, only digits are supported: "${value}"` + } + } + } + ``` + +## Language workflow + +```mermaid +graph TD; + user_input[User input]-->ql_run; + ql_run-->filterButtons[filterButtons]; + ql_run-->tokenizer-->tokens; + tokens-->searchBarProps; + tokens-->output; + + subgraph tokenizer + tokenize_regex[Query language regular expression: decomposition and extract quoted values] + end + + subgraph searchBarProps; + searchBarProps_suggestions[suggestions]-->searchBarProps_suggestions_input_isvalid{Input is valid} + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_success[Yes] + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_fail[No] + searchBarProps_suggestions_input_isvalid_success[Yes]--->searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_suggestions_input_isvalid_fail[No]-->searchBarProps_suggestions_invalid[Invalid with error message] + searchBarProps_suggestions_invalid[Invalid with error message]-->EuiSuggestItem + searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{options.implicitQuery} + searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton + searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null + searchBarProps_disableFocusTrap:true[disableFocusTrap = true] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick_suggestion_search[Search suggestion]-->searchBarProps_onItemClick_suggestion_search_run[Run search] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_error[Error] + searchBarProps_isInvalid[isInvalid]-->searchBarProps_validate_input[validate input] + end + + subgraph output[output]; + output_input_options_implicitFilter[options.implicitFilter]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] + output_input_user_input_QL[User input in QL]-->output_input_user_input_UQL[User input in UQL]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] + end + + subgraph filterButtons; + filterButtons_optional{options.filterButtons}-->filterButtons_optional_yes[Yes]-->filterButtons_optional_yes_component[Render fitter button] + filterButtons_optional{options.filterButtons}-->filterButtons_optional_no[No]-->filterButtons_optional_no_null[null] + end +``` + +## Notes + +- The value that contains the following characters: `!`, `~` are not supported by the AQL and this +could cause problems when do the request to the API. +- The value with spaces are wrapped with `"`. If the value contains the `\"` sequence this is +replaced by `"`. This could cause a problem with values that are intended to have the mentioned +sequence. \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/query-language/wql.test.tsx new file mode 100644 index 0000000000..4de5de790b --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -0,0 +1,476 @@ +import { + getSuggestions, + tokenizer, + transformSpecificQLToUnifiedQL, + WQL, +} from './wql'; +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { SearchBar } from '../index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: WQL.id, + input: '', + modes: [ + { + id: WQL.id, + options: { + implicitQuery: { + query: 'id!=000', + conjunction: ';', + }, + }, + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { field }) { + return []; + }, + }, + }, + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {}, + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly to match the snapshot of query language', async () => { + const wrapper = render(); + + await waitFor(() => { + const elementImplicitQuery = wrapper.container.querySelector( + '.euiCodeBlock__code', + ); + expect(elementImplicitQuery?.innerHTML).toEqual('id!=000 and '); + expect(wrapper.container).toMatchSnapshot(); + }); + }); +}); + +/* eslint-disable max-len */ +describe('Query language - WQL', () => { + // Tokenize the input + function tokenCreator({ type, value, formattedValue }) { + return { type, value, ...(formattedValue ? { formattedValue } : {}) }; + } + + const t = { + opGroup: (value = undefined) => + tokenCreator({ type: 'operator_group', value }), + opCompare: (value = undefined) => + tokenCreator({ type: 'operator_compare', value }), + field: (value = undefined) => tokenCreator({ type: 'field', value }), + value: (value = undefined, formattedValue = undefined) => + tokenCreator({ + type: 'value', + value, + formattedValue: formattedValue ?? value, + }), + whitespace: (value = undefined) => + tokenCreator({ type: 'whitespace', value }), + conjunction: (value = undefined) => + tokenCreator({ type: 'conjunction', value }), + }; + + // Token undefined + const tu = { + opGroup: tokenCreator({ type: 'operator_group', value: undefined }), + opCompare: tokenCreator({ type: 'operator_compare', value: undefined }), + whitespace: tokenCreator({ type: 'whitespace', value: undefined }), + field: tokenCreator({ type: 'field', value: undefined }), + value: tokenCreator({ + type: 'value', + value: undefined, + formattedValue: undefined, + }), + conjunction: tokenCreator({ type: 'conjunction', value: undefined }), + }; + + const tuBlankSerie = [ + tu.opGroup, + tu.whitespace, + tu.field, + tu.whitespace, + tu.opCompare, + tu.whitespace, + tu.value, + tu.whitespace, + tu.opGroup, + tu.whitespace, + tu.conjunction, + tu.whitespace, + ]; + + it.each` + input | tokens + ${''} | ${tuBlankSerie} + ${'f'} | ${[tu.opGroup, tu.whitespace, t.field('f'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=or'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('or'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueand'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueand'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueor'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueor'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value!='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value>'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value>'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value<'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value<'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value~'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value~'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"', 'value and value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"', 'value or value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"', 'value = value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"', 'value != value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"', 'value > value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"', 'value < value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"', 'value ~ value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} + ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"', 'value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!=value2 and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('value2'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + `(`Tokenizer API input $input`, ({ input, tokens }) => { + expect(tokenizer(input)).toEqual(tokens); + }); + + // Get suggestions + it.each` + input | suggestions + ${''} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'w'} | ${[]} + ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} + ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} + ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value and'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + `('Get suggestion from the input: $input', async ({ input, suggestions }) => { + expect( + await getSuggestions(tokenizer(input), { + id: 'aql', + suggestions: { + field(currentValue) { + return [ + { label: 'field', description: 'Field' }, + { label: 'field2', description: 'Field2' }, + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); + }, + value(currentValue = '', { field }) { + switch (field) { + case 'field': + return ['value', 'value2', 'value3', 'value4'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + case 'field2': + return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + default: + return []; + break; + } + }, + }, + }), + ).toEqual(suggestions); + }); + + // Transform specific query language to UQL (Unified Query Language) + it.each` + WQL | UQL + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field=value'} | ${'field=value'} + ${'field=value()'} | ${'field=value()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field="custom value"'} | ${'field=custom value'} + ${'field="custom value()"'} | ${'field=custom value()'} + ${'field="value and value2"'} | ${'field=value and value2'} + ${'field="value or value2"'} | ${'field=value or value2'} + ${'field="value = value2"'} | ${'field=value = value2'} + ${'field="value != value2"'} | ${'field=value != value2'} + ${'field="value > value2"'} | ${'field=value > value2'} + ${'field="value < value2"'} | ${'field=value < value2'} + ${'field="value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} + ${'field="custom \\"value"'} | ${'field=custom "value'} + ${'field="custom \\"value\\""'} | ${'field=custom "value"'} + ${'field=value and'} | ${'field=value;'} + ${'field="custom value" and'} | ${'field=custom value;'} + ${'(field=value'} | ${'(field=value'} + ${'(field=value)'} | ${'(field=value)'} + ${'(field=value) and'} | ${'(field=value);'} + ${'(field=value) and field2'} | ${'(field=value);field2'} + ${'(field=value) and field2>'} | ${'(field=value);field2>'} + ${'(field=value) and field2>"wrappedcommas"'} | ${'(field=value);field2>wrappedcommas'} + ${'(field=value) and field2>"value with spaces"'} | ${'(field=value);field2>value with spaces'} + ${'field ='} | ${'field='} + ${'field = value'} | ${'field=value'} + ${'field = value()'} | ${'field=value()'} + ${'field = valueand'} | ${'field=valueand'} + ${'field = valueor'} | ${'field=valueor'} + ${'field = value='} | ${'field=value='} + ${'field = value!='} | ${'field=value!='} + ${'field = value>'} | ${'field=value>'} + ${'field = value<'} | ${'field=value<'} + ${'field = value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field = "custom value"'} | ${'field=custom value'} + ${'field = "custom value()"'} | ${'field=custom value()'} + ${'field = "value and value2"'} | ${'field=value and value2'} + ${'field = "value or value2"'} | ${'field=value or value2'} + ${'field = "value = value2"'} | ${'field=value = value2'} + ${'field = "value != value2"'} | ${'field=value != value2'} + ${'field = "value > value2"'} | ${'field=value > value2'} + ${'field = "value < value2"'} | ${'field=value < value2'} + ${'field = "value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} + ${'field = value or'} | ${'field=value,'} + ${'field = value or field2'} | ${'field=value,field2'} + ${'field = value or field2 <'} | ${'field=value,field2<'} + ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} + `('transformSpecificQLToUnifiedQL - WQL $WQL TO UQL $UQL', ({ WQL, UQL }) => { + expect(transformSpecificQLToUnifiedQL(WQL)).toEqual(UQL); + }); + + // When a suggestion is clicked, change the input text + it.each` + WQL | clikedSuggestion | changedInput + ${''} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'field'} + ${'field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field2'} + ${'field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'field='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value()' }} | ${'field=value()'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueand' }} | ${'field=valueand'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueor' }} | ${'field=valueor'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value=' }} | ${'field=value='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value!=' }} | ${'field=value!='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value>' }} | ${'field=value>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value<' }} | ${'field=value<'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value~' }} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field='} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!=' }} | ${'field!='} + ${'field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'field=value2'} + ${'field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'field=value and '} + ${'field=value and'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'field=value or'} + ${'field=value and'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value and field2'} + ${'field=value and '} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'field=value or '} + ${'field=value and '} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value and field2'} + ${'field=value and field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'field=value and field2>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces' }} | ${'field="with spaces"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${'field="with \\"spaces"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with value()' }} | ${'field="with value()"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with and value' }} | ${'field="with and value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with or value' }} | ${'field="with or value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with = value' }} | ${'field="with = value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with != value' }} | ${'field="with != value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with > value' }} | ${'field="with > value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value' }} | ${'field="with < value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value' }} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${'field="\\"value"'} + ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'other spaces' }} | ${'field="other spaces"'} + ${''} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '(' }} | ${'('} + ${'('} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'(field'} + ${'(field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field2'} + ${'(field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'(field='} + ${'(field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'(field=value'} + ${'(field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value2'} + ${'(field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'(field=value or '} + ${'(field=value or'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'(field=value and'} + ${'(field=value or'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value or field2'} + ${'(field=value or '} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'(field=value and '} + ${'(field=value or '} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value or field2'} + ${'(field=value or field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value or field2~'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value or field2>value2'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value or field2>value3'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2)'} + `( + 'click suggestion - WQL $WQL => $changedInput', + async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { + // Mock input + let input = currentInput; + + const qlOutput = await WQL.run(input, { + setInput: (value: string): void => { + input = value; + }, + queryLanguage: { + parameters: { + options: {}, + suggestions: { + field: () => [], + value: () => [], + }, + }, + }, + }); + qlOutput.searchBarProps.onItemClick(clikedSuggestion); + expect(input).toEqual(changedInput); + }, + ); + + // Transform the external input in UQL (Unified Query Language) to QL + it.each` + UQL | WQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field=()'} | ${'field=()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~'} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value and '} + ${'field=value;field2'} | ${'field=value and field2'} + ${'field="'} | ${'field="\\""'} + ${'field=with spaces'} | ${'field="with spaces"'} + ${'field=with "spaces'} | ${'field="with \\"spaces"'} + ${'field=value ()'} | ${'field="value ()"'} + ${'field=with and value'} | ${'field="with and value"'} + ${'field=with or value'} | ${'field="with or value"'} + ${'field=with = value'} | ${'field="with = value"'} + ${'field=with > value'} | ${'field="with > value"'} + ${'field=with < value'} | ${'field="with < value"'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value or '} + ${'(field=value,field2'} | ${'(field=value or field2'} + ${'(field=value,field2>'} | ${'(field=value or field2>'} + ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} + ${'implicit=value;'} | ${''} + ${'implicit=value;field'} | ${'field'} + `( + 'Transform the external input UQL to QL - UQL $UQL => $WQL', + async ({ UQL, WQL: changedInput }) => { + expect( + WQL.transformInput(UQL, { + parameters: { + options: { + implicitQuery: { + query: 'implicit=value', + conjunction: ';', + }, + }, + }, + }), + ).toEqual(changedInput); + }, + ); + + /* The ! and ~ characters can't be part of a value that contains examples. The tests doesn't + include these cases. + + Value examples: + - with != value + - with ~ value + */ + + // Validate the tokens + // Some examples of value tokens are based on this API test: https://github.com/wazuh/wazuh/blob/813595cf58d753c1066c3e7c2018dbb4708df088/framework/wazuh/core/tests/test_utils.py#L987-L1050 + it.each` + WQL | validationError + ${''} | ${undefined} + ${'field1'} | ${undefined} + ${'field2'} | ${undefined} + ${'field1='} | ${['The value for field "field1" is missing.']} + ${'field2='} | ${['The value for field "field2" is missing.']} + ${'field='} | ${['"field" is not a valid field.']} + ${'custom='} | ${['"custom" is not a valid field.']} + ${'field1=value'} | ${undefined} + ${'field_not_number=1'} | ${['Numbers are not valid for field_not_number']} + ${'field_not_number=value1'} | ${['Numbers are not valid for field_not_number']} + ${'field2=value'} | ${undefined} + ${'field=value'} | ${['"field" is not a valid field.']} + ${'custom=value'} | ${['"custom" is not a valid field.']} + ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} + ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !&']} + ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !$&']} + ${'field1=value,'} | ${['"value," is not a valid value.']} + ${'field1="Mozilla Firefox 53.0 (x64 en-US)"'} | ${undefined} + ${'field1="[\\"https://example-link@<>=,%?\\"]"'} | ${undefined} + ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and field2'} | ${['The operator for field "field2" is missing.']} + ${'field2=value and field1'} | ${['The operator for field "field1" is missing.']} + ${'field1=value and field'} | ${['"field" is not a valid field.']} + ${'field2=value and field'} | ${['"field" is not a valid field.']} + ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} + ${'('} | ${undefined} + ${'(field'} | ${undefined} + ${'(field='} | ${['"field" is not a valid field.']} + ${'(field=value'} | ${['"field" is not a valid field.']} + ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} + ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} + ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field "field2" is missing.']} + ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} + ${'(field=value or field2>value2'} | ${['"field" is not a valid field.']} + `( + 'validate the tokens - WQL $WQL => $validationError', + async ({ WQL: currentInput, validationError }) => { + const qlOutput = await WQL.run(currentInput, { + queryLanguage: { + parameters: { + options: {}, + suggestions: { + field: () => + ['field1', 'field2', 'field_not_number'].map(label => ({ + label, + })), + value: () => [], + }, + validate: { + value: (token, { field, operator_compare }) => { + if (field === 'field_not_number') { + const value = token.formattedValue || token.value; + return /\d/.test(value) + ? `Numbers are not valid for ${field}` + : undefined; + } + }, + }, + }, + }, + }); + expect(qlOutput.output.error).toEqual(validationError); + }, + ); +}); diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx new file mode 100644 index 0000000000..9df7dbbf01 --- /dev/null +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -0,0 +1,1157 @@ +import React from 'react'; +import { + EuiButtonEmpty, + EuiButtonGroup, + EuiPopover, + EuiText, + EuiCode, +} from '@elastic/eui'; +import { tokenizer as tokenizerUQL } from './aql'; +import { PLUGIN_VERSION } from '../../../../common/constants'; + +/* UI Query language +https://documentation.wazuh.com/current/user-manual/api/queries.html + +// Example of another query language definition +*/ + +type ITokenType = + | 'field' + | 'operator_compare' + | 'operator_group' + | 'value' + | 'conjunction' + | 'whitespace'; +type IToken = { type: ITokenType; value: string; formattedValue?: string }; +type ITokens = IToken[]; + +/* API Query Language +Define the API Query Language to use in the search bar. +It is based in the language used by the q query parameter. +https://documentation.wazuh.com/current/user-manual/api/queries.html + +Use the regular expression of API with some modifications to allow the decomposition of +input in entities that doesn't compose a valid query. It allows get not-completed queries. + +API schema: +??? + +Implemented schema: +???????????? +*/ + +// Language definition +const language = { + // Tokens + tokens: { + // eslint-disable-next-line camelcase + operator_compare: { + literal: { + '=': 'equality', + '!=': 'not equality', + '>': 'bigger', + '<': 'smaller', + '~': 'like as', + }, + }, + conjunction: { + literal: { + and: 'and', + or: 'or', + }, + }, + // eslint-disable-next-line camelcase + operator_group: { + literal: { + '(': 'open group', + ')': 'close group', + }, + }, + }, + equivalencesToUQL: { + conjunction: { + literal: { + and: ';', + or: ',', + }, + }, + }, +}; + +// Suggestion mapper by language token type +const suggestionMappingLanguageTokenType = { + field: { iconType: 'kqlField', color: 'tint4' }, + // eslint-disable-next-line camelcase + operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, + value: { iconType: 'kqlValue', color: 'tint0' }, + conjunction: { iconType: 'kqlSelector', color: 'tint3' }, + // eslint-disable-next-line camelcase + operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, + // eslint-disable-next-line camelcase + function_search: { iconType: 'search', color: 'tint5' }, + // eslint-disable-next-line camelcase + validation_error: { iconType: 'alert', color: 'tint2' }, +}; + +/** + * Creator of intermediate interface of EuiSuggestItem + * @param type + * @returns + */ +function mapSuggestionCreator(type: ITokenType) { + return function ({ ...params }) { + return { + type, + ...params, + }; + }; +} + +const mapSuggestionCreatorField = mapSuggestionCreator('field'); +const mapSuggestionCreatorValue = mapSuggestionCreator('value'); + +/** + * Transform the conjunction to the query language syntax + * @param conjunction + * @returns + */ +function transformQLConjunction(conjunction: string): string { + // If the value has a whitespace or comma, then + return conjunction === language.equivalencesToUQL.conjunction.literal['and'] + ? ` ${language.tokens.conjunction.literal['and']} ` + : ` ${language.tokens.conjunction.literal['or']} `; +} + +/** + * Transform the value to the query language syntax + * @param value + * @returns + */ +function transformQLValue(value: string): string { + // If the value has a whitespace or comma, then + return /[\s|"]/.test(value) + ? // Escape the commas (") => (\") and wraps the string with commas ("") + `"${value.replace(/"/, '\\"')}"` + : // Raw value + value; +} + +/** + * Tokenize the input string. Returns an array with the tokens. + * @param input + * @returns + */ +export function tokenizer(input: string): ITokens { + const re = new RegExp( + // A ( character. + '(?\\()?' + + // Whitespace + '(?\\s+)?' + + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys( + language.tokens.operator_compare.literal, + )}]{1,2})?` + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Value: A string. + // Simple value + // Quoted ", "value, "value", "escaped \"quote" + // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes + '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + + // Whitespace + '(?\\s+)?' + + // A ) character. + '(?\\))?' + + // Whitespace + '(?\\s+)?' + + `(?${Object.keys(language.tokens.conjunction.literal).join( + '|', + )})?` + + // Whitespace + '(?\\s+)?', + 'g', + ); + + return [...input.matchAll(re)] + .map(({ groups }) => + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') // Transform operator_group group match + ? 'operator_group' + : key.startsWith('whitespace') // Transform whitespace group match + ? 'whitespace' + : key, + value, + ...(key === 'value' && + (value && /^"([\s\S]+)"$/.test(value) + ? { formattedValue: value.match(/^"([\s\S]+)"$/)[1] } + : { formattedValue: value })), + })), + ) + .flat(); +} + +type QLOptionSuggestionEntityItem = { + description?: string; + label: string; +}; + +type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { + type: + | 'operator_group' + | 'field' + | 'operator_compare' + | 'value' + | 'conjunction' + | 'function_search'; +}; + +type SuggestItem = QLOptionSuggestionEntityItem & { + type: { iconType: string; color: string }; +}; + +type QLOptionSuggestionHandler = ( + currentValue: string | undefined, + { field, operatorCompare }: { field: string; operatorCompare: string }, +) => Promise; + +type OptionsQLImplicitQuery = { + query: string; + conjunction: string; +}; +type OptionsQL = { + options?: { + implicitQuery?: OptionsQLImplicitQuery; + searchTermFields?: string[]; + filterButtons: { id: string; label: string; input: string }[]; + }; + suggestions: { + field: QLOptionSuggestionHandler; + value: QLOptionSuggestionHandler; + }; + validate?: { + value?: { + [key: string]: ( + token: IToken, + nearTokens: { field: string; operator: string }, + ) => string | undefined; + }; + }; +}; + +export interface ISearchBarModeWQL extends OptionsQL { + id: 'wql'; +} + +/** + * Get the last token with value + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenDefined(tokens: ITokens): IToken | undefined { + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type !== 'whitespace' && value, + ); + return tokenFound; +} + +/** + * Get the last token with value by type + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenDefinedByType( + tokens: ITokens, + tokenType: ITokenType, +): IToken | undefined { + // Find the last token by type + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type === tokenType && value, + ); + return tokenFound; +} + +/** + * Get the token that is near to a token position of the token type. + * @param tokens + * @param tokenReferencePosition + * @param tokenType + * @param mode + * @returns + */ +function getTokenNearTo( + tokens: ITokens, + tokenType: ITokenType, + mode: 'previous' | 'next' = 'previous', + options: { + tokenReferencePosition?: number; + tokenFoundShouldHaveValue?: boolean; + } = {}, +): IToken | undefined { + const shallowCopyTokens = Array.from([...tokens]); + const computedShallowCopyTokens = + mode === 'previous' + ? shallowCopyTokens + .slice(0, options?.tokenReferencePosition || tokens.length) + .reverse() + : shallowCopyTokens.slice(options?.tokenReferencePosition || 0); + return computedShallowCopyTokens.find( + ({ type, value }) => + type === tokenType && (options?.tokenFoundShouldHaveValue ? value : true), + ); +} + +/** + * Get the suggestions from the tokens + * @param tokens + * @param language + * @param options + * @returns + */ +export async function getSuggestions( + tokens: ITokens, + options: OptionsQL, +): Promise { + if (!tokens.length) { + return []; + } + + // Get last token + const lastToken = getLastTokenDefined(tokens); + + // If it can't get a token with value, then returns fields and open operator group + if (!lastToken?.type) { + return [ + // Search function + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + } + + switch (lastToken.type) { + case 'field': + return [ + // fields that starts with the input but is not equals + ...(await options.suggestions.field()) + .filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ) + .map(mapSuggestionCreatorField), + // operators if the input field is exact + ...((await options.suggestions.field()).some( + ({ label }) => label === lastToken.value, + ) + ? [ + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] + : []), + ]; + break; + case 'operator_compare': { + const field = getLastTokenDefinedByType(tokens, 'field')?.value; + const operatorCompare = getLastTokenDefinedByType( + tokens, + 'operator_compare', + )?.value; + + // If there is no a previous field, then no return suggestions because it would be an syntax + // error + if (!field) { + return []; + } + + return [ + ...Object.keys(language.tokens.operator_compare.literal) + .filter( + operator => + operator.startsWith(lastToken.value) && + operator !== lastToken.value, + ) + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), + ...(Object.keys(language.tokens.operator_compare.literal).some( + operator => operator === lastToken.value, + ) + ? [ + ...( + await options.suggestions.value(undefined, { + field, + operatorCompare, + }) + ).map(mapSuggestionCreatorValue), + ] + : []), + ]; + break; + } + case 'value': { + const field = getLastTokenDefinedByType(tokens, 'field')?.value; + const operatorCompare = getLastTokenDefinedByType( + tokens, + 'operator_compare', + )?.value; + + /* If there is no a previous field or operator_compare, then no return suggestions because + it would be an syntax error */ + if (!field || !operatorCompare) { + return []; + } + + return [ + ...(lastToken.formattedValue + ? [ + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + ] + : []), + ...( + await options.suggestions.value(lastToken.formattedValue, { + field, + operatorCompare, + }) + ).map(mapSuggestionCreatorValue), + ...Object.entries(language.tokens.conjunction.literal).map( + ([conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), + ), + { + type: 'operator_group', + label: ')', + description: language.tokens.operator_group.literal[')'], + }, + ]; + break; + } + case 'conjunction': + return [ + ...Object.keys(language.tokens.conjunction.literal) + .filter( + conjunction => + conjunction.startsWith(lastToken.value) && + conjunction !== lastToken.value, + ) + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), + // fields if the input field is exact + ...(Object.keys(language.tokens.conjunction.literal).some( + conjunction => conjunction === lastToken.value, + ) + ? [ + ...(await options.suggestions.field()).map( + mapSuggestionCreatorField, + ), + ] + : []), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + break; + case 'operator_group': + if (lastToken.value === '(') { + return [ + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + ]; + } else if (lastToken.value === ')') { + return [ + // conjunction + ...Object.keys(language.tokens.conjunction.literal).map( + conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }), + ), + ]; + } + break; + default: + return []; + break; + } + + return []; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param param0 + * @returns + */ +export function transformSuggestionToEuiSuggestItem( + suggestion: QLOptionSuggestionEntityItemTyped, +): SuggestItem { + const { type, ...rest } = suggestion; + return { + type: { ...suggestionMappingLanguageTokenType[type] }, + ...rest, + }; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param suggestions + * @returns + */ +function transformSuggestionsToEuiSuggestItem( + suggestions: QLOptionSuggestionEntityItemTyped[], +): SuggestItem[] { + return suggestions.map(transformSuggestionToEuiSuggestItem); +} + +/** + * Transform the UQL (Unified Query Language) to QL + * @param input + * @returns + */ +export function transformUQLToQL(input: string) { + const tokens = tokenizerUQL(input); + return tokens + .filter(({ value }) => value) + .map(({ type, value }) => { + switch (type) { + case 'conjunction': + return transformQLConjunction(value); + break; + case 'value': + return transformQLValue(value); + break; + default: + return value; + break; + } + }) + .join(''); +} + +export function shouldUseSearchTerm(tokens: ITokens): boolean { + return !( + tokens.some(({ type, value }) => type === 'operator_compare' && value) && + tokens.some(({ type, value }) => type === 'field' && value) + ); +} + +export function transformToSearchTerm( + searchTermFields: string[], + input: string, +): string { + return searchTermFields + .map(searchTermField => `${searchTermField}~${input}`) + .join(','); +} + +/** + * Transform the input in QL to UQL (Unified Query Language) + * @param input + * @returns + */ +export function transformSpecificQLToUnifiedQL( + input: string, + searchTermFields: string[], +) { + const tokens = tokenizer(input); + + if (input && searchTermFields && shouldUseSearchTerm(tokens)) { + return transformToSearchTerm(searchTermFields, input); + } + + return tokens + .filter( + ({ type, value, formattedValue }) => + type !== 'whitespace' && (formattedValue ?? value), + ) + .map(({ type, value, formattedValue }) => { + switch (type) { + case 'value': { + // If the value is wrapped with ", then replace the escaped double quotation mark (\") + // by double quotation marks (") + // WARN: This could cause a problem with value that contains this sequence \" + const extractedValue = + formattedValue !== value + ? formattedValue.replace(/\\"/g, '"') + : formattedValue; + return extractedValue || value; + break; + } + case 'conjunction': + return value === 'and' + ? language.equivalencesToUQL.conjunction.literal['and'] + : language.equivalencesToUQL.conjunction.literal['or']; + break; + default: + return value; + break; + } + }) + .join(''); +} + +/** + * Get the output from the input + * @param input + * @returns + */ +function getOutput(input: string, options: OptionsQL) { + // Implicit query + const implicitQueryAsUQL = options?.options?.implicitQuery?.query ?? ''; + const implicitQueryAsQL = transformUQLToQL(implicitQueryAsUQL); + + // Implicit query conjunction + const implicitQueryConjunctionAsUQL = + options?.options?.implicitQuery?.conjunction ?? ''; + const implicitQueryConjunctionAsQL = transformUQLToQL( + implicitQueryConjunctionAsUQL, + ); + + // User input query + const inputQueryAsQL = input; + const inputQueryAsUQL = transformSpecificQLToUnifiedQL( + inputQueryAsQL, + options?.options?.searchTermFields ?? [], + ); + + return { + language: WQL.id, + apiQuery: { + q: [ + implicitQueryAsUQL, + implicitQueryAsUQL && inputQueryAsUQL + ? implicitQueryConjunctionAsUQL + : '', + implicitQueryAsUQL && inputQueryAsUQL + ? `(${inputQueryAsUQL})` + : inputQueryAsUQL, + ].join(''), + }, + query: [ + implicitQueryAsQL, + implicitQueryAsQL && inputQueryAsQL ? implicitQueryConjunctionAsQL : '', + implicitQueryAsQL && inputQueryAsQL + ? `(${inputQueryAsQL})` + : inputQueryAsQL, + ].join(''), + }; +} + +/** + * Validate the token value + * @param token + * @returns + */ +function validateTokenValue(token: IToken): string | undefined { + const re = new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', + ); + + const value = token.formattedValue ?? token.value; + const match = value.match(re); + + if (match?.groups?.value === value) { + return undefined; + } + + const invalidCharacters: string[] = token.value + .split('') + .filter((value, index, array) => array.indexOf(value) === index) + .filter( + character => + !new RegExp('[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}\\(\\)]').test( + character, + ), + ); + + return [ + `"${value}" is not a valid value.`, + ...(invalidCharacters.length + ? [`Invalid characters found: ${invalidCharacters.join('')}`] + : []), + ].join(' '); +} + +type ITokenValidator = ( + tokenValue: IToken, + proximityTokens: any, +) => string | undefined; +/** + * Validate the tokens while the user is building the query + * @param tokens + * @param validate + * @returns + */ +function validatePartial( + tokens: ITokens, + validate: { field: ITokenValidator; value: ITokenValidator }, +): undefined | string { + // Ensure is not in search term mode + if (!shouldUseSearchTerm(tokens)) { + return ( + tokens + .map((token: IToken, index) => { + if (token.value) { + if (token.type === 'field') { + // Ensure there is a operator next to field to check if the fields is valid or not. + // This allows the user can type the field token and get the suggestions for the field. + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + return tokenOperatorNearToField + ? validate.field(token) + : undefined; + } + // Check if the value is allowed + if (token.type === 'value') { + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + return ( + validateTokenValue(token) || + (tokenFieldNearToValue && + tokenOperatorCompareNearToValue && + validate.value + ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value, + }) + : undefined) + ); + } + } + }) + .filter(t => typeof t !== 'undefined') + .join('\n') || undefined + ); + } +} + +/** + * Validate the tokens if they are a valid syntax + * @param tokens + * @param validate + * @returns + */ +function validate( + tokens: ITokens, + validate: { field: ITokenValidator; value: ITokenValidator }, +): undefined | string[] { + if (!shouldUseSearchTerm(tokens)) { + const errors = tokens + .map((token: IToken, index) => { + const errors = []; + if (token.value) { + if (token.type === 'field') { + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenValueNearToField = getTokenNearTo( + tokens, + 'value', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + if (validate.field(token)) { + errors.push(`"${token.value}" is not a valid field.`); + } else if (!tokenOperatorNearToField) { + errors.push( + `The operator for field "${token.value}" is missing.`, + ); + } else if (!tokenValueNearToField) { + errors.push(`The value for field "${token.value}" is missing.`); + } + } + // Check if the value is allowed + if (token.type === 'value') { + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const validationError = + validateTokenValue(token) || + (tokenFieldNearToValue && + tokenOperatorCompareNearToValue && + validate.value + ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value, + }) + : undefined); + + validationError && errors.push(validationError); + } + + // Check if the value is allowed + if (token.type === 'conjunction') { + const tokenWhitespaceNearToFieldNext = getTokenNearTo( + tokens, + 'whitespace', + 'next', + { tokenReferencePosition: index }, + ); + const tokenFieldNearToFieldNext = getTokenNearTo( + tokens, + 'field', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + !tokenWhitespaceNearToFieldNext?.value?.length && + errors.push( + `There is no whitespace after conjunction "${token.value}".`, + ); + !tokenFieldNearToFieldNext?.value?.length && + errors.push( + `There is no sentence after conjunction "${token.value}".`, + ); + } + } + return errors.length ? errors : undefined; + }) + .filter(errors => errors) + .flat(); + return errors.length ? errors : undefined; + } + return undefined; +} + +export const WQL = { + id: 'wql', + label: 'WQL', + description: + 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', + documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${PLUGIN_VERSION}/plugins/main/public/components/search-bar/query-language/wql.md`, + getConfiguration() { + return { + isOpenPopoverImplicitFilter: false, + }; + }, + async run(input, params) { + // Get the tokens from the input + const tokens: ITokens = tokenizer(input); + + // Get the implicit query as query language syntax + const implicitQueryAsQL = params.queryLanguage.parameters?.options + ?.implicitQuery + ? transformUQLToQL( + params.queryLanguage.parameters.options.implicitQuery.query + + params.queryLanguage.parameters.options.implicitQuery.conjunction, + ) + : ''; + + const fieldsSuggestion: string[] = + await params.queryLanguage.parameters.suggestions + .field() + .map(({ label }) => label); + + const validators = { + field: ({ value }) => + fieldsSuggestion.includes(value) + ? undefined + : `"${value}" is not valid field.`, + ...(params.queryLanguage.parameters?.validate?.value + ? { + value: params.queryLanguage.parameters?.validate?.value, + } + : {}), + }; + + // Validate the user input + const validationPartial = validatePartial(tokens, validators); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; + + const onSearch = () => { + if (output?.error) { + params.setQueryLanguageOutput(state => ({ + ...state, + searchBarProps: { + ...state.searchBarProps, + suggestions: transformSuggestionsToEuiSuggestItem( + output.error.map(error => ({ + type: 'validation_error', + label: 'Invalid', + description: error, + })), + ), + }, + })); + } else { + params.onSearch(output); + } + }; + + return { + filterButtons: params.queryLanguage.parameters?.options?.filterButtons ? ( + ({ id, label }), + )} + idToSelectedMap={{}} + type='multi' + onChange={(id: string) => { + const buttonParams = + params.queryLanguage.parameters?.options?.filterButtons.find( + ({ id: buttonID }) => buttonID === id, + ); + if (buttonParams) { + params.setInput(buttonParams.input); + const output = { + ...getOutput( + buttonParams.input, + params.queryLanguage.parameters, + ), + error: undefined, + }; + params.onSearch(output); + } + }} + /> + ) : null, + searchBarProps: { + // Props that will be used by the EuiSuggest component + // Suggestions + suggestions: transformSuggestionsToEuiSuggestItem( + validationPartial + ? [ + { + type: 'validation_error', + label: 'Invalid', + description: validationPartial, + }, + ] + : await getSuggestions(tokens, params.queryLanguage.parameters), + ), + // Handler to manage when clicking in a suggestion item + onItemClick: item => { + // There is an error, clicking on the item does nothing + if (item.type.iconType === 'alert') { + return; + } + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + onSearch(); + } else { + // When the clicked item has another iconType + const lastToken: IToken | undefined = getLastTokenDefined(tokens); + // if the clicked suggestion is of same type of last token + if ( + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token with the current one. + // if the current token is a value, then transform it + lastToken.value = + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label; + } else { + // add a whitespace for conjunction + !/\s$/.test(input) && + (item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType || + lastToken?.type === 'conjunction') && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + + // add a new token of the selected type and value + tokens.push({ + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label, + }); + + // add a whitespace for conjunction + item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + } + + // Change the input + params.setInput( + tokens + .filter(value => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); + } + }, + prepend: implicitQueryAsQL ? ( + + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: + !state.isOpenPopoverImplicitFilter, + })) + } + iconType='filter' + > + {implicitQueryAsQL} + + } + isOpen={ + params.queryLanguage.configuration.isOpenPopoverImplicitFilter + } + closePopover={() => + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: false, + })) + } + > + + Implicit query: {implicitQueryAsQL} + + This query is added to the input. + + ) : null, + // Disable the focus trap in the EuiInputPopover. + // This causes when using the Search suggestion, the suggestion popover can be closed. + // If this is disabled, then the suggestion popover is open after a short time for this + // use case. + disableFocusTrap: true, + // Show the input is invalid + isInvalid: Boolean(validationStrict), + // Define the handler when the a key is pressed while the input is focused + onKeyPress: event => { + if (event.key === 'Enter') { + onSearch(); + } + }, + }, + output, + }; + }, + transformInput: (unifiedQuery: string, { parameters }) => { + const input = + unifiedQuery && parameters?.options?.implicitQuery + ? unifiedQuery.replace( + new RegExp( + `^${parameters.options.implicitQuery.query}${parameters.options.implicitQuery.conjunction}`, + ), + '', + ) + : unifiedQuery; + + return transformUQLToQL(input); + }, +}; diff --git a/plugins/main/public/controllers/agent/wazuh-config/index.ts b/plugins/main/public/controllers/agent/wazuh-config/index.ts index c8bafbbe2a..70b345bf8e 100644 --- a/plugins/main/public/controllers/agent/wazuh-config/index.ts +++ b/plugins/main/public/controllers/agent/wazuh-config/index.ts @@ -6,7 +6,7 @@ const architectureButtons = [ { id: 'x86_64', label: 'x86_64', - default: true + default: true, }, { id: 'armhf', @@ -26,7 +26,7 @@ const architectureButtonsWithPPC64LE = [ { id: 'x86_64', label: 'x86_64', - default: true + default: true, }, { id: 'armhf', @@ -54,7 +54,7 @@ const architectureButtonsWithPPC64LEAlpine = [ { id: 'x86_64', label: 'x86_64', - default: true + default: true, }, { id: 'armhf', @@ -85,7 +85,7 @@ const architecturei386Andx86_64 = [ { id: 'x86_64', label: 'x86_64', - default: true + default: true, }, ]; @@ -93,7 +93,7 @@ const architectureButtonsSolaris = [ { id: 'i386', label: 'i386', - default: true + default: true, }, { id: 'sparc', @@ -138,7 +138,7 @@ const versionButtonAmazonLinux = [ { id: 'amazonlinux2022', label: 'Amazon Linux 2022', - default: true + default: true, }, ]; @@ -154,7 +154,7 @@ const versionButtonsRedHat = [ { id: 'redhat7', label: 'Red Hat 7 +', - default: true + default: true, }, ]; @@ -170,7 +170,7 @@ const versionButtonsCentos = [ { id: 'centos7', label: 'CentOS 7 +', - default: true + default: true, }, ]; @@ -186,7 +186,7 @@ const versionButtonsDebian = [ { id: 'debian9', label: 'Debian 9 +', - default: true + default: true, }, ]; @@ -205,7 +205,7 @@ const versionButtonsUbuntu = [ { id: 'ubuntu15', label: 'Ubuntu 15 +', - default: true + default: true, }, ]; @@ -221,7 +221,7 @@ const versionButtonsWindows = [ { id: 'windows7', label: 'Windows 7 +', - default: true + default: true, }, ]; @@ -233,7 +233,7 @@ const versionButtonsSuse = [ { id: 'suse12', label: 'SUSE 12', - default: true + default: true, }, ]; @@ -259,7 +259,7 @@ const versionButtonsSolaris = [ { id: 'solaris11', label: 'Solaris 11', - default: true + default: true, }, ]; @@ -285,7 +285,7 @@ const versionButtonsOracleLinux = [ { id: 'oraclelinux6', label: 'Oracle Linux 6 +', - default: true + default: true, }, ]; diff --git a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js index a73a6bad55..27ee6ae656 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js +++ b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js @@ -28,18 +28,18 @@ import { webDocumentationLink } from '../../../../../../../common/services/web_d const columns = [ { field: 'key', name: 'Label key' }, { field: 'value', name: 'Label value' }, - { field: 'hidden', name: 'Hidden' } + { field: 'hidden', name: 'Hidden' }, ]; const helpLinks = [ { text: 'Agent labels', - href: webDocumentationLink('user-manual/capabilities/labels.html') + href: webDocumentationLink('user-manual/agents/labels.html'), }, { text: 'Labels reference', - href: webDocumentationLink('user-manual/reference/ossec-conf/labels.html') - } + href: webDocumentationLink('user-manual/reference/ossec-conf/labels.html'), + }, ]; class WzConfigurationAlertsLabels extends Component { @@ -50,69 +50,32 @@ class WzConfigurationAlertsLabels extends Component { const { currentConfig, agent, wazuhNotReadyYet } = this.props; return ( - {currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] && - isString( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] - ) && ( + {currentConfig['agent-labels'] && + isString(currentConfig['agent-labels']) && ( )} - {currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] && - !isString( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] - ) && - !hasSize( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ].labels - ) && } + {currentConfig['agent-labels'] && + !isString(currentConfig['agent-labels']) && + !hasSize(currentConfig['agent-labels'].labels) && ( + + )} {wazuhNotReadyYet && - (!currentConfig || - !currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ]) && } - {currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] && - !isString( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] - ) && - hasSize( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ].labels - ) ? ( + (!currentConfig || !currentConfig['agent-labels']) && ( + + )} + {currentConfig['agent-labels'] && + !isString(currentConfig['agent-labels']) && + hasSize(currentConfig['agent-labels'].labels) ? ( ) : null} @@ -122,7 +85,7 @@ class WzConfigurationAlertsLabels extends Component { } const mapStateToProps = state => ({ - wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet + wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, }); export default connect(mapStateToProps)(WzConfigurationAlertsLabels); @@ -131,13 +94,13 @@ const sectionsAgent = [{ component: 'agent', configuration: 'labels' }]; export const WzConfigurationAlertsLabelsAgent = compose( connect(mapStateToProps), - withWzConfig(sectionsAgent) + withWzConfig(sectionsAgent), )(WzConfigurationAlertsLabels); WzConfigurationAlertsLabels.propTypes = { - wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) + wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; WzConfigurationAlertsLabelsAgent.propTypes = { - wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) + wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; diff --git a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js index 825c81fa06..126ca791d9 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js +++ b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js @@ -14,7 +14,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import WzTabSelector, { - WzTabSelectorTab + WzTabSelectorTab, } from '../util-components/tab-selector'; import withWzConfig from '../util-hocs/wz-config'; import WzConfigurationAlertsGeneral from './alerts-general'; @@ -34,19 +34,19 @@ class WzConfigurationAlerts extends Component { return ( - + - + - + - + - + @@ -57,21 +57,21 @@ class WzConfigurationAlerts extends Component { const sections = [ { component: 'analysis', configuration: 'alerts' }, - { component: 'analysis', configuration: 'labels' }, + { component: 'agent', configuration: 'labels' }, { component: 'mail', configuration: 'alerts' }, { component: 'monitor', configuration: 'reports' }, - { component: 'csyslog', configuration: 'csyslog' } + { component: 'csyslog', configuration: 'csyslog' }, ]; const mapStateToProps = state => ({ - wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet + wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, }); WzConfigurationAlerts.propTypes = { - wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) + wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; export default compose( withWzConfig(sections), - connect(mapStateToProps) + connect(mapStateToProps), )(WzConfigurationAlerts); diff --git a/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx b/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx index a612f347ed..31064c60fe 100644 --- a/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx +++ b/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx @@ -39,7 +39,6 @@ export default function CommandOutput(props: ICommandSectionProps) { setHavePassword(false); setCommandToShow(commandText); } - }, [password, commandText, showPassword]); const osdfucatePassword = (password: string) => { @@ -52,7 +51,7 @@ export default function CommandOutput(props: ICommandSectionProps) { setCommandToShow(osdfucatePasswordInCommand(password, commandText, os)); } }; - + const onChangeShowPassword = (event: EuiSwitchEvent) => { setShowPassword(event.target.checked); }; diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss index 55dd4092fa..636996765b 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss @@ -18,10 +18,6 @@ margin-right: 10px; } -.euiCard__content .euiCard__titleButton { - text-decoration: none !important; -} - .cardText { font-style: normal; font-weight: 700; diff --git a/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.tsx b/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.tsx index e5e1591c78..8ae23213cd 100644 --- a/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.tsx +++ b/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.tsx @@ -30,8 +30,6 @@ import { validateServerAddress, validateAgentName, } from '../../utils/validations'; -import { getPasswordWithScapedSpecialCharacters } from '../../services/wazuh-password-service'; - interface IRegisterAgentProps { getWazuhVersion: () => Promise; @@ -129,7 +127,6 @@ export const RegisterAgent = withReduxProvider( configuration['enrollment.password'] || authInfo['authd.pass'] || ''; - //wazuhPassword = getPasswordWithScapedSpecialCharacters(wazuhPassword); } const groups = await getGroups(); setNeedsPassword(needsPassword); diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.scss b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss index 337cc41298..17bdbef44c 100644 --- a/plugins/main/public/controllers/register-agent/containers/steps/steps.scss +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss @@ -6,50 +6,50 @@ letter-spacing: 0.6px; flex-direction: row; } -} -.stepSubtitleServerAddress { - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 24px; - margin-bottom: 9px; -} + .stepSubtitleServerAddress { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 9px; + } -.stepSubtitle { - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 24px; - margin-bottom: 20px; -} + .stepSubtitle { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 20px; + } -.titleAndIcon { - display: flex; - flex-direction: row; -} + .titleAndIcon { + display: flex; + flex-direction: row; + } -.warningForAgentName { - margin-top: 10px; -} + .warningForAgentName { + margin-top: 10px; + } -.euiToolTipAnchor { - margin-left: 7px; -} + .euiToolTipAnchor { + margin-left: 7px; + } -.subtitleAgentName { - flex-direction: 'row'; - font-style: 'normal'; - font-weight: 700; - font-size: '12px'; - line-height: '20px'; - color: '#343741'; -} + .subtitleAgentName { + flex-direction: 'row'; + font-style: 'normal'; + font-weight: 700; + font-size: '12px'; + line-height: '20px'; + color: '#343741'; + } -.euiStep__titleWrapper { - align-items: center; -} + .euiStep__titleWrapper { + align-items: center; + } -.euiButtonEmpty .euiButtonEmpty__content { - padding: 0; + .euiButtonEmpty .euiButtonEmpty__content { + padding: 0; + } } diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx index 653bc929d5..2c0dec80e2 100644 --- a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx @@ -114,7 +114,10 @@ export const Steps = ({ ) { selectOS(registerAgentFormValues.operatingSystem as tOperatingSystem); } - setOptionalParams({ ...registerAgentFormValues.optionalParams }, registerAgentFormValues.operatingSystem as tOperatingSystem); + setOptionalParams( + { ...registerAgentFormValues.optionalParams }, + registerAgentFormValues.operatingSystem as tOperatingSystem, + ); setInstallCommandWasCopied(false); setStartCommandWasCopied(false); }, [registerAgentFormValues]); diff --git a/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts b/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts index 55189d3956..b3a2ed3651 100644 --- a/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts +++ b/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts @@ -1,12 +1,17 @@ -import { +import { getDEBInstallCommand, getRPMInstallCommand, - getLinuxStartCommand, - getMacOsInstallCommand, - getMacosStartCommand, - getWindowsInstallCommand, - getWindowsStartCommand } from '../../services/register-agent-os-commands-services'; -import { scapeSpecialCharsForLinux, scapeSpecialCharsForMacOS, scapeSpecialCharsForWindows } from '../../services/wazuh-password-service'; + getLinuxStartCommand, + getMacOsInstallCommand, + getMacosStartCommand, + getWindowsInstallCommand, + getWindowsStartCommand, +} from '../../services/register-agent-os-commands-services'; +import { + scapeSpecialCharsForLinux, + scapeSpecialCharsForMacOS, + scapeSpecialCharsForWindows, +} from '../../services/wazuh-password-service'; import { IOSDefinition, tOptionalParams } from '../register-commands/types'; // Defined OS combinations @@ -97,7 +102,7 @@ const linuxDefinition: IOSDefinition = { { architecture: 'RPM aarch64', urlPackage: props => - `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.rpm`, + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.rpm`, installCommand: props => getRPMInstallCommand(props), startCommand: props => getLinuxStartCommand(props), }, @@ -150,21 +155,21 @@ export const osCommandsDefinitions = [ export const optionalParamsDefinitions: tOptionalParams = { serverAddress: { property: 'WAZUH_MANAGER', - getParamCommand: (props,selectedOS) => { + getParamCommand: (props, selectedOS) => { const { property, value } = props; return value !== '' ? `${property}='${value}'` : ''; }, }, agentName: { property: 'WAZUH_AGENT_NAME', - getParamCommand: (props,selectedOS) => { + getParamCommand: (props, selectedOS) => { const { property, value } = props; return value !== '' ? `${property}='${value}'` : ''; }, }, agentGroups: { property: 'WAZUH_AGENT_GROUP', - getParamCommand: (props,selectedOS) => { + getParamCommand: (props, selectedOS) => { const { property, value } = props; let parsedValue = value; if (Array.isArray(value)) { @@ -175,31 +180,30 @@ export const optionalParamsDefinitions: tOptionalParams = { }, protocol: { property: 'WAZUH_PROTOCOL', - getParamCommand: (props,selectedOS) => { + getParamCommand: (props, selectedOS) => { const { property, value } = props; return value !== '' ? `${property}='${value}'` : ''; }, }, wazuhPassword: { property: 'WAZUH_REGISTRATION_PASSWORD', - getParamCommand: (props,selectedOS) => { + getParamCommand: (props, selectedOS) => { const { property, value } = props; - if(!value){ + if (!value) { return ''; } - if(selectedOS){ + if (selectedOS) { let osName = selectedOS.name.toLocaleLowerCase(); - switch(osName){ - case "linux": + switch (osName) { + case 'linux': return `${property}=$'${scapeSpecialCharsForLinux(value)}'`; - case "macos": + case 'macos': return `${property}='${scapeSpecialCharsForMacOS(value)}'`; - case "windows": + case 'windows': return `${property}='${scapeSpecialCharsForWindows(value)}'`; default: return `${property}=$'${value}'`; } - } return value !== '' ? `${property}=$'${value}'` : ''; diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts b/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts index c858fc3908..3cec8b9772 100644 --- a/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts +++ b/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts @@ -14,10 +14,18 @@ import { validateOSDefinitionsDuplicated, } from '../services/search-os-definitions.service'; import { OptionalParametersManager } from '../optional-parameters-manager/optional-parameters-manager'; -import { NoArchitectureSelectedException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; +import { + NoArchitectureSelectedException, + NoOSSelectedException, + WazuhVersionUndefinedException, +} from '../exceptions'; import { version } from '../../../../../../package.json'; -export class CommandGenerator implements ICommandGenerator { +export class CommandGenerator< + OS extends IOperationSystem, + Params extends string, +> implements ICommandGenerator +{ os: OS['name'] | null = null; osDefinitionSelected: IOSCommandsDefinition | null = null; optionalsManager: IOptionalParametersManager; @@ -30,7 +38,7 @@ export class CommandGenerator, selectedOS?: OS): void { // Get all the optional parameters based on the given parameters - this.optionals = this.optionalsManager.getAllOptionalParams(props, selectedOS); + this.optionals = this.optionalsManager.getAllOptionalParams( + props, + selectedOS, + ); } /** @@ -97,7 +108,8 @@ export class CommandGenerator, @@ -133,7 +146,8 @@ export class CommandGenerator, }); @@ -152,7 +166,8 @@ export class CommandGenerator | object { return this.optionals; } - } diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/plugins/main/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts index 0328a1efd2..881894186a 100644 --- a/plugins/main/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts +++ b/plugins/main/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts @@ -1,8 +1,15 @@ -import { tOperatingSystem } from '../../config/os-commands-definitions'; import { NoOptionalParamFoundException } from '../exceptions'; -import { IOperationSystem, IOptionalParamInput, IOptionalParameters, IOptionalParametersManager, tOptionalParams } from '../types'; +import { + IOperationSystem, + IOptionalParamInput, + IOptionalParameters, + IOptionalParametersManager, + tOptionalParams, +} from '../types'; -export class OptionalParametersManager implements IOptionalParametersManager { +export class OptionalParametersManager + implements IOptionalParametersManager +{ constructor(private optionalParamsConfig: tOptionalParams) {} /** @@ -11,17 +18,22 @@ export class OptionalParametersManager implements IOption * @returns The command string for the given optional parameter. * @throws NoOptionalParamFoundException if the given optional parameter name is not found in the configuration. */ - getOptionalParam(props: IOptionalParamInput, selectedOS?: IOperationSystem) { + getOptionalParam( + props: IOptionalParamInput, + selectedOS?: IOperationSystem, + ) { const { value, name } = props; if (!this.optionalParamsConfig[name]) { throw new NoOptionalParamFoundException(name); } - return this.optionalParamsConfig[name].getParamCommand({ + return this.optionalParamsConfig[name].getParamCommand( + { value, property: this.optionalParamsConfig[name].property, - name - }, - selectedOS); + name, + }, + selectedOS, + ); } /** @@ -30,22 +42,30 @@ export class OptionalParametersManager implements IOption * @returns An object containing the command strings for all optional parameters with non-empty values. * @throws NoOptionalParamFoundException if any of the given optional parameter names is not found in the configuration. */ - getAllOptionalParams(paramsValues: IOptionalParameters, selectedOS: IOperationSystem){ - // get keys for only the optional params with values !== '' - const optionalParams = Object.keys(paramsValues).filter(key => paramsValues[key as keyof typeof paramsValues] !== '') as Array; - const resolvedOptionalParams: any = {}; - for(const param of optionalParams){ - if(!this.optionalParamsConfig[param]){ - throw new NoOptionalParamFoundException(param as string); - } + getAllOptionalParams( + paramsValues: IOptionalParameters, + selectedOS: IOperationSystem, + ) { + // get keys for only the optional params with values !== '' + const optionalParams = Object.keys(paramsValues).filter( + key => paramsValues[key as keyof typeof paramsValues] !== '', + ) as Array; + const resolvedOptionalParams: any = {}; + for (const param of optionalParams) { + if (!this.optionalParamsConfig[param]) { + throw new NoOptionalParamFoundException(param as string); + } - const paramDef = this.optionalParamsConfig[param]; - resolvedOptionalParams[param as string] = paramDef.getParamCommand({ + const paramDef = this.optionalParamsConfig[param]; + resolvedOptionalParams[param as string] = paramDef.getParamCommand( + { name: param as Params, value: paramsValues[param] as string, - property: paramDef.property - }, selectedOS) as string; - } - return resolvedOptionalParams; + property: paramDef.property, + }, + selectedOS, + ) as string; } + return resolvedOptionalParams; + } } diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/types.ts b/plugins/main/public/controllers/register-agent/core/register-commands/types.ts index fc1a0a5638..c09e828a78 100644 --- a/plugins/main/public/controllers/register-agent/core/register-commands/types.ts +++ b/plugins/main/public/controllers/register-agent/core/register-commands/types.ts @@ -1,8 +1,5 @@ ///////////////////////////////////////////////////////// /// Domain - -import { tOperatingSystem } from "../../hooks/use-register-agent-commands.test"; - ///////////////////////////////////////////////////////// export interface IOperationSystem { name: string; @@ -17,20 +14,27 @@ export type IOptionalParameters = { /// Operating system commands definitions /////////////////////////////////////////////////////////////////// -export interface IOSDefinition { +export interface IOSDefinition< + OS extends IOperationSystem, + Params extends string, +> { name: OS['name']; - options: IOSCommandsDefinition[]; + options: IOSCommandsDefinition[]; } interface IOptionalParamsWithValues { - optionals?: IOptionalParameters + optionals?: IOptionalParameters; } +export type tOSEntryProps = IOSProps & + IOptionalParamsWithValues; +export type tOSEntryInstallCommand = + tOSEntryProps & { urlPackage: string }; -export type tOSEntryProps = IOSProps & IOptionalParamsWithValues; -export type tOSEntryInstallCommand = tOSEntryProps & { urlPackage: string }; - -export interface IOSCommandsDefinition { +export interface IOSCommandsDefinition< + OS extends IOperationSystem, + Param extends string, +> { architecture: OS['architecture']; urlPackage: (props: tOSEntryProps) => string; installCommand: (props: tOSEntryInstallCommand) => string; @@ -49,12 +53,16 @@ interface IOptionalParamProps { value: string; } -export type tOptionalParamsCommandProps = IOptionalParamProps & { - name: T; -}; +export type tOptionalParamsCommandProps = + IOptionalParamProps & { + name: T; + }; export interface IOptionsParamConfig { property: string; - getParamCommand: (props: tOptionalParamsCommandProps, selectedOS?: IOperationSystem) => string; + getParamCommand: ( + props: tOptionalParamsCommandProps, + selectedOS?: IOperationSystem, + ) => string; } export type tOptionalParams = { @@ -67,22 +75,32 @@ export interface IOptionalParamInput { } export interface IOptionalParametersManager { getOptionalParam(props: IOptionalParamInput): string; - getAllOptionalParams(paramsValues: IOptionalParameters, selectedOs?: IOperationSystem): object; + getAllOptionalParams( + paramsValues: IOptionalParameters, + selectedOs?: IOperationSystem, + ): object; } /////////////////////////////////////////////////////////////////// /// Command creator class /////////////////////////////////////////////////////////////////// -export type IOSInputs = IOperationSystem & IOptionalParameters; -export interface ICommandGenerator extends ICommandGeneratorMethods { +export type IOSInputs = IOperationSystem & + IOptionalParameters; +export interface ICommandGenerator< + OS extends IOperationSystem, + Params extends string, +> extends ICommandGeneratorMethods { osDefinitions: IOSDefinition[]; wazuhVersion: string; } export interface ICommandGeneratorMethods { selectOS(params: IOperationSystem): void; - addOptionalParams(props: IOptionalParameters, osSelected?: IOperationSystem): void; + addOptionalParams( + props: IOptionalParameters, + osSelected?: IOperationSystem, + ): void; getInstallCommand(): string; getStartCommand(): string; getUrlPackage(): string; diff --git a/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.ts b/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.ts index fb157c15cd..80c3db0530 100644 --- a/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.ts +++ b/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.ts @@ -8,33 +8,48 @@ import { } from '../core/register-commands/types'; import { version } from '../../../../package.json'; -interface IUseRegisterCommandsProps { +interface IUseRegisterCommandsProps< + OS extends IOperationSystem, + Params extends string, +> { osDefinitions: IOSDefinition[]; optionalParamsDefinitions: tOptionalParams; } -interface IUseRegisterCommandsOutput { +interface IUseRegisterCommandsOutput< + OS extends IOperationSystem, + Params extends string, +> { selectOS: (params: OS) => void; - setOptionalParams: (params: IOptionalParameters, selectedOS?: OS) => void; + setOptionalParams: ( + params: IOptionalParameters, + selectedOS?: OS, + ) => void; installCommand: string; startCommand: string; optionalParamsParsed: IOptionalParameters | {}; } - /** * Custom hook that generates install and start commands based on the selected OS and optional parameters. - * + * * @template T - The type of the selected OS. * @param {IUseRegisterCommandsProps} props - The properties to configure the command generator. * @returns {IUseRegisterCommandsOutput} - An object containing the generated commands and methods to update the selected OS and optional parameters. */ -export function useRegisterAgentCommands(props: IUseRegisterCommandsProps): IUseRegisterCommandsOutput { +export function useRegisterAgentCommands< + OS extends IOperationSystem, + Params extends string, +>( + props: IUseRegisterCommandsProps, +): IUseRegisterCommandsOutput { const { osDefinitions, optionalParamsDefinitions } = props; // command generator settings const wazuhVersion = version; - const osCommands: IOSDefinition[] = osDefinitions as IOSDefinition[]; - const optionalParams: tOptionalParams = optionalParamsDefinitions as tOptionalParams; + const osCommands: IOSDefinition[] = + osDefinitions as IOSDefinition[]; + const optionalParams: tOptionalParams = + optionalParamsDefinitions as tOptionalParams; const commandGenerator = new CommandGenerator( osCommands, optionalParams, @@ -43,13 +58,14 @@ export function useRegisterAgentCommands(null); const [optionalParamsValues, setOptionalParamsValues] = useState< - IOptionalParameters| {} + IOptionalParameters | {} + >({}); + const [optionalParamsParsed, setOptionalParamsParsed] = useState< + IOptionalParameters | {} >({}); - const [optionalParamsParsed, setOptionalParamsParsed] = useState | {}>({}); const [installCommand, setInstallCommand] = useState(''); const [startCommand, setStartCommand] = useState(''); - /** * Generates the install and start commands based on the selected OS and optional parameters. * If no OS is selected, the method returns early without generating any commands. @@ -62,23 +78,23 @@ export function useRegisterAgentCommands, osSelected + optionalParamsValues as IOptionalParameters, + osSelected, ); } const installCommand = commandGenerator.getInstallCommand(); const startCommand = commandGenerator.getStartCommand(); setInstallCommand(installCommand); setStartCommand(startCommand); - } + }; useEffect(() => { generateCommands(); }, [osSelected, optionalParamsValues]); - /** * Sets the selected OS for the command generator and updates the state variables accordingly. - * + * * @param {T} params - The selected OS to be set. * @returns {void} */ @@ -89,21 +105,24 @@ export function useRegisterAgentCommands, selectedOS?: OS): void => { - commandGenerator.addOptionalParams(params,selectedOS); + const setOptionalParams = ( + params: IOptionalParameters, + selectedOS?: OS, + ): void => { + commandGenerator.addOptionalParams(params, selectedOS); setOptionalParamsValues(params); setOptionalParamsParsed(commandGenerator.getOptionalParamsCommands()); }; - + return { selectOS, setOptionalParams, installCommand, startCommand, - optionalParamsParsed - } -}; + optionalParamsParsed, + }; +} diff --git a/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx b/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx index 8fdb6a96ec..c9a4d9e342 100644 --- a/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx @@ -28,32 +28,32 @@ const getAllOptionals = ( if (osName && osName.toLowerCase() === 'windows' && optionals.serverAddress) { // when os is windows we must to add wazuh registration server with server address paramsText = - paramsText + `WAZUH_REGISTRATION_SERVER=${optionals.serverAddress.replace('WAZUH_MANAGER=','')} `; + paramsText + + `WAZUH_REGISTRATION_SERVER=${optionals.serverAddress.replace( + 'WAZUH_MANAGER=', + '', + )} `; } return paramsText; }; const getAllOptionalsMacos = ( - optionals: IOptionalParameters + optionals: IOptionalParameters, ) => { // create paramNameOrderList, which is an array of the keys of optionals add interface const paramNameOrderList: (keyof IOptionalParameters)[] = ['serverAddress', 'agentGroups', 'agentName', 'protocol']; if (!optionals) return ''; - return Object.entries(paramNameOrderList).reduce( - (acc, [key, value]) => { - if (optionals[value]) { - acc += `${optionals[value]}\\n`; - } - return acc; - }, - '', - ); + return Object.entries(paramNameOrderList).reduce((acc, [key, value]) => { + if (optionals[value]) { + acc += `${optionals[value]}\\n`; + } + return acc; + }, ''); }; - /******* RPM *******/ // curl -o wazuh-agent-4.4.5-1.x86_64.rpm https://packages.wazuh.com/4.x/yum/wazuh-agent-4.4.5-1.x86_64.rpm && sudo WAZUH_MANAGER='172.30.30.20' rpm -ihv wazuh-agent-4.4.5-1.x86_64.rpm @@ -62,7 +62,7 @@ export const getRPMInstallCommand = ( props: tOSEntryInstallCommand, ) => { const { optionals, urlPackage, wazuhVersion } = props; - const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm` + const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm`; return `curl -o ${packageName} ${urlPackage} && sudo ${ optionals && getAllOptionals(optionals) }rpm -ihv ${packageName}`; @@ -76,7 +76,7 @@ export const getDEBInstallCommand = ( props: tOSEntryInstallCommand, ) => { const { optionals, urlPackage, wazuhVersion } = props; - const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb` + const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb`; return `wget ${urlPackage} && sudo ${ optionals && getAllOptionals(optionals) }dpkg -i ./${packageName}`; @@ -129,7 +129,7 @@ export const getMacOsInstallCommand = ( let wazuhPasswordParamWithValue = ''; if (optionals?.wazuhPassword) { /** - * We use the JSON.stringify to prevent that the scaped specials characters will be removed + * We use the JSON.stringify to prevent that the scaped specials characters will be removed * and mantain the format of the password The JSON.stringify mantain the password format but adds " to wrap the characters */ diff --git a/plugins/main/public/react-services/reporting.js b/plugins/main/public/react-services/reporting.js index b74ed090ff..4fa1dee29d 100644 --- a/plugins/main/public/react-services/reporting.js +++ b/plugins/main/public/react-services/reporting.js @@ -89,13 +89,15 @@ export class ReportingService { } const appliedFilters = await this.visHandlers.getAppliedFilters(syscollectorFilters); - + const dataplugin = await getDataPlugin(); + const serverSideQuery = dataplugin.query.getOpenSearchQuery(); const array = await this.vis2png.checkArray(visualizationIDList); const browserTimezone = moment.tz.guess(true); const data = { array, + serverSideQuery, // Used for applying the same filters on the server side requests filters: appliedFilters.filters, time: appliedFilters.time, searchBar: appliedFilters.searchBar, diff --git a/plugins/main/server/controllers/wazuh-reporting.ts b/plugins/main/server/controllers/wazuh-reporting.ts index 2b02bf4068..224c1daf0a 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -322,6 +322,7 @@ export class WazuhReportingCtrl { browserTimezone, searchBar, filters, + serverSideQuery, time, tables, section, @@ -375,7 +376,7 @@ export class WazuhReportingCtrl { apiId, new Date(from).getTime(), new Date(to).getTime(), - sanitizedFilters, + serverSideQuery, agentsFilter, indexPatternTitle, agents, @@ -1040,8 +1041,14 @@ export class WazuhReportingCtrl { `Report started`, 'info', ); - const { searchBar, filters, time, indexPatternTitle, apiId } = - request.body; + const { + searchBar, + filters, + time, + indexPatternTitle, + apiId, + serverSideQuery, + } = request.body; const { agentID } = request.params; const { from, to } = time || {}; // Init @@ -1274,6 +1281,15 @@ export class WazuhReportingCtrl { }; if (time) { + // Add Vulnerability Detector filter to the Server Side Query + serverSideQuery?.bool?.must?.push?.({ + match_phrase: { + 'rule.groups': { + query: 'vulnerability-detector', + }, + }, + }); + await extendedInformation( context, printer, @@ -1282,7 +1298,7 @@ export class WazuhReportingCtrl { apiId, from, to, - sanitizedFilters + ' AND rule.groups: "vulnerability-detector"', + serverSideQuery, agentsFilter, indexPatternTitle, agentID, diff --git a/plugins/main/server/lib/reporting/base-query.ts b/plugins/main/server/lib/reporting/base-query.ts index 09d1f35f50..7e67e541d8 100644 --- a/plugins/main/server/lib/reporting/base-query.ts +++ b/plugins/main/server/lib/reporting/base-query.ts @@ -9,45 +9,28 @@ * * Find more information about this on the LICENSE file. */ + +import { cloneDeep } from 'lodash'; + export function Base(pattern: string, filters: any, gte: number, lte: number, allowedAgentsFilter: any = null) { + const clonedFilter = cloneDeep(filters); + clonedFilter?.bool?.must?.push?.({ + range: { + timestamp: { + gte: gte, + lte: lte, + format: 'epoch_millis' + } + } + }); const base = { - // index: pattern, - from: 0, size: 500, aggs: {}, sort: [], script_fields: {}, - query: { - bool: { - must: [ - { - query_string: { - query: filters, - analyze_wildcard: true, - default_field: '*' - } - }, - { - range: { - timestamp: { - gte: gte, - lte: lte, - format: 'epoch_millis' - } - } - } - ], - must_not: [] - } - } + query: clonedFilter }; - //Add allowed agents filter - if(allowedAgentsFilter?.query?.bool){ - base.query.bool.minimum_should_match = allowedAgentsFilter.query.bool.minimum_should_match; - base.query.bool.should = allowedAgentsFilter.query.bool.should; - } - return base; } diff --git a/plugins/main/server/lib/reporting/extended-information.ts b/plugins/main/server/lib/reporting/extended-information.ts index a533abff0b..377ba9408c 100644 --- a/plugins/main/server/lib/reporting/extended-information.ts +++ b/plugins/main/server/lib/reporting/extended-information.ts @@ -24,7 +24,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; * @param {Array} ids ids of agents * @param {String} apiId API id */ - export async function buildAgentsTable(context, printer: ReportPrinter, agentIDs: string[], apiId: string, groupID: string = '') { +export async function buildAgentsTable(context, printer: ReportPrinter, agentIDs: string[], apiId: string, groupID: string = '') { const dateFormat = await context.core.uiSettings.client.get('dateFormat'); if ((!agentIDs || !agentIDs.length) && !groupID) return; log('reporting:buildAgentsTable', `${agentIDs.length} agents for API ${apiId}`, 'info'); @@ -32,7 +32,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; let agentsData = []; if (groupID) { let totalAgentsInGroup = null; - do{ + do { const { data: { data: { affected_items, total_affected_items } } } = await context.wazuh.api.client.asCurrentUser.request( 'GET', `/groups/${groupID}/agents`, @@ -46,7 +46,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; ); !totalAgentsInGroup && (totalAgentsInGroup = total_affected_items); agentsData = [...agentsData, ...affected_items]; - }while(agentsData.length < totalAgentsInGroup); + } while (agentsData.length < totalAgentsInGroup); } else { for (const agentID of agentIDs) { try { @@ -72,7 +72,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; } } - if(agentsData.length){ + if (agentsData.length) { // Print a table with agent/s information printer.addSimpleTable({ columns: [ @@ -96,7 +96,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; } }), }); - }else if(!agentsData.length && groupID){ + } else if (!agentsData.length && groupID) { // For group reports when there is no agents in the group printer.addContent({ text: 'There are no agents in this group.', @@ -135,12 +135,12 @@ export async function extendedInformation( filters, allowedAgentsFilter, pattern = getSettingDefaultValue('pattern'), - agent = null + agent = null, ) { try { log( 'reporting:extendedInformation', - `Section ${section} and tab ${tab}, API is ${apiId}. From ${from} to ${to}. Filters ${filters}. Index pattern ${pattern}`, + `Section ${section} and tab ${tab}, API is ${apiId}. From ${from} to ${to}. Filters ${JSON.stringify(filters)}. Index pattern ${pattern}`, 'info' ); if (section === 'agents' && !agent) { @@ -181,7 +181,7 @@ export async function extendedInformation( return count ? `${count} of ${totalAgents} agents have ${vulnerabilitiesLevel.toLocaleLowerCase()} vulnerabilities.` : undefined; - } catch (error) {} + } catch (error) { } }) ) ).filter((vulnerabilitiesResponse) => vulnerabilitiesResponse); diff --git a/plugins/main/server/lib/reporting/gdpr-request.ts b/plugins/main/server/lib/reporting/gdpr-request.ts index e058804be2..26fa191c99 100644 --- a/plugins/main/server/lib/reporting/gdpr-request.ts +++ b/plugins/main/server/lib/reporting/gdpr-request.ts @@ -28,10 +28,6 @@ export const topGDPRRequirements = async ( allowedAgentsFilter, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.gdpr: exists')) { - const [head, tail] = filters.split('AND rule.gdpr: exists'); - filters = head + tail; - }; try { const base = {}; @@ -50,12 +46,6 @@ export const topGDPRRequirements = async ( } }); - base.query.bool.must.push({ - exists: { - field: 'rule.gdpr' - } - }); - const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, body: base @@ -86,10 +76,6 @@ export const getRulesByRequirement = async ( requirement, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.gdpr: exists')) { - const [head, tail] = filters.split('AND rule.gdpr: exists'); - filters = head + tail; - }; try { const base = {}; @@ -119,8 +105,13 @@ export const getRulesByRequirement = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + ` AND rule.gdpr: "${requirement}"`; + base.query.bool.filter.push({ + match_phrase: { + 'rule.gdpr': { + query: requirement + } + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, diff --git a/plugins/main/server/lib/reporting/pci-request.ts b/plugins/main/server/lib/reporting/pci-request.ts index 811d615561..65a39755c2 100644 --- a/plugins/main/server/lib/reporting/pci-request.ts +++ b/plugins/main/server/lib/reporting/pci-request.ts @@ -28,9 +28,6 @@ export const topPCIRequirements = async ( allowedAgentsFilter, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.pci_dss: exists')) { - filters = filters.replace('AND rule.pci_dss: exists', ''); - }; try { const base = {}; @@ -49,12 +46,6 @@ export const topPCIRequirements = async ( } }); - base.query.bool.must.push({ - exists: { - field: 'rule.pci_dss' - } - }); - const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, body: base @@ -100,9 +91,6 @@ export const getRulesByRequirement = async ( requirement, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.pci_dss: exists')) { - filters = filters.replace('AND rule.pci_dss: exists', ''); - }; try { const base = {}; @@ -132,11 +120,13 @@ export const getRulesByRequirement = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND rule.pci_dss: "' + - requirement + - '"'; + base.query.bool.filter.push({ + match_phrase: { + 'rule.pci_dss': { + query: requirement + } + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, @@ -154,7 +144,7 @@ export const getRulesByRequirement = async ( ) { return accum; }; - accum.push({ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key}); + accum.push({ ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key }); return accum; }, []); } catch (error) { diff --git a/plugins/main/server/lib/reporting/rootcheck-request.ts b/plugins/main/server/lib/reporting/rootcheck-request.ts index 0eede80de9..8318bbc22a 100644 --- a/plugins/main/server/lib/reporting/rootcheck-request.ts +++ b/plugins/main/server/lib/reporting/rootcheck-request.ts @@ -46,9 +46,11 @@ export const top5RootkitsDetected = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND "rootkit" AND "detected"'; + base.query?.bool?.must?.push({ + query_string: { + query: '"rootkit" AND "detected"' + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, @@ -97,9 +99,11 @@ export const agentsWithHiddenPids = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND "process" AND "hidden"'; + base.query?.bool?.must?.push({ + query_string: { + query: '"process" AND "hidden"' + } + }); // "aggregations": { "1": { "value": 1 } } const response = await context.core.opensearch.client.asCurrentUser.search({ @@ -126,7 +130,7 @@ export const agentsWithHiddenPids = async ( * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability * @returns {Array} */ -export const agentsWithHiddenPorts = async( +export const agentsWithHiddenPorts = async ( context, gte, lte, @@ -147,8 +151,11 @@ export const agentsWithHiddenPorts = async( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + ' AND "port" AND "hidden"'; + base.query?.bool?.must?.push({ + query_string: { + query: '"port" AND "hidden"' + } + }); // "aggregations": { "1": { "value": 1 } } const response = await context.core.opensearch.client.asCurrentUser.search({ diff --git a/plugins/main/server/lib/reporting/tsc-request.ts b/plugins/main/server/lib/reporting/tsc-request.ts index aa59d6f6bc..2d03c804b8 100644 --- a/plugins/main/server/lib/reporting/tsc-request.ts +++ b/plugins/main/server/lib/reporting/tsc-request.ts @@ -12,14 +12,14 @@ import { Base } from './base-query'; import { getSettingDefaultValue } from '../../../common/services/settings'; - /** - * Returns top 5 TSC requirements - * @param {Number} context Endpoint context - * @param {Number} gte Timestamp (ms) from - * @param {Number} lte Timestamp (ms) to - * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability - * @returns {Array} - */ +/** + * Returns top 5 TSC requirements + * @param {Number} context Endpoint context + * @param {Number} gte Timestamp (ms) from + * @param {Number} lte Timestamp (ms) to + * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability + * @returns {Array} + */ export const topTSCRequirements = async ( context, gte, @@ -28,9 +28,6 @@ export const topTSCRequirements = async ( allowedAgentsFilter, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.tsc: exists')) { - filters = filters.replace('AND rule.tsc: exists', ''); - }; try { const base = {}; @@ -49,12 +46,6 @@ export const topTSCRequirements = async ( } }); - base.query.bool.must.push({ - exists: { - field: 'rule.tsc' - } - }); - const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, body: base @@ -100,9 +91,6 @@ export const getRulesByRequirement = async ( requirement, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.tsc: exists')) { - filters = filters.replace('AND rule.tsc: exists', ''); - }; try { const base = {}; @@ -132,11 +120,13 @@ export const getRulesByRequirement = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND rule.tsc: "' + - requirement + - '"'; + base.query.bool.filter.push({ + match_phrase: { + 'rule.tsc': { + query: requirement + } + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, @@ -155,7 +145,7 @@ export const getRulesByRequirement = async ( ) { return accum; }; - accum.push({ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key}); + accum.push({ ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key }); return accum; }, []); } catch (error) { diff --git a/plugins/main/server/routes/wazuh-reporting.test.ts b/plugins/main/server/routes/wazuh-reporting.test.ts index 034377cbeb..45a9482c24 100644 --- a/plugins/main/server/routes/wazuh-reporting.test.ts +++ b/plugins/main/server/routes/wazuh-reporting.test.ts @@ -10,20 +10,23 @@ import { WazuhReportingRoutes } from './wazuh-reporting'; import { WazuhUtilsCtrl } from '../controllers/wazuh-utils/wazuh-utils'; import md5 from 'md5'; import path from 'path'; -import { createDataDirectoryIfNotExists, createDirectoryIfNotExists } from '../lib/filesystem'; +import { + createDataDirectoryIfNotExists, + createDirectoryIfNotExists, +} from '../lib/filesystem'; import { WAZUH_DATA_CONFIG_APP_PATH, WAZUH_DATA_CONFIG_DIRECTORY_PATH, WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, WAZUH_DATA_LOGS_DIRECTORY_PATH, WAZUH_DATA_ABSOLUTE_PATH, - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH + WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, } from '../../common/constants'; import { execSync } from 'child_process'; import fs from 'fs'; jest.mock('../lib/reporting/extended-information', () => ({ - extendedInformation: jest.fn() + extendedInformation: jest.fn(), })); const USER_NAME = 'admin'; const loggingService = loggingSystemMock.create(); @@ -31,18 +34,19 @@ const logger = loggingService.get(); const context = { wazuh: { security: { - getCurrentUser: (request) => { + getCurrentUser: request => { // x-test-username header doesn't exist when the platform or plugin are running. // It is used to generate the output of this method so we can simulate the user // that does the request to the endpoint and is expected by the endpoint handlers // of the plugin. const username = request.headers['x-test-username']; - return { username, hashUsername: md5(username) } - } - } - } + return { username, hashUsername: md5(username) }; + }, + }, + }, }; -const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, context); +const enhanceWithContext = (fn: (...args: any[]) => any) => + fn.bind(null, context); let server, innerServer; // BEFORE ALL @@ -71,12 +75,24 @@ beforeAll(async () => { } as any; server = new HttpServer(loggingService, 'tests'); const router = new Router('', logger, enhanceWithContext); - const { registerRouter, server: innerServerTest, ...rest } = await server.setup(config); + const { + registerRouter, + server: innerServerTest, + ...rest + } = await server.setup(config); innerServer = innerServerTest; // Mock decorator - jest.spyOn(WazuhUtilsCtrl.prototype as any, 'routeDecoratorProtectedAdministratorRoleValidToken') - .mockImplementation((handler) => async (...args) => handler(...args)); + jest + .spyOn( + WazuhUtilsCtrl.prototype as any, + 'routeDecoratorProtectedAdministratorRoleValidToken', + ) + .mockImplementation( + handler => + async (...args) => + handler(...args), + ); // Register routes WazuhUtilsRoutes(router); @@ -124,11 +140,21 @@ describe('[endpoint] GET /reports', () => { // Create directories and file/s within directory. directories.forEach(({ username, files }) => { const hashUsername = md5(username); - createDirectoryIfNotExists(path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername)); + createDirectoryIfNotExists( + path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername), + ); if (files) { Array.from(Array(files).keys()).forEach(indexFile => { - console.log('Generating', username, indexFile) - fs.closeSync(fs.openSync(path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername, `report_${indexFile}.pdf`), 'w')); + fs.closeSync( + fs.openSync( + path.join( + WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, + hashUsername, + `report_${indexFile}.pdf`, + ), + 'w', + ), + ); }); } }); @@ -139,13 +165,16 @@ describe('[endpoint] GET /reports', () => { execSync(`rm -rf ${WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH}`); }); - it.each(directories)('get reports of $username. status response: $responseStatus', async ({ username, files }) => { - const response = await supertest(innerServer.listener) - .get(`/reports`) - .set('x-test-username', username) - .expect(200); - expect(response.body.reports).toHaveLength(files); - }); + it.each(directories)( + 'get reports of $username. status response: $responseStatus', + async ({ username, files }) => { + const response = await supertest(innerServer.listener) + .get(`/reports`) + .set('x-test-username', username) + .expect(200); + expect(response.body.reports).toHaveLength(files); + }, + ); }); describe('[endpoint] PUT /utils/configuration', () => { @@ -174,16 +203,33 @@ describe('[endpoint] PUT /utils/configuration', () => { // expectedMD5 variable is a verified md5 of a report generated with this header and footer // If any of the parameters is changed this variable should be updated with the new md5 it.each` - footer | header | responseStatusCode | expectedMD5 | tab - ${null} | ${null} | ${200} | ${'7b6fa0e2a5911880d17168800c173f89'} | ${'pm'} - ${'Custom\nFooter'} | ${'info@company.com\nFake Avenue 123'}| ${200} | ${'51b268066bb5107e5eb0a9d791a89d0c'} | ${'general'} - ${''} | ${''} | ${200} | ${'23d5e0eedce38dc6df9e98e898628f68'} | ${'fim'} - ${'Custom Footer'} | ${null} | ${200} | ${'2b16be2ea88d3891cda7acb6075826d9'} | ${'aws'} - ${null} | ${'Custom Header'} | ${200} | ${'91e30564f157942718afdd97db3b4ddf'} | ${'gcp'} -`(`Set custom report header and footer - Verify PDF output`, async ({footer, header, responseStatusCode, expectedMD5, tab}) => { - + footer | header | responseStatusCode | expectedMD5 | tab + ${null} | ${null} | ${200} | ${'a261be6b2e5fb18bb7434ee46a01e174'} | ${'pm'} + ${'Custom\nFooter'} | ${'info@company.com\nFake Avenue 123'} | ${200} | ${'51b268066bb5107e5eb0a9d791a89d0c'} | ${'general'} + ${''} | ${''} | ${200} | ${'8e8fbd90e08b810f700fcafbfdcdf638'} | ${'fim'} + ${'Custom Footer'} | ${null} | ${200} | ${'2b16be2ea88d3891cda7acb6075826d9'} | ${'aws'} + ${null} | ${'Custom Header'} | ${200} | ${'4a55136aaf8b5f6b544a03fe46917552'} | ${'gcp'} + `( + `Set custom report header and footer - Verify PDF output`, + async ({ footer, header, responseStatusCode, expectedMD5, tab }) => { // Mock PDF report parameters - const reportBody = { "array": [], "filters": [], "time": { "from": '2022-10-01T09:59:40.825Z', "to": '2022-10-04T09:59:40.825Z' }, "searchBar": "", "tables": [], "tab": tab, "section": "overview", "agents": false, "browserTimezone": "Europe/Madrid", "indexPatternTitle": "wazuh-alerts-*", "apiId": "default" }; + const reportBody = { + array: [], + serverSideQuery: [], + filters: [], + time: { + from: '2022-10-01T09:59:40.825Z', + to: '2022-10-04T09:59:40.825Z', + }, + searchBar: '', + tables: [], + tab: tab, + section: 'overview', + agents: false, + browserTimezone: 'Europe/Madrid', + indexPatternTitle: 'wazuh-alerts-*', + apiId: 'default', + }; // Define custom configuration const configurationBody = {}; @@ -203,10 +249,18 @@ describe('[endpoint] PUT /utils/configuration', () => { .expect(responseStatusCode); if (typeof footer == 'string') { - expect(responseConfig.body?.data?.updatedConfiguration?.['customization.reports.footer']).toMatch(configurationBody['customization.reports.footer']); + expect( + responseConfig.body?.data?.updatedConfiguration?.[ + 'customization.reports.footer' + ], + ).toMatch(configurationBody['customization.reports.footer']); } if (typeof header == 'string') { - expect(responseConfig.body?.data?.updatedConfiguration?.['customization.reports.header']).toMatch(configurationBody['customization.reports.header']); + expect( + responseConfig.body?.data?.updatedConfiguration?.[ + 'customization.reports.header' + ], + ).toMatch(configurationBody['customization.reports.header']); } } @@ -216,16 +270,19 @@ describe('[endpoint] PUT /utils/configuration', () => { .set('x-test-username', USER_NAME) .send(reportBody) .expect(200); - const fileName = responseReport.body?.message.match(/([A-Z-0-9]*\.pdf)/gi)[0]; + const fileName = + responseReport.body?.message.match(/([A-Z-0-9]*\.pdf)/gi)[0]; const userPath = md5(USER_NAME); const reportPath = `${WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH}/${userPath}/${fileName}`; const PDFbuffer = fs.readFileSync(reportPath); const PDFcontent = PDFbuffer.toString('utf8'); - const content = PDFcontent - .replace(/\[<[a-z0-9].+> <[a-z0-9].+>\]/gi, '') - .replace(/(obj\n\(D:[0-9].+Z\)\nendobj)/gi, ''); + const content = PDFcontent.replace( + /\[<[a-z0-9].+> <[a-z0-9].+>\]/gi, + '', + ).replace(/(obj\n\(D:[0-9].+Z\)\nendobj)/gi, ''); const PDFmd5 = md5(content); expect(PDFmd5).toBe(expectedMD5); - }); + }, + ); }); diff --git a/plugins/main/server/routes/wazuh-reporting.ts b/plugins/main/server/routes/wazuh-reporting.ts index 946e73ac5d..7f78a27458 100644 --- a/plugins/main/server/routes/wazuh-reporting.ts +++ b/plugins/main/server/routes/wazuh-reporting.ts @@ -55,30 +55,31 @@ export function WazuhReportingRoutes(router: IRouter) { ]); router.post({ - path: '/reports/modules/{moduleID}', - validate: { - body: schema.object({ - array: schema.any(), - browserTimezone: schema.string(), - filters: schema.maybe(schema.any()), - agents: schema.maybe(schema.oneOf([agentIDValidation, schema.boolean()])), - components: schema.maybe(schema.any()), - searchBar: schema.maybe(schema.string()), - section: schema.maybe(schema.string()), - tab: schema.string(), - tables: schema.maybe(schema.any()), - time: schema.oneOf([schema.object({ - from: schema.string(), - to: schema.string() - }), schema.string()]), - indexPatternTitle: schema.string(), - apiId: schema.string() - }), - params: schema.object({ - moduleID: moduleIDValidation - }) - } - }, + path: '/reports/modules/{moduleID}', + validate: { + body: schema.object({ + array: schema.any(), + browserTimezone: schema.string(), + serverSideQuery: schema.maybe(schema.any()), + filters: schema.maybe(schema.any()), + agents: schema.maybe(schema.oneOf([agentIDValidation, schema.boolean()])), + components: schema.maybe(schema.any()), + searchBar: schema.maybe(schema.string()), + section: schema.maybe(schema.string()), + tab: schema.string(), + tables: schema.maybe(schema.any()), + time: schema.oneOf([schema.object({ + from: schema.string(), + to: schema.string() + }), schema.string()]), + indexPatternTitle: schema.string(), + apiId: schema.string() + }), + params: schema.object({ + moduleID: moduleIDValidation + }) + } + }, (context, request, response) => ctrl.createReportsModules(context, request, response) ); @@ -124,6 +125,7 @@ export function WazuhReportingRoutes(router: IRouter) { body: schema.object({ array: schema.any(), browserTimezone: schema.string(), + serverSideQuery: schema.maybe(schema.any()), filters: schema.maybe(schema.any()), agents: schema.maybe(schema.oneOf([schema.string(), schema.boolean()])), components: schema.maybe(schema.any()), @@ -148,33 +150,33 @@ export function WazuhReportingRoutes(router: IRouter) { // Fetch specific report router.get({ - path: '/reports/{name}', - validate: { - params: schema.object({ - name: ReportFilenameValidation - }) - } - }, + path: '/reports/{name}', + validate: { + params: schema.object({ + name: ReportFilenameValidation + }) + } + }, (context, request, response) => ctrl.getReportByName(context, request, response) ); // Delete specific report router.delete({ - path: '/reports/{name}', - validate: { - params: schema.object({ - name: ReportFilenameValidation - }) - } - }, + path: '/reports/{name}', + validate: { + params: schema.object({ + name: ReportFilenameValidation + }) + } + }, (context, request, response) => ctrl.deleteReportByName(context, request, response) ) // Fetch the reports list router.get({ - path: '/reports', - validate: false - }, + path: '/reports', + validate: false + }, (context, request, response) => ctrl.getReports(context, request, response) ); }