From 4833f59ab330e718aa39203c0702f57018c2ca34 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Thu, 10 Aug 2023 17:12:46 +0200 Subject: [PATCH 1/6] [Redesign add agent] Fix custom Eui styles in register agent wizard (#5769) Add wrapper to custom eui styles --- .../os-selector/os-card/os-card.scss | 6 +- .../containers/steps/steps.scss | 95 ++++++++++--------- 2 files changed, 49 insertions(+), 52 deletions(-) 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..a71216b6a6 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; @@ -56,4 +52,4 @@ .cardsCallOut { margin-top: 16px; -} +} \ No newline at end of file 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..005ccb6379 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,51 @@ letter-spacing: 0.6px; flex-direction: row; } -} - -.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; -} - -.titleAndIcon { - display: flex; - flex-direction: row; -} - -.warningForAgentName { - margin-top: 10px; -} - -.euiToolTipAnchor { - margin-left: 7px; -} - -.subtitleAgentName { - flex-direction: 'row'; - font-style: 'normal'; - font-weight: 700; - font-size: '12px'; - line-height: '20px'; - color: '#343741'; -} - -.euiStep__titleWrapper { - align-items: center; -} - -.euiButtonEmpty .euiButtonEmpty__content { - padding: 0; -} + + + .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; + } + + .titleAndIcon { + display: flex; + flex-direction: row; + } + + .warningForAgentName { + margin-top: 10px; + } + + .euiToolTipAnchor { + margin-left: 7px; + } + + .subtitleAgentName { + flex-direction: 'row'; + font-style: 'normal'; + font-weight: 700; + font-size: '12px'; + line-height: '20px'; + color: '#343741'; + } + + .euiStep__titleWrapper { + align-items: center; + } + + .euiButtonEmpty .euiButtonEmpty__content { + padding: 0; + } +} \ No newline at end of file From afc2b3bd958f06327a6cb7368282c588dbb16261 Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:21:35 +0200 Subject: [PATCH 2/6] Merge 4.5.2 into 4.6.0 (#5775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- CHANGELOG.md | 4 + docker/imposter/agents/configuration.js | 15 ++ .../agents/configuration/agent_labels.json | 12 ++ .../agents/configuration/default.json | 33 ++++ .../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 +++++++------ .../controllers/agent/wazuh-config/index.ts | 28 +-- .../configuration/alerts/alerts-labels.js | 85 +++------ .../management/configuration/alerts/alerts.js | 22 +-- .../main/public/react-services/reporting.js | 4 +- .../server/controllers/wazuh-reporting.ts | 161 +++++++++--------- .../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 ++++----- 26 files changed, 590 insertions(+), 409 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae71b32b4..53d4eb1ee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,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 @@ -73,6 +76,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/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 028dd20d3f..faffab6698 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 0691b9fb38..9323cdbaf6 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 403b9153e1..74cb8e555d 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/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 d0a9b448ae..49db124470 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 @@ -27,18 +27,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 { @@ -49,71 +49,34 @@ 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} @@ -123,7 +86,7 @@ class WzConfigurationAlertsLabels extends Component { } const mapStateToProps = state => ({ - wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet + wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, }); export default connect(mapStateToProps)(WzConfigurationAlertsLabels); @@ -132,15 +95,15 @@ const sectionsAgent = [{ component: 'agent', configuration: 'labels' }]; export const WzConfigurationAlertsLabelsAgent = compose( connect(mapStateToProps), - withWzConfig(sectionsAgent) + withWzConfig(sectionsAgent), )(WzConfigurationAlertsLabels); WzConfigurationAlertsLabels.propTypes = { // currentConfig: PropTypes.object.isRequired, - wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) + wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; WzConfigurationAlertsLabelsAgent.propTypes = { // currentConfig: PropTypes.object.isRequired, - 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 704a38befc..c72e0b4cca 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,22 +57,22 @@ 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 = { // currentConfig: PropTypes.object.isRequired, - 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/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 51cd76ea6f..5a13636cd1 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -36,7 +36,7 @@ interface AgentsFilter { } export class WazuhReportingCtrl { - constructor() {} + constructor() { } /** * This do format to filters * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability @@ -70,22 +70,21 @@ export class WazuhReportingCtrl { const { negate, key, value, params, type } = filters[i].meta; str += `${negate ? 'NOT ' : ''}`; str += `${key}: `; - str += `${ - type === 'range' - ? `${params.gte}-${params.lt}` - : type === 'phrases' - ? '(' + params.join(" OR ") + ')' - : type === 'exists' - ? '*' - : !!value - ? value - : (params || {}).query - }`; + str += `${type === 'range' + ? `${params.gte}-${params.lt}` + : type === 'phrases' + ? '(' + params.join(" OR ") + ')' + : type === 'exists' + ? '*' + : !!value + ? value + : (params || {}).query + }`; str += `${i === len - 1 ? '' : ' AND '}`; } if (searchBar) { - str += ` AND (${ searchBar})`; + str += ` AND (${searchBar})`; } agentsFilter.agentsText = agentsList.map((filter) => filter.meta.value).join(','); @@ -211,8 +210,8 @@ export class WazuhReportingCtrl { plainData[key] = Array.isArray(data[key]) && typeof data[key][0] !== 'object' ? data[key].map((x) => { - return typeof x === 'object' ? JSON.stringify(x) : x + '\n'; - }) + return typeof x === 'object' ? JSON.stringify(x) : x + '\n'; + }) : data[key]; } else if (Array.isArray(data[key]) && typeof data[key][0] === 'object') { tableData[key] = data[key]; @@ -229,7 +228,7 @@ export class WazuhReportingCtrl { title: (section.options || {}).hideHeader ? '' : (section.tabs || [])[tab] || - (section.isGroupConfig ? ((section.labels || [])[0] || [])[tab] : ''), + (section.isGroupConfig ? ((section.labels || [])[0] || [])[tab] : ''), columns: ['', ''], type: 'config', rows: this.getConfigRows(plainData, (section.labels || [])[0]), @@ -247,10 +246,10 @@ export class WazuhReportingCtrl { typeof x[key] !== 'object' ? x[key] : Array.isArray(x[key]) - ? x[key].map((x) => { + ? x[key].map((x) => { return x + '\n'; }) - : JSON.stringify(x[key]) + : JSON.stringify(x[key]) ); } while (row.length < columns.length) { @@ -291,6 +290,7 @@ export class WazuhReportingCtrl { browserTimezone, searchBar, filters, + serverSideQuery, time, tables, section, @@ -327,7 +327,7 @@ export class WazuhReportingCtrl { apiId, new Date(from).getTime(), new Date(to).getTime(), - sanitizedFilters, + serverSideQuery, agentsFilter, indexPatternTitle, agents @@ -356,7 +356,7 @@ export class WazuhReportingCtrl { } catch (error) { return ErrorResponse(error.message || error, 5029, 500, response); } - },({body:{ agents }, params: { moduleID }}) => `wazuh-module-${agents ? `agents-${agents}` : 'overview'}-${moduleID}-${this.generateReportTimestamp()}.pdf`) + }, ({ body: { agents }, params: { moduleID } }) => `wazuh-module-${agents ? `agents-${agents}` : 'overview'}-${moduleID}-${this.generateReportTimestamp()}.pdf`) /** * Create a report for the groups @@ -365,7 +365,7 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsGroups = this.checkReportsUserDirectoryIsValidRouteDecorator(async( + createReportsGroups = this.checkReportsUserDirectoryIsValidRouteDecorator(async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory @@ -486,10 +486,10 @@ export class WazuhReportingCtrl { typeof x[key] !== 'object' ? x[key] : Array.isArray(x[key]) - ? x[key].map((x) => { + ? x[key].map((x) => { return x + '\n'; }) - : JSON.stringify(x[key]) + : JSON.stringify(x[key]) ); }); return row; @@ -613,7 +613,7 @@ export class WazuhReportingCtrl { log('reporting:createReportsGroups', error.message || error); return ErrorResponse(error.message || error, 5029, 500, response); } - }, ({params: { groupID }}) => `wazuh-group-configuration-${groupID}-${this.generateReportTimestamp()}.pdf`) + }, ({ params: { groupID } }) => `wazuh-group-configuration-${groupID}-${this.generateReportTimestamp()}.pdf`) /** * Create a report for the agents @@ -622,7 +622,7 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsAgentsConfiguration = this.checkReportsUserDirectoryIsValidRouteDecorator( async ( + createReportsAgentsConfiguration = this.checkReportsUserDirectoryIsValidRouteDecorator(async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory @@ -745,10 +745,10 @@ export class WazuhReportingCtrl { typeof x[key] !== 'object' ? x[key] : Array.isArray(x[key]) - ? x[key].map((x) => { + ? x[key].map((x) => { return x + '\n'; }) - : JSON.stringify(x[key]) + : JSON.stringify(x[key]) ); }); return row; @@ -775,13 +775,13 @@ export class WazuhReportingCtrl { } else { /*INTEGRITY MONITORING MONITORED DIRECTORIES */ if (conf.matrix) { - const {directories,diff,synchronization,file_limit,...rest} = agentConfig[agentConfigKey]; + const { directories, diff, synchronization, file_limit, ...rest } = agentConfig[agentConfigKey]; tables.push( ...this.getConfigTables(rest, section, idx), - ...(diff && diff.disk_quota ? this.getConfigTables(diff.disk_quota, {tabs:['Disk quota']}, 0 ): []), - ...(diff && diff.file_size ? this.getConfigTables(diff.file_size, {tabs:['File size']}, 0 ): []), - ...(synchronization ? this.getConfigTables(synchronization, {tabs:['Synchronization']}, 0 ): []), - ...(file_limit ? this.getConfigTables(file_limit, {tabs:['File limit']}, 0 ): []), + ...(diff && diff.disk_quota ? this.getConfigTables(diff.disk_quota, { tabs: ['Disk quota'] }, 0) : []), + ...(diff && diff.file_size ? this.getConfigTables(diff.file_size, { tabs: ['File size'] }, 0) : []), + ...(synchronization ? this.getConfigTables(synchronization, { tabs: ['Synchronization'] }, 0) : []), + ...(file_limit ? this.getConfigTables(file_limit, { tabs: ['File limit'] }, 0) : []), ); let diffOpts = []; Object.keys(section.opts).forEach((x) => { @@ -860,7 +860,7 @@ export class WazuhReportingCtrl { log('reporting:createReportsAgentsConfiguration', error.message || error); return ErrorResponse(error.message || error, 5029, 500, response); } - }, ({ params: { agentID }}) => `wazuh-agent-configuration-${agentID}-${this.generateReportTimestamp()}.pdf`) + }, ({ params: { agentID } }) => `wazuh-agent-configuration-${agentID}-${this.generateReportTimestamp()}.pdf`) /** * Create a report for the agents @@ -869,14 +869,14 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsAgentsInventory = this.checkReportsUserDirectoryIsValidRouteDecorator( async ( + createReportsAgentsInventory = this.checkReportsUserDirectoryIsValidRouteDecorator(async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory ) => { try { log('reporting:createReportsAgentsInventory', `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 @@ -924,18 +924,18 @@ export class WazuhReportingCtrl { columns: agentOs === 'windows' ? [ - { id: 'name', label: 'Name' }, - { id: 'architecture', label: 'Architecture' }, - { id: 'version', label: 'Version' }, - { id: 'vendor', label: 'Vendor' }, - ] + { id: 'name', label: 'Name' }, + { id: 'architecture', label: 'Architecture' }, + { id: 'version', label: 'Version' }, + { id: 'vendor', label: 'Vendor' }, + ] : [ - { id: 'name', label: 'Name' }, - { id: 'architecture', label: 'Architecture' }, - { id: 'version', label: 'Version' }, - { id: 'vendor', label: 'Vendor' }, - { id: 'description', label: 'Description' }, - ], + { id: 'name', label: 'Name' }, + { id: 'architecture', label: 'Architecture' }, + { id: 'version', label: 'Version' }, + { id: 'vendor', label: 'Vendor' }, + { id: 'description', label: 'Description' }, + ], }, }, { @@ -946,17 +946,17 @@ export class WazuhReportingCtrl { columns: agentOs === 'windows' ? [ - { id: 'name', label: 'Name' }, - { id: 'cmd', label: 'CMD' }, - { id: 'priority', label: 'Priority' }, - { id: 'nlwp', label: 'NLWP' }, - ] + { id: 'name', label: 'Name' }, + { id: 'cmd', label: 'CMD' }, + { id: 'priority', label: 'Priority' }, + { id: 'nlwp', label: 'NLWP' }, + ] : [ - { id: 'name', label: 'Name' }, - { id: 'euser', label: 'Effective user' }, - { id: 'nice', label: 'Priority' }, - { id: 'state', label: 'State' }, - ], + { id: 'name', label: 'Name' }, + { id: 'euser', label: 'Effective user' }, + { id: 'nice', label: 'Priority' }, + { id: 'state', label: 'State' }, + ], }, mapResponseItems: (item) => agentOs === 'windows' ? item : { ...item, state: ProcessEquivalence[item.state] }, @@ -969,18 +969,18 @@ export class WazuhReportingCtrl { columns: agentOs === 'windows' ? [ - { id: 'local_ip', label: 'Local IP address' }, - { id: 'local_port', label: 'Local port' }, - { id: 'process', label: 'Process' }, - { id: 'state', label: 'State' }, - { id: 'protocol', label: 'Protocol' }, - ] + { id: 'local_ip', label: 'Local IP address' }, + { id: 'local_port', label: 'Local port' }, + { id: 'process', label: 'Process' }, + { id: 'state', label: 'State' }, + { id: 'protocol', label: 'Protocol' }, + ] : [ - { id: 'local_ip', label: 'Local IP address' }, - { id: 'local_port', label: 'Local port' }, - { id: 'state', label: 'State' }, - { id: 'protocol', label: 'Protocol' }, - ], + { id: 'local_ip', label: 'Local IP address' }, + { id: 'local_port', label: 'Local port' }, + { id: 'state', label: 'State' }, + { id: 'protocol', label: 'Protocol' }, + ], }, mapResponseItems: (item) => ({ ...item, @@ -1062,6 +1062,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, @@ -1070,7 +1079,7 @@ export class WazuhReportingCtrl { apiId, from, to, - sanitizedFilters + ' AND rule.groups: "vulnerability-detector"', + serverSideQuery, agentsFilter, indexPatternTitle, agentID @@ -1095,7 +1104,7 @@ export class WazuhReportingCtrl { log('reporting:createReportsAgents', error.message || error); return ErrorResponse(error.message || error, 5029, 500, response); } - }, ({params: { agentID }}) => `wazuh-agent-inventory-${agentID}-${this.generateReportTimestamp()}.pdf`) + }, ({ params: { agentID } }) => `wazuh-agent-inventory-${agentID}-${this.generateReportTimestamp()}.pdf`) /** * Fetch the reports list @@ -1194,21 +1203,21 @@ export class WazuhReportingCtrl { log('reporting:deleteReportByName', error.message || error); return ErrorResponse(error.message || error, 5032, 500, response); } - },(request) => request.params.name) + }, (request) => request.params.name) - checkReportsUserDirectoryIsValidRouteDecorator(routeHandler, reportFileNameAccessor){ + checkReportsUserDirectoryIsValidRouteDecorator(routeHandler, reportFileNameAccessor) { return (async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory ) => { - try{ + try { const { username, hashUsername } = await context.wazuh.security.getCurrentUser(request, context); const userReportsDirectoryPath = path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername); const filename = reportFileNameAccessor(request); const pathFilename = path.join(userReportsDirectoryPath, filename); log('reporting:checkReportsUserDirectoryIsValidRouteDecorator', `Checking the user ${username}(${hashUsername}) can do actions in the reports file: ${pathFilename}`, 'debug'); - if(!pathFilename.startsWith(userReportsDirectoryPath) || pathFilename.includes('../')){ + if (!pathFilename.startsWith(userReportsDirectoryPath) || pathFilename.includes('../')) { log('security:reporting:checkReportsUserDirectoryIsValidRouteDecorator', `User ${username}(${hashUsername}) tried to access to a non user report file: ${pathFilename}`, 'warn'); return response.badRequest({ body: { @@ -1217,15 +1226,15 @@ export class WazuhReportingCtrl { }); }; log('reporting:checkReportsUserDirectoryIsValidRouteDecorator', 'Checking the user can do actions in the reports file', 'debug'); - return await routeHandler.bind(this)({...context, wazuhEndpointParams: { hashUsername, filename, pathFilename }}, request, response); - }catch(error){ + return await routeHandler.bind(this)({ ...context, wazuhEndpointParams: { hashUsername, filename, pathFilename } }, request, response); + } catch (error) { log('reporting:checkReportsUserDirectoryIsValidRouteDecorator', error.message || error); return ErrorResponse(error.message || error, 5040, 500, response); } }) } - private generateReportTimestamp(){ + private generateReportTimestamp() { return `${(Date.now() / 1000) | 0}`; } } 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) ); } From f987351304eae552577fe3c25e8fed200ebd4057 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:22:53 +0200 Subject: [PATCH 3/6] Replace search bar on TableWzAPI component (#5442) * 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): replace search bar in TableWzAPI Replace search bar in TableWzAPI Replace each usage of TableWzAPI Adapt external filters * feat(vulnerabilities): change filter by severity tooltip * fix(test): update test and snapthost * fix(test): update snapshot * feat(table-wz-api): add field selection to the TableWzAPI component Add field selection to the TableWzAPI Possibility to save the selected fields on storage (localStorage, sessionStorage) Create useStateStorage that allows save the state in localStorage or sessionStorage * 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 * feat(table-wz-api): Adapt TableWzAPI usage to the recent changes when using the search bar Enhance Management/Decoders table * fix(test): fixed test and update snapshot * fix(table-wz-api): minor fixes on TableWzAPI usage * fix: fixed prop type * fix: fix search term field on TableWzAPI for composed column * fix(table-wz-api): enhance TableWithSearchBar types and fix error HTML attributes * fix: enhance search bar and WQL types * fix: test snapshot * fix: remove duplicated search bar * feat: add the distinct values for the search bar suggestions in some sections - Add the distinct values for the search bar suggestions in some sections: - Modules > Security Configuration Assessment policy checks table - Modules > Vulnerabilities > Inventory table - Modules > MITRE ATT&CK > Inventory table - Management > Rules table - Management > Decoders table - Management > CDB Lists table - Add Path column to Rules files table - Add Path column to Decoders files table * fix: remove exact validation for the token value due to performance problems * fix: fix token value validation * fix: fix Management > Rules search bar filters Add id field Fix groups filter in Rule info flyout Remove onFiltersChange handler * 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): update test snapshot * fix(table-wz-api): avoid the double toast message when there is an error fetching data and replace the error.name to RequestError * fix(search-bar): add validation for value token in WQL * fix(search-bar): value token in message related to this is invalid * fix(search-bar): fix error related to details.program_name suggestion in the Decoders section * feat(search-bar): add constant to define the count of distinct values to get in the suggestions * fix(search-bar): fix value suggestions in the Decoders section * fix: add comment to constant * workaround(search-bar): add a filter to the value suggestions in WQL When getting the distinct values for some fields, the value could not match the regular expression that validates them, and this causes that the search can not be run. So, we filters the distinct values to ensure or reduce the suggestions can be used to search. This causes some possible values are not displayed in the suggestions. To undone this, then the API should allow these values. * changelog: add entry * changelog: add entry * fix(wql): add whitespace before closing grouping operator ) when using the suggestions * feat(search-bar): add a debounce time to update the search bar state * fix(search-bar): fix prop type error related to EuiSuggestItem * fix(search-bar-wql): problem related to execute the search before the input is analyzed due to this process is debounced * fix(search-bar-wql): remove unnued parameter in function * fix(search-bar): fix tests * fix(search-bar): suggestions in Modules > Vulenerabilities > Inventory --- CHANGELOG.md | 1 + plugins/main/common/constants.ts | 1847 ++++++++++------- .../components/agents/sca/inventory.tsx | 6 +- .../sca/inventory/agent-policies-table.tsx | 12 +- .../agents/sca/inventory/checks-table.tsx | 230 +- .../agents/sca/inventory/lib/api-request.ts | 23 +- .../components/agents/vuls/inventory.tsx | 15 +- .../agents/vuls/inventory/lib/api-requests.ts | 39 +- .../agents/vuls/inventory/table.tsx | 194 +- .../public/components/common/hooks/index.ts | 1 + .../common/hooks/use-state-storage.ts | 30 + .../table-with-search-bar.test.tsx.snap | 651 ++++-- .../tables/components/export-table-csv.tsx | 4 +- .../common/tables/table-default.tsx | 4 +- .../tables/table-with-search-bar.test.tsx | 15 + .../common/tables/table-with-search-bar.tsx | 126 +- .../components/common/tables/table-wz-api.tsx | 197 +- .../__snapshots__/intelligence.test.tsx.snap | 61 +- .../intelligence.tsx | 33 +- .../mitre_attack_intelligence/resource.tsx | 14 +- .../mitre_attack_intelligence/resources.tsx | 130 +- .../public/components/search-bar/index.tsx | 140 +- .../search-bar/query-language/aql.test.tsx | 218 +- .../search-bar/query-language/aql.tsx | 244 ++- .../search-bar/query-language/wql.test.tsx | 6 +- .../search-bar/query-language/wql.tsx | 110 +- .../cdblists/components/cdblists-table.tsx | 136 +- .../decoders/components/columns.tsx | 141 +- .../components/decoders-suggestions.ts | 161 +- .../decoders/components/decoders-table.tsx | 180 +- .../decoders/views/decoder-info.tsx | 6 +- .../management/ruleset/components/columns.tsx | 184 +- .../ruleset/components/ruleset-suggestions.ts | 321 +-- .../ruleset/components/ruleset-table.tsx | 188 +- .../management/ruleset/views/rule-info.tsx | 330 ++- 35 files changed, 3710 insertions(+), 2288 deletions(-) create mode 100644 plugins/main/public/components/common/hooks/use-state-storage.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d4eb1ee5..da4ce9c9ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) +- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}) [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) ### Fixed diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 74cb8e555d..3917f85354 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -24,10 +24,10 @@ export const WAZUH_ALERTS_PREFIX = 'wazuh-alerts-'; export const WAZUH_ALERTS_PATTERN = 'wazuh-alerts-*'; // Job - Wazuh monitoring -export const WAZUH_INDEX_TYPE_MONITORING = "monitoring"; -export const WAZUH_MONITORING_PREFIX = "wazuh-monitoring-"; -export const WAZUH_MONITORING_PATTERN = "wazuh-monitoring-*"; -export const WAZUH_MONITORING_TEMPLATE_NAME = "wazuh-agent"; +export const WAZUH_INDEX_TYPE_MONITORING = 'monitoring'; +export const WAZUH_MONITORING_PREFIX = 'wazuh-monitoring-'; +export const WAZUH_MONITORING_PATTERN = 'wazuh-monitoring-*'; +export const WAZUH_MONITORING_TEMPLATE_NAME = 'wazuh-agent'; export const WAZUH_MONITORING_DEFAULT_INDICES_SHARDS = 1; export const WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS = 0; export const WAZUH_MONITORING_DEFAULT_CREATION = 'w'; @@ -36,9 +36,9 @@ export const WAZUH_MONITORING_DEFAULT_FREQUENCY = 900; export const WAZUH_MONITORING_DEFAULT_CRON_FREQ = '0 * * * * *'; // Job - Wazuh statistics -export const WAZUH_INDEX_TYPE_STATISTICS = "statistics"; -export const WAZUH_STATISTICS_DEFAULT_PREFIX = "wazuh"; -export const WAZUH_STATISTICS_DEFAULT_NAME = "statistics"; +export const WAZUH_INDEX_TYPE_STATISTICS = 'statistics'; +export const WAZUH_STATISTICS_DEFAULT_PREFIX = 'wazuh'; +export const WAZUH_STATISTICS_DEFAULT_NAME = 'statistics'; export const WAZUH_STATISTICS_PATTERN = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}-*`; export const WAZUH_STATISTICS_TEMPLATE_NAME = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}`; export const WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS = 1; @@ -60,7 +60,8 @@ export const WAZUH_SAMPLE_ALERT_PREFIX = 'wazuh-alerts-4.x-'; export const WAZUH_SAMPLE_ALERTS_INDEX_SHARDS = 1; export const WAZUH_SAMPLE_ALERTS_INDEX_REPLICAS = 0; export const WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY = 'security'; -export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = 'auditing-policy-monitoring'; +export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = + 'auditing-policy-monitoring'; export const WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION = 'threat-detection'; export const WAZUH_SAMPLE_ALERTS_DEFAULT_NUMBER_ALERTS = 3000; export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { @@ -74,7 +75,7 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { { apache: true, alerts: 2000 }, { web: true }, { windows: { service_control_manager: true }, alerts: 1000 }, - { github: true } + { github: true }, ], [WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING]: [ { rootcheck: true }, @@ -92,7 +93,8 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { }; // Security -export const WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY = 'OpenSearch Dashboards Security'; +export const WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY = + 'OpenSearch Dashboards Security'; export const WAZUH_SECURITY_PLUGINS = [ WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY, @@ -103,40 +105,49 @@ export const WAZUH_CONFIGURATION_CACHE_TIME = 10000; // time in ms; // Reserved ids for Users/Role mapping export const WAZUH_API_RESERVED_ID_LOWER_THAN = 100; -export const WAZUH_API_RESERVED_WUI_SECURITY_RULES = [ - 1, - 2 -]; +export const WAZUH_API_RESERVED_WUI_SECURITY_RULES = [1, 2]; // Wazuh data path const WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH = 'data'; export const WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH = path.join( __dirname, '../../../', - WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH + WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH, +); +export const WAZUH_DATA_ABSOLUTE_PATH = path.join( + WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, + 'wazuh', ); -export const WAZUH_DATA_ABSOLUTE_PATH = path.join(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, 'wazuh'); // Wazuh data path - config -export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'config'); -export const WAZUH_DATA_CONFIG_APP_PATH = path.join(WAZUH_DATA_CONFIG_DIRECTORY_PATH, 'wazuh.yml'); +export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'config', +); +export const WAZUH_DATA_CONFIG_APP_PATH = path.join( + WAZUH_DATA_CONFIG_DIRECTORY_PATH, + 'wazuh.yml', +); export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( WAZUH_DATA_CONFIG_DIRECTORY_PATH, - 'wazuh-registry.json' + 'wazuh-registry.json', ); // Wazuh data path - logs export const MAX_MB_LOG_FILES = 100; -export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'logs'); +export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'logs', +); export const WAZUH_DATA_LOGS_PLAIN_FILENAME = 'wazuhapp-plain.log'; export const WAZUH_DATA_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_DATA_LOGS_PLAIN_FILENAME + WAZUH_DATA_LOGS_PLAIN_FILENAME, ); export const WAZUH_DATA_LOGS_RAW_FILENAME = 'wazuhapp.log'; export const WAZUH_DATA_LOGS_RAW_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_DATA_LOGS_RAW_FILENAME + WAZUH_DATA_LOGS_RAW_FILENAME, ); // Wazuh data path - UI logs @@ -144,15 +155,21 @@ export const WAZUH_UI_LOGS_PLAIN_FILENAME = 'wazuh-ui-plain.log'; export const WAZUH_UI_LOGS_RAW_FILENAME = 'wazuh-ui.log'; export const WAZUH_UI_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_UI_LOGS_PLAIN_FILENAME + WAZUH_UI_LOGS_PLAIN_FILENAME, +); +export const WAZUH_UI_LOGS_RAW_PATH = path.join( + WAZUH_DATA_LOGS_DIRECTORY_PATH, + WAZUH_UI_LOGS_RAW_FILENAME, ); -export const WAZUH_UI_LOGS_RAW_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, WAZUH_UI_LOGS_RAW_FILENAME); // Wazuh data path - downloads -export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'downloads'); +export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'downloads', +); export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join( WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - 'reports' + 'reports', ); // Queue @@ -191,8 +208,8 @@ export enum WAZUH_MODULES_ID { CIS_CAT = 'ciscat', VIRUSTOTAL = 'virustotal', GDPR = 'gdpr', - GITHUB = 'github' -}; + GITHUB = 'github', +} export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { MANAGEMENT = 'management', @@ -209,19 +226,19 @@ export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { LOGS = 'logs', REPORTING = 'reporting', STATISTICS = 'statistics', -}; +} export enum WAZUH_MENU_TOOLS_SECTIONS_ID { API_CONSOLE = 'devTools', RULESET_TEST = 'logtest', -}; +} export enum WAZUH_MENU_SECURITY_SECTIONS_ID { USERS = 'users', ROLES = 'roles', POLICIES = 'policies', ROLES_MAPPING = 'roleMapping', -}; +} export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { SETTINGS = 'settings', @@ -232,13 +249,14 @@ export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { LOGS = 'logs', MISCELLANEOUS = 'miscellaneous', ABOUT = 'about', -}; +} export const AUTHORIZED_AGENTS = 'authorized-agents'; // Wazuh links export const WAZUH_LINK_GITHUB = 'https://github.com/wazuh'; -export const WAZUH_LINK_GOOGLE_GROUPS = 'https://groups.google.com/forum/#!forum/wazuh'; +export const WAZUH_LINK_GOOGLE_GROUPS = + 'https://groups.google.com/forum/#!forum/wazuh'; export const WAZUH_LINK_SLACK = 'https://wazuh.com/community/join-us-on-slack'; export const HEALTH_CHECK = 'health-check'; @@ -252,7 +270,8 @@ export const WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER = { from: 'now-24h', to: 'now', }; -export const PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER = 'timepicker:timeDefaults'; +export const PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER = + 'timepicker:timeDefaults'; // Default maxBuckets set by the app export const WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS = 200000; @@ -280,24 +299,30 @@ export const ASSETS_BASE_URL_PREFIX = '/plugins/wazuh/assets/'; 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_LOGO_IMAGE_ASSETS_RELATIVE_PATH = + 'images/logo_reports.png'; export const REPORTS_PRIMARY_COLOR = '#256BD1'; export const REPORTS_PAGE_FOOTER_TEXT = 'Copyright © 2023 Wazuh, Inc.'; export const REPORTS_PAGE_HEADER_TEXT = 'info@wazuh.com\nhttps://wazuh.com'; // Plugin platform export const PLUGIN_PLATFORM_NAME = 'Wazuh dashboard'; -export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = '/usr/share/wazuh-dashboard/data/wazuh/'; +export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = + '/usr/share/wazuh-dashboard/data/wazuh/'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'wazuh-dashboard'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = 'upgrade-guide'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = 'user-manual/wazuh-dashboard/troubleshooting.html'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = 'user-manual/wazuh-dashboard/config-file.html'; -export const PLUGIN_PLATFORM_URL_GUIDE = 'https://opensearch.org/docs/1.2/opensearch/index/'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = + 'upgrade-guide'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = + 'user-manual/wazuh-dashboard/troubleshooting.html'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = + 'user-manual/wazuh-dashboard/config-file.html'; +export const PLUGIN_PLATFORM_URL_GUIDE = + 'https://opensearch.org/docs/1.2/opensearch/index/'; export const PLUGIN_PLATFORM_URL_GUIDE_TITLE = 'OpenSearch guide'; export const PLUGIN_PLATFORM_REQUEST_HEADERS = { - 'osd-xsrf': 'kibana' + 'osd-xsrf': 'kibana', }; // Plugin app @@ -316,7 +341,7 @@ export const UI_COLOR_AGENT_STATUS = { [API_NAME_AGENT_STATUS.DISCONNECTED]: '#BD271E', [API_NAME_AGENT_STATUS.PENDING]: '#FEC514', [API_NAME_AGENT_STATUS.NEVER_CONNECTED]: '#646A77', - default: '#000000' + default: '#000000', } as const; export const UI_LABEL_NAME_AGENT_STATUS = { @@ -324,23 +349,23 @@ export const UI_LABEL_NAME_AGENT_STATUS = { [API_NAME_AGENT_STATUS.DISCONNECTED]: 'Disconnected', [API_NAME_AGENT_STATUS.PENDING]: 'Pending', [API_NAME_AGENT_STATUS.NEVER_CONNECTED]: 'Never connected', - default: 'Unknown' + default: 'Unknown', } as const; export const UI_ORDER_AGENT_STATUS = [ API_NAME_AGENT_STATUS.ACTIVE, API_NAME_AGENT_STATUS.DISCONNECTED, API_NAME_AGENT_STATUS.PENDING, - API_NAME_AGENT_STATUS.NEVER_CONNECTED -] + API_NAME_AGENT_STATUS.NEVER_CONNECTED, +]; export const AGENT_SYNCED_STATUS = { SYNCED: 'synced', NOT_SYNCED: 'not synced', -} +}; // Documentation -export const DOCUMENTATION_WEB_BASE_URL = "https://documentation.wazuh.com"; +export const DOCUMENTATION_WEB_BASE_URL = 'https://documentation.wazuh.com'; // Default Elasticsearch user name context export const ELASTIC_NAME = 'elastic'; @@ -360,62 +385,62 @@ export enum SettingCategory { STATISTICS, SECURITY, CUSTOMIZATION, -}; +} type TPluginSettingOptionsTextArea = { - maxRows?: number - minRows?: number - maxLength?: number + maxRows?: number; + minRows?: number; + maxLength?: number; }; type TPluginSettingOptionsSelect = { - select: { text: string, value: any }[] + select: { text: string; value: any }[]; }; type TPluginSettingOptionsEditor = { - editor: { - language: string - } + editor: { + language: string; + }; }; type TPluginSettingOptionsFile = { - file: { - type: 'image' - extensions?: string[] - size?: { - maxBytes?: number - minBytes?: number - } - recommended?: { - dimensions?: { - width: number, - height: number, - unit: string - } - } - store?: { - relativePathFileSystem: string - filename: string - resolveStaticURL: (filename: string) => string - } - } + file: { + type: 'image'; + extensions?: string[]; + size?: { + maxBytes?: number; + minBytes?: number; + }; + recommended?: { + dimensions?: { + width: number; + height: number; + unit: string; + }; + }; + store?: { + relativePathFileSystem: string; + filename: string; + resolveStaticURL: (filename: string) => string; + }; + }; }; type TPluginSettingOptionsNumber = { number: { - min?: number - max?: number - integer?: boolean - } + min?: number; + max?: number; + integer?: boolean; + }; }; type TPluginSettingOptionsSwitch = { switch: { values: { - disabled: { label?: string, value: any }, - enabled: { label?: string, value: any }, - } - } + disabled: { label?: string; value: any }; + enabled: { label?: string; value: any }; + }; + }; }; export enum EpluginSettingType { @@ -425,61 +450,63 @@ export enum EpluginSettingType { number = 'number', editor = 'editor', select = 'select', - filepicker = 'filepicker' -}; + filepicker = 'filepicker', +} export type TPluginSetting = { // Define the text displayed in the UI. - title: string + title: string; // Description. - description: string + description: string; // Category. - category: SettingCategory + category: SettingCategory; // Type. - type: EpluginSettingType + type: EpluginSettingType; // Default value. - defaultValue: any + defaultValue: any; // Default value if it is not set. It has preference over `default`. - defaultValueIfNotSet?: any + defaultValueIfNotSet?: any; // Configurable from the configuration file. - isConfigurableFromFile: boolean + isConfigurableFromFile: boolean; // Configurable from the UI (Settings/Configuration). - isConfigurableFromUI: boolean + isConfigurableFromUI: boolean; // Modify the setting requires running the plugin health check (frontend). - requiresRunningHealthCheck?: boolean + requiresRunningHealthCheck?: boolean; // Modify the setting requires reloading the browser tab (frontend). - requiresReloadingBrowserTab?: boolean + requiresReloadingBrowserTab?: boolean; // Modify the setting requires restarting the plugin platform to take effect. - requiresRestartingPluginPlatform?: boolean + requiresRestartingPluginPlatform?: boolean; // Define options related to the `type`. options?: - TPluginSettingOptionsEditor | - TPluginSettingOptionsFile | - TPluginSettingOptionsNumber | - TPluginSettingOptionsSelect | - TPluginSettingOptionsSwitch | - TPluginSettingOptionsTextArea + | TPluginSettingOptionsEditor + | TPluginSettingOptionsFile + | TPluginSettingOptionsNumber + | TPluginSettingOptionsSelect + | TPluginSettingOptionsSwitch + | TPluginSettingOptionsTextArea; // Transform the input value. The result is saved in the form global state of Settings/Configuration - uiFormTransformChangedInputValue?: (value: any) => any + uiFormTransformChangedInputValue?: (value: any) => any; // Transform the configuration value or default as initial value for the input in Settings/Configuration - uiFormTransformConfigurationValueToInputValue?: (value: any) => any + uiFormTransformConfigurationValueToInputValue?: (value: any) => any; // Transform the input value changed in the form of Settings/Configuration and returned in the `changed` property of the hook useForm - uiFormTransformInputValueToConfigurationValue?: (value: any) => any + uiFormTransformInputValueToConfigurationValue?: (value: any) => any; // Validate the value in the form of Settings/Configuration. It returns a string if there is some validation error. - validate?: (value: any) => string | undefined - // Validate function creator to validate the setting in the backend. It uses `schema` of the `@kbn/config-schema` package. - validateBackend?: (schema: any) => (value: unknown) => string | undefined + validate?: (value: any) => string | undefined; + // Validate function creator to validate the setting in the backend. It uses `schema` of the `@kbn/config-schema` package. + validateBackend?: (schema: any) => (value: unknown) => string | undefined; }; export type TPluginSettingWithKey = TPluginSetting & { key: TPluginSettingKey }; export type TPluginSettingCategory = { - title: string - description?: string - documentationLink?: string - renderOrder?: number + title: string; + description?: string; + documentationLink?: string; + renderOrder?: number; }; -export const PLUGIN_SETTINGS_CATEGORIES: { [category: number]: TPluginSettingCategory } = { +export const PLUGIN_SETTINGS_CATEGORIES: { + [category: number]: TPluginSettingCategory; +} = { [SettingCategory.HEALTH_CHECK]: { title: 'Health check', description: "Checks will be executed by the app's Healthcheck.", @@ -487,40 +514,45 @@ export const PLUGIN_SETTINGS_CATEGORIES: { [category: number]: TPluginSettingCat }, [SettingCategory.GENERAL]: { title: 'General', - description: "Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.", + description: + 'Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.', renderOrder: SettingCategory.GENERAL, }, [SettingCategory.EXTENSIONS]: { title: 'Initial display state of the modules of the new API host entries.', - description: "Extensions.", + description: 'Extensions.', }, [SettingCategory.SECURITY]: { title: 'Security', - description: "Application security options such as unauthorized roles.", + description: 'Application security options such as unauthorized roles.', renderOrder: SettingCategory.SECURITY, }, [SettingCategory.MONITORING]: { title: 'Task:Monitoring', - description: "Options related to the agent status monitoring job and its storage in indexes.", + description: + 'Options related to the agent status monitoring job and its storage in indexes.', renderOrder: SettingCategory.MONITORING, }, [SettingCategory.STATISTICS]: { title: 'Task:Statistics', - description: "Options related to the daemons manager monitoring job and their storage in indexes.", + description: + 'Options related to the daemons manager monitoring job and their storage in indexes.', renderOrder: SettingCategory.STATISTICS, }, [SettingCategory.CUSTOMIZATION]: { title: 'Custom branding', - description: "If you want to use custom branding elements such as logos, you can do so by editing the settings below.", + description: + 'If you want to use custom branding elements such as logos, you can do so by editing the settings below.', documentationLink: 'user-manual/wazuh-dashboard/white-labeling.html', renderOrder: SettingCategory.CUSTOMIZATION, - } + }, }; export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { - "alerts.sample.prefix": { - title: "Sample alerts prefix", - description: "Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.", + 'alerts.sample.prefix': { + title: 'Sample alerts prefix', + description: + 'Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_SAMPLE_ALERT_PREFIX, @@ -532,15 +564,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "checks.api": { - title: "API connection", - description: "Enable or disable the API health check when opening the app.", + 'checks.api': { + title: 'API connection', + description: 'Enable or disable the API health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -551,20 +594,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.fields": { - title: "Known fields", - description: "Enable or disable the known fields health check when opening the app.", + 'checks.fields': { + title: 'Known fields', + description: + 'Enable or disable the known fields health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -575,20 +621,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.maxBuckets": { - title: "Set max buckets to 200000", - description: "Change the default value of the plugin platform max buckets configuration.", + 'checks.maxBuckets': { + title: 'Set max buckets to 200000', + description: + 'Change the default value of the plugin platform max buckets configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -599,20 +648,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } + }, }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.metaFields": { - title: "Remove meta fields", - description: "Change the default value of the plugin platform metaField configuration.", + 'checks.metaFields': { + title: 'Remove meta fields', + description: + 'Change the default value of the plugin platform metaField configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -623,20 +675,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.pattern": { - title: "Index pattern", - description: "Enable or disable the index pattern health check when opening the app.", + 'checks.pattern': { + title: 'Index pattern', + description: + 'Enable or disable the index pattern health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -647,20 +702,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.setup": { - title: "API version", - description: "Enable or disable the setup health check when opening the app.", + 'checks.setup': { + title: 'API version', + description: + 'Enable or disable the setup health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -671,20 +729,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.template": { - title: "Index template", - description: "Enable or disable the template health check when opening the app.", + 'checks.template': { + title: 'Index template', + description: + 'Enable or disable the template health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -695,20 +756,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.timeFilter": { - title: "Set time filter to 24h", - description: "Change the default value of the plugin platform timeFilter configuration.", + 'checks.timeFilter': { + title: 'Set time filter to 24h', + description: + 'Change the default value of the plugin platform timeFilter configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -719,20 +783,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "cron.prefix": { - title: "Cron prefix", - description: "Define the index prefix of predefined jobs.", + 'cron.prefix': { + title: 'Cron prefix', + description: 'Define the index prefix of predefined jobs.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_PREFIX, @@ -743,15 +809,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.apis": { - title: "Includes APIs", - description: "Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.", + 'cron.statistics.apis': { + title: 'Includes APIs', + description: + 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', category: SettingCategory.STATISTICS, type: EpluginSettingType.editor, defaultValue: [], @@ -759,72 +837,87 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } + }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + }), + ); }, - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )})); - }, }, - "cron.statistics.index.creation": { - title: "Index creation", - description: "Define the interval in which a new index will be created.", + 'cron.statistics.index.creation': { + title: 'Index creation', + description: 'Define the interval in which a new index will be created.', category: SettingCategory.STATISTICS, type: EpluginSettingType.select, options: { select: [ { - text: "Hourly", - value: "h" + text: 'Hourly', + value: 'h', }, { - text: "Daily", - value: "d" + text: 'Daily', + value: 'd', }, { - text: "Weekly", - value: "w" + text: 'Weekly', + value: 'w', }, { - text: "Monthly", - value: "m" - } - ] + text: 'Monthly', + value: 'm', + }, + ], }, defaultValue: WAZUH_STATISTICS_DEFAULT_CREATION, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRunningHealthCheck: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "cron.statistics.index.name": { - title: "Index name", - description: "Define the name of the index in which the documents will be saved.", + 'cron.statistics.index.name': { + title: 'Index name', + description: + 'Define the name of the index in which the documents will be saved.', category: SettingCategory.STATISTICS, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_NAME, @@ -836,15 +929,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.index.replicas": { - title: "Index replicas", - description: "Define the number of replicas to use for the statistics indices.", + 'cron.statistics.index.replicas': { + title: 'Index replicas', + description: + 'Define the number of replicas to use for the statistics indices.', category: SettingCategory.STATISTICS, type: EpluginSettingType.number, defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, @@ -854,25 +959,30 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 0, - integer: true - } + integer: true, + }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number): string { + uiFormTransformConfigurationValueToInputValue: function ( + value: number, + ): string { return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value) - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "cron.statistics.index.shards": { - title: "Index shards", - description: "Define the number of shards to use for the statistics indices.", + 'cron.statistics.index.shards': { + title: 'Index shards', + description: + 'Define the number of shards to use for the statistics indices.', category: SettingCategory.STATISTICS, type: EpluginSettingType.number, defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, @@ -882,41 +992,46 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value) - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "cron.statistics.interval": { - title: "Interval", - description: "Define the frequency of task execution using cron schedule expressions.", + 'cron.statistics.interval': { + title: 'Interval', + description: + 'Define the frequency of task execution using cron schedule expressions.', category: SettingCategory.STATISTICS, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_CRON_FREQ, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRestartingPluginPlatform: true, - validate: function(value: string){ - return validateNodeCronInterval(value) ? undefined : "Interval is not valid." - }, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validate: function (value: string) { + return validateNodeCronInterval(value) + ? undefined + : 'Interval is not valid.'; + }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.status": { - title: "Status", - description: "Enable or disable the statistics tasks.", + 'cron.statistics.status': { + title: 'Status', + description: 'Enable or disable the statistics tasks.', category: SettingCategory.STATISTICS, type: EpluginSettingType.switch, defaultValue: WAZUH_STATISTICS_DEFAULT_STATUS, @@ -927,217 +1042,248 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "customization.enabled": { - title: "Status", - description: "Enable or disable the customization.", - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, + 'customization.enabled': { + title: 'Status', + description: 'Enable or disable the customization.', + category: SettingCategory.CUSTOMIZATION, + type: EpluginSettingType.switch, + defaultValue: true, + isConfigurableFromFile: true, + isConfigurableFromUI: true, requiresReloadingBrowserTab: true, - options: { - switch: { - values: { - disabled: {label: 'false', value: false}, - enabled: {label: 'true', value: true}, - } - } - }, - uiFormTransformChangedInputValue: function(value: boolean | string): boolean{ - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, - }, - "customization.logo.app": { - title: "App main logo", + options: { + switch: { + values: { + disabled: { label: 'false', value: false }, + enabled: { label: 'true', value: true }, + }, + }, + }, + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { + return Boolean(value); + }, + validate: SettingsValidator.isBoolean, + validateBackend: function (schema) { + return schema.boolean(); + }, + }, + 'customization.logo.app': { + title: 'App main logo', description: `This logo is used in the app main menu, at the top left corner.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.app', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 300, + height: 70, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.app', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.healthcheck": { - title: "Healthcheck logo", + 'customization.logo.healthcheck': { + title: 'Healthcheck logo', description: `This logo is displayed during the Healthcheck routine of the app.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.healthcheck', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 300, + height: 70, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.healthcheck', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.reports": { - title: "PDF reports logo", + 'customization.logo.reports': { + title: 'PDF reports logo', description: `This logo is used in the PDF reports generated by the app. It's placed at the top left corner of every page of the PDF.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', defaultValueIfNotSet: REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH, isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 190, - height: 40, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.reports', - resolveStaticURL: (filename: string) => `custom/images/${filename}` - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 190, + height: 40, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.reports', + resolveStaticURL: (filename: string) => `custom/images/${filename}`, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.sidebar": { - title: "Navigation drawer logo", + 'customization.logo.sidebar': { + title: 'Navigation drawer logo', description: `This is the logo for the app to display in the platform's navigation drawer, this is, the main sidebar collapsible menu.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, requiresReloadingBrowserTab: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 80, - height: 80, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.sidebar', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 80, + height: 80, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.sidebar', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.reports.footer": { - title: "Reports footer", - description: "Set the footer of the reports.", - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.textarea, - defaultValue: "", + 'customization.reports.footer': { + title: 'Reports footer', + description: 'Set the footer of the reports.', + category: SettingCategory.CUSTOMIZATION, + type: EpluginSettingType.textarea, + defaultValue: '', defaultValueIfNotSet: REPORTS_PAGE_FOOTER_TEXT, - isConfigurableFromFile: true, - isConfigurableFromUI: true, + isConfigurableFromFile: true, + isConfigurableFromUI: true, options: { maxRows: 2, maxLength: 50 }, validate: function (value) { return SettingsValidator.multipleLinesString({ maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength - })(value) + maxLength: this.options?.maxLength, + })(value); }, validateBackend: function (schema) { return schema.string({ validate: this.validate.bind(this) }); }, }, - "customization.reports.header": { - title: "Reports header", - description: "Set the header of the reports.", + 'customization.reports.header': { + title: 'Reports header', + description: 'Set the header of the reports.', category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.textarea, - defaultValue: "", + defaultValue: '', defaultValueIfNotSet: REPORTS_PAGE_HEADER_TEXT, isConfigurableFromFile: true, isConfigurableFromUI: true, @@ -1145,16 +1291,16 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validate: function (value) { return SettingsValidator.multipleLinesString({ maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength - })(value) - }, - validateBackend: function(schema){ - return schema.string({validate: this.validate.bind(this)}); - }, - }, - "disabled_roles": { - title: "Disable roles", - description: "Disabled the plugin visibility for users with the roles.", + maxLength: this.options?.maxLength, + })(value); + }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate.bind(this) }); + }, + }, + disabled_roles: { + title: 'Disable roles', + description: 'Disabled the plugin visibility for users with the roles.', category: SettingCategory.SECURITY, type: EpluginSettingType.editor, defaultValue: [], @@ -1162,62 +1308,74 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } + }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + }), + ); }, - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )})); - }, }, - "enrollment.dns": { - title: "Enrollment DNS", - description: "Specifies the Wazuh registration server, used for the agent enrollment.", + 'enrollment.dns': { + title: 'Enrollment DNS', + description: + 'Specifies the Wazuh registration server, used for the agent enrollment.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, validate: SettingsValidator.hasNoSpaces, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "enrollment.password": { - title: "Enrollment password", - description: "Specifies the password used to authenticate during the agent enrollment.", + 'enrollment.password': { + title: 'Enrollment password', + description: + 'Specifies the password used to authenticate during the agent enrollment.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: false, validate: SettingsValidator.isNotEmptyString, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "extensions.audit": { - title: "System auditing", - description: "Enable or disable the Audit tab on Overview and Agents.", + 'extensions.audit': { + title: 'System auditing', + description: 'Enable or disable the Audit tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1228,20 +1386,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.aws": { - title: "Amazon AWS", - description: "Enable or disable the Amazon (AWS) tab on Overview.", + 'extensions.aws': { + title: 'Amazon AWS', + description: 'Enable or disable the Amazon (AWS) tab on Overview.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1252,20 +1412,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.ciscat": { - title: "CIS-CAT", - description: "Enable or disable the CIS-CAT tab on Overview and Agents.", + 'extensions.ciscat': { + title: 'CIS-CAT', + description: 'Enable or disable the CIS-CAT tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1276,20 +1438,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.docker": { - title: "Docker listener", - description: "Enable or disable the Docker listener tab on Overview and Agents.", + 'extensions.docker': { + title: 'Docker listener', + description: + 'Enable or disable the Docker listener tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1300,20 +1465,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.gcp": { - title: "Google Cloud platform", - description: "Enable or disable the Google Cloud Platform tab on Overview.", + 'extensions.gcp': { + title: 'Google Cloud platform', + description: 'Enable or disable the Google Cloud Platform tab on Overview.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1324,20 +1491,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.gdpr": { - title: "GDPR", - description: "Enable or disable the GDPR tab on Overview and Agents.", + 'extensions.gdpr': { + title: 'GDPR', + description: 'Enable or disable the GDPR tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1348,20 +1517,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.github": { - title: "GitHub", - description: "Enable or disable the GitHub tab on Overview and Agents.", + 'extensions.github': { + title: 'GitHub', + description: 'Enable or disable the GitHub tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1372,20 +1543,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.hipaa": { - title: "HIPAA", - description: "Enable or disable the HIPAA tab on Overview and Agents.", + 'extensions.hipaa': { + title: 'HIPAA', + description: 'Enable or disable the HIPAA tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1396,20 +1569,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.nist": { - title: "NIST", - description: "Enable or disable the NIST 800-53 tab on Overview and Agents.", + 'extensions.nist': { + title: 'NIST', + description: + 'Enable or disable the NIST 800-53 tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1420,20 +1596,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.office": { - title: "Office 365", - description: "Enable or disable the Office 365 tab on Overview and Agents.", + 'extensions.office': { + title: 'Office 365', + description: 'Enable or disable the Office 365 tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1444,20 +1622,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.oscap": { - title: "OSCAP", - description: "Enable or disable the Open SCAP tab on Overview and Agents.", + 'extensions.oscap': { + title: 'OSCAP', + description: 'Enable or disable the Open SCAP tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1468,20 +1648,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.osquery": { - title: "Osquery", - description: "Enable or disable the Osquery tab on Overview and Agents.", + 'extensions.osquery': { + title: 'Osquery', + description: 'Enable or disable the Osquery tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1492,20 +1674,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.pci": { - title: "PCI DSS", - description: "Enable or disable the PCI DSS tab on Overview and Agents.", + 'extensions.pci': { + title: 'PCI DSS', + description: 'Enable or disable the PCI DSS tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1516,20 +1700,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.tsc": { - title: "TSC", - description: "Enable or disable the TSC tab on Overview and Agents.", + 'extensions.tsc': { + title: 'TSC', + description: 'Enable or disable the TSC tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1540,20 +1726,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.virustotal": { - title: "Virustotal", - description: "Enable or disable the VirusTotal tab on Overview and Agents.", + 'extensions.virustotal': { + title: 'Virustotal', + description: 'Enable or disable the VirusTotal tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1564,20 +1752,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "hideManagerAlerts": { - title: "Hide manager alerts", - description: "Hide the alerts of the manager in every dashboard.", + hideManagerAlerts: { + title: 'Hide manager alerts', + description: 'Hide the alerts of the manager in every dashboard.', category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: false, @@ -1589,20 +1779,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "ip.ignore": { - title: "Index pattern ignore", - description: "Disable certain index pattern names from being available in index pattern selector.", + 'ip.ignore': { + title: 'Index pattern ignore', + description: + 'Disable certain index pattern names from being available in index pattern selector.', category: SettingCategory.GENERAL, type: EpluginSettingType.editor, defaultValue: [], @@ -1610,43 +1803,74 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') - )})); - }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + SettingsValidator.noLiteralString('.', '..'), + SettingsValidator.noStartsWithString('-', '_', '+', '.'), + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + SettingsValidator.noLiteralString('.', '..'), + SettingsValidator.noStartsWithString('-', '_', '+', '.'), + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), + ), + }), + ); + }, }, - "ip.selector": { - title: "IP selector", - description: "Define if the user is allowed to change the selected index pattern directly from the top menu bar.", + 'ip.selector': { + title: 'IP selector', + description: + 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: true, @@ -1657,48 +1881,55 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "logs.level": { - title: "Log level", - description: "Logging level of the App.", + 'logs.level': { + title: 'Log level', + description: 'Logging level of the App.', category: SettingCategory.GENERAL, type: EpluginSettingType.select, options: { select: [ { - text: "Info", - value: "info" + text: 'Info', + value: 'info', }, { - text: "Debug", - value: "debug" - } - ] + text: 'Debug', + value: 'debug', + }, + ], }, - defaultValue: "info", + defaultValue: 'info', isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRestartingPluginPlatform: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "pattern": { - title: "Index pattern", - description: "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", + pattern: { + title: 'Index pattern', + description: + "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_ALERTS_PATTERN, @@ -1711,15 +1942,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "timeout": { - title: "Request timeout", - description: "Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.", + timeout: { + title: 'Request timeout', + description: + 'Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.', category: SettingCategory.GENERAL, type: EpluginSettingType.number, defaultValue: 20000, @@ -1728,61 +1970,69 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1500, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.creation": { - title: "Index creation", - description: "Define the interval in which a new wazuh-monitoring index will be created.", + 'wazuh.monitoring.creation': { + title: 'Index creation', + description: + 'Define the interval in which a new wazuh-monitoring index will be created.', category: SettingCategory.MONITORING, type: EpluginSettingType.select, options: { select: [ { - text: "Hourly", - value: "h" + text: 'Hourly', + value: 'h', }, { - text: "Daily", - value: "d" + text: 'Daily', + value: 'd', }, { - text: "Weekly", - value: "w" + text: 'Weekly', + value: 'w', }, { - text: "Monthly", - value: "m" - } - ] + text: 'Monthly', + value: 'm', + }, + ], }, defaultValue: WAZUH_MONITORING_DEFAULT_CREATION, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRunningHealthCheck: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "wazuh.monitoring.enabled": { - title: "Status", - description: "Enable or disable the wazuh-monitoring index creation and/or visualization.", + 'wazuh.monitoring.enabled': { + title: 'Status', + description: + 'Enable or disable the wazuh-monitoring index creation and/or visualization.', category: SettingCategory.MONITORING, type: EpluginSettingType.switch, defaultValue: WAZUH_MONITORING_DEFAULT_ENABLED, @@ -1794,20 +2044,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "wazuh.monitoring.frequency": { - title: "Frequency", - description: "Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.", + 'wazuh.monitoring.frequency': { + title: 'Frequency', + description: + 'Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_FREQUENCY, @@ -1817,25 +2070,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 60, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.pattern": { - title: "Index pattern", - description: "Default index pattern to use for Wazuh monitoring.", + 'wazuh.monitoring.pattern': { + title: 'Index pattern', + description: 'Default index pattern to use for Wazuh monitoring.', category: SettingCategory.MONITORING, type: EpluginSettingType.text, defaultValue: WAZUH_MONITORING_PATTERN, @@ -1847,15 +2102,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), ), - validateBackend: function(schema){ - return schema.string({minLength: 1, validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ minLength: 1, validate: this.validate }); + }, }, - "wazuh.monitoring.replicas": { - title: "Index replicas", - description: "Define the number of replicas to use for the wazuh-monitoring-* indices.", + 'wazuh.monitoring.replicas': { + title: 'Index replicas', + description: + 'Define the number of replicas to use for the wazuh-monitoring-* indices.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, @@ -1865,25 +2131,28 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 0, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.shards": { - title: "Index shards", - description: "Define the number of shards to use for the wazuh-monitoring-* indices.", + 'wazuh.monitoring.shards': { + title: 'Index shards', + description: + 'Define the number of shards to use for the wazuh-monitoring-* indices.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, @@ -1893,22 +2162,24 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, - } + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, + }, }; export type TPluginSettingKey = keyof typeof PLUGIN_SETTINGS; @@ -1969,12 +2240,22 @@ export enum HTTP_STATUS_CODES { GATEWAY_TIMEOUT = 504, HTTP_VERSION_NOT_SUPPORTED = 505, INSUFFICIENT_STORAGE = 507, - NETWORK_AUTHENTICATION_REQUIRED = 511 + NETWORK_AUTHENTICATION_REQUIRED = 511, } // Module Security configuration assessment export const MODULE_SCA_CHECK_RESULT_LABEL = { passed: 'Passed', failed: 'Failed', - 'not applicable': 'Not applicable' -} + 'not applicable': 'Not applicable', +}; + +// Search bar + +// This limits the results in the API request +export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT = 30; +// This limits the suggestions for the token of type value displayed in the search bar +export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT = 10; +/* Time in milliseconds to debounce the analysis of search bar. This mitigates some problems related +to changes running in parallel */ +export const SEARCH_BAR_DEBOUNCE_UPDATE_TIME = 400; diff --git a/plugins/main/public/components/agents/sca/inventory.tsx b/plugins/main/public/components/agents/sca/inventory.tsx index 28b2a0c885..4540f2f0c1 100644 --- a/plugins/main/public/components/agents/sca/inventory.tsx +++ b/plugins/main/public/components/agents/sca/inventory.tsx @@ -61,7 +61,7 @@ type InventoryState = { loading: boolean; checksIsLoading: boolean; redirect: boolean; - filters: object[]; + filters: object; pageTableChecks: { pageIndex: number; pageSize?: number }; policies: object[]; checks: object[]; @@ -81,7 +81,7 @@ export class Inventory extends Component { itemIdToExpandedRowMap: {}, showMoreInfo: false, loading: false, - filters: [], + filters: {}, pageTableChecks: { pageIndex: 0 }, policies: [], checks: [], @@ -370,7 +370,7 @@ export class Inventory extends Component { buttonStat(text, field, value) { return ( - ); diff --git a/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx b/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx index 2ab8309e7e..5327d08fc2 100644 --- a/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx +++ b/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx @@ -16,18 +16,18 @@ export default function SCAPoliciesTable(props: Props) { 'data-test-subj': `sca-row-${idx}`, className: 'customRowClass', onClick: rowProps ? () => rowProps(item) : null - } - } + }; + }; return ( <> - + /> ); } diff --git a/plugins/main/public/components/agents/sca/inventory/checks-table.tsx b/plugins/main/public/components/agents/sca/inventory/checks-table.tsx index 8f8641797f..e1c5d32279 100644 --- a/plugins/main/public/components/agents/sca/inventory/checks-table.tsx +++ b/plugins/main/public/components/agents/sca/inventory/checks-table.tsx @@ -2,7 +2,6 @@ import { EuiButtonIcon, EuiDescriptionList, EuiHealth } from '@elastic/eui'; import React, { Component } from 'react'; import { MODULE_SCA_CHECK_RESULT_LABEL } from '../../../../../common/constants'; import { TableWzAPI } from '../../../common/tables'; -import { IWzSuggestItem } from '../../../wz-search-bar'; import { ComplianceText, RuleText } from '../components'; import { getFilterValues } from './lib'; @@ -20,9 +19,42 @@ type State = { pageTableChecks: { pageIndex: 0 }; }; +const searchBarWQLFieldSuggestions = [ + { label: 'condition', description: 'filter by check condition' }, + { label: 'description', description: 'filter by check description' }, + { label: 'file', description: 'filter by check file' }, + { label: 'rationale', description: 'filter by check rationale' }, + { label: 'reason', description: 'filter by check reason' }, + { label: 'registry', description: 'filter by check registry' }, + { label: 'remediation', description: 'filter by check remediation' }, + { label: 'result', description: 'filter by check result' }, + { label: 'title', description: 'filter by check title' }, +]; + +const searchBarWQLOptions = { + searchTermFields: [ + 'command', + 'compliance.key', + 'compliance.value', + 'description', + 'directory', + 'file', + 'id', + 'title', + 'process', + 'registry', + 'rationale', + 'reason', + 'references', + 'remediation', + 'result', + 'rules.type', + 'rules.rule', + ], +}; + export class InventoryPolicyChecksTable extends Component { _isMount = false; - suggestions: IWzSuggestItem[] = []; columnsChecks: any; constructor(props) { super(props); @@ -31,108 +63,9 @@ export class InventoryPolicyChecksTable extends Component { agent, lookingPolicy, itemIdToExpandedRowMap: {}, - filters: filters || [], + filters: filters || '', pageTableChecks: { pageIndex: 0 }, }; - this.suggestions = [ - { - type: 'params', - label: 'condition', - description: 'Filter by check condition', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'condition', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'description', - description: 'Filter by check description', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'description', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'file', - description: 'Filter by check file', - operators: ['=', '!='], - values: (value) => - getFilterValues('file', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'registry', - description: 'Filter by check registry', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'registry', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'rationale', - description: 'Filter by check rationale', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'rationale', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'reason', - description: 'Filter by check reason', - operators: ['=', '!='], - values: (value) => - getFilterValues('reason', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'remediation', - description: 'Filter by check remediation', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'remediation', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'result', - description: 'Filter by check result', - operators: ['=', '!='], - values: (value) => - getFilterValues('result', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'title', - description: 'Filter by check title', - operators: ['=', '!='], - values: (value) => - getFilterValues('title', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - ]; this.columnsChecks = [ { field: 'id', @@ -149,7 +82,7 @@ export class InventoryPolicyChecksTable extends Component { { name: 'Target', truncateText: true, - render: (item) => ( + render: item => (
{item.file ? ( @@ -189,21 +122,25 @@ export class InventoryPolicyChecksTable extends Component { align: 'right', width: '40px', isExpander: true, - render: (item) => ( + render: item => ( this.toggleDetails(item)} - aria-label={this.state.itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} - iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + aria-label={ + this.state.itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand' + } + iconType={ + this.state.itemIdToExpandedRowMap[item.id] + ? 'arrowUp' + : 'arrowDown' + } /> ), }, ]; } - async componentDidMount() {} - async componentDidUpdate(prevProps) { - const { filters } = this.props + const { filters } = this.props; if (filters !== prevProps.filters) { this.setState({ filters: filters }); } @@ -217,7 +154,7 @@ export class InventoryPolicyChecksTable extends Component { * * @param item */ - toggleDetails = (item) => { + toggleDetails = item => { const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; if (itemIdToExpandedRowMap[item.id]) { @@ -228,7 +165,7 @@ export class InventoryPolicyChecksTable extends Component { checks += item.condition ? ` (Condition: ${item.condition})` : ''; const complianceText = item.compliance && item.compliance.length - ? item.compliance.map((el) => `${el.key}: ${el.value}`).join('\n') + ? item.compliance.map(el => `${el.key}: ${el.value}`).join('\n') : ''; const listItems = [ { @@ -260,10 +197,12 @@ export class InventoryPolicyChecksTable extends Component { description: , }, ]; - const itemsToShow = listItems.filter((x) => { + const itemsToShow = listItems.filter(x => { return x.description; }); - itemIdToExpandedRowMap[item.id] = ; + itemIdToExpandedRowMap[item.id] = ( + + ); } this.setState({ itemIdToExpandedRowMap }); }; @@ -306,28 +245,51 @@ export class InventoryPolicyChecksTable extends Component { }; }; + const { filters } = this.state; + const agentID = this.props?.agent?.id; + const scaPolicyID = this.props?.lookingPolicy?.policy_id; + return ( - <> - this.setState({ filters })} - tablePageSizeOptions={[10, 25, 50, 100]} - /> - + { + try { + return await getFilterValues( + field, + agentID, + scaPolicyID, + { + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + item => ({ label: item }), + ); + } catch (error) { + return []; + } + }, + }, + }} + /> ); } } diff --git a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts index 5d7844d49f..1804a72529 100644 --- a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts +++ b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts @@ -1,26 +1,29 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../common/constants'; import { WzRequest } from '../../../../../react-services/wz-request'; export async function getFilterValues( field: string, - value: string, agentId: string, policyId: string, filters: { [key: string]: string } = {}, - format = (item) => item) { + format = item => item, +) { const filter = { ...filters, distinct: true, select: field, - limit: 30, + sort: `+${field}`, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', `/sca/${agentId}/checks/${policyId}`, { - params: filter, - }); + const result = await WzRequest.apiReq( + 'GET', + `/sca/${agentId}/checks/${policyId}`, + { + params: filter, + }, + ); return ( - result?.data?.data?.affected_items?.map((item) => { + result?.data?.data?.affected_items?.map(item => { return format(item[field]); }) || [] ); diff --git a/plugins/main/public/components/agents/vuls/inventory.tsx b/plugins/main/public/components/agents/vuls/inventory.tsx index c9f796e3aa..4476d2a56f 100644 --- a/plugins/main/public/components/agents/vuls/inventory.tsx +++ b/plugins/main/public/components/agents/vuls/inventory.tsx @@ -59,7 +59,7 @@ interface TitleColors { export class Inventory extends Component { _isMount = false; state: { - filters: []; + filters: object; isLoading: boolean; isLoadingStats: boolean; customBadges: ICustomBadges[]; @@ -82,7 +82,7 @@ export class Inventory extends Component { isLoading: true, isLoadingStats: true, customBadges: [], - filters: [], + filters: {}, stats: [ { title: 0, @@ -167,12 +167,9 @@ export class Inventory extends Component { } buildFilterQuery(field = '', selectedItem = '') { - return [ - { - field: 'q', - value: `${field}=${selectedItem}`, - }, - ]; + return { + q: `${field}=${selectedItem}` + }; } async loadAgent() { @@ -220,7 +217,7 @@ export class Inventory extends Component { textAlign='center' isLoading={isLoadingStats} title={ - + item) { - +export async function getFilterValues( + field, + agentId, + filters = {}, + format = item => item, +) { const filter = { ...filters, distinct: true, select: field, - limit: 30, + sort: `+${field}`, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { params: filter }); - return result?.data?.data?.affected_items?.map((item) => { return format(item[field]) }) || []; + const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { + params: filter, + }); + return ( + result?.data?.data?.affected_items?.map(item => { + return format(item[field]); + }) || [] + ); } export async function getLastScan(agentId: string = '000') { const response = await WzRequest.apiReq( 'GET', `/vulnerability/${agentId}/last_scan`, - {} + {}, ); return response?.data?.data?.affected_items[0] || {}; } diff --git a/plugins/main/public/components/agents/vuls/inventory/table.tsx b/plugins/main/public/components/agents/vuls/inventory/table.tsx index 2c5c604905..d3bb882ed3 100644 --- a/plugins/main/public/components/agents/vuls/inventory/table.tsx +++ b/plugins/main/public/components/agents/vuls/inventory/table.tsx @@ -11,86 +11,34 @@ */ import React, { Component } from 'react'; -import { Direction } from '@elastic/eui'; import { FlyoutDetail } from './flyout'; -import { filtersToObject, IFilter, IWzSuggestItem } from '../../../wz-search-bar'; import { TableWzAPI } from '../../../../components/common/tables'; import { getFilterValues } from './lib'; import { formatUIDate } from '../../../../react-services/time-service'; +import { EuiIconTip } from '@elastic/eui'; + +const searchBarWQLOptions = { + searchTermFields: [ + 'name', + 'cve', + 'version', + 'architecture', + 'severity', + 'cvss2_score', + 'cvss3_score', + ], +}; export class InventoryTable extends Component { state: { error?: string; - pageIndex: number; - pageSize: number; - sortField: string; isFlyoutVisible: Boolean; - sortDirection: Direction; isLoading: boolean; currentItem: {}; }; - suggestions: IWzSuggestItem[] = [ - { - type: 'q', - label: 'name', - description: 'Filter by package ID', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('name', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cve', - description: 'Filter by CVE ID', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cve', value, this.props.agent.id), - }, - { - type: 'q', - label: 'version', - description: 'Filter by CVE version', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('version', value, this.props.agent.id), - }, - { - type: 'q', - label: 'architecture', - description: 'Filter by architecture', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('architecture', value, this.props.agent.id), - }, - { - type: 'q', - label: 'severity', - description: 'Filter by Severity', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('severity', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cvss2_score', - description: 'Filter by CVSS2', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cvss2_score', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cvss3_score', - description: 'Filter by CVSS3', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cvss3_score', value, this.props.agent.id), - }, - { - type: 'q', - label: 'detection_time', - description: 'Filter by Detection Time', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('detection_time', value, this.props.agent.id), - }, - ]; - props!: { - filters: IFilter[]; + filters: string; agent: any; items: []; onFiltersChange: Function; @@ -100,11 +48,6 @@ export class InventoryTable extends Component { super(props); this.state = { - pageIndex: 0, - pageSize: 15, - sortField: 'name', - sortDirection: 'asc', - isLoading: false, isFlyoutVisible: false, currentItem: {}, }; @@ -117,36 +60,10 @@ export class InventoryTable extends Component { async showFlyout(item, redirect = false) { //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. this.setState({ isFlyoutVisible: false }, () => - this.setState({ isFlyoutVisible: true, currentItem: item }) + this.setState({ isFlyoutVisible: true, currentItem: item }), ); } - async componentDidUpdate(prevProps) { - const { filters } = this.props; - if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }); - } - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + sortField; - } - - buildFilter() { - const { pageIndex, pageSize } = this.state; - const filters = filtersToObject(this.props.filters); - const filter = { - ...filters, - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - }; - return filter; - } - columns() { let width; (((this.props.agent || {}).os || {}).platform || false) === 'windows' @@ -199,7 +116,17 @@ export class InventoryTable extends Component { }, { field: 'detection_time', - name: 'Detection Time', + name: ( + + Detection Time{' '} + + + ), sortable: true, width: `100px`, render: formatUIDate, @@ -208,7 +135,7 @@ export class InventoryTable extends Component { } renderTable() { - const getRowProps = (item) => { + const getRowProps = item => { const id = `${item.name}-${item.cve}-${item.architecture}-${item.version}-${item.severity}-${item.cvss2_score}-${item.cvss3_score}-${item.detection_time}`; return { 'data-test-subj': `row-${id}`, @@ -217,7 +144,6 @@ export class InventoryTable extends Component { }; const { error } = this.state; - const { filters, onFiltersChange } = this.props; const columns = this.columns(); const selectFields = `select=${[ 'cve', @@ -232,33 +158,71 @@ export class InventoryTable extends Component { 'condition', 'updated', 'published', - 'external_references' + 'external_references', ].join(',')}`; + const agentID = this.props.agent.id; + return ( ({ + mapResponseItem={item => ({ ...item, // Some vulnerability data could not contain the external_references field. // This causes the rendering of them can crash when opening the flyout with the details. // So, we ensure the fields are defined with the expected data structure. - external_references: Array.isArray(item?.external_references) + external_references: Array.isArray(item?.external_references) ? item?.external_references - : [] + : [], })} error={error} - downloadCsv={true} - filters={filters} - onFiltersChange={onFiltersChange} + searchTable + downloadCsv + showReload tablePageSizeOptions={[10, 25, 50, 100]} + filters={this.props.filters} + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + { + label: 'architecture', + description: 'filter by architecture', + }, + { label: 'cve', description: 'filter by CVE ID' }, + { label: 'cvss2_score', description: 'filter by CVSS2' }, + { label: 'cvss3_score', description: 'filter by CVSS3' }, + { + label: 'detection_time', + description: 'filter by detection time', + }, + { label: 'name', description: 'filter by package name' }, + { label: 'severity', description: 'filter by severity' }, + { label: 'version', description: 'filter by CVE version' }, + ]; + }, + value: async (currentValue, { field }) => { + try { + return await getFilterValues( + field, + agentID, + { + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + label => ({ label }), + ); + } catch (error) { + return []; + } + }, + }, + }} /> ); } @@ -266,7 +230,7 @@ export class InventoryTable extends Component { render() { const table = this.renderTable(); return ( -
+
{table} {this.state.isFlyoutVisible && ( this.closeFlyout()} - type="vulnerability" - view="inventory" + type='vulnerability' + view='inventory' showViewInEvents={true} outsideClickCloses={true} {...this.props} diff --git a/plugins/main/public/components/common/hooks/index.ts b/plugins/main/public/components/common/hooks/index.ts index 944998ea1a..e3ce7584c7 100644 --- a/plugins/main/public/components/common/hooks/index.ts +++ b/plugins/main/public/components/common/hooks/index.ts @@ -27,3 +27,4 @@ export * from './use_async_action'; export * from './use_async_action_run_on_start'; export { useEsSearch } from './use-es-search'; export { useValueSuggestion, IValueSuggestion } from './use-value-suggestion'; +export * from './use-state-storage'; diff --git a/plugins/main/public/components/common/hooks/use-state-storage.ts b/plugins/main/public/components/common/hooks/use-state-storage.ts new file mode 100644 index 0000000000..3251ea1be5 --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-state-storage.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; + +function transformValueToStorage(value: any){ + return typeof value !== 'string' ? JSON.stringify(value) : value; +}; + +function transformValueFromStorage(value: any){ + return typeof value === 'string' ? JSON.parse(value) : value; +}; + +export function useStateStorage(initialValue: any, storageSystem?: 'sessionStorage' | 'localStorage', storageKey?: string){ + const [state, setState] = useState( + (storageSystem && storageKey && window?.[storageSystem]?.getItem(storageKey)) + ? transformValueFromStorage(window?.[storageSystem]?.getItem(storageKey)) + : initialValue + ); + + function setStateStorage(value: any){ + setState((state) => { + const formattedValue = typeof value === 'function' + ? value(state) + : value; + + storageSystem && storageKey && window?.[storageSystem]?.setItem(storageKey, transformValueToStorage(formattedValue)); + return formattedValue; + }); + }; + + return [state, setStateStorage]; +}; 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 0c57fd5b26..7b13e589a4 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 @@ -6,6 +6,17 @@ exports[`Table With Search Bar component renders correctly to match the snapshot reload={[Function]} rowProps={[Function]} searchBarSuggestions={Array []} + searchBarWQL={ + Object { + "options": Object { + "searchTermFields": Array [], + }, + "suggestions": Object { + "field": [Function], + "value": [Function], + }, + } + } tableColumns={ Array [ Object { @@ -44,222 +55,494 @@ exports[`Table With Search Bar component renders correctly to match the snapshot } tableProps={Object {}} > - - -
- + + WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS +
- + + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ + } + inputRef={ + Object { + "current": , + } + } + isPopoverOpen={false} + onChange={[Function]} + onClosePopover={[Function]} + onInputChange={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Search" + suggestions={Array []} + value="" + > +
+ + WQL + } - status="unchanged" - suggestions={Array []} - value="" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" > + + SYNTAX OPTIONS +
- + style={ + Object { + "width": "350px", } - sendValue={[Function]} - status="unchanged" - suggestions={Array []} - value="" - > -
- - } - value="" - /> - } - isOpen={false} - panelPaddingSize="none" + } + > + + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ + } + inputRef={ + Object { + "current": , + } + } + isPopoverOpen={false} + onChange={[Function]} + onClosePopover={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Search" + sendValue={[Function]} + status="unchanged" + suggestions={Array []} + value="" + > +
+ - [Function] - + WQL + } - buttonRef={[Function]} - className="euiInputPopover euiInputPopover--fullWidth" closePopover={[Function]} - display="block" + display="inlineBlock" hasArrow={true} - id="popover" isOpen={false} - ownFocus={false} - panelPaddingSize="none" - panelRef={[Function]} + ownFocus={true} + panelPaddingSize="m" > - + SYNTAX OPTIONS + +
-
+ WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + inputRef={ + Object { + "current": , + } + } + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Search" + value="" + /> + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiInputPopover--fullWidth" + closePopover={[Function]} + display="block" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS + +
+ + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + inputRef={ + Object { + "current": , + } + } + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Search" + value="" > -
+ WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS + +
+ + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + isLoading={false} > - -
- + + + + - } - value="" + /> +
+ + WQL + + } + className="euiFormControlLayout__append" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + key="0/.0" + ownFocus={true} + panelPaddingSize="m" + > +
- - } +
-
- -
- -
- - - - -
-
- - -
- -
-
- -
- + + + WQL + + + + + +
+
+ +
+ + +
+ +
-
-
- + + +
-
+
-
-
+ + diff --git a/plugins/main/public/components/common/tables/components/export-table-csv.tsx b/plugins/main/public/components/common/tables/components/export-table-csv.tsx index 01486f31e6..d4bc30b2dc 100644 --- a/plugins/main/public/components/common/tables/components/export-table-csv.tsx +++ b/plugins/main/public/components/common/tables/components/export-table-csv.tsx @@ -15,7 +15,6 @@ import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -import { filtersToObject } from '../../../wz-search-bar/'; import exportCsv from '../../../../react-services/wz-csv'; import { getToasts } from '../../../../kibana-services'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; @@ -34,8 +33,7 @@ export function ExportTableCsv({ endpoint, totalItems, filters, title }) { const downloadCsv = async () => { try { - const filtersObject = filtersToObject(filters); - const formatedFilters = Object.keys(filtersObject).map(key => ({name: key, value: filtersObject[key]})); + const formatedFilters = Object.entries(filters).map(([name, value]) => ({name, value})); showToast('success', 'Your download should begin automatically...', 3000); await exportCsv( endpoint, diff --git a/plugins/main/public/components/common/tables/table-default.tsx b/plugins/main/public/components/common/tables/table-default.tsx index 97e3d50974..ed068e3d23 100644 --- a/plugins/main/public/components/common/tables/table-default.tsx +++ b/plugins/main/public/components/common/tables/table-default.tsx @@ -111,9 +111,8 @@ export function TableDefault({ hidePerPageOptions }; return ( - <> ({...rest}))} items={items} loading={loading} pagination={tablePagination} @@ -122,6 +121,5 @@ export function TableDefault({ rowProps={rowProps} {...tableProps} /> - ); } diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx index 263b98b51b..60d83b8a52 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx @@ -63,6 +63,10 @@ const columns = [ }, ]; +const searchBarWQLOptions = { + searchTermFields: [] +} + const tableProps = { onSearch: () => {}, tableColumns: columns, @@ -73,6 +77,17 @@ const tableProps = { reload: () => {}, searchBarSuggestions: [], rowProps: () => {}, + searchBarWQL: { + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return []; + }, + value: async (currentValue, { field }) => { + return []; + }, + }, + } }; describe('Table With Search Bar component', () => { diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.tsx index 6804dcb1a8..d8f0df3e7a 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.tsx @@ -10,18 +10,80 @@ * Find more information about this on the LICENSE file. */ -import React, { useState, useEffect, useRef } from 'react'; -import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { EuiBasicTable, EuiBasicTableProps, EuiSpacer } from '@elastic/eui'; import _ from 'lodash'; -import { WzSearchBar } from '../../wz-search-bar/'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { SearchBar, SearchBarProps } from '../../search-bar'; -export function TableWithSearchBar({ +export interface ITableWithSearcHBarProps{ + /** + * Function to fetch the data + */ + onSearch: ( + endpoint: string, + filters: Record, + pagination: {pageIndex: number, pageSize: number}, + sorting: {sort: {field: string, direction: string}} + ) => Promise<{items: any[], totalItems: number}> + /** + * Properties for the search bar + */ + searchBarProps?: Omit + /** + * Columns for the table + */ + tableColumns: EuiBasicTableProps['columns'] & { + composeField?: string[], + searchable?: string + show?: boolean, + } + /** + * Table row properties for the table + */ + rowProps?: EuiBasicTableProps['rowProps'] + /** + * Table page size options + */ + tablePageSizeOptions?: number[] + /** + * Table initial sorting direction + */ + tableInitialSortingDirection?: 'asc' | 'dsc' + /** + * Table initial sorting field + */ + tableInitialSortingField?: string + /** + * Table properties + */ + tableProps?: Omit, 'columns' | 'items' | 'loading' | 'pagination' | 'sorting' | 'onChange' | 'rowProps'> + /** + * Refresh the fetch of data + */ + reload?: number + /** + * API endpoint + */ + endpoint: string + /** + * Search bar properties for WQL + */ + searchBarWQL?: any + /** + * Visible fields + */ + selectedFields: string[] + /** + * API request filters + */ + filters?: any +} + +export function TableWithSearchBar({ onSearch, - searchBarSuggestions, - searchBarPlaceholder = 'Filter or search', searchBarProps = {}, tableColumns, rowProps, @@ -32,25 +94,38 @@ export function TableWithSearchBar({ reload, endpoint, ...rest -}) { +}: ITableWithSearcHBarProps) { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState(rest.filters || []); + const [filters, setFilters] = useState(rest.filters || {}); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: tablePageSizeOptions[0], }); - const [sorting, setSorting] = useState({ sort: { field: tableInitialSortingField, direction: tableInitialSortingDirection, }, }); + const [refresh, setRefresh] = useState(Date.now()); const isMounted = useRef(false); + const searchBarWQLOptions = useMemo(() => ({ + searchTermFields: tableColumns + .filter(({field, searchable}) => searchable && rest.selectedFields.includes(field)) + .map(({field, composeField}) => ([composeField || field].flat())) + .flat(), + ...(rest?.searchBarWQL?.options || {}) + }), [rest?.searchBarWQL?.options, rest?.selectedFields]); + + function updateRefresh() { + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + setRefresh(Date.now()); + }; + function tableOnChange({ page = {}, sort = {} }) { if (isMounted.current) { const { index: pageIndex, size: pageSize } = page; @@ -73,9 +148,9 @@ export function TableWithSearchBar({ // We don't want to set the pagination state because there is another effect that has this dependency // and will cause the effect is triggered (redoing the onSearch function). if (isMounted.current) { - // Reset the page index when the endpoint changes. + // Reset the page index when the endpoint or reload changes. // This will cause that onSearch function is triggered because to changes in pagination in the another effect. - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + updateRefresh(); } }, [endpoint, reload]); @@ -103,14 +178,15 @@ export function TableWithSearchBar({ } setLoading(false); })(); - }, [filters, pagination, sorting]); + }, [filters, pagination, sorting, refresh]); useEffect(() => { // This effect is triggered when the component is mounted because of how to the useEffect hook works. // We don't want to set the filters state because there is another effect that has this dependency // and will cause the effect is triggered (redoing the onSearch function). if (isMounted.current && !_.isEqual(rest.filters, filters)) { - setFilters(rest.filters || []); + setFilters(rest.filters || {}); + updateRefresh(); } }, [rest.filters]); @@ -128,17 +204,27 @@ export function TableWithSearchBar({ }; return ( <> - { + // Set the query, reset the page index and update the refresh + setFilters(apiQuery); + updateRefresh(); + }} /> ({...rest}))} items={items} loading={loading} pagination={tablePagination} diff --git a/plugins/main/public/components/common/tables/table-wz-api.tsx b/plugins/main/public/components/common/tables/table-wz-api.tsx index cb46407446..fc11c05c42 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.tsx @@ -18,8 +18,11 @@ import { EuiFlexItem, EuiText, EuiButtonEmpty, + EuiSpacer, + EuiToolTip, + EuiIcon, + EuiCheckboxGroup, } from '@elastic/eui'; -import { filtersToObject } from '../../wz-search-bar'; import { TableWithSearchBar } from './table-with-search-bar'; import { TableDefault } from './table-default'; import { WzRequest } from '../../../react-services/wz-request'; @@ -27,6 +30,7 @@ import { ExportTableCsv } from './components/export-table-csv'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { useStateStorage } from '../hooks'; /** * Search input custom filter button @@ -37,6 +41,11 @@ interface CustomFilterButton { value: string; } +const getFilters = filters => { + const { default: defaultFilters, ...restFilters } = filters; + return Object.keys(restFilters).length ? restFilters : defaultFilters; +}; + export function TableWzAPI({ actionButtons, ...rest @@ -44,7 +53,7 @@ export function TableWzAPI({ actionButtons?: ReactNode | ReactNode[]; title?: string; description?: string; - downloadCsv?: boolean; + downloadCsv?: boolean | string; searchTable?: boolean; endpoint: string; buttonOptions?: CustomFilterButton[]; @@ -54,17 +63,35 @@ export function TableWzAPI({ reload?: boolean; }) { const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState([]); + const [filters, setFilters] = useState({}); const [isLoading, setIsLoading] = useState(false); - const onFiltersChange = (filters) => - typeof rest.onFiltersChange === 'function' ? rest.onFiltersChange(filters) : null; + const onFiltersChange = filters => + typeof rest.onFiltersChange === 'function' + ? rest.onFiltersChange(filters) + : null; /** * Changing the reloadFootprint timestamp will trigger reloading the table */ const [reloadFootprint, setReloadFootprint] = useState(rest.reload || 0); - const onSearch = useCallback(async function (endpoint, filters, pagination, sorting) { + const [selectedFields, setSelectedFields] = useStateStorage( + rest.tableColumns.some(({ show }) => show) + ? rest.tableColumns.filter(({ show }) => show).map(({ field }) => field) + : rest.tableColumns.map(({ field }) => field), + rest?.saveStateStorage?.system, + rest?.saveStateStorage?.key + ? `${rest?.saveStateStorage?.key}-visible-fields` + : undefined, + ); + const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); + + const onSearch = useCallback(async function ( + endpoint, + filters, + pagination, + sorting, + ) { try { const { pageIndex, pageSize } = pagination; const { field, direction } = sorting.sort; @@ -72,7 +99,7 @@ export function TableWzAPI({ setFilters(filters); onFiltersChange(filters); const params = { - ...filtersToObject(filters), + ...getFilters(filters), offset: pageIndex * pageSize, limit: pageSize, sort: `${direction === 'asc' ? '+' : '-'}${field}`, @@ -85,23 +112,25 @@ export function TableWzAPI({ ).data; setIsLoading(false); setTotalItems(totalItems); - return { items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, totalItems }; + return { + items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, + totalItems, + }; } catch (error) { setIsLoading(false); setTotalItems(0); - const options = { - context: `${TableWithSearchBar.name}.useEffect`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error searching items`, - }, - }; - getErrorOrchestrator().handleError(options); + if (error?.name) { + /* This replaces the error name. The intention is that an AxiosError + doesn't appear in the toast message. + TODO: This should be managed by the service that does the request instead of only changing + the name in this case. + */ + error.name = 'RequestError'; + } + throw error; } - }, []); + }, + []); const renderActionButtons = ( <> @@ -130,58 +159,116 @@ export function TableWzAPI({ const ReloadButton = ( - triggerReload()} - > + triggerReload()}> Refresh ); const header = ( - - - {rest.title && ( - -

- {rest.title}{' '} - {isLoading ? : ({totalItems})} -

-
- )} - {rest.description && {rest.description}} -
- - - {/* Render optional custom action button */} - {renderActionButtons} - {/* Render optional reload button */} - {rest.showReload && ReloadButton} - {/* Render optional export to CSV button */} - {rest.downloadCsv && ( - + <> + + + {rest.title && ( + +

+ {rest.title}{' '} + {isLoading ? ( + + ) : ( + ({totalItems}) + )} +

+
+ )} + {rest.description && ( + {rest.description} )} +
+ + + {/* Render optional custom action button */} + {renderActionButtons} + {/* Render optional reload button */} + {rest.showReload && ReloadButton} + {/* Render optional export to CSV button */} + {rest.downloadCsv && ( + + )} + {rest.showFieldSelector && ( + + + setIsOpenFieldSelector(state => !state)} + > + + + + + )} + + +
+ {isOpenFieldSelector && ( + + + ({ + id: item.field, + label: item.name, + checked: selectedFields.includes(item.field), + }))} + onChange={optionID => { + setSelectedFields(state => { + if (state.includes(optionID)) { + if (state.length > 1) { + return state.filter(field => field !== optionID); + } + return state; + } + return [...state, optionID]; + }); + }} + className='columnsSelectedCheckboxs' + idToSelectedMap={{}} + /> + -
-
+ )} + + ); + + const tableColumns = rest.tableColumns.filter(({ field }) => + selectedFields.includes(field), ); const table = rest.searchTable ? ( - + ) : ( - + ); return ( <> {header} + {rest.description && } {table} ); diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap b/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap index ce40ecc0ea..adf70d0cc4 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap @@ -163,40 +163,51 @@ exports[`Module Mitre Att&ck intelligence container should render the component />
-
+
-
+
-
+
-
+
+ +
+
-
-
- -
+ + + WQL + + +
diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx index 5e64bf018e..7eaa3c39dd 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx @@ -29,7 +29,7 @@ export const ModuleMitreAttackIntelligence = compose( const [selectedResource, setSelectedResource] = useState(MitreAttackResources[0].id); const [searchTermAllResources, setSearchTermAllResources] = useState(''); const searchTermAllResourcesLastSearch = useRef(''); - const [resourceFilters, setResourceFilters] = useState([]); + const [resourceFilters, setResourceFilters] = useState({}); const searchTermAllResourcesUsed = useRef(false); const searchTermAllResourcesAction = useAsyncAction( async (searchTerm) => { @@ -37,11 +37,23 @@ export const ModuleMitreAttackIntelligence = compose( searchTermAllResourcesUsed.current = true; searchTermAllResourcesLastSearch.current = searchTerm; const limitResults = 5; + const fields = ['name', 'description', 'external_id']; return ( await Promise.all( MitreAttackResources.map(async (resource) => { const response = await WzRequest.apiReq('GET', resource.apiEndpoint, { - params: { search: searchTerm, limit: limitResults }, + params: { + ...( + searchTerm + ? { + q: fields + .map(key => `${key}~${searchTerm}`) + .join(',') + } + : {} + ), + limit: limitResults + } }); return { id: resource.id, @@ -53,9 +65,18 @@ export const ModuleMitreAttackIntelligence = compose( response?.data?.data?.total_affected_items && response?.data?.data?.total_affected_items > limitResults && (() => { - setResourceFilters([ - { field: 'search', value: searchTermAllResourcesLastSearch.current }, - ]); + setResourceFilters({ + ...( + searchTermAllResourcesLastSearch.current + ? { + q: fields + .map(key => `${key}~${searchTermAllResourcesLastSearch.current}`) + .join(',') + } + : {} + ) + } + ); setSelectedResource(resource.id); }), }; @@ -76,7 +97,7 @@ export const ModuleMitreAttackIntelligence = compose( const onSelectResource = useCallback( (resourceID) => { - setResourceFilters([]); + setResourceFilters({}); setSelectedResource((prevSelectedResource) => prevSelectedResource === resourceID && searchTermAllResourcesUsed.current ? null diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx index 9dbf394150..fe311cfbea 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -21,7 +21,7 @@ import { getErrorOrchestrator } from '../../../react-services/common-services'; export const ModuleMitreAttackIntelligenceResource = ({ label, - searchBarSuggestions, + searchBar, apiEndpoint, tableColumnsCreator, initialSortingField, @@ -64,7 +64,7 @@ export const ModuleMitreAttackIntelligenceResource = ({ getErrorOrchestrator().handleError(options); } }; - + const tableColumns = useMemo(() => tableColumnsCreator(setDetails), []); const closeFlyout = useCallback(() => { @@ -78,11 +78,13 @@ export const ModuleMitreAttackIntelligenceResource = ({ title={label} tableColumns={tableColumns} tableInitialSortingField={initialSortingField} - searchBarPlaceholder={`Search in ${label}`} - searchBarSuggestions={searchBarSuggestions} endpoint={apiEndpoint} tablePageSizeOptions={[10, 15, 25, 50, 100]} filters={resourceFilters} + searchBarWQL={{ + options: searchBar.wql.options, + suggestions: searchBar.wql.suggestions, + }} /> {details && ( )} - - ) + + ); }; diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx index cc63a43d78..bf90d04a65 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx @@ -16,19 +16,31 @@ import { Markdown } from '../../common/util'; import { formatUIDate } from '../../../react-services'; import React from 'react'; import { EuiLink } from '@elastic/eui'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + UI_LOGGER_LEVELS, +} from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) => async (input: string) => { - try{ - const response = await WzRequest.apiReq('GET', endpoint, {}); - return response?.data?.data.affected_items - .map(item => item[field]) - .filter(item => item && item.toLowerCase().includes(input.toLowerCase())) - .sort() - .slice(0,9) - }catch(error){ +const getMitreAttackIntelligenceSuggestions = async ( + endpoint: string, + field: string, + currentValue: string, +) => { + try { + const params = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const response = await WzRequest.apiReq('GET', endpoint, { params }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { const options = { context: `${ModuleMitreAttackIntelligenceResource.name}.getMitreItemToRedirect`, level: UI_LOGGER_LEVELS.ERROR, @@ -43,62 +55,74 @@ const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) }; getErrorOrchestrator().handleError(options); return []; - }; + } }; -function buildResource(label: string, labelResource: string){ +function buildResource(label: string) { const id = label.toLowerCase(); const endpoint: string = `/mitre/${id}`; + const fieldsMitreAttactResource = [ + { field: 'description', name: 'description' }, + { field: 'external_id', name: 'external ID' }, + { field: 'name', name: 'name' }, + ]; return { label: label, id, - searchBarSuggestions: [ - { - type: 'q', - label: 'description', - description: `${labelResource} description`, - operators: ['~'], - values: (input) => input ? [input] : [] - }, - { - type: 'q', - label: 'name', - description: `${labelResource} name`, - operators: ['=', '!='], - values: getMitreAttackIntelligenceSuggestions(endpoint, 'name') + searchBar: { + wql: { + options: { + searchTermFields: fieldsMitreAttactResource.map(({ field }) => field), + }, + suggestions: { + field(currentValue) { + return fieldsMitreAttactResource.map(({ field, name }) => ({ + label: field, + description: `filter by ${name}`, + })); + }, + value: async (currentValue, { field }) => { + try { + return await getMitreAttackIntelligenceSuggestions( + endpoint, + field, + currentValue, + ); + } catch (error) { + return []; + } + }, + }, }, - { - type: 'q', - label: 'external_id', - description: `${labelResource} ID`, - operators: ['=', '!='], - values: getMitreAttackIntelligenceSuggestions(endpoint, 'external_id') - } - ], + }, apiEndpoint: endpoint, fieldName: 'name', initialSortingField: 'name', - tableColumnsCreator: (openResourceDetails) => [ + tableColumnsCreator: openResourceDetails => [ { field: 'external_id', name: 'ID', width: '12%', - render: (value, item) => openResourceDetails(item)}>{value} + render: (value, item) => ( + openResourceDetails(item)}>{value} + ), }, { field: 'name', name: 'Name', sortable: true, width: '30%', - render: (value, item) => openResourceDetails(item)}>{value} + render: (value, item) => ( + openResourceDetails(item)}>{value} + ), }, { field: 'description', name: 'Description', sortable: true, - render: (value) => value ? : '', - truncateText: true - } + render: value => (value ? : ''), + truncateText: true, + }, ], mitreFlyoutHeaderProperties: [ { @@ -107,34 +131,30 @@ function buildResource(label: string, labelResource: string){ }, { label: 'Name', - id: 'name' + id: 'name', }, { label: 'Created Time', id: 'created_time', - render: (value) => value ? ( - formatUIDate(value) - ) : '' + render: value => (value ? formatUIDate(value) : ''), }, { label: 'Modified Time', id: 'modified_time', - render: (value) => value ? ( - formatUIDate(value) - ) : '' + render: value => (value ? formatUIDate(value) : ''), }, { label: 'Version', - id: 'mitre_version' + id: 'mitre_version', }, ], - } -}; + }; +} export const MitreAttackResources = [ - buildResource('Groups', 'Group'), - buildResource('Mitigations', 'Mitigation'), - buildResource('Software', 'Software'), - buildResource('Tactics', 'Tactic'), - buildResource('Techniques', 'Technique') + buildResource('Groups'), + buildResource('Mitigations'), + buildResource('Software'), + buildResource('Tactics'), + buildResource('Techniques'), ]; diff --git a/plugins/main/public/components/search-bar/index.tsx b/plugins/main/public/components/search-bar/index.tsx index 4a82d5d360..d0538739de 100644 --- a/plugins/main/public/components/search-bar/index.tsx +++ b/plugins/main/public/components/search-bar/index.tsx @@ -9,21 +9,22 @@ import { EuiSelect, EuiText, EuiFlexGroup, - EuiFlexItem + EuiFlexItem, } from '@elastic/eui'; import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; import _ from 'lodash'; import { ISearchBarModeWQL } from './query-language/wql'; +import { SEARCH_BAR_DEBOUNCE_UPDATE_TIME } from '../../../common/constants'; -export interface SearchBarProps{ +export interface SearchBarProps { defaultMode?: string; modes: ISearchBarModeWQL[]; onChange?: (params: any) => void; onSearch: (params: any) => void; - buttonsRender?: () => React.ReactNode + buttonsRender?: () => React.ReactNode; input?: string; -}; +} export const SearchBar = ({ defaultMode, @@ -54,12 +55,16 @@ export const SearchBar = ({ output: undefined, }); // Cache the previous output - const queryLanguageOutputRunPreviousOutput = useRef(queryLanguageOutputRun.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(); + // Debounce update timer + const debounceUpdateSearchBarTimer = useRef(); // Handler when searching const _onSearch = (output: any) => { @@ -79,55 +84,69 @@ export const SearchBar = ({ } }; - const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); + 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 && + 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); + debounceUpdateSearchBarTimer.current && + clearTimeout(debounceUpdateSearchBarTimer.current); + // Debounce the updating of the search bar state + debounceUpdateSearchBarTimer.current = setTimeout(async () => { + 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); + }, SEARCH_BAR_DEBOUNCE_UPDATE_TIME); })(); }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); useEffect(() => { - onChange - // Ensure the previous output is different to the new one - && !_.isEqual(queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current) - && onChange(queryLanguageOutputRun.output); + onChange && + // Ensure the previous output is different to the new one + !_.isEqual( + queryLanguageOutputRun.output, + queryLanguageOutputRunPreviousOutput.current, + ) && + onChange(queryLanguageOutputRun.output); }, [queryLanguageOutputRun.output]); const onQueryLanguagePopoverSwitch = () => @@ -163,7 +182,7 @@ export const SearchBar = ({ closePopover={onQueryLanguagePopoverSwitch} > SYNTAX OPTIONS -
+
{searchBarQueryLanguages[queryLanguage.id].description} @@ -173,7 +192,8 @@ export const SearchBar = ({
) => { + onChange={( + event: React.ChangeEvent, + ) => { const queryLanguageID: string = event.target.value; setQueryLanguage({ id: queryLanguageID, @@ -214,16 +236,28 @@ export const SearchBar = ({ } {...queryLanguageOutputRun.searchBarProps} + {...(queryLanguageOutputRun.searchBarProps?.onItemClick + ? { + onItemClick: + queryLanguageOutputRun.searchBarProps?.onItemClick(input), + } + : {})} /> ); - return rest.buttonsRender || queryLanguageOutputRun.filterButtons - ? ( - - {searchBar} - {rest.buttonsRender && {rest.buttonsRender()}} - {queryLanguageOutputRun.filterButtons && {queryLanguageOutputRun.filterButtons}} - - ) - : searchBar; + 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/aql.test.tsx b/plugins/main/public/components/search-bar/query-language/aql.test.tsx index a5f7c7d36c..3c6a57caf3 100644 --- a/plugins/main/public/components/search-bar/query-language/aql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/aql.test.tsx @@ -15,27 +15,25 @@ describe('SearchBar component', () => { field(currentValue) { return []; }, - value(currentValue, { previousField }){ + value(currentValue, { previousField }) { return []; }, }, - } + }, ], /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, - onSearch: () => {} + onSearch: () => {}, /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly to match the snapshot of query language', async () => { - const wrapper = render( - - ); + const wrapper = render(); await waitFor(() => { - const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); + const elementImplicitQuery = wrapper.container.querySelector( + '.euiCodeBlock__code', + ); expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); expect(wrapper.container).toMatchSnapshot(); }); @@ -45,32 +43,32 @@ describe('SearchBar component', () => { 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}) => { + 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); }); @@ -127,79 +125,87 @@ describe('Query language - AQL', () => { // 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; + 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); - }); + 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); - }); + 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 index 8c898af3e2..68d1292a23 100644 --- a/plugins/main/public/components/search-bar/query-language/aql.tsx +++ b/plugins/main/public/components/search-bar/query-language/aql.tsx @@ -71,28 +71,27 @@ const suggestionMappingLanguageTokenType = { /** * Creator of intermediate interface of EuiSuggestItem - * @param type - * @returns + * @param type + * @returns */ -function mapSuggestionCreator(type: ITokenType ){ - return function({...params}){ +function mapSuggestionCreator(type: ITokenType) { + return function ({ ...params }) { return { type, - ...params + ...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{ +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( @@ -118,44 +117,50 @@ export function tokenizer(input: string): ITokens{ // 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' + // 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(); -}; + 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 + description?: string; + label: string; }; -type QLOptionSuggestionEntityItemTyped = - QLOptionSuggestionEntityItem - & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; +type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { + type: + | 'operator_group' + | 'field' + | 'operator_compare' + | 'value' + | 'conjunction'; +}; type SuggestItem = QLOptionSuggestionEntityItem & { - type: { iconType: string, color: string } + type: { iconType: string; color: string }; }; type QLOptionSuggestionHandler = ( @@ -179,15 +184,11 @@ type optionsQL = { * @param tokenType token type to search * @returns */ -function getLastTokenWithValue( - tokens: ITokens -): IToken | undefined { +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, - ); + const tokenFound = shallowCopyArrayReversed.find(({ value }) => value); return tokenFound; } @@ -218,7 +219,10 @@ function getLastTokenWithValueByType( * @param options * @returns */ -export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { +export async function getSuggestions( + tokens: ITokens, + options: optionsQL, +): Promise { if (!tokens.length) { return []; } @@ -227,40 +231,42 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi const lastToken = getLastTokenWithValue(tokens); // If it can't get a token with value, then returns fields and open operator group - if(!lastToken?.type){ - return [ + 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), + ...(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], - }), - ), - ] + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] : []), ]; break; @@ -281,14 +287,17 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi operator => operator === lastToken.value, ) ? [ - ...(await options.suggestions.value(undefined, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })).map(mapSuggestionCreatorValue), - ] + ...( + await options.suggestions.value(undefined, { + previousField: getLastTokenWithValueByType(tokens, 'field')! + .value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(mapSuggestionCreatorValue), + ] : []), ]; break; @@ -296,22 +305,24 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi return [ ...(lastToken.value ? [ - { - type: 'function_search', - label: 'Search', - description: 'run the search query', - }, - ] + { + 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), + ...( + 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]) => ({ + ([conjunction, description]) => ({ type: 'conjunction', label: conjunction, description, @@ -342,8 +353,10 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi conjunction => conjunction === lastToken.value, ) ? [ - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ] + ...(await options.suggestions.field()).map( + mapSuggestionCreatorField, + ), + ] : []), { type: 'operator_group', @@ -381,16 +394,18 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi /** * Transform the suggestion object to the expected object by EuiSuggestItem - * @param param0 - * @returns + * @param param0 + * @returns */ -export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ - const { type, ...rest} = suggestion; +export function transformSuggestionToEuiSuggestItem( + suggestion: QLOptionSuggestionEntityItemTyped, +): SuggestItem { + const { type, ...rest } = suggestion; return { type: { ...suggestionMappingLanguageTokenType[type] }, - ...rest + ...rest, }; -}; +} /** * Transform the suggestion object to the expected object by EuiSuggestItem @@ -398,24 +413,26 @@ export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggesti * @returns */ function transformSuggestionsToEuiSuggestItem( - suggestions: QLOptionSuggestionEntityItemTyped[] + 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}`; +function getOutput(input: string, options: { implicitQuery?: string } = {}) { + const unifiedQuery = `${options?.implicitQuery ?? ''}${ + options?.implicitQuery ? `(${input})` : input + }`; return { language: AQL.id, query: unifiedQuery, - unifiedQuery + unifiedQuery, }; -}; +} export const AQL = { id: 'aql', @@ -436,21 +453,24 @@ export const AQL = { // Props that will be used by the EuiSuggest component // Suggestions suggestions: transformSuggestionsToEuiSuggestItem( - await getSuggestions(tokens, params.queryLanguage.parameters) + await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: item => { + onItemClick: currentInput => 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)); + params.onSearch( + getOutput(currentInput, 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 + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType ) { // replace the value of last token lastToken.value = item.label; @@ -462,15 +482,17 @@ export const AQL = { )[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('')); + 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 ? ( @@ -512,7 +534,7 @@ export const AQL = { // 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 + disableFocusTrap: true, }, output: getOutput(input, params.queryLanguage.parameters), }; 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 index 4de5de790b..2d2a1b3171 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -303,9 +303,9 @@ describe('Query language - WQL', () => { ${'(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)'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2 )'} `( - 'click suggestion - WQL $WQL => $changedInput', + 'click suggestion - WQL "$WQL" => "$changedInput"', async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { // Mock input let input = currentInput; @@ -324,7 +324,7 @@ describe('Query language - WQL', () => { }, }, }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); expect(input).toEqual(changedInput); }, ); diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 9df7dbbf01..539f90a076 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -7,7 +7,10 @@ import { EuiCode, } from '@elastic/eui'; import { tokenizer as tokenizerUQL } from './aql'; -import { PLUGIN_VERSION } from '../../../../common/constants'; +import { + PLUGIN_VERSION, + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT, +} from '../../../../common/constants'; /* UI Query language https://documentation.wazuh.com/current/user-manual/api/queries.html @@ -99,10 +102,14 @@ const suggestionMappingLanguageTokenType = { * @returns */ function mapSuggestionCreator(type: ITokenType) { - return function ({ ...params }) { + return function ({ label, ...params }) { return { type, ...params, + /* WORKAROUND: ensure the label is a string. If it is not a string, an warning is + displayed in the console related to prop types + */ + ...(typeof label !== 'undefined' ? { label: String(label) } : {}), }; }; } @@ -314,6 +321,37 @@ function getTokenNearTo( ); } +/** + * It returns the regular expression that validate the token of type value + * @returns The regular expression + */ +function getTokenValueRegularExpression() { + return new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', + ); +} + +/** + * It filters the values that matche the validation regular expression and returns the first items + * defined by SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT constant. + * @param suggestions Suggestions provided by the suggestions.value method of each instance of the + * search bar + * @returns + */ +function filterTokenValueSuggestion( + suggestions: QLOptionSuggestionEntityItemTyped[], +) { + return suggestions + .filter(({ label }: QLOptionSuggestionEntityItemTyped) => { + const re = getTokenValueRegularExpression(); + return re.test(label); + }) + .slice(0, SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT); +} + /** * Get the suggestions from the tokens * @param tokens @@ -407,11 +445,18 @@ export async function getSuggestions( operator => operator === lastToken.value, ) ? [ - ...( + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( await options.suggestions.value(undefined, { field, operatorCompare, - }) + }), ).map(mapSuggestionCreatorValue), ] : []), @@ -441,11 +486,18 @@ export async function getSuggestions( }, ] : []), - ...( + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( await options.suggestions.value(lastToken.formattedValue, { field, operatorCompare, - }) + }), ).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( ([conjunction, description]) => ({ @@ -685,12 +737,7 @@ function getOutput(input: string, options: OptionsQL) { * @returns */ function validateTokenValue(token: IToken): string | undefined { - const re = new RegExp( - // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', - ); + const re = getTokenValueRegularExpression(); const value = token.formattedValue ?? token.value; const match = value.match(re); @@ -959,7 +1006,7 @@ export const WQL = { error: validationStrict, }; - const onSearch = () => { + const onSearch = output => { if (output?.error) { params.setQueryLanguageOutput(state => ({ ...state, @@ -972,6 +1019,7 @@ export const WQL = { description: error, })), ), + isInvalid: true, }, })); } else { @@ -1024,7 +1072,7 @@ export const WQL = { : await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: item => { + onItemClick: currentInput => item => { // There is an error, clicking on the item does nothing if (item.type.iconType === 'alert') { return; @@ -1032,7 +1080,18 @@ export const WQL = { // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action - onSearch(); + // Get the tokens from the input + const tokens: ITokens = tokenizer(currentInput); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(currentInput, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); } else { // When the clicked item has another iconType const lastToken: IToken | undefined = getLastTokenDefined(tokens); @@ -1051,10 +1110,15 @@ export const WQL = { : item.label; } else { // add a whitespace for conjunction + // add a whitespace for grouping operator ) !/\s$/.test(input) && (item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType || - lastToken?.type === 'conjunction') && + lastToken?.type === 'conjunction' || + (item.type.iconType === + suggestionMappingLanguageTokenType.operator_group + .iconType && + item.label === ')')) && tokens.push({ type: 'whitespace', value: ' ', @@ -1134,7 +1198,19 @@ export const WQL = { // Define the handler when the a key is pressed while the input is focused onKeyPress: event => { if (event.key === 'Enter') { - onSearch(); + // Get the tokens from the input + const input = event.currentTarget.value; + const tokens: ITokens = tokenizer(input); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); } }, }, diff --git a/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx index d49792d817..11f4fc4854 100644 --- a/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx +++ b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx @@ -13,12 +13,22 @@ import React, { useState } from 'react'; import { TableWzAPI } from '../../../../../../components/common/tables'; import { getToasts } from '../../../../../../kibana-services'; -import { resourceDictionary, ResourcesConstants, ResourcesHandler } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesConstants, + ResourcesHandler, +} from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + UI_LOGGER_LEVELS, +} from '../../../../../../../common/constants'; -import { SECTION_CDBLIST_SECTION, SECTION_CDBLIST_KEY } from '../../common/constants'; +import { + SECTION_CDBLIST_SECTION, + SECTION_CDBLIST_KEY, +} from '../../common/constants'; import CDBListsColumns from './columns'; import { withUserPermissions } from '../../../../../../components/common/hocs/withUserPermissions'; @@ -29,43 +39,50 @@ import { AddNewFileButton, AddNewCdbListButton, UploadFilesButton, -} from '../../common/actions-buttons' +} from '../../common/actions-buttons'; +import { WzRequest } from '../../../../../../react-services'; + +const searchBarWQLOptions = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/lists', + label: 'Custom lists', + }, + ], +}; function CDBListsTable(props) { - const [filters, setFilters] = useState([]); const [showingFiles, setShowingFiles] = useState(false); const [tableFootprint, setTableFootprint] = useState(0); const resourcesHandler = new ResourcesHandler(ResourcesConstants.LISTS); - const updateFilters = (filters) => { - setFilters(filters); - } - const toggleShowFiles = () => { setShowingFiles(!showingFiles); - } - + }; const getColumns = () => { const cdblistsColumns = new CDBListsColumns({ removeItems: removeItems, state: { section: SECTION_CDBLIST_KEY, - defaultItems: [] - }, ...props + defaultItems: [], + }, + ...props, }).columns; const columns = cdblistsColumns[SECTION_CDBLIST_KEY]; return columns; - } + }; /** * Columns and Rows properties */ - const getRowProps = (item) => { + const getRowProps = item => { const { id, name } = item; - const getRequiredPermissions = (item) => { + const getRequiredPermissions = item => { const { permissionResource } = resourceDictionary[SECTION_CDBLIST_KEY]; return [ { @@ -80,17 +97,17 @@ function CDBListsTable(props) { className: 'customRowClass', onClick: !WzUserPermissions.checkMissingUserPermissions( getRequiredPermissions(item), - props.userPermissions + props.userPermissions, ) - ? async (ev) => { - const result = await resourcesHandler.getFileContent(item.filename); - const file = { - name: item.filename, - content: result, - path: item.relative_dirname, - }; - updateListContent(file); - } + ? async ev => { + const result = await resourcesHandler.getFileContent(item.filename); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + updateListContent(file); + } : undefined, }; }; @@ -98,13 +115,13 @@ function CDBListsTable(props) { /** * Remove files method */ - const removeItems = async (items) => { + const removeItems = async items => { try { const results = items.map(async (item, i) => { await resourcesHandler.deleteFile(item.filename || item.name); }); - Promise.all(results).then((completed) => { + Promise.all(results).then(completed => { setTableFootprint(Date.now()); getToasts().add({ color: 'success', @@ -126,7 +143,7 @@ function CDBListsTable(props) { }; getErrorOrchestrator().handleError(options); } - } + }; const { updateRestartClusterManager, updateListContent } = props; const columns = getColumns(); @@ -153,38 +170,63 @@ function CDBListsTable(props) { { updateRestartClusterManager && updateRestartClusterManager() }} + onSuccess={() => { + updateRestartClusterManager && updateRestartClusterManager(); + }} />, ]; - - return ( -
+
{ + try { + const response = await WzRequest.apiReq('GET', '/lists', { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} endpoint={'/lists'} isExpandable={true} rowProps={getRowProps} - downloadCsv={true} - showReload={true} - filters={filters} - onFiltersChange={updateFilters} + downloadCsv + showReload tablePageSizeOptions={[10, 25, 50, 100]} />
); - } - -export default compose( - withUserPermissions -)(CDBListsTable); +export default compose(withUserPermissions)(CDBListsTable); diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx b/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx index 9e3e160425..a8b24717c2 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx @@ -1,5 +1,9 @@ import React from 'react'; -import { resourceDictionary, ResourcesHandler, ResourcesConstants } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesHandler, + ResourcesConstants, +} from '../../common/resources-handler'; import { WzButtonPermissions } from '../../../../../../components/common/permissions/button'; import { WzButtonPermissionsModalConfirm } from '../../../../../../components/common/buttons'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; @@ -7,9 +11,7 @@ import { UIErrorLog } from '../../../../../../react-services/error-orchestrator/ import { getErrorOptions } from '../../common/error-helper'; import { Columns } from '../../common/interfaces'; - export default class DecodersColumns { - columns: Columns = {}; constructor(props) { @@ -24,19 +26,19 @@ export default class DecodersColumns { field: 'name', name: 'Name', align: 'left', - sortable: true + sortable: true, }, { field: 'details.program_name', name: 'Program name', align: 'left', - sortable: false + sortable: false, }, { field: 'details.order', name: 'Order', align: 'left', - sortable: false + sortable: false, }, { field: 'filename', @@ -49,39 +51,52 @@ export default class DecodersColumns { buttonType='link' permissions={getReadButtonPermissions(item)} tooltip={{ position: 'top', content: `Show ${value} content` }} - onClick={async (ev) => { + onClick={async ev => { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.DECODERS, + ); const result = await resourcesHandler.getFileContent(value); - const file = { name: value, content: result, path: item.relative_dirname }; + const file = { + name: value, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Decoders.readFileContent' + 'Decoders.readFileContent', ); getErrorOrchestrator().handleError(options); } - }}> + }} + > {value} ); - } + }, }, { field: 'relative_dirname', name: 'Path', align: 'left', - sortable: true - } + sortable: true, + }, ], files: [ { field: 'filename', name: 'File', align: 'left', - sortable: true + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, }, { name: 'Actions', @@ -92,25 +107,36 @@ export default class DecodersColumns { { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.DECODERS, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Decoders.readFileContent' + 'Decoders.readFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> ); } else { @@ -119,44 +145,58 @@ export default class DecodersColumns { { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.DECODERS, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.editFileContent' + 'Files.editFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> { try { this.props.removeItems([item]); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.deleteFile' + 'Files.deleteFile', ); getErrorOrchestrator().handleError(options); } }} - color="danger" + color='danger' modalTitle={'Are you sure?'} modalProps={{ buttonColor: 'danger', @@ -165,13 +205,14 @@ export default class DecodersColumns {
); } - } - } - ] + }, + }, + ], }; - const getReadButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.DECODERS]; + const getReadButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.DECODERS]; return [ { action: `${ResourcesConstants.DECODERS}:read`, @@ -180,19 +221,24 @@ export default class DecodersColumns { ]; }; - const getEditButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.DECODERS]; + const getEditButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.DECODERS]; return [ { action: `${ResourcesConstants.DECODERS}:read`, resource: permissionResource(item.filename), }, - { action: `${ResourcesConstants.DECODERS}:update`, resource: permissionResource(item.filename) }, + { + action: `${ResourcesConstants.DECODERS}:update`, + resource: permissionResource(item.filename), + }, ]; }; - const getDeleteButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.DECODERS]; + const getDeleteButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.DECODERS]; return [ { action: `${ResourcesConstants.DECODERS}:delete`, @@ -200,6 +246,5 @@ export default class DecodersColumns { }, ]; }; - } } diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index fbd4ed6999..d81f0e825c 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -1,44 +1,141 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../../common/constants'; import { WzRequest } from '../../../../../../react-services/wz-request'; -const decodersItems = [ - { - type: 'params', - label: 'filename', - description: 'Filters the decoders by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/decoders/files', filter); - return (((result || {}).data || {}).data || {}).affected_items.map((item) => { return item.filename }); - }, +const decodersItems = { + field(currentValue) { + return [ + { label: 'details.order', description: 'filter by program name' }, + { label: 'details.program_name', description: 'filter by program name' }, + { label: 'filename', description: 'filter by filename' }, + { label: 'name', description: 'filter by name' }, + { label: 'relative_dirname', description: 'filter by relative path' }, + ]; }, - { - type: 'params', - label: 'relative_dirname', - description: 'Path of the decoders files.', - values: async () => { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'decoder_dir' + value: async (currentValue, { field }) => { + try { + switch (field) { + case 'details.order': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return ( + result?.data?.data?.affected_items + // There are some affected items that doesn't return any value for the selected property + ?.filter(item => typeof item?.details?.order === 'string') + ?.map(item => ({ + label: item?.details?.order, + })) + ); + } + case 'details.program_name': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + // FIX: this breaks the search bar component because returns a non-string value. + return result?.data?.data?.affected_items + ?.filter(item => typeof item?.details?.program_name === 'string') + .map(item => ({ + label: item?.details?.program_name, + })); + } + case 'filename': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } + case 'name': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); } + case 'relative_dirname': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); + } + default: { + return []; + } + } + } catch (error) { + return []; + } + }, +}; + +const decodersFiles = { + field(currentValue) { + return [ + { label: 'filename', description: 'filter by filename' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + ]; + }, + value: async (currentValue, { field }) => { + try { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders/files', { + params: filter, }); - return (((result || {}).data || {}).data || {}).affected_items[0].ruleset.decoder_dir; + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; } }, - { - type: 'params', - label: 'status', - description: 'Filters the decoders by status.', - values: ['enabled', 'disabled'] - } -]; +}; const apiSuggestsItems = { items: decodersItems, - files: [], + files: decodersFiles, }; -export default apiSuggestsItems; \ No newline at end of file +export default apiSuggestsItems; diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx index fe7fa07d4f..f626e875d1 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx @@ -13,7 +13,11 @@ import React, { useState, useCallback } from 'react'; import { TableWzAPI } from '../../../../../../components/common/tables'; import { getToasts } from '../../../../../../kibana-services'; -import { resourceDictionary, ResourcesConstants, ResourcesHandler } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesConstants, + ResourcesHandler, +} from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; @@ -23,48 +27,79 @@ import { FlyoutDetail } from './flyout-detail'; import { withUserPermissions } from '../../../../../../components/common/hocs/withUserPermissions'; import { WzUserPermissions } from '../../../../../../react-services/wz-user-permissions'; import { compose } from 'redux'; -import { SECTION_DECODERS_SECTION, SECTION_DECODERS_KEY } from '../../common/constants'; +import { + SECTION_DECODERS_SECTION, + SECTION_DECODERS_KEY, +} from '../../common/constants'; import { ManageFiles, AddNewFileButton, UploadFilesButton, -} from '../../common/actions-buttons' +} from '../../common/actions-buttons'; import apiSuggestsItems from './decoders-suggestions'; +const searchBarWQLOptions = { + searchTermFields: [ + 'details.order', + 'details.program_name', + 'filename', + 'name', + 'relative_dirname', + ], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/decoders', + label: 'Custom decoders', + }, + ], +}; + +const searchBarWQLOptionsFiles = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], +}; + /*************************************** * Render tables */ const FilesTable = ({ actionButtons, - buttonOptions, columns, searchBarSuggestions, filters, - updateFilters, - reload -}) => ( + +); const DecodersFlyoutTable = ({ actionButtons, - buttonOptions, columns, searchBarSuggestions, getRowProps, @@ -75,23 +110,25 @@ const DecodersFlyoutTable = ({ closeFlyout, cleanFilters, ...props -}) => <> +}) => ( + <> {isFlyoutVisible && ( @@ -107,13 +144,16 @@ const DecodersFlyoutTable = ({ /> )} +); /*************************************** * Main component */ -export default compose( - withUserPermissions -)(function DecodersTable({ setShowingFiles, showingFiles, ...props }) { +export default compose(withUserPermissions)(function DecodersTable({ + setShowingFiles, + showingFiles, + ...props +}) { const [filters, setFilters] = useState([]); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [currentItem, setCurrentItem] = useState(null); @@ -121,26 +161,23 @@ export default compose( const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - // Table custom filter options - const buttonOptions = [{ label: "Custom decoders", field: "relative_dirname", value: "etc/decoders" },]; - - const updateFilters = (filters) => { + const updateFilters = filters => { setFilters(filters); - } + }; const cleanFilters = () => { setFilters([]); - } + }; const toggleShowFiles = () => { setFilters([]); setShowingFiles(!showingFiles); - } + }; const closeFlyout = () => { setIsFlyoutVisible(false); setCurrentItem(null); - } + }; /** * Columns and Rows properties @@ -149,15 +186,17 @@ export default compose( const decodersColumns = new DecodersColumns({ removeItems: removeItems, state: { - section: SECTION_DECODERS_KEY - }, ...props + section: SECTION_DECODERS_KEY, + }, + ...props, }).columns; - const columns = decodersColumns[showingFiles ? 'files' : SECTION_DECODERS_KEY]; + const columns = + decodersColumns[showingFiles ? 'files' : SECTION_DECODERS_KEY]; return columns; - } + }; - const getRowProps = (item) => { - const getRequiredPermissions = (item) => { + const getRowProps = item => { + const getRequiredPermissions = item => { const { permissionResource } = resourceDictionary[SECTION_DECODERS_KEY]; return [ { @@ -172,12 +211,12 @@ export default compose( className: 'customRowClass', onClick: !WzUserPermissions.checkMissingUserPermissions( getRequiredPermissions(item), - props.userPermissions + props.userPermissions, ) ? () => { - setCurrentItem(item) - setIsFlyoutVisible(true); - } + setCurrentItem(item); + setIsFlyoutVisible(true); + } : undefined, }; }; @@ -185,13 +224,13 @@ export default compose( /** * Remove files method */ - const removeItems = async (items) => { + const removeItems = async items => { try { const results = items.map(async (item, i) => { await resourcesHandler.deleteFile(item.filename || item.name); }); - Promise.all(results).then((completed) => { + Promise.all(results).then(completed => { setTableFootprint(Date.now()); getToasts().add({ color: 'success', @@ -213,7 +252,7 @@ export default compose( }; getErrorOrchestrator().handleError(options); } - } + }; const { updateRestartClusterManager, updateFileContent } = props; const columns = getColumns(); @@ -235,44 +274,45 @@ export default compose( />, ]; if (showingFiles) - buttons.push( { updateRestartClusterManager && updateRestartClusterManager() }} - />); + buttons.push( + { + updateRestartClusterManager && updateRestartClusterManager(); + }} + />, + ); return buttons; }, [showingFiles]); const actionButtons = buildActionButtons(); return ( -
+
{showingFiles ? ( ) : ( - - )} + + )}
); }); diff --git a/plugins/main/public/controllers/management/components/management/decoders/views/decoder-info.tsx b/plugins/main/public/controllers/management/components/management/decoders/views/decoder-info.tsx index 5e9446c259..bc5e6a39ac 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/views/decoder-info.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/views/decoder-info.tsx @@ -129,7 +129,7 @@ export default class WzDecoderInfo extends Component { - this.setNewFiltersAndBack([{ field: 'filename', value: file }]) + this.setNewFiltersAndBack({q: `filename=${file}`}) } >  {file} @@ -143,7 +143,7 @@ export default class WzDecoderInfo extends Component { - this.setNewFiltersAndBack([{ field: 'relative_dirname', value: path }]) + this.setNewFiltersAndBack({q: `relative_dirname=${path}`}) } >  {path} @@ -328,7 +328,7 @@ export default class WzDecoderInfo extends Component { {currentDecoder?.filename && `); + let newValue = oldValue.replace( + '$(', + ``, + ); newValue = newValue.replace(')', ' '); value = value.replace(oldValue, newValue); } } return (
- {haveTooltip === false ? - : - + {haveTooltip === false ? ( + + ) : ( + - } + )}
); - } + }, }, { field: 'groups', name: 'Groups', align: 'left', sortable: false, - width: '10%' + width: '10%', }, { name: 'Regulatory compliance', - render: this.buildComplianceBadges + render: this.buildComplianceBadges, }, { field: 'level', name: 'Level', align: 'left', sortable: true, - width: '5%' + width: '5%', }, { field: 'filename', @@ -91,40 +97,54 @@ export default class RulesetColumns { buttonType='link' permissions={getReadButtonPermissions(item)} tooltip={{ position: 'top', content: `Show ${value} content` }} - onClick={async (ev) => { - try{ + onClick={async ev => { + try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.RULES); + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.RULES, + ); const result = await resourcesHandler.getFileContent(value); - const file = { name: value, content: result, path: item.relative_dirname }; + const file = { + name: value, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); - }catch(error){ + } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Rules.readFileContent' + 'Rules.readFileContent', ); getErrorOrchestrator().handleError(options); } - }}> + }} + > {value} ); - } + }, }, { field: 'relative_dirname', name: 'Path', align: 'left', sortable: true, - width: '10%' - } + width: '10%', + }, ], files: [ { field: 'filename', name: 'File', align: 'left', - sortable: true + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + width: '10%', }, { name: 'Actions', @@ -135,25 +155,36 @@ export default class RulesetColumns { { - try{ + try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(this.props.state.section); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + this.props.state.section, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); - }catch(error){ + } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.readFileContent' + 'Files.readFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> ); } else { @@ -162,44 +193,58 @@ export default class RulesetColumns { { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.RULES); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.RULES, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.editFileContent' + 'Files.editFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> { try { this.props.removeItems([item]); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.deleteFile' + 'Files.deleteFile', ); getErrorOrchestrator().handleError(options); } }} - color="danger" + color='danger' modalTitle={'Are you sure?'} modalProps={{ buttonColor: 'danger', @@ -208,13 +253,14 @@ export default class RulesetColumns {
); } - } - } - ] + }, + }, + ], }; - const getReadButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.RULES]; + const getReadButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.RULES]; return [ { action: `${ResourcesConstants.RULES}:read`, @@ -223,19 +269,24 @@ export default class RulesetColumns { ]; }; - const getEditButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.RULES]; + const getEditButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.RULES]; return [ { action: `${ResourcesConstants.RULES}:read`, resource: permissionResource(item.filename), }, - { action: `${ResourcesConstants.RULES}:update`, resource: permissionResource(item.filename) }, + { + action: `${ResourcesConstants.RULES}:update`, + resource: permissionResource(item.filename), + }, ]; }; - const getDeleteButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.RULES]; + const getDeleteButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.RULES]; return [ { action: `${ResourcesConstants.RULES}:delete`, @@ -247,18 +298,25 @@ export default class RulesetColumns { buildComplianceBadges(item) { const badgeList = []; - const fields = ['pci_dss', 'gpg13', 'hipaa', 'gdpr', 'nist_800_53', 'tsc', 'mitre']; + const fields = [ + 'pci_dss', + 'gpg13', + 'hipaa', + 'gdpr', + 'nist_800_53', + 'tsc', + 'mitre', + ]; const buildBadge = field => { - return ( ev.stopPropagation()} onClickAriaLabel={field.toUpperCase()} style={{ margin: '1px 2px' }} @@ -274,7 +332,7 @@ export default class RulesetColumns { badgeList.push(buildBadge(field)); } } - } catch (error) { } + } catch (error) {} return
{badgeList}
; } diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts index 84e4115185..aa6486bdb4 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts @@ -1,137 +1,208 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../../common/constants'; import { WzRequest } from '../../../../../../react-services/wz-request'; -const rulesItems = [ - { - type: 'params', - label: 'status', - description: 'Filters the rules by status.', - values: ['enabled', 'disabled'] +const rulesItems = { + field(currentValue) { + return [ + { label: 'id', description: 'filter by ID' }, + { label: 'filename', description: 'filter by filename' }, + { label: 'gdpr', description: 'filter by GDPR requirement' }, + { label: 'gpg13', description: 'filter by GPG requirement' }, + { label: 'groups', description: 'filter by group' }, + { label: 'hipaa', description: 'filter by HIPAA requirement' }, + { label: 'level', description: 'filter by level' }, + { label: 'mitre', description: 'filter by MITRE ATT&CK requirement' }, + { label: 'nist_800_53', description: 'filter by NIST requirement' }, + { label: 'pci_dss', description: 'filter by PCI DSS requirement' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + { label: 'status', description: 'filter by status' }, + { label: 'tsc', description: 'filter by TSC requirement' }, + ]; }, - { - type: 'params', - label: 'group', - description: 'Filters the rules by group', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/rules/groups', filter); - return result?.data?.data?.affected_items; - }, - }, - { - type: 'params', - label: 'level', - description: 'Filters the rules by level', - values: [...Array(16).keys()] - }, - { - type: 'params', - label: 'filename', - description: 'Filters the rules by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => { return item.filename }); - }, - }, - { - type: 'params', - label: 'relative_dirname', - description: 'Path of the rules files', - values: async () => { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'rule_dir' + value: async (currentValue, { field }) => { + try { + switch (field) { + case 'id': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `id~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules', { + params: filter, + }); + return result?.data?.data?.affected_items.map(label => ({ + label: label[field], + })); } - }); - return result?.data?.data?.affected_items?.[0].ruleset.rule_dir; - } - }, - { - type: 'params', - label: 'hipaa', - description: 'Filters the rules by HIPAA requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/hipaa', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'gdpr', - description: 'Filters the rules by GDPR requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gdpr', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'nist-800-53', - description: 'Filters the rules by NIST requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/nist-800-53', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'gpg13', - description: 'Filters the rules by GPG requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gpg13', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'pci_dss', - description: 'Filters the rules by PCI DSS requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/pci_dss', {}); - return result?.data?.data?.affected_items; + case 'status': { + return ['enabled', 'disabled'].map(label => ({ label })); + } + case 'groups': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/groups', { + params: filter, + }); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'level': { + return [...Array(16).keys()].map(label => ({ label })); + } + case 'filename': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } + case 'relative_dirname': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules', { + params: filter, + }); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); + } + case 'hipaa': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/hipaa', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'gdpr': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/gdpr', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'nist_800_53': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/nist-800-53', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'gpg13': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/gpg13', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'pci_dss': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/pci_dss', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'tsc': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/tsc', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'mitre': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/mitre', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + default: + return []; + } + } catch (error) { + return []; } }, - { - type: 'params', - label: 'tsc', - description: 'Filters the rules by TSC requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/tsc', {}); - return result?.data?.data?.affected_items; - } +}; + +const rulesFiles = { + field(currentValue) { + return [ + { label: 'filename', description: 'filter by filename' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + ]; }, - { - type: 'params', - label: 'mitre', - description: 'Filters the rules by MITRE requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/mitre', {}); - return result?.data?.data?.affected_items; + value: async (currentValue, { field }) => { + try { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; } - } -]; -const rulesFiles = [ - { - type: 'params', - label: 'filename', - description: 'Filters the rules by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => { return item.filename }); - }, }, -]; +}; const apiSuggestsItems = { items: rulesItems, diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx index 270fe3bb69..2358773423 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx @@ -12,13 +12,20 @@ import React, { useEffect, useState, useCallback } from 'react'; import { getToasts } from '../../../../../../kibana-services'; -import { resourceDictionary, ResourcesConstants, ResourcesHandler } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesConstants, + ResourcesHandler, +} from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; import { TableWzAPI } from '../../../../../../components/common/tables'; -import { SECTION_RULES_SECTION, SECTION_RULES_KEY } from '../../common/constants'; +import { + SECTION_RULES_SECTION, + SECTION_RULES_KEY, +} from '../../common/constants'; import RulesetColumns from './columns'; import { FlyoutDetail } from './flyout-detail'; import { withUserPermissions } from '../../../../../../components/common/hocs/withUserPermissions'; @@ -28,10 +35,45 @@ import { ManageFiles, AddNewFileButton, UploadFilesButton, -} from '../../common/actions-buttons' +} from '../../common/actions-buttons'; import apiSuggestsItems from './ruleset-suggestions'; +const searchBarWQLOptions = { + searchTermFields: [ + 'id', + 'description', + 'filename', + 'gdpr', + 'gpg13', + 'groups', + 'level', + 'mitre', + 'nist_800_53', + 'pci_dss', + 'relative_dirname', + 'tsc', + ], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], +}; + +const searchBarWQLOptionsFiles = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], +}; + /*************************************** * Render tables */ @@ -42,25 +84,28 @@ const FilesTable = ({ searchBarSuggestions, filters, updateFilters, - reload -}) => ( + +); const RulesFlyoutTable = ({ actionButtons, @@ -75,23 +120,25 @@ const RulesFlyoutTable = ({ closeFlyout, cleanFilters, ...props -}) => <> +}) => ( + <> {isFlyoutVisible && ( @@ -107,6 +154,7 @@ const RulesFlyoutTable = ({ /> )} +); /*************************************** * Main component @@ -124,42 +172,43 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { const regex = new RegExp('redirectRule=' + '[^&]*'); const match = window.location.href.match(regex); if (match && match[0]) { - setCurrentItem(parseInt(match[0].split('=')[1])) - setIsFlyoutVisible(true) + setCurrentItem(parseInt(match[0].split('=')[1])); + setIsFlyoutVisible(true); } - }, []) + }, []); // Table custom filter options - const buttonOptions = [{ label: "Custom rules", field: "relative_dirname", value: "etc/rules" },]; + const buttonOptions = [ + { label: 'Custom rules', field: 'relative_dirname', value: 'etc/rules' }, + ]; - const updateFilters = (filters) => { + const updateFilters = filters => { setFilters(filters); - } + }; const cleanFilters = () => { setFilters([]); - } + }; const toggleShowFiles = () => { setFilters([]); setShowingFiles(!showingFiles); - } + }; const closeFlyout = () => { setIsFlyoutVisible(false); - } - + }; /** * Remove files method */ - const removeItems = async (items) => { + const removeItems = async items => { try { const results = items.map(async (item, i) => { await resourcesHandler.deleteFile(item.filename || item.name); }); - Promise.all(results).then((completed) => { + Promise.all(results).then(completed => { setTableFootprint(Date.now()); getToasts().add({ color: 'success', @@ -181,7 +230,7 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { }; getErrorOrchestrator().handleError(options); } - } + }; /** * Columns and Rows properties @@ -190,17 +239,18 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { const rulesetColumns = new RulesetColumns({ removeItems: removeItems, state: { - section: SECTION_RULES_KEY - }, ...props + section: SECTION_RULES_KEY, + }, + ...props, }).columns; const columns = rulesetColumns[showingFiles ? 'files' : SECTION_RULES_KEY]; return columns; - } + }; - const getRowProps = (item) => { + const getRowProps = item => { const { id, name } = item; - const getRequiredPermissions = (item) => { + const getRequiredPermissions = item => { const { permissionResource } = resourceDictionary[SECTION_RULES_KEY]; return [ { @@ -215,12 +265,12 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { className: 'customRowClass', onClick: !WzUserPermissions.checkMissingUserPermissions( getRequiredPermissions(item), - props.userPermissions + props.userPermissions, ) - ? (item) => { - setCurrentItem(id) - setIsFlyoutVisible(true); - } + ? item => { + setCurrentItem(id); + setIsFlyoutVisible(true); + } : undefined, }; }; @@ -245,18 +295,22 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { />, ]; if (showingFiles) - buttons.push( { updateRestartClusterManager && updateRestartClusterManager() }} - />); + buttons.push( + { + updateRestartClusterManager && updateRestartClusterManager(); + }} + />, + ); return buttons; }, [showingFiles]); const actionButtons = buildActionButtons(); return ( -
+
{showingFiles ? ( ) : ( - - )} + + )}
); - } - -export default compose( - withUserPermissions -)(RulesetTable); +export default compose(withUserPermissions)(RulesetTable); diff --git a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx index 3c95a77f3f..c2c70e3084 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx @@ -18,7 +18,10 @@ import { import { WzRequest } from '../../../../../../react-services/wz-request'; -import { ResourcesHandler, ResourcesConstants } from '../../common/resources-handler'; +import { + ResourcesHandler, + ResourcesConstants, +} from '../../common/resources-handler'; import WzTextWithTooltipTruncated from '../../../../../../components/common/wz-text-with-tooltip-if-truncated'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; @@ -46,7 +49,7 @@ export default class WzRuleInfo extends Component { mitreRuleId: '', mitreIds: [], currentRuleInfo: {}, - isLoading: true + isLoading: true, }; this.resourcesHandler = new ResourcesHandler(ResourcesConstants.RULES); @@ -95,7 +98,10 @@ export default class WzRuleInfo extends Component { let result = value.match(regex); if (result !== null) { for (const oldValue of result) { - let newValue = oldValue.replace('$(', ``); + let newValue = oldValue.replace( + '$(', + ``, + ); newValue = newValue.replace(')', ' '); value = value.replace(oldValue, newValue); } @@ -133,8 +139,10 @@ export default class WzRuleInfo extends Component { width: '15%', render: (value, item) => { return ( - - handleFileClick(event, item)}>{value} + + handleFileClick(event, item)}> + {value} + ); }, @@ -155,7 +163,7 @@ export default class WzRuleInfo extends Component { async componentDidUpdate(prevProps, prevState) { if (prevState.currentRuleId !== this.state.currentRuleId) - await this.loadRule() + await this.loadRule(); } async loadRule() { @@ -167,7 +175,9 @@ export default class WzRuleInfo extends Component { rule_ids: currentRuleId, }, }); - const currentRule = result?.data?.affected_items?.length ? result.data.affected_items[0] : {}; + const currentRule = result?.data?.affected_items?.length + ? result.data.affected_items[0] + : {}; const compliance = this.buildCompliance(currentRule); if (compliance?.mitre?.length && currentRuleId !== mitreRuleId) { @@ -179,14 +189,14 @@ export default class WzRuleInfo extends Component { mitreIds: [], mitreTactics: [], mitreTechniques: [], - } + }; } this.setState({ currentRuleInfo: currentRule, compliance: compliance, isLoading: false, - ...mitreState + ...mitreState, }); } catch (error) { const options = { @@ -201,7 +211,6 @@ export default class WzRuleInfo extends Component { }; getErrorOrchestrator().handleError(options); } - } /** * Build an object with the compliance info about a rule @@ -209,25 +218,45 @@ export default class WzRuleInfo extends Component { */ buildCompliance(ruleInfo) { const compliance = {}; - const complianceKeys = ['gdpr', 'gpg13', 'hipaa', 'nist-800-53', 'pci', 'tsc', 'mitre']; - Object.keys(ruleInfo).forEach((key) => { - if (complianceKeys.includes(key) && ruleInfo[key].length) compliance[key] = ruleInfo[key]; + const complianceKeys = [ + 'gdpr', + 'gpg13', + 'hipaa', + 'nist-800-53', + 'pci', + 'tsc', + 'mitre', + ]; + Object.keys(ruleInfo).forEach(key => { + if (complianceKeys.includes(key) && ruleInfo[key].length) + compliance[key] = ruleInfo[key]; }); return compliance || {}; } buildComplianceBadges(item) { const badgeList = []; - const fields = ['pci_dss', 'gpg13', 'hipaa', 'gdpr', 'nist_800_53', 'tsc', 'mitre']; - const buildBadge = (field) => { - + const fields = [ + 'pci_dss', + 'gpg13', + 'hipaa', + 'gdpr', + 'nist_800_53', + 'tsc', + 'mitre', + ]; + const buildBadge = field => { return ( - + ev.stopPropagation()} + onClick={ev => ev.stopPropagation()} onClickAriaLabel={field.toUpperCase()} - color="hollow" + color='hollow' style={{ margin: '1px 2px' }} > {field.toUpperCase()} @@ -250,7 +279,7 @@ export default class WzRuleInfo extends Component { error: error, message: error.message || error, title: error.name || error, - } + }, }; getErrorOrchestrator().handleError(options); } @@ -262,9 +291,10 @@ export default class WzRuleInfo extends Component { * Clean the existing filters and sets the new ones and back to the previous section */ setNewFiltersAndBack(filters) { - window.history.pushState("", + window.history.pushState( + '', window.document.title, - window.location.href.replace(new RegExp('&redirectRule=' + '[^&]*'), '') + window.location.href.replace(new RegExp('&redirectRule=' + '[^&]*'), ''), ); this.props.cleanFilters(); this.props.onFiltersChange(filters); @@ -281,37 +311,47 @@ export default class WzRuleInfo extends Component { renderInfo(id = '', level = '', file = '', path = '', groups = []) { return ( - + ID - + this.setNewFiltersAndBack([{ field: 'rule_ids', value: id }])} + onClick={async () => + this.setNewFiltersAndBack({ q: `id=${id}` }) + } > {id} - + Level - + this.setNewFiltersAndBack([{ field: 'level', value: level }])} + onClick={async () => + this.setNewFiltersAndBack({ q: `level=${level}` }) + } > {level} - + File - + - this.setNewFiltersAndBack([{ field: 'filename', value: file }]) + this.setNewFiltersAndBack({ q: `filename=${file}` }) } > {file} @@ -319,13 +359,13 @@ export default class WzRuleInfo extends Component { - + Path - + - this.setNewFiltersAndBack([{ field: 'relative_dirname', value: path }]) + this.setNewFiltersAndBack({ q: `relative_dirname=${path}` }) } > {path} @@ -333,11 +373,11 @@ export default class WzRuleInfo extends Component { - + Groups {this.renderGroups(groups)} - + ); } @@ -347,16 +387,16 @@ export default class WzRuleInfo extends Component { let link = ''; let name = ''; - value.forEach((item) => { - if (item.type === 'cve'){ + value.forEach(item => { + if (item.type === 'cve') { name = item.name; } - if (item.type === 'link'){ + if (item.type === 'link') { link = ( {item.name} @@ -369,7 +409,11 @@ export default class WzRuleInfo extends Component { {name}: {link} ); - } else if (value && typeof value === 'object' && value.constructor === Object) { + } else if ( + value && + typeof value === 'object' && + value.constructor === Object + ) { let list = []; Object.keys(value).forEach((key, idx) => { list.push( @@ -378,7 +422,7 @@ export default class WzRuleInfo extends Component { {value[key]} {idx < Object.keys(value).length - 1 && ', '}
- + , ); }); return ( @@ -387,7 +431,11 @@ export default class WzRuleInfo extends Component { ); } else { - return {value}; + return ( + + {value} + + ); } } @@ -400,13 +448,21 @@ export default class WzRuleInfo extends Component { // Exclude group key of details Object.keys(details) - .filter((key) => key !== 'group') - .forEach((key) => { + .filter(key => key !== 'group') + .forEach(key => { detailsToRender.push( - - {key} - {details[key] === '' ? 'true' : this.getFormattedDetails(details[key])} - + + + {key} + + {details[key] === '' + ? 'true' + : this.getFormattedDetails(details[key])} + , ); }); return {detailsToRender}; @@ -422,14 +478,19 @@ export default class WzRuleInfo extends Component { listGroups.push( this.setNewFiltersAndBack([{ field: 'group', value: group }])} + onClick={async () => + this.setNewFiltersAndBack({ q: `groups=${group}` }) + } > - + {group} {index < groups.length - 1 && ', '} - + , ); }); return ( @@ -447,9 +508,10 @@ export default class WzRuleInfo extends Component { tactic_ids: tactics.toString(), }, }); - const formattedData = ((data || {}).data.data || {}).affected_items || [] || {}; + const formattedData = + ((data || {}).data.data || {}).affected_items || [] || {}; formattedData && - formattedData.forEach((item) => { + formattedData.forEach(item => { tacticsObj.push(item.name); }); return tacticsObj; @@ -465,21 +527,23 @@ export default class WzRuleInfo extends Component { const mitreName = []; const mitreIds = []; const mitreTactics = await Promise.all( - compliance.map(async (i) => { + compliance.map(async i => { const data = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { q: `external_id=${i}`, }, }); - const formattedData = (((data || {}).data.data || {}).affected_items || [])[0] || {}; + const formattedData = + (((data || {}).data.data || {}).affected_items || [])[0] || {}; const tactics = this.getTacticsNames(formattedData.tactics) || []; mitreName.push(formattedData.name); mitreIds.push(i); return tactics; - }) + }), ); if (mitreTactics.length) { - let removeDuplicates = (arr) => arr.filter((v, i) => arr.indexOf(v) === i); + let removeDuplicates = arr => + arr.filter((v, i) => arr.indexOf(v) === i); const uniqueTactics = removeDuplicates(mitreTactics.flat()); Object.assign(newMitreState, { mitreRuleId: currentRuleId, @@ -515,8 +579,6 @@ export default class WzRuleInfo extends Component { ? this.state.currentRuleId : this.props.state.ruleInfo.current; - - const listCompliance = []; if (compliance.mitre) delete compliance.mitre; const keys = Object.keys(compliance); @@ -527,9 +589,11 @@ export default class WzRuleInfo extends Component { return ( this.setNewFiltersAndBack([{ field: key, value: element }])} + onClick={async () => + this.setNewFiltersAndBack({ q: `${key}=${element}` }) + } > - + {element} @@ -539,11 +603,15 @@ export default class WzRuleInfo extends Component { }); listCompliance.push( - + {this.complianceEquivalences[key]}

{values}

- -
+ +
, ); } @@ -553,10 +621,12 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([{ field: 'mitre', value: this.state.mitreIds[index] }]) + this.setNewFiltersAndBack({ + q: `mitre=${this.state.mitreIds[index]}`, + }) } > - + {element} @@ -565,21 +635,35 @@ export default class WzRuleInfo extends Component { ); }); listCompliance.push( - - {this.complianceEquivalences['mitreTechniques']} - {(this.state.mitreLoading && ) ||

{values}

} - -
+ + + {this.complianceEquivalences['mitreTechniques']} + + {(this.state.mitreLoading && ) || ( +

{values}

+ )} + +
, ); } if (this.state.mitreTactics && this.state.mitreTactics.length) { listCompliance.push( - - {this.complianceEquivalences['mitreTactics']} + + + {this.complianceEquivalences['mitreTactics']} +

{this.state.mitreTactics.toString()}

- -
+ +
, ); } @@ -598,7 +682,7 @@ export default class WzRuleInfo extends Component { window.location.href = window.location.href.replace( new RegExp('redirectRule=' + '[^&]*'), - `redirectRule=${ruleId}` + `redirectRule=${ruleId}`, ); this.setState({ currentRuleId: ruleId, isLoading: true }); } @@ -621,7 +705,7 @@ export default class WzRuleInfo extends Component { return value; } - onClickRow = (item) => { + onClickRow = item => { return { onClick: () => { this.changeBetweenRules(item.id); @@ -630,38 +714,47 @@ export default class WzRuleInfo extends Component { }; render() { - const { description, details, filename, relative_dirname, level, id, groups } = this.state.currentRuleInfo; + const { + description, + details, + filename, + relative_dirname, + level, + id, + groups, + } = this.state.currentRuleInfo; const compliance = this.buildCompliance(this.state.currentRuleInfo); return ( <> - + - { - description && ( - ) - } + {description && ( + + )} View alerts of this Rule - + - + {/* Cards */} @@ -669,19 +762,25 @@ export default class WzRuleInfo extends Component { {/* General info */} +

Information

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} isLoading={this.state.isLoading} isLoadingMessage={''} > - - {this.renderInfo(id, level, filename, relative_dirname, groups)} + + {this.renderInfo( + id, + level, + filename, + relative_dirname, + groups, + )}
@@ -690,18 +789,20 @@ export default class WzRuleInfo extends Component { +

Details

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} isLoading={this.state.isLoading} isLoadingMessage={''} > - {this.renderDetails(details)} + + {this.renderDetails(details)} +
@@ -710,18 +811,18 @@ export default class WzRuleInfo extends Component { +

Compliance

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} isLoading={this.state.isLoading} isLoadingMessage={''} > - + {this.renderCompliance(compliance)}
@@ -732,29 +833,32 @@ export default class WzRuleInfo extends Component { +

Related rules

} isLoading={this.state.isLoading} isLoadingMessage={''} - paddingSize="none" + paddingSize='none' initialIsOpen={true} > - + - {this.state.currentRuleInfo?.filename && + {this.state.currentRuleInfo?.filename && ( - } + )} From 0656efb5d1749d358bd413efe32877cbee104749 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:28:52 +0200 Subject: [PATCH 4/6] Replace search bar on Agent > Inventory data (#5443) * 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): replace search bar in TableWzAPI Replace search bar in TableWzAPI Replace each usage of TableWzAPI Adapt external filters * feat(search-bar): replace search bar on Agent/Inventory data section Replace search bar on Agents/Invetory data section Add table columns definitions on files Remove SyscollectorTable component * feat(vulnerabilities): change filter by severity tooltip * fix(test): update test and snapthost * fix(test): update snapshot * feat(table-wz-api): add field selection to the TableWzAPI component Add field selection to the TableWzAPI Possibility to save the selected fields on storage (localStorage, sessionStorage) Create useStateStorage that allows save the state in localStorage or sessionStorage * 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 * feat(table-wz-api): Adapt TableWzAPI usage to the recent changes when using the search bar Enhance Management/Decoders table * fix(test): fixed test and update snapshot * fix(table-wz-api): minor fixes on TableWzAPI usage * fix: fixed prop type * fix(search-bar): adapt search bar parameters on TableWzAPI of Agents/Inventory * fix(table-wz-api): adapt the search bar parameters * fix: fix search term field on TableWzAPI for composed column * fix(table-wz-api): enhance TableWithSearchBar types and fix error HTML attributes * fix: enhance search bar and WQL types * fix: test snapshot * fix: test snapshot * fix: remove duplicated search bar * feat: add the distinct values for the search bar suggestions in some sections - Add the distinct values for the search bar suggestions in some sections: - Modules > Security Configuration Assessment policy checks table - Modules > Vulnerabilities > Inventory table - Modules > MITRE ATT&CK > Inventory table - Management > Rules table - Management > Decoders table - Management > CDB Lists table - Add Path column to Rules files table - Add Path column to Decoders files table * fix: remove exact validation for the token value due to performance problems * fix: add suggestions for search bar in the Agents inventory data tables * fix: fix token value validation * fix: fix Management > Rules search bar filters Add id field Fix groups filter in Rule info flyout Remove onFiltersChange handler * 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): update test snapshot * fix(table-wz-api): avoid the double toast message when there is an error fetching data and replace the error.name to RequestError * fix(search-bar): add validation for value token in WQL * fix(search-bar): value token in message related to this is invalid * fix(search-bar): fix error related to details.program_name suggestion in the Decoders section * feat(search-bar): add constant to define the count of distinct values to get in the suggestions * feat(search-bar): use constant to define the count of distinct values to get in the suggestions * fix(search-bar): fix value suggestions in the Decoders section * fix: add comment to constant * workaround(search-bar): add a filter to the value suggestions in WQL When getting the distinct values for some fields, the value could not match the regular expression that validates them, and this causes that the search can not be run. So, we filters the distinct values to ensure or reduce the suggestions can be used to search. This causes some possible values are not displayed in the suggestions. To undone this, then the API should allow these values. * changelog: add entry * changelog: add entry * changelog: add entry * fix(search-bar): fix prop type error related to EuiSuggestItem * fix(wql): add whitespace before closing grouping operator ) when using the suggestions * feat(search-bar): add a debounce time to update the search bar state * fix(search-bar): fix prop type error related to EuiSuggestItem * fix(search-bar-wql): problem related to execute the search before the input is analyzed due to this process is debounced * fix(search-bar-wql): remove unnued parameter in function * fix(search-bar): fix tests * fix(search-bar): suggestions in Modules > Vulenerabilities > Inventory --- CHANGELOG.md | 3 +- .../agents/syscollector/columns/index.ts | 10 +- .../syscollector/columns/netaddr-columns.ts | 9 + .../syscollector/columns/netiface-columns.ts | 9 + .../syscollector/columns/packages-columns.ts | 50 +- .../syscollector/columns/ports-columns.ts | 24 +- .../syscollector/columns/process-columns.ts | 60 +-- .../columns/windows-updates-columns.ts | 5 + .../components/syscollector-table.tsx | 182 -------- .../agents/syscollector/inventory.tsx | 438 ++++++++++++++---- 10 files changed, 456 insertions(+), 334 deletions(-) create mode 100644 plugins/main/public/components/agents/syscollector/columns/netaddr-columns.ts create mode 100644 plugins/main/public/components/agents/syscollector/columns/netiface-columns.ts create mode 100644 plugins/main/public/components/agents/syscollector/columns/windows-updates-columns.ts delete mode 100644 plugins/main/public/components/agents/syscollector/components/syscollector-table.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index da4ce9c9ea..22f0ffb4a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new global error treatment (client-side) [#4163](https://github.com/wazuh/wazuh-kibana-app/pull/4163) - Added new CLI to generate API data from specification file [#5519](https://github.com/wazuh/wazuh-kibana-app/pull/5519) - Added specific RBAC permissions to Security section [#5551](https://github.com/wazuh/wazuh-kibana-app/pull/5551) +- Added Refresh and Export formatted button to panels in Agents > Inventory data [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) ### Changed @@ -20,7 +21,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) -- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}) [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) +- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}), Agent Inventory data [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) ### Fixed diff --git a/plugins/main/public/components/agents/syscollector/columns/index.ts b/plugins/main/public/components/agents/syscollector/columns/index.ts index 233876ed88..fe2b7fb3b4 100644 --- a/plugins/main/public/components/agents/syscollector/columns/index.ts +++ b/plugins/main/public/components/agents/syscollector/columns/index.ts @@ -1,3 +1,7 @@ -export { processColumns } from './process-columns' -export { portsColumns } from './ports-columns' -export { packagesColumns } from './packages-columns' +export { netaddrColumns } from './netaddr-columns'; +export { netifaceColumns } from './netiface-columns'; +export { processColumns } from './process-columns'; +export { portsColumns } from './ports-columns'; +export { packagesColumns } from './packages-columns'; +export { windowsUpdatesColumns } from './windows-updates-columns'; + diff --git a/plugins/main/public/components/agents/syscollector/columns/netaddr-columns.ts b/plugins/main/public/components/agents/syscollector/columns/netaddr-columns.ts new file mode 100644 index 0000000000..59a8f96de2 --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/columns/netaddr-columns.ts @@ -0,0 +1,9 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + +export const netaddrColumns = [ + { field: 'iface', searchable: true, sortable: true }, + { field: 'address', searchable: true, sortable: true }, + { field: 'netmask', searchable: true, sortable: true }, + { field: 'proto', searchable: true, sortable: true }, + { field: 'broadcast', searchable: true, sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); \ No newline at end of file diff --git a/plugins/main/public/components/agents/syscollector/columns/netiface-columns.ts b/plugins/main/public/components/agents/syscollector/columns/netiface-columns.ts new file mode 100644 index 0000000000..f411c3d583 --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/columns/netiface-columns.ts @@ -0,0 +1,9 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + +export const netifaceColumns = [ + { field: 'name', searchable: true, sortable: true, }, + { field: 'mac', searchable: true, sortable: true }, + { field: 'state', searchable: true, name: 'State', sortable: true }, + { field: 'mtu', searchable: true, name: 'MTU', sortable: true }, + { field: 'type', searchable: true, name: 'Type', sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); \ No newline at end of file diff --git a/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts b/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts index 0310e58678..e7d1fffcfc 100644 --- a/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts +++ b/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts @@ -1,31 +1,33 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + const windowsColumns = [ - { id: 'name' }, - { id: 'architecture', width: '10%' }, - { id: 'version' }, - { id: 'vendor', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'architecture', searchable: true, sortable: true, width: '10%' }, + { field: 'version', searchable: true, sortable: true }, + { field: 'vendor', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const linuxColumns = [ - { id: 'name' }, - { id: 'architecture', width: '10%' }, - { id: 'version' }, - { id: 'vendor', width: '30%' }, - { id: 'description', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'architecture', searchable: true, sortable: true, width: '10%' }, + { field: 'version', searchable: true, sortable: true }, + { field: 'vendor', searchable: true, sortable: true, width: '30%' }, + { field: 'description', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const MacColumns = [ - { id: 'name' }, - { id: 'version' }, - { id: 'format' }, - { id: 'location', width: '30%' }, - { id: 'description', width: '20%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'version', searchable: true, sortable: true }, + { field: 'format', searchable: true, sortable: true }, + { field: 'location', searchable: true, sortable: true, width: '30%' }, + { field: 'description', searchable: true, sortable: true, width: '20%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const FreebsdColumns = [ - { id: 'name' }, - { id: 'version' }, - { id: 'format' }, - { id: 'architecture', width: '20%' }, - { id: 'vendor', width: '20%' }, - { id: 'description', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'version', searchable: true, sortable: true }, + { field: 'format', searchable: true, sortable: true }, + { field: 'architecture', searchable: true, sortable: true, width: '20%' }, + { field: 'vendor', searchable: true, sortable: true, width: '20%' }, + { field: 'description', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); export const packagesColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts b/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts index c8e7462a19..5c1890e51f 100644 --- a/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts +++ b/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts @@ -1,16 +1,18 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + const windowsColumns = [ - { id: 'process' }, - { id: 'local.ip', sortable: false }, - { id: 'local.port', sortable: false }, - { id: 'state' }, - { id: 'protocol' }, -]; + { field: 'process', searchable: true, sortable: true }, + { field: 'local.ip', searchable: true, sortable: false }, + { field: 'local.port', searchable: true, sortable: false }, + { field: 'state', searchable: true, sortable: true }, + { field: 'protocol', searchable: true, sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const defaultColumns = [ - { id: 'local.ip', sortable: false }, - { id: 'local.port', sortable: false }, - { id: 'state' }, - { id: 'protocol' }, -]; + { field: 'local.ip', searchable: true, sortable: false }, + { field: 'local.port', searchable: true, sortable: false }, + { field: 'state', searchable: true, sortable: true }, + { field: 'protocol', searchable: true, sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); export const portsColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/agents/syscollector/columns/process-columns.ts b/plugins/main/public/components/agents/syscollector/columns/process-columns.ts index 01e5bff9e7..a21ee0447d 100644 --- a/plugins/main/public/components/agents/syscollector/columns/process-columns.ts +++ b/plugins/main/public/components/agents/syscollector/columns/process-columns.ts @@ -1,35 +1,37 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + const windowsColumns = [ - { id: 'name', width: '10%' }, - { id: 'pid' }, - { id: 'ppid' }, - { id: 'vm_size' }, - { id: 'priority' }, - { id: 'nlwp' }, - { id: 'cmd', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true, width: '10%' }, + { field: 'pid', searchable: true, sortable: true }, + { field: 'ppid', searchable: true, sortable: true }, + { field: 'vm_size', searchable: true, sortable: true }, + { field: 'priority', searchable: true, sortable: true }, + { field: 'nlwp', searchable: true, sortable: true }, + { field: 'cmd', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const linuxColumns = [ - { id: 'name', width: '10%' }, - { id: 'euser' }, - { id: 'egroup' }, - { id: 'pid' }, - { id: 'ppid' }, - { id: 'cmd', width: '15%' }, - { id: 'argvs', width: '15%' }, - { id: 'vm_size' }, - { id: 'size' }, - { id: 'session' }, - { id: 'nice' }, - { id: 'state', width: '15%' }, -]; + { field: 'name', searchable: true, sortable: true, width: '10%' }, + { field: 'euser', searchable: true, sortable: true }, + { field: 'egroup', searchable: true, sortable: true }, + { field: 'pid', searchable: true, sortable: true }, + { field: 'ppid', searchable: true, sortable: true }, + { field: 'cmd', searchable: true, sortable: true, width: '15%' }, + { field: 'argvs', searchable: true, sortable: true, width: '15%' }, + { field: 'vm_size', searchable: true, sortable: true }, + { field: 'size', searchable: true, sortable: true }, + { field: 'session', searchable: true, sortable: true }, + { field: 'nice', searchable: true, sortable: true }, + { field: 'state', searchable: true, sortable: true, width: '15%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const macColumns = [ - { id: 'name', width: '10%' }, - { id: 'euser' }, - { id: 'pid' }, - { id: 'ppid' }, - { id: 'vm_size' }, - { id: 'nice' }, - { id: 'state', width: '15%' }, -]; + { field: 'name', searchable: true, sortable: true, width: '10%' }, + { field: 'euser', searchable: true, sortable: true }, + { field: 'pid', searchable: true, sortable: true }, + { field: 'ppid', searchable: true, sortable: true }, + { field: 'vm_size', searchable: true, sortable: true }, + { field: 'nice', searchable: true, sortable: true }, + { field: 'state', searchable: true, sortable: true, width: '15%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); export const processColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/agents/syscollector/columns/windows-updates-columns.ts b/plugins/main/public/components/agents/syscollector/columns/windows-updates-columns.ts new file mode 100644 index 0000000000..815cae842d --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/columns/windows-updates-columns.ts @@ -0,0 +1,5 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + +export const windowsUpdatesColumns = [ + { field: 'hotfix', searchable: true, sortable: true} +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); \ No newline at end of file diff --git a/plugins/main/public/components/agents/syscollector/components/syscollector-table.tsx b/plugins/main/public/components/agents/syscollector/components/syscollector-table.tsx deleted file mode 100644 index e5e5b0056f..0000000000 --- a/plugins/main/public/components/agents/syscollector/components/syscollector-table.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React, { useState } from 'react'; -import { - EuiPanel, - EuiFlexGroup, - EuiButtonEmpty, - EuiFlexItem, - EuiText, - EuiLoadingSpinner, - EuiFieldSearch, - EuiHorizontalRule, - EuiIcon, - EuiBasicTable, -} from '@elastic/eui'; -import { useApiRequest } from '../../../common/hooks/useApiRequest'; -import { KeyEquivalence } from '../../../../../common/csv-key-equivalence'; -import { AppState } from '../../../../react-services/app-state'; - -export function SyscollectorTable({ tableParams }) { - const [params, setParams] = useState<{ - limit: number; - offset: number; - select: string; - q?: string; - }>({ - limit: 10, - offset: 0, - select: tableParams.columns.map(({ id }) => id).join(','), - }); - const [pageIndex, setPageIndex] = useState(0); - const [searchBarValue, setSearchBarValue] = useState(''); - const [pageSize, setPageSize] = useState(10); - const [sortField, setSortField] = useState(''); - const [timerDelaySearch, setTimerDelaySearch] = useState(); - const [sortDirection, setSortDirection] = useState(''); - const [loading, data, error] = useApiRequest('GET', tableParams.path, params); - - const onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - setPageIndex(pageIndex); - setPageSize(pageSize); - setSortField(sortField); - setSortDirection(sortDirection); - const field = sortField === 'os_name' ? '' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - const newParams = { - ...params, - limit: pageSize, - offset: Math.floor((pageIndex * pageSize) / params.limit) * params.limit, - ...(!!field ? { sort: `${direction}${field}` } : {}), - }; - - setParams(newParams); - }; - - const buildColumns = () => { - return (tableParams.columns || []).map(item => { - return { - field: item.id, - name: KeyEquivalence[item.id] || item.id, - sortable: typeof item.sortable !== 'undefined' ? item.sortable : true, - width: item.width || undefined, - }; - }); - }; - - const columns = buildColumns(); - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: data.total_affected_items || 0, - pageSizeOptions: [10, 25, 50], - }; - - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - const onChange = e => { - const value = e.target.value; - setSearchBarValue(value); - timerDelaySearch && clearTimeout(timerDelaySearch); - const timeoutId = setTimeout(() => { - const { q, ...rest } = params; - const newParams = { - ...rest, - ...(value - ? { - q: tableParams.columns - .map(({ id }) => `${id}~${value}`) - .join(','), - } - : {}), - }; - setParams(newParams); - setPageIndex(0); - }, 400); - setTimerDelaySearch(timeoutId); - }; - - const getTotal = () => { - if (loading) - return ( - <> - {'( '} - - {' )'} - - ); - else return `(${data.total_affected_items})`; - }; - - const downloadCsv = async () => { - await AppState.downloadCsv( - tableParams.path, - tableParams.exportFormatted, - !!params.q ? [{ name: 'q', value: params.q }] : [], - ); - }; - - return ( - - - - - {' '} - {' '} -  {' '} - - {tableParams.title} {tableParams.hasTotal ? getTotal() : ''} - {' '} - - - - - {tableParams.searchBar && ( - - - - - - )} - - - - - - {tableParams.exportFormatted && tableParams.columns && ( - - - - - Download CSV - - - - )} - - ); -} diff --git a/plugins/main/public/components/agents/syscollector/inventory.tsx b/plugins/main/public/components/agents/syscollector/inventory.tsx index ef9cf66de2..9fbe6a1e43 100644 --- a/plugins/main/public/components/agents/syscollector/inventory.tsx +++ b/plugins/main/public/components/agents/syscollector/inventory.tsx @@ -10,20 +10,35 @@ * Find more information about this on the LICENSE file. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiEmptyPrompt, EuiButton, EuiFlexGroup, EuiFlexItem, EuiCallOut, - EuiLink + EuiLink, + EuiPanel, } from '@elastic/eui'; import { InventoryMetrics } from './components/syscollector-metrics'; -import { SyscollectorTable } from './components/syscollector-table'; -import { processColumns, portsColumns, packagesColumns } from './columns'; -import { API_NAME_AGENT_STATUS } from '../../../../common/constants'; +import { + netaddrColumns, + netifaceColumns, + processColumns, + portsColumns, + packagesColumns, + windowsUpdatesColumns, +} from './columns'; +import { + API_NAME_AGENT_STATUS, + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, +} from '../../../../common/constants'; import { webDocumentationLink } from '../../../../common/services/web_documentation'; +import { TableWzAPI } from '../../common/tables'; +import { WzRequest } from '../../../react-services'; +import { get as getLodash } from 'lodash'; + +const sortFieldSuggestion = (a, b) => (a.label > b.label ? 1 : -1); export function SyscollectorInventory({ agent }) { if (agent && agent.status === API_NAME_AGENT_STATUS.NEVER_CONNECTED) { @@ -33,7 +48,7 @@ export function SyscollectorInventory({ agent }) { style={{ marginTop: 20 }} title={

Agent has never connected.

} body={ - + <>

The agent has been registered but has not yet connected to the manager. @@ -48,7 +63,7 @@ export function SyscollectorInventory({ agent }) { > Checking connection with the Wazuh server - + } actions={ @@ -72,21 +87,6 @@ export function SyscollectorInventory({ agent }) { soPlatform = 'solaris'; } - const netifaceColumns = [ - { id: 'name' }, - { id: 'mac' }, - { id: 'state', value: 'State' }, - { id: 'mtu', value: 'MTU' }, - { id: 'type', value: 'Type' }, - ]; - const netaddrColumns = [ - { id: 'iface' }, - { id: 'address' }, - { id: 'netmask' }, - { id: 'proto' }, - { id: 'broadcast' }, - ]; - return (

{agent && agent.status === API_NAME_AGENT_STATUS.DISCONNECTED && ( @@ -104,89 +104,359 @@ export function SyscollectorInventory({ agent }) { - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return netifaceColumns + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/netiface`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return portsColumns[soPlatform] + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/ports`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return netaddrColumns + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/netaddr`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + {agent && agent.os && agent.os.platform === 'windows' && ( - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return windowsUpdatesColumns + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/hotfixes`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map( + item => ({ + label: getLodash(item, field), + }), + ); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + )} - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return packagesColumns[soPlatform] + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/packages`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return processColumns[soPlatform] + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/processes`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> +
From a628371a4091cb949a9711e4c42e7769c4c792ff Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:35:10 +0200 Subject: [PATCH 5/6] Replace search bar on Modules > Integrity monitoring > Inventory (#5444) * 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): replace search bar in TableWzAPI Replace search bar in TableWzAPI Replace each usage of TableWzAPI Adapt external filters * feat(vulnerabilities): change filter by severity tooltip * fix(test): update test and snapthost * feat(search-bar): replace search bar and table component on Modules/Integrity monitoring/Inventory Replace search bar and table component with TableWzAPI * fix(test): update snapshot * feat(table-wz-api): add field selection to the TableWzAPI component Add field selection to the TableWzAPI Possibility to save the selected fields on storage (localStorage, sessionStorage) Create useStateStorage that allows save the state in localStorage or sessionStorage * 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 * feat(table-wz-api): Adapt TableWzAPI usage to the recent changes when using the search bar Enhance Management/Decoders table * fix(test): fixed test and update snapshot * fix(table-wz-api): minor fixes on TableWzAPI usage * fix: fixed prop type * fix(search-bar): fix TableWzAPI serach bar parameters * fix: fix search term field on TableWzAPI for composed column * fix(table-wz-api): enhance TableWithSearchBar types and fix error HTML attributes * fix: enhance search bar and WQL types * feat: add date field validation to the fied suggestion on Modules/Integrity monitoring/Inventory tables * fix: test snapshot * fix: remove duplicated search bar * update: test snapshot * move: moved search bar files * feat: add the distinct values for the search bar suggestions in some sections - Add the distinct values for the search bar suggestions in some sections: - Modules > Security Configuration Assessment policy checks table - Modules > Vulnerabilities > Inventory table - Modules > MITRE ATT&CK > Inventory table - Management > Rules table - Management > Decoders table - Management > CDB Lists table - Add Path column to Rules files table - Add Path column to Decoders files table * fix: remove exact validation for the token value due to performance problems * fix: fix token value validation * fix: add suggestions for table in Modules > Integrity monitoring > Inventory * fix: fix Management > Rules search bar filters Add id field Fix groups filter in Rule info flyout Remove onFiltersChange handler * 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): update test snapshot * fix(table-wz-api): avoid the double toast message when there is an error fetching data and replace the error.name to RequestError * fix(search-bar): add validation for value token in WQL * fix(search-bar): value token in message related to this is invalid * fix(search-bar): fix error related to details.program_name suggestion in the Decoders section * feat(search-bar): add constant to define the count of distinct values to get in the suggestions * feat(search-bar): use constant to define the count of distinct values to get in the suggestions * fix(search-bar): fix value suggestions in the Decoders section * fix: add comment to constant * workaround(search-bar): add a filter to the value suggestions in WQL When getting the distinct values for some fields, the value could not match the regular expression that validates them, and this causes that the search can not be run. So, we filters the distinct values to ensure or reduce the suggestions can be used to search. This causes some possible values are not displayed in the suggestions. To undone this, then the API should allow these values. * changelog: add entry * changelog: add entry * changelog: add entry * fix(wql): add whitespace before closing grouping operator ) when using the suggestions * feat(search-bar): add a debounce time to update the search bar state * fix(search-bar): fix prop type error related to EuiSuggestItem * fix(search-bar): implicit filter in Modules > Integrity monitoring > Inventory tables * fix(search-bar-wql): problem related to execute the search before the input is analyzed due to this process is debounced * fix(search-bar-wql): remove unnued parameter in function * fix(search-bar): fix tests * fix(search-bar): suggestions in Modules > Vulenerabilities > Inventory --- CHANGELOG.md | 2 +- .../components/agents/fim/inventory.tsx | 97 ++---- .../agents/fim/inventory/filterBar.tsx | 171 ----------- .../components/agents/fim/inventory/index.ts | 2 - .../agents/fim/inventory/registry-table.tsx | 256 +++++++--------- .../components/agents/fim/inventory/table.tsx | 284 ++++++++---------- 6 files changed, 256 insertions(+), 556 deletions(-) delete mode 100644 plugins/main/public/components/agents/fim/inventory/filterBar.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f0ffb4a9..a85a6ce893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) -- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}), Agent Inventory data [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) +- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}, Integrity monitoring > Inventory > Files, Integrity monitoring > Inventory > Registry), Agent Inventory data [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) [#5444](https://github.com/wazuh/wazuh-kibana-app/pull/5444) ### Fixed diff --git a/plugins/main/public/components/agents/fim/inventory.tsx b/plugins/main/public/components/agents/fim/inventory.tsx index b468416cd2..e6bdf38716 100644 --- a/plugins/main/public/components/agents/fim/inventory.tsx +++ b/plugins/main/public/components/agents/fim/inventory.tsx @@ -12,7 +12,6 @@ import React, { Component } from 'react'; import { - EuiButtonEmpty, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -26,9 +25,8 @@ import { EuiTabs, EuiTitle, } from '@elastic/eui'; -import { FilterBar, InventoryTable, RegistryTable } from './inventory/'; +import { InventoryTable, RegistryTable } from './inventory/'; import { WzRequest } from '../../../react-services/wz-request'; -import exportCsv from '../../../react-services/wz-csv'; import { getToasts } from '../../../kibana-services'; import { ICustomBadges } from '../../wz-search-bar/components'; import { filtersToObject } from '../../wz-search-bar'; @@ -194,45 +192,20 @@ export class Inventory extends Component { const { isLoading } = this.state; if (tabs.length > 1) { return ( -
- - {tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - disabled={tab.disabled} - key={index}> - {tab.name} {isLoading === true && } - - ))} - - - - - - this.downloadCsv()}> - Export formatted - - - -
- ) - } else { - return ( - - - -

{tabs[0].name} {isLoading === true && }

-
-
- - this.downloadCsv()}> - Export formatted - - -
- ) - } + + {tabs.map((tab, index) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.state.selectedTabId} + disabled={tab.disabled} + key={index}> + {tab.name} {isLoading === true && } + + ))} + + ); + }; + return null; } showToast = (color, title, time) => { @@ -243,44 +216,10 @@ export class Inventory extends Component { }); }; - async downloadCsv() { - const { filters } = this.state; - try { - const filtersObject = filtersToObject(filters); - const formatedFilters = Object.keys(filtersObject).map(key => ({name: key, value: filtersObject[key]})); - this.showToast('success', 'Your download should begin automatically...', 3000); - await exportCsv( - '/syscheck/' + this.props.agent.id, - [ - { name: 'type', value: this.state.selectedTabId === 'files' ? 'file' : this.state.selectedTabId }, - ...formatedFilters - ], - `fim-${this.state.selectedTabId}` - ); - } catch (error) { - const options: UIErrorLog = { - context: `${Inventory.name}.downloadCsv`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - renderTable() { const { filters, syscheck, selectedTabId, customBadges, totalItemsRegistry, totalItemsFile } = this.state; return ( -
- + <> {selectedTabId === 'files' && } -
- ) + + ); } noConfiguredMonitoring() { diff --git a/plugins/main/public/components/agents/fim/inventory/filterBar.tsx b/plugins/main/public/components/agents/fim/inventory/filterBar.tsx deleted file mode 100644 index 9830e33456..0000000000 --- a/plugins/main/public/components/agents/fim/inventory/filterBar.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import React, { Component } from 'react'; -import { getFilterValues } from './lib'; -import { IFilter, IWzSuggestItem, WzSearchBar } from '../../../../components/wz-search-bar'; -import { ICustomBadges } from '../../../wz-search-bar/components'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { formatUIDate } from '../../../../react-services/time-service'; - -export class FilterBar extends Component { - // TODO: Change the type - suggestions: { [key: string]: IWzSuggestItem[] } = { - files: [ - { - type: 'q', - label: 'file', - description: 'Name of the file', - operators: ['=', '!=', '~'], - values: async (value) => - getFilterValues('file', value, this.props.agent.id, { type: 'file' }), - }, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'perm', - description: 'Permissions of the file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('perm', value, this.props.agent.id), - }, - ] - : []), - { - type: 'q', - label: 'mtime', - description: 'Date the file was modified', - operators: ['=', '!=', '>', '<'], - values: async (value) => - getFilterValues('mtime', value, this.props.agent.id, {}, formatUIDate), - }, - { - type: 'q', - label: 'date', - description: 'Date of registration of the event', - operators: ['=', '!=', '>', '<'], - values: async (value) => - getFilterValues('date', value, this.props.agent.id, {}, formatUIDate), - }, - { - type: 'q', - label: 'uname', - description: 'Owner of the file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('uname', value, this.props.agent.id), - }, - { - type: 'q', - label: 'uid', - description: 'Id of the owner file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('uid', value, this.props.agent.id), - }, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'gname', - description: 'Name of the group owner file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('gname', value, this.props.agent.id), - }, - ] - : []), - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'gid', - description: 'Id of the group owner', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('gid', value, this.props.agent.id), - }, - ] - : []), - { - type: 'q', - label: 'md5', - description: 'md5 hash', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('md5', value, this.props.agent.id), - }, - { - type: 'q', - label: 'sha1', - description: 'sha1 hash', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('sha1', value, this.props.agent.id), - }, - { - type: 'q', - label: 'sha256', - description: 'sha256 hash', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('sha256', value, this.props.agent.id), - }, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'inode', - description: 'Inode of the file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('inode', value, this.props.agent.id), - }, - ] - : []), - { - type: 'q', - label: 'size', - description: 'Size of the file in Bytes', - values: async (value) => getFilterValues('size', value, this.props.agent.id), - }, - ], - registry: [ - { - type: 'q', - label: 'file', - description: 'Name of the registry_key', - operators: ['=', '!=', '~'], - values: async (value) => - getFilterValues('file', value, this.props.agent.id, { q: 'type=registry_key' }), - }, - ], - }; - - props!: { - onFiltersChange(filters: IFilter[]): void; - selectView: 'files' | 'registry'; - agent: { id: string; agentPlatform: string }; - onChangeCustomBadges?(customBadges: ICustomBadges[]): void; - customBadges?: ICustomBadges[]; - filters: IFilter[]; - }; - - render() { - const { onFiltersChange, selectView, filters } = this.props; - return ( - - - - - - ); - } -} diff --git a/plugins/main/public/components/agents/fim/inventory/index.ts b/plugins/main/public/components/agents/fim/inventory/index.ts index 484ed526bb..28a9313aaf 100644 --- a/plugins/main/public/components/agents/fim/inventory/index.ts +++ b/plugins/main/public/components/agents/fim/inventory/index.ts @@ -1,4 +1,2 @@ -export { FilterBar } from './filterBar'; - export { InventoryTable } from './table'; export { RegistryTable } from './registry-table' \ No newline at end of file diff --git a/plugins/main/public/components/agents/fim/inventory/registry-table.tsx b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx index a03ab38ae6..c25bec7bb4 100644 --- a/plugins/main/public/components/agents/fim/inventory/registry-table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx @@ -11,36 +11,26 @@ */ import React, { Component } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - Direction, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; -import { filtersToObject } from '../../../wz-search-bar'; import { formatUIDate } from '../../../../react-services/time-service'; -import { - UI_ERROR_SEVERITIES, - UIErrorLog, - UIErrorSeverity, - UILogLevel, -} from '../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; -import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { TableWzAPI } from '../../../common/tables'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../common/constants'; + +const searchBarWQLOptions = { + implicitQuery: { + query: 'type=registry_key', + conjunction: ';', + }, +}; + +const searchBarWQLFilters = { default: { q: 'type=registry_key' } }; export class RegistryTable extends Component { state: { syscheck: []; - error?: string; - pageIndex: number; - pageSize: number; - totalItems: number; - sortField: string; isFlyoutVisible: Boolean; - sortDirection: Direction; - isLoading: boolean; currentFile: { file: string; type: string; @@ -58,12 +48,6 @@ export class RegistryTable extends Component { this.state = { syscheck: [], - pageIndex: 0, - pageSize: 15, - totalItems: 0, - sortField: 'file', - sortDirection: 'asc', - isLoading: true, isFlyoutVisible: false, currentFile: { file: '', @@ -74,141 +58,83 @@ export class RegistryTable extends Component { } async componentDidMount() { - await this.getSyscheck(); const regex = new RegExp('file=' + '[^&]*'); const match = window.location.href.match(regex); - this.setState({ totalItems: this.props.totalItems }); if (match && match[0]) { const file = match[0].split('=')[1]; this.showFlyout(decodeURIComponent(file), true); } } - componentDidUpdate(prevProps) { - const { filters } = this.props; - if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }, this.getSyscheck); - } - } - closeFlyout() { this.setState({ isFlyoutVisible: false, currentFile: {} }); } async showFlyout(file, item, redirect = false) { - window.location.href = window.location.href.replace(new RegExp('&file=' + '[^&]*', 'g'), ''); + window.location.href = window.location.href.replace( + new RegExp('&file=' + '[^&]*', 'g'), + '', + ); let fileData = false; if (!redirect) { - fileData = this.state.syscheck.filter((item) => { + fileData = this.state.syscheck.filter(item => { return item.file === file; }); } else { - const response = await WzRequest.apiReq('GET', `/syscheck/${this.props.agent.id}`, { - params: { - file: file, + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + file: file, + }, }, - }); + ); fileData = ((response.data || {}).data || {}).affected_items || []; } - if (!redirect) window.location.href = window.location.href += `&file=${file}`; + if (!redirect) + window.location.href = window.location.href += `&file=${file}`; //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. const currentFile = { file, type: item.type, }; this.setState({ isFlyoutVisible: false }, () => - this.setState({ isFlyoutVisible: true, currentFile, syscheckItem: item }) + this.setState({ isFlyoutVisible: true, currentFile, syscheckItem: item }), ); } - async getSyscheck() { - const agentID = this.props.agent.id; - try { - const syscheck = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { - params: this.buildFilter(), - }); - - this.setState({ - syscheck: (((syscheck || {}).data || {}).data || {}).affected_items || {}, - totalItems: (((syscheck || {}).data || {}).data || {}).total_affected_items - 1, - isLoading: false, - error: undefined, - }); - } catch (error) { - this.setState({ error, isLoading: false }); - - const options: UIErrorLog = { - context: `${RegistryTable.name}.getSyscheck`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - - const field = sortField === 'os_name' ? '' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + field; - } - - buildFilter() { - const { pageIndex, pageSize } = this.state; - const filters = filtersToObject(this.props.filters); - - const filter = { - ...filters, - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - q: 'type=registry_key', - }; - - return filter; - } - - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this.setState( - { - pageIndex, - pageSize, - sortField, - sortDirection, - isLoading: true, - }, - () => this.getSyscheck() - ); - }; - columns() { return [ { field: 'file', name: 'Registry', sortable: true, + searchable: true, }, { field: 'mtime', - name: 'Last Modified', + name: ( + + Last Modified{' '} + + + ), sortable: true, width: '200px', render: formatUIDate, + searchable: false, }, ]; } renderRegistryTable() { - const getRowProps = (item) => { + const getRowProps = item => { const { file } = item; return { 'data-test-subj': `row-${file}`, @@ -216,44 +142,76 @@ export class RegistryTable extends Component { }; }; - const { - syscheck, - pageIndex, - pageSize, - totalItems, - sortField, - sortDirection, - isLoading, - error, - } = this.state; const columns = this.columns(); - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [15, 25, 50, 100], - }; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; return ( - [ + { label: 'file', description: 'filter by file' }, + { + label: 'mtime', + description: 'filter by modification time', + }, + ], + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + // Add the implicit query + q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + } + : { + q: `${searchBarWQLOptions.implicitQuery.query}`, + }), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + validate: { + value: ({ formattedValue, value: rawValue }, { field }) => { + const value = formattedValue ?? rawValue; + if (value) { + if (['mtime'].some(dateField => dateField === field)) { + return /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2}(.\d{1,6})?Z?)?$/.test( + value, + ) + ? undefined + : `"${value}" is not a expected format. Valid formats: YYYY-MM-DD, YYYY-MM-DD HH:mm:ss, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ.`; + } + } + }, + }, + }} + filters={searchBarWQLFilters} + showReload + downloadCsv={`fim-registry-${this.props.agent.id}`} + searchTable={true} rowProps={getRowProps} - sorting={sorting} - itemId="file" - isExpandable={true} - loading={isLoading} /> @@ -272,7 +230,7 @@ export class RegistryTable extends Component { item={this.state.syscheckItem} closeFlyout={() => this.closeFlyout()} type={this.state.currentFile.type} - view="inventory" + view='inventory' {...this.props} /> )} diff --git a/plugins/main/public/components/agents/fim/inventory/table.tsx b/plugins/main/public/components/agents/fim/inventory/table.tsx index dcb550a693..61de5e489a 100644 --- a/plugins/main/public/components/agents/fim/inventory/table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/table.tsx @@ -11,38 +11,26 @@ */ import React, { Component } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - Direction, - EuiOverlayMask, - EuiOutsideClickDetector, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; -import { filtersToObject, IFilter } from '../../../wz-search-bar'; import { formatUIDate } from '../../../../react-services/time-service'; -import { - UI_ERROR_SEVERITIES, - UIErrorLog, - UIErrorSeverity, - UILogLevel, -} from '../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; -import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { TableWzAPI } from '../../../common/tables'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../common/constants'; + +const searchBarWQLOptions = { + implicitQuery: { + query: 'type=file', + conjunction: ';', + }, +}; + +const searchBarWQLFilters = { default: { q: 'type=file' } }; export class InventoryTable extends Component { state: { syscheck: []; - error?: string; - pageIndex: number; - pageSize: number; - totalItems: number; - sortField: string; isFlyoutVisible: Boolean; - sortDirection: Direction; - isLoading: boolean; currentFile: { file: string; }; @@ -50,7 +38,7 @@ export class InventoryTable extends Component { }; props!: { - filters: IFilter[]; + filters: any; agent: any; items: []; totalItems: number; @@ -62,12 +50,6 @@ export class InventoryTable extends Component { this.state = { syscheck: props.items, - pageIndex: 0, - pageSize: 15, - totalItems: 0, - sortField: 'file', - sortDirection: 'asc', - isLoading: false, isFlyoutVisible: false, currentFile: { file: '', @@ -79,10 +61,9 @@ export class InventoryTable extends Component { async componentDidMount() { const regex = new RegExp('file=' + '[^&]*'); const match = window.location.href.match(regex); - this.setState({ totalItems: this.props.totalItems }); if (match && match[0]) { const file = match[0].split('=')[1]; - this.showFlyout(decodeURIComponent(file), true); + this.showFlyout(decodeURIComponent(file), true); // FIX: second parameter is the item. Why is this a boolean? } } @@ -91,103 +72,45 @@ export class InventoryTable extends Component { } async showFlyout(file, item, redirect = false) { - window.location.href = window.location.href.replace(new RegExp('&file=' + '[^&]*', 'g'), ''); - let fileData = false; + window.location.href = window.location.href.replace( + new RegExp('&file=' + '[^&]*', 'g'), + '', + ); + let fileData = false; // FIX: fileData variable is unused if (!redirect) { - fileData = this.state.syscheck.filter((item) => { + fileData = this.state.syscheck.filter(item => { return item.file === file; }); } else { - const response = await WzRequest.apiReq('GET', `/syscheck/${this.props.agent.id}`, { - params: { - file: file, + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + file: file, + }, }, - }); + ); fileData = ((response.data || {}).data || {}).affected_items || []; } - if (!redirect) window.location.href = window.location.href += `&file=${file}`; + if (!redirect) + window.location.href = window.location.href += `&file=${file}`; //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. this.setState({ isFlyoutVisible: false }, () => - this.setState({ isFlyoutVisible: true, currentFile: file, syscheckItem: item }) + this.setState({ + isFlyoutVisible: true, + currentFile: file, + syscheckItem: item, + }), ); } - async componentDidUpdate(prevProps) { - const { filters } = this.props; - if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }, this.getSyscheck); - } - } - - async getSyscheck() { - const agentID = this.props.agent.id; - try { - const syscheck = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { - params: this.buildFilter(), - }); - - this.props.onTotalItemsChange( + // TODO: connect to total items change on parent component + /* + tis.props.onTotalItemsChange( (((syscheck || {}).data || {}).data || {}).total_affected_items ); - - this.setState({ - syscheck: (((syscheck || {}).data || {}).data || {}).affected_items || {}, - totalItems: (((syscheck || {}).data || {}).data || {}).total_affected_items - 1, - isLoading: false, - error: undefined, - }); - } catch (error) { - this.setState({ error, isLoading: false }); - const options: UIErrorLog = { - context: `${InventoryTable.name}.getSyscheck`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - - const field = sortField === 'os_name' ? '' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + field; - } - - buildFilter() { - const { pageIndex, pageSize } = this.state; - const filters = filtersToObject(this.props.filters); - const filter = { - ...filters, - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - type: 'file', - }; - return filter; - } - - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this.setState( - { - pageIndex, - pageSize, - sortField, - sortDirection, - isLoading: true, - }, - () => this.getSyscheck() - ); - }; + */ columns() { let width; @@ -200,13 +123,25 @@ export class InventoryTable extends Component { name: 'File', sortable: true, width: '250px', + searchable: true, }, { field: 'mtime', - name: 'Last Modified', + name: ( + + Last Modified{' '} + + + ), sortable: true, width: '100px', render: formatUIDate, + searchable: false, }, { field: 'uname', @@ -214,6 +149,7 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'uid', @@ -221,6 +157,7 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'gname', @@ -228,6 +165,7 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'gid', @@ -235,63 +173,101 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'size', name: 'Size', sortable: true, width: `${width}`, + searchable: true, }, ]; } renderFilesTable() { - const getRowProps = (item) => { + const getRowProps = item => { const { file } = item; return { 'data-test-subj': `row-${file}`, onClick: () => this.showFlyout(file, item), }; }; - - const { - syscheck, - pageIndex, - pageSize, - totalItems, - sortField, - sortDirection, - isLoading, - error, - } = this.state; const columns = this.columns(); - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [15, 25, 50, 100], - }; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; return ( - [ + { label: 'file', description: 'filter by file' }, + { label: 'gid', description: 'filter by group id' }, + { label: 'gname', description: 'filter by group name' }, + { + label: 'mtime', + description: 'filter by modification time', + }, + { label: 'size', description: 'filter by size' }, + { label: 'uname', description: 'filter by user name' }, + { label: 'uid', description: 'filter by user id' }, + ], + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + // Add the implicit query + q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + } + : { + q: `${searchBarWQLOptions.implicitQuery.query}`, + }), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + validate: { + value: ({ formattedValue, value: rawValue }, { field }) => { + const value = formattedValue ?? rawValue; + if (value) { + if (['mtime'].some(dateField => dateField === field)) { + return /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2}(.\d{1,6})?Z?)?$/.test( + value, + ) + ? undefined + : `"${value}" is not a expected format. Valid formats: YYYY-MM-DD, YYYY-MM-DD HH:mm:ss, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ.`; + } + } + }, + }, + }} + filters={searchBarWQLFilters} + showReload + downloadCsv={`fim-files-${this.props.agent.id}`} + searchTable={true} rowProps={getRowProps} - sorting={sorting} - itemId="file" - isExpandable={true} - loading={isLoading} /> @@ -301,7 +277,7 @@ export class InventoryTable extends Component { render() { const filesTable = this.renderFilesTable(); return ( -
+
{filesTable} {this.state.isFlyoutVisible && ( this.closeFlyout()} - type="file" - view="inventory" + type='file' + view='inventory' showViewInEvents={true} {...this.props} /> From 1c145d98ac1b92394c77c3096cbbff8e7d75a513 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:42:52 +0200 Subject: [PATCH 6/6] Replace search bar on Management > Groups (#5445) * 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): replace search bar in TableWzAPI Replace search bar in TableWzAPI Replace each usage of TableWzAPI Adapt external filters * feat(vulnerabilities): change filter by severity tooltip * fix(test): update test and snapthost * feat(search-bar): replace search bar and table component on Management/Groups Replace serach bar and table componnet on Management/Groups, Management/Groups/Group/Agents and Management/Groups/Group/Files * fix(test): update snapshot * feat(table-wz-api): add field selection to the TableWzAPI component Add field selection to the TableWzAPI Possibility to save the selected fields on storage (localStorage, sessionStorage) Create useStateStorage that allows save the state in localStorage or sessionStorage * 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 * feat(table-wz-api): Adapt TableWzAPI usage to the recent changes when using the search bar Enhance Management/Decoders table * fix(test): fixed test and update snapshot * fix(table-wz-api): minor fixes on TableWzAPI usage * fix: fixed prop type * fix: adapt search bar parameters on Management/Groups tables Adapt search bar parameters on Management/Groups tables Enhance group agents table: - Removed os.name and os.version columns - Add new Operating system column - Enhance the rendering of: - IP address - Operating system - Status * fix: fix search term field on TableWzAPI for composed column * fix(table-wz-api): enhance TableWithSearchBar types and fix error HTML attributes * fix: enhance search bar and WQL types * fix: test snapshot * fix: remove duplicated search bar * move: move search bar * feat: add the distinct values for the search bar suggestions in some sections - Add the distinct values for the search bar suggestions in some sections: - Modules > Security Configuration Assessment policy checks table - Modules > Vulnerabilities > Inventory table - Modules > MITRE ATT&CK > Inventory table - Management > Rules table - Management > Decoders table - Management > CDB Lists table - Add Path column to Rules files table - Add Path column to Decoders files table * fix: remove exact validation for the token value due to performance problems * fix: fix token value validation * fix: add suggestions for the search bar related to Management > Groups section Tables: - Management > Groups - Management > Groups > {group_id} > Agents - Management > Groups > {group_id} > Files * fix: fix Management > Rules search bar filters Add id field Fix groups filter in Rule info flyout Remove onFiltersChange handler * 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): update test snapshot * fix(table-wz-api): avoid the double toast message when there is an error fetching data and replace the error.name to RequestError * fix(search-bar): add validation for value token in WQL * fix(search-bar): value token in message related to this is invalid * fix(search-bar): fix error related to details.program_name suggestion in the Decoders section * feat(search-bar): add constant to define the count of distinct values to get in the suggestions * feat(search-bar): use constant to define the count of distinct values to get in the suggestions * fix(search-bar): fix value suggestions in the Decoders section * fix: add comment to constant * workaround(search-bar): add a filter to the value suggestions in WQL When getting the distinct values for some fields, the value could not match the regular expression that validates them, and this causes that the search can not be run. So, we filters the distinct values to ensure or reduce the suggestions can be used to search. This causes some possible values are not displayed in the suggestions. To undone this, then the API should allow these values. * changelog: add entry * changelog: add entry * changelog: add entry * fix(wql): add whitespace before closing grouping operator ) when using the suggestions * feat(search-bar): add a debounce time to update the search bar state * fix(search-bar): fix prop type error related to EuiSuggestItem * fix(search-bar-wql): problem related to execute the search before the input is analyzed due to this process is debounced * fix(search-bar-wql): remove unnued parameter in function * fix(search-bar): fix tests * fix(search-bar): suggestions in Modules > Vulenerabilities > Inventory * fix(serach-bar): search term that doesn't define the value for os.name --- CHANGELOG.md | 2 +- .../groups/actions-buttons-agents.js | 196 ---------- .../groups/actions-buttons-files.js | 196 ---------- .../management/groups/actions-buttons-main.js | 192 ++-------- .../management/groups/group-agents-table.js | 237 ++++++------ .../management/groups/group-detail.js | 33 +- .../management/groups/group-files-table.js | 228 +++--------- .../management/groups/groups-overview.js | 350 ++++++++++++++++-- .../management/groups/groups-table.js | 303 --------------- .../management/groups/utils/columns-files.js | 2 + 10 files changed, 531 insertions(+), 1208 deletions(-) delete mode 100644 plugins/main/public/controllers/management/components/management/groups/groups-table.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a85a6ce893..1baa655b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) -- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}, Integrity monitoring > Inventory > Files, Integrity monitoring > Inventory > Registry), Agent Inventory data [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) [#5444](https://github.com/wazuh/wazuh-kibana-app/pull/5444) +- Changed the search bar in Management (Rules, Decoders, CDB List, Groups) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}, Integrity monitoring > Inventory > Files, Integrity monitoring > Inventory > Registry), Agent Inventory data [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) [#5444](https://github.com/wazuh/wazuh-kibana-app/pull/5444) [#5445](https://github.com/wazuh/wazuh-kibana-app/pull/5445) ### Fixed diff --git a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-agents.js b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-agents.js index 4b688f74f5..39e04635a5 100644 --- a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-agents.js +++ b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-agents.js @@ -16,20 +16,12 @@ import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { connect } from 'react-redux'; import { - updateLoadingStatus, - updateIsProcessing, updateShowAddAgents, - updateReload, } from '../../../../../redux/actions/groupsActions'; -import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; import { ExportConfiguration } from '../../../../agent/components/export-configuration'; import { ReportingService } from '../../../../../react-services/reporting'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzGroupsActionButtonsAgents extends Component { _isMounted = false; @@ -38,65 +30,10 @@ class WzGroupsActionButtonsAgents extends Component { super(props); this.reportingService = new ReportingService(); - this.state = { - generatingCsv: false, - isPopoverOpen: false, - newGroupName: '', - }; - this.exportCsv = exportCsv; this.groupsHandler = GroupsHandler; - this.refreshTimeoutId = null; } - componentDidMount() { - this._isMounted = true; - if (this._isMounted) this.bindEnterToInput(); - } - - componentDidUpdate() { - this.bindEnterToInput(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Refresh the items - */ - async refresh() { - try { - this.props.updateReload(); - this.props.updateIsProcessing(true); - this.onRefreshLoading(); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsAgents.name}.refresh`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.message || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - onRefreshLoading() { - clearInterval(this.refreshTimeoutId); - - this.props.updateLoadingStatus(true); - this.refreshTimeoutId = setInterval(() => { - if (!this.props.state.isProcessing) { - this.props.updateLoadingStatus(false); - clearInterval(this.refreshTimeoutId); - } - }, 100); - } showManageAgents() { const { itemDetail } = this.props.state; @@ -105,117 +42,6 @@ class WzGroupsActionButtonsAgents extends Component { this.props.updateShowAddAgents(true); } - closePopover() { - this.setState({ - isPopoverOpen: false, - msg: false, - newGroupName: '', - }); - } - - clearGroupName() { - this.setState({ - newGroupName: '', - }); - } - - onChangeNewGroupName = (e) => { - this.setState({ - newGroupName: e.target.value, - }); - }; - - /** - * Looking for the input element to bind the keypress event, once the input is found the interval is clear - */ - bindEnterToInput() { - try { - const interval = setInterval(async () => { - const input = document.getElementsByClassName('groupNameInput'); - if (input.length) { - const i = input[0]; - if (!i.onkeypress) { - i.onkeypress = async (e) => { - if (e.which === 13) { - await this.createGroup(); - } - }; - } - clearInterval(interval); - } - }, 150); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsAgents.name}.bindEnterToInput`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.message || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - async createGroup() { - try { - this.props.updateLoadingStatus(true); - await this.groupsHandler.saveGroup(this.state.newGroupName); - this.showToast('success', 'Success', 'The group has been created successfully', 2000); - this.clearGroupName(); - - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.closePopover(); - } catch (error) { - this.props.updateLoadingStatus(false); - throw new Error(error); - } - } - - /** - * Generates a CSV - */ - async generateCsv() { - try { - this.setState({ generatingCsv: true }); - const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store - await this.exportCsv(`/groups/${this.props.state.itemDetail.name}/agents`, filters, 'Groups'); - this.showToast( - 'success', - 'Success', - 'CSV. Your download should begin automatically...', - 2000 - ); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsAgents.name}.generateCsv`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error when exporting the CSV file: ${error.message || error}`, - }, - }; - getErrorOrchestrator().handleError(options); - } - this.setState({ generatingCsv: false }); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - render() { // Add new group button const manageAgentsButton = ( @@ -237,30 +63,11 @@ class WzGroupsActionButtonsAgents extends Component { type="group" /> ); - // Export button - const exportCSVButton = ( - await this.generateCsv()} - isLoading={this.state.generatingCsv} - > - Export formatted - - ); - - // Refresh - const refreshButton = ( - await this.refresh()}> - Refresh - - ); return ( {manageAgentsButton} {exportPDFButton} - {exportCSVButton} - {refreshButton} ); } @@ -274,10 +81,7 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), updateShowAddAgents: (showAddAgents) => dispatch(updateShowAddAgents(showAddAgents)), - updateReload: () => dispatch(updateReload()), }; }; diff --git a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js index f32424dc95..2236648697 100644 --- a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js +++ b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js @@ -16,88 +16,24 @@ import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { connect } from 'react-redux'; import { - updateLoadingStatus, - updateIsProcessing, updateFileContent, } from '../../../../../redux/actions/groupsActions'; -import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; import { ExportConfiguration } from '../../../../agent/components/export-configuration'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { ReportingService } from '../../../../../react-services/reporting'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzGroupsActionButtonsFiles extends Component { - _isMounted = false; - constructor(props) { super(props); this.reportingService = new ReportingService(); - this.state = { - generatingCsv: false, - isPopoverOpen: false, - newGroupName: '', - }; - this.exportCsv = exportCsv; - this.groupsHandler = GroupsHandler; this.refreshTimeoutId = null; } - componentDidMount() { - this._isMounted = true; - if (this._isMounted) this.bindEnterToInput(); - } - - componentDidUpdate() { - this.bindEnterToInput(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Refresh the items - */ - async refresh() { - try { - this.props.updateIsProcessing(true); - this.onRefreshLoading(); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsFiles.name}.refresh`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - onRefreshLoading() { - clearInterval(this.refreshTimeoutId); - - this.props.updateLoadingStatus(true); - this.refreshTimeoutId = setInterval(() => { - if (!this.props.state.isProcessing) { - this.props.updateLoadingStatus(false); - clearInterval(this.refreshTimeoutId); - } - }, 100); - } - autoFormat = (xml) => { var reg = /(>)\s*(<)(\/*)/g; var wsexp = / *(.*) +\n/g; @@ -173,117 +109,6 @@ class WzGroupsActionButtonsFiles extends Component { this.props.updateFileContent(file); } - closePopover() { - this.setState({ - isPopoverOpen: false, - msg: false, - newGroupName: '', - }); - } - - clearGroupName() { - this.setState({ - newGroupName: '', - }); - } - - onChangeNewGroupName = (e) => { - this.setState({ - newGroupName: e.target.value, - }); - }; - - /** - * Looking for the input element to bind the keypress event, once the input is found the interval is clear - */ - bindEnterToInput() { - try { - const interval = setInterval(async () => { - const input = document.getElementsByClassName('groupNameInput'); - if (input.length) { - const i = input[0]; - if (!i.onkeypress) { - i.onkeypress = async (e) => { - if (e.which === 13) { - await this.createGroup(); - } - }; - } - clearInterval(interval); - } - }, 150); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsFiles.name}.bindEnterToInput`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - async createGroup() { - try { - this.props.updateLoadingStatus(true); - await this.groupsHandler.saveGroup(this.state.newGroupName); - this.showToast('success', 'Success', 'The group has been created successfully', 2000); - this.clearGroupName(); - - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.closePopover(); - } catch (error) { - this.props.updateLoadingStatus(false); - throw new Error(error); - } - } - - /** - * Generates a CSV - */ - async generateCsv() { - try { - this.setState({ generatingCsv: true }); - const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store - await this.exportCsv(`/groups/${this.props.state.itemDetail.name}/files`, filters, 'Groups'); - this.showToast( - 'success', - 'Success', - 'CSV. Your download should begin automatically...', - 2000 - ); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsFiles.name}.generateCsv`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error when exporting the CSV file: ${error.message || error}`, - }, - }; - getErrorOrchestrator().handleError(options); - } - this.setState({ generatingCsv: false }); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - render() { // Add new group button const groupConfigurationButton = ( @@ -313,30 +138,11 @@ class WzGroupsActionButtonsFiles extends Component { type="group" /> ); - // Export button - const exportCSVButton = ( - await this.generateCsv()} - isLoading={this.state.generatingCsv} - > - Export formatted - - ); - - // Refresh - const refreshButton = ( - await this.refresh()}> - Refresh - - ); return ( {groupConfigurationButton} {exportPDFButton} - {exportCSVButton} - {refreshButton} ); } @@ -350,8 +156,6 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), updateFileContent: (content) => dispatch(updateFileContent(content)), }; }; diff --git a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js index d286ab652c..9e6b7cc061 100644 --- a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js +++ b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js @@ -9,28 +9,18 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; // Eui components import { EuiFlexItem, - EuiButtonEmpty, EuiPopover, EuiFormRow, EuiFieldText, - EuiSpacer, EuiFlexGroup, - EuiButton, } from '@elastic/eui'; -import { connect } from 'react-redux'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; -import { - updateLoadingStatus, - updateIsProcessing, -} from '../../../../../redux/actions/groupsActions'; - -import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; import { getToasts } from '../../../../../kibana-services'; import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; @@ -45,14 +35,10 @@ class WzGroupsActionButtons extends Component { super(props); this.state = { - generatingCsv: false, isPopoverOpen: false, newGroupName: '', }; - this.exportCsv = exportCsv; - this.groupsHandler = GroupsHandler; - this.refreshTimeoutId = null; } componentDidMount() { @@ -68,41 +54,6 @@ class WzGroupsActionButtons extends Component { this._isMounted = false; } - /** - * Refresh the items - */ - async refresh() { - try { - this.props.updateIsProcessing(true); - this.onRefreshLoading(); - } catch (error) { - const options = { - context: `${WzGroupsActionButtons.name}.refresh`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.message || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - onRefreshLoading() { - clearInterval(this.refreshTimeoutId); - - this.props.updateLoadingStatus(true); - this.refreshTimeoutId = setInterval(() => { - if (!this.props.state.isProcessing) { - this.props.updateLoadingStatus(false); - clearInterval(this.refreshTimeoutId); - } - }, 100); - } - togglePopover() { if (this.state.isPopoverOpen) { this.closePopover(); @@ -169,13 +120,11 @@ class WzGroupsActionButtons extends Component { async createGroup() { try { if (this.isOkNameGroup(this.state.newGroupName)) { - this.props.updateLoadingStatus(true); - await this.groupsHandler.saveGroup(this.state.newGroupName); + await GroupsHandler.saveGroup(this.state.newGroupName); this.showToast('success', 'Success', 'The group has been created successfully', 2000); this.clearGroupName(); - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); + this.props.reloadTable(); this.closePopover(); } } catch (error) { @@ -192,42 +141,10 @@ class WzGroupsActionButtons extends Component { }, }; getErrorOrchestrator().handleError(options); - this.props.updateLoadingStatus(false); throw new Error(error); } } - /** - * Generates a CSV - */ - async generateCsv() { - try { - this.setState({ generatingCsv: true }); - const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store - await this.exportCsv('/groups', filters, 'Groups'); - this.showToast( - 'success', - 'Success', - 'CSV. Your download should begin automatically...', - 2000 - ); - } catch (error) { - const options = { - context: `${WzGroupsActionButtons.name}.generateCsv`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error when exporting the CSV file: ${error.message || error}`, - }, - }; - getErrorOrchestrator().handleError(options); - } - this.setState({ generatingCsv: false }); - } - showToast = (color, title, text, time) => { getToasts().add({ color: color, @@ -255,78 +172,41 @@ class WzGroupsActionButtons extends Component { ); - // Export button - const exportButton = ( - await this.generateCsv()} - isLoading={this.state.generatingCsv} - > - Export formatted - - ); - - // Refresh - const refreshButton = ( - await this.refresh()}> - Refresh - - ); - return ( - - - this.closePopover()} - > - - - - - - - - { - await this.createGroup(); - }} - > - Save new group - - - - - - {exportButton} - {refreshButton} - + this.closePopover()} + > + + + + + + + + { + await this.createGroup(); + }} + > + Save new group + + + + ); } } -const mapStateToProps = (state) => { - return { - state: state.groupsReducers, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(WzGroupsActionButtons); +export default WzGroupsActionButtons; diff --git a/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js b/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js index e7c4a11aba..f1b6225e9e 100644 --- a/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js +++ b/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js @@ -9,7 +9,8 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { connect } from 'react-redux'; import GroupsHandler from './utils/groups-handler'; @@ -25,171 +26,80 @@ import { updateSortFieldAgents, updateReload, } from '../../../../../redux/actions/groupsActions'; -import { EuiCallOut } from '@elastic/eui'; -import { getAgentFilterValues } from './get-agents-filters-values'; import { TableWzAPI } from '../../../../../components/common/tables'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { WzButtonPermissionsModalConfirm } from '../../../../../components/common/buttons'; import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, UI_LOGGER_LEVELS, UI_ORDER_AGENT_STATUS, } from '../../../../../../common/constants'; +import { get as getLodash } from 'lodash'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; +import { AgentStatus } from '../../../../../components/agents/agent_status'; +import { WzRequest } from '../../../../../react-services'; class WzGroupAgentsTable extends Component { _isMounted = false; constructor(props) { super(props); - this.suggestions = [ - { - type: 'q', - label: 'status', - description: 'Filter by agent connection status', - operators: ['=', '!='], - values: UI_ORDER_AGENT_STATUS, - }, - { - type: 'q', - label: 'os.platform', - description: 'Filter by operating system platform', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('os.platform', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'ip', - description: 'Filter by agent IP address', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('ip', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'name', - description: 'Filter by agent name', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('name', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'id', - description: 'Filter by agent id', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('id', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'node_name', - description: 'Filter by node name', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('node_name', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'manager', - description: 'Filter by manager', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('manager', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'version', - description: 'Filter by agent version', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('version', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'configSum', - description: 'Filter by agent config sum', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('configSum', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'mergedSum', - description: 'Filter by agent merged sum', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('mergedSum', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - //{ type: 'q', label: 'dateAdd', description: 'Filter by add date', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('dateAdd', value, {q: `group=${this.props.state.itemDetail.name}`})}, - //{ type: 'q', label: 'lastKeepAlive', description: 'Filter by last keep alive', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('lastKeepAlive', value, {q: `group=${this.props.state.itemDetail.name}`})}, - ]; - this.groupsHandler = GroupsHandler; this.columns = [ { field: 'id', name: 'Id', align: 'left', + searchable: true, sortable: true, }, { field: 'name', name: 'Name', align: 'left', + searchable: true, sortable: true, }, { field: 'ip', name: 'IP address', - sortable: true, - show: true, - }, - { - field: 'status', - name: 'Status', align: 'left', + searchable: true, sortable: true, }, { - field: 'os.name', - name: 'Operating system name', + field: 'os.name,os.version', + composeField: ['os.name', 'os.version'], + name: 'Operating system', align: 'left', + searchable: true, sortable: true, + render: (field, agentData) => this.addIconPlatformRender(agentData), }, { - field: 'os.version', - name: 'Operating system version', + field: 'version', + name: 'Version', align: 'left', + searchable: true, sortable: true, }, { - field: 'version', - name: 'Version', + field: 'status', + name: 'Status', align: 'left', + searchable: true, sortable: true, + render: status => ( + + ), }, { name: 'Actions', align: 'left', + searchable: false, render: item => { return (
@@ -251,20 +161,105 @@ class WzGroupAgentsTable extends Component { }, }, ]; + + this.searchBar = { + wql: { + suggestionFields: [ + { label: 'id', description: `filter by ID` }, + { label: 'ip', description: `filter by IP address` }, + { label: 'name', description: `filter by Name` }, + { label: 'os.name', description: `filter by Operating system name` }, + { + label: 'os.version', + description: `filter by Operating system version`, + }, + { label: 'status', description: `filter by Status` }, + { label: 'version', description: `filter by Version` }, + ], + }, + }; } componentWillUnmount() { this._isMounted = false; } + + addIconPlatformRender(agent) { + let icon = ''; + const os = agent?.os || {}; + + if ((os?.uname || '').includes('Linux')) { + icon = 'linux'; + } else if (os?.platform === 'windows') { + icon = 'windows'; + } else if (os?.platform === 'darwin') { + icon = 'apple'; + } + const os_name = `${agent?.os?.name || ''} ${agent?.os?.version || ''}`; + + return ( + + + + {' '} + {os_name.trim() || '-'} + + ); + } + render() { const { error } = this.props.state; + const groupName = this.props.state?.itemDetail?.name; + const searchBarSuggestionsFields = this.searchBar.wql.suggestionFields; if (!error) { return ( searchBarSuggestionsFields, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/groups/${groupName}/agents`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + mapResponseItem={item => ({ + ...item, + ...(item.ip ? { ip: item.ip } : { ip: '-' }), + ...(typeof item.version === 'string' + ? { version: item.version.match(/(v\d.+)/)?.[1] } + : { version: '-' }), + })} + showReload + downloadCsv={`agents-group-${groupName}`} reload={this.props.state.reload} searchTable={true} tableProps={{ tableLayout: 'auto' }} @@ -289,9 +284,7 @@ class WzGroupAgentsTable extends Component { this.props.updateLoadingStatus(true); try { await Promise.all( - items.map(item => - this.groupsHandler.deleteAgent(item.id, itemDetail.name), - ), + items.map(item => GroupsHandler.deleteAgent(item.id, itemDetail.name)), ); this.props.updateIsProcessing(true); this.props.updateLoadingStatus(false); diff --git a/plugins/main/public/controllers/management/components/management/groups/group-detail.js b/plugins/main/public/controllers/management/components/management/groups/group-detail.js index 99ca3e3fc9..bbfc161651 100644 --- a/plugins/main/public/controllers/management/components/management/groups/group-detail.js +++ b/plugins/main/public/controllers/management/components/management/groups/group-detail.js @@ -83,40 +83,13 @@ class WzGroupDetail extends Component { renderAgents() { return ( - - - - - From here you can list and manage your agents - - - - - - - - - + ); } renderFiles() { return ( - - - - - From here you can list and see your group files, also, you can - edit the group configuration - - - - - - - - - + ); } @@ -142,7 +115,7 @@ class WzGroupDetail extends Component { - +

{itemDetail.name}

diff --git a/plugins/main/public/controllers/management/components/management/groups/group-files-table.js b/plugins/main/public/controllers/management/components/management/groups/group-files-table.js index b919bac86c..b5a0ed3f41 100644 --- a/plugins/main/public/controllers/management/components/management/groups/group-files-table.js +++ b/plugins/main/public/controllers/management/components/management/groups/group-files-table.js @@ -9,12 +9,8 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; -import { EuiBasicTable, EuiCallOut, EuiSpacer } from '@elastic/eui'; - +import React, { Component } from 'react'; import { connect } from 'react-redux'; -import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; import { updateLoadingStatus, @@ -22,185 +18,86 @@ import { updatePageIndexFile, updateSortDirectionFile, updateSortFieldFile, - updateFileContent + updateFileContent, } from '../../../../../redux/actions/groupsActions'; import GroupsFilesColumns from './utils/columns-files'; -import { WzSearchBar, filtersToObject } from '../../../../../components/wz-search-bar'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; - +import { TableWzAPI } from '../../../../../components/common/tables'; +import { WzRequest } from '../../../../../react-services'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../common/constants'; class WzGroupFilesTable extends Component { _isMounted = false; - suggestions = [ - //{ type: 'q', label: 'filename', description: 'Filter by file name', operators: ['=', '!=',], values: async (value) => getGroupsFilesValues('filename', value, {},this.props.state.itemDetail.name )}, - //{ type: 'params', label: 'hash', description: 'Filter by hash', operators: ['=', '!=',], values: async (value) => getGroupsFilesValues('hash', value, {},this.props.state.itemDetail.name )}, - ]; constructor(props) { super(props); this.state = { - items: [], - pageSize: 10, - totalItems: 0, - filters: [] + filters: {}, }; - this.groupsHandler = GroupsHandler; - } - - async componentDidMount() { - await this.getItems(); - this._isMounted = true; - } - - async componentDidUpdate(prevProps, prevState) { - if (this.props.state.isProcessing && this._isMounted) { - await this.getItems(); - } - const { filters } = this.state; - if (JSON.stringify(filters) !== JSON.stringify(prevState.filters)) { - await this.getItems(); - } - } - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Loads the initial information - */ - async getItems() { - try { - const rawItems = await this.groupsHandler.filesGroup( - this.props.state.itemDetail.name, - { params: this.buildFilter() } - ); - const { affected_items, total_affected_items } = ((rawItems || {}).data || {}).data; - - this.setState({ - items: affected_items, - totalItems: total_affected_items, - isProcessing: false - }); - this.props.state.isProcessing && this.props.updateIsProcessing(false); - } catch (error) { - this.props.state.isProcessing && this.props.updateIsProcessing(false); - const options = { - context: `${WzGroupFilesTable.name}.getItems`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error loading the groups: ${error.message || error}`, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - buildFilter() { - const { pageIndexFile } = this.props.state; - const { pageSize, filters } = this.state; - const filter = { - ...filtersToObject(filters), - offset: pageIndexFile * pageSize, - limit: pageSize, - sort: this.buildSortFilter() + this.searchBar = { + wql: { + suggestionsFields: [ + { label: 'filename', description: 'filter by filename' }, + { label: 'hash', description: 'filter by hash' }, + ], + }, }; - - return filter; - } - - buildSortFilter() { - const { sortFieldFile, sortDirectionFile } = this.props.state; - - const field = sortFieldFile; - const direction = sortDirectionFile === 'asc' ? '+' : '-'; - - return direction + field; } - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndexFile, size: pageSize } = page; - const { field: sortFieldFile, direction: sortDirectionFile } = sort; - this.setState({ pageSize }); - this.props.updatePageIndexFile(pageIndexFile); - this.props.updateSortDirectionFile(sortDirectionFile); - this.props.updateSortFieldFile(sortFieldFile); - this.props.updateIsProcessing(true); - }; - render() { this.groupsAgentsColumns = new GroupsFilesColumns(this.props); - const { - isLoading, - pageIndexFile, - error, - sortFieldFile, - sortDirectionFile - } = this.props.state; - const { items, pageSize, totalItems, filters } = this.state; const columns = this.groupsAgentsColumns.columns; - const message = isLoading ? null : 'No results...'; - const pagination = { - pageIndex: pageIndexFile, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [10, 25, 50, 100] - }; - const sorting = { - sort: { - field: sortFieldFile, - direction: sortDirectionFile - } - }; - - if (!error) { - return ( - - this.setState({filters})} - placeholder='Search file' - /> - - - - ); - } else { - return ; - } + const groupName = this.props.state?.itemDetail?.name; + const searchBarWQL = this.searchBar.wql; + + return ( + searchBarWQL.suggestionsFields, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/groups/${groupName}/files`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} + showReload + downloadCsv={`files-group-${groupName}`} + searchTable={true} + /> + ); } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time - }); - }; } const mapStateToProps = state => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; @@ -215,11 +112,8 @@ const mapDispatchToProps = dispatch => { dispatch(updateSortDirectionFile(sortDirectionFile)), updateSortFieldFile: sortFieldFile => dispatch(updateSortFieldFile(sortFieldFile)), - updateFileContent: content => dispatch(updateFileContent(content)) + updateFileContent: content => dispatch(updateFileContent(content)), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzGroupFilesTable); +export default connect(mapStateToProps, mapDispatchToProps)(WzGroupFilesTable); diff --git a/plugins/main/public/controllers/management/components/management/groups/groups-overview.js b/plugins/main/public/controllers/management/components/management/groups/groups-overview.js index 54218b174f..e3d7046d38 100644 --- a/plugins/main/public/controllers/management/components/management/groups/groups-overview.js +++ b/plugins/main/public/controllers/management/components/management/groups/groups-overview.js @@ -12,58 +12,326 @@ */ import React, { Component } from 'react'; import { - EuiFlexItem, - EuiFlexGroup, EuiPanel, - EuiTitle, - EuiText, - EuiPage + EuiPage, + EuiOverlayMask, + EuiConfirmModal, } from '@elastic/eui'; // Wazuh components -import WzGroupsTable from './groups-table'; import WzGroupsActionButtons from './actions-buttons-main'; import { connect } from 'react-redux'; -import { withUserAuthorizationPrompt } from '../../../../../components/common/hocs' +import { + withUserAuthorizationPrompt, + withUserPermissions, +} from '../../../../../components/common/hocs'; import { compose } from 'redux'; +import { TableWzAPI } from '../../../../../components/common/tables'; +import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; +import { + updateFileContent, + updateGroupDetail, + updateListItemsForRemove, + updateShowModal, +} from '../../../../../redux/actions/groupsActions'; +import { WzRequest, WzUserPermissions } from '../../../../../react-services'; +import { getToasts } from '../../../../../kibana-services'; +import GroupsHandler from './utils/groups-handler'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../common/constants'; export class WzGroupsOverview extends Component { _isMounted = false; constructor(props) { super(props); + this.state = { + reload: Date.now(), + }; + this.tableColumns = [ + { + field: 'name', + name: 'Name', + align: 'left', + searchable: true, + sortable: true, + }, + { + field: 'count', + name: 'Agents', + align: 'left', + searchable: true, + sortable: true, + }, + { + field: 'configSum', + name: 'Configuration checksum', + align: 'left', + searchable: true, + }, + { + name: 'Actions', + align: 'left', + searchable: false, + render: item => { + return ( +
+ { + this.props.updateGroupDetail(item); + }} + color='primary' + /> + { + ev.stopPropagation(); + this.showGroupConfiguration(item.name); + }} + /> + { + ev.stopPropagation(); + this.props.updateListItemsForRemove([item]); + this.props.updateShowModal(true); + }} + color='danger' + isDisabled={item.name === 'default'} + /> +
+ ); + }, + }, + ]; + this.reloadTable = this.reloadTable.bind(this); + } + + reloadTable() { + this.setState({ reload: Date.now() }); + } + + async removeItems(items) { + try { + const promises = items.map( + async (item, i) => await GroupsHandler.deleteGroup(item.name), + ); + await Promise.all(promises); + getToasts().add({ + color: 'success', + title: 'Success', + text: 'Deleted successfully', + toastLifeTimeMs: 3000, + }); + } catch (error) { + getToasts().add({ + color: 'danger', + title: 'Error', + text: error, + toastLifeTimeMs: 3000, + }); + } finally { + this.reloadTable(); + } + } + + async showGroupConfiguration(groupId) { + const result = await GroupsHandler.getFileContent( + `/groups/${groupId}/files/agent.conf/xml`, + ); + + const file = { + name: 'agent.conf', + content: this.autoFormat(result), + isEditable: true, + groupName: groupId, + }; + this.props.updateFileContent(file); } + autoFormat = xml => { + var reg = /(>)\s*(<)(\/*)/g; + var wsexp = / *(.*) +\n/g; + var contexp = /(<.+>)(.+\n)/g; + xml = xml + .replace(reg, '$1\n$2$3') + .replace(wsexp, '$1\n') + .replace(contexp, '$1\n$2'); + var formatted = ''; + var lines = xml.split('\n'); + var indent = 0; + var lastType = 'other'; + var transitions = { + 'single->single': 0, + 'single->closing': -1, + 'single->opening': 0, + 'single->other': 0, + 'closing->single': 0, + 'closing->closing': -1, + 'closing->opening': 0, + 'closing->other': 0, + 'opening->single': 1, + 'opening->closing': 0, + 'opening->opening': 1, + 'opening->other': 1, + 'other->single': 0, + 'other->closing': -1, + 'other->opening': 0, + 'other->other': 0, + }; + + for (var i = 0; i < lines.length; i++) { + var ln = lines[i]; + if (ln.match(/\s*<\?xml/)) { + formatted += ln + '\n'; + continue; + } + var single = Boolean(ln.match(/<.+\/>/)); // is this line a single tag? ex.
+ var closing = Boolean(ln.match(/<\/.+>/)); // is this a closing tag? ex. + var opening = Boolean(ln.match(/<[^!].*>/)); // is this even a tag (that's not ) + var type = single + ? 'single' + : closing + ? 'closing' + : opening + ? 'opening' + : 'other'; + var fromTo = lastType + '->' + type; + lastType = type; + var padding = ''; + + indent += transitions[fromTo]; + for (var j = 0; j < indent; j++) { + padding += '\t'; + } + if (fromTo == 'opening->closing') + formatted = formatted.substr(0, formatted.length - 1) + ln + '\n'; + // substr removes line break (\n) from prev loop + else formatted += padding + ln + '\n'; + } + return formatted.trim(); + }; + render() { + const actionButtons = [ + , + ]; + + const getRowProps = item => { + const { id } = item; + return { + 'data-test-subj': `row-${id}`, + className: 'customRowClass', + onClick: !WzUserPermissions.checkMissingUserPermissions( + [{ action: 'group:read', resource: `group:id:${item.name}` }], + this.props.userPermissions, + ) + ? () => this.props.updateGroupDetail(item) + : undefined, + }; + }; + return ( - - - - - -

Groups

-
-
-
-
- -
- - - - From here you can list and check your groups, its agents and - files. - - - - - - - - + [ + { label: 'name', description: 'filter by name' }, + { label: 'count', description: 'filter by count' }, + { + label: 'configSum', + description: 'filter by configuration checksum', + }, + ], + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq('GET', '/groups', { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} + rowProps={getRowProps} + endpoint={'/groups'} + downloadCsv={true} + showReload={true} + tablePageSizeOptions={[10, 25, 50, 100]} + />
+ {this.props.state.showModal ? ( + + this.props.updateShowModal(false)} + onConfirm={() => { + this.removeItems(this.props.state.itemList); + this.props.updateShowModal(false); + }} + cancelButtonText='Cancel' + confirmButtonText='Delete' + defaultFocusedButton='cancel' + buttonColor='danger' + > + + ) : null}
); } @@ -71,14 +339,22 @@ export class WzGroupsOverview extends Component { const mapStateToProps = state => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; +const mapDispatchToProps = dispatch => ({ + updateShowModal: showModal => dispatch(updateShowModal(showModal)), + updateListItemsForRemove: itemList => + dispatch(updateListItemsForRemove(itemList)), + updateGroupDetail: itemDetail => dispatch(updateGroupDetail(itemDetail)), + updateFileContent: content => dispatch(updateFileContent(content)), +}); export default compose( - withUserAuthorizationPrompt([{action: 'group:read', resource: 'group:id:*'}]), - connect( - mapStateToProps - ), + withUserAuthorizationPrompt([ + { action: 'group:read', resource: 'group:id:*' }, + ]), + connect(mapStateToProps, mapDispatchToProps), + withUserPermissions, )(WzGroupsOverview); diff --git a/plugins/main/public/controllers/management/components/management/groups/groups-table.js b/plugins/main/public/controllers/management/components/management/groups/groups-table.js deleted file mode 100644 index 421fcbe1ad..0000000000 --- a/plugins/main/public/controllers/management/components/management/groups/groups-table.js +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Wazuh app - React component for groups main table. - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component, Fragment } from 'react'; -import { - EuiBasicTable, - EuiCallOut, - EuiOverlayMask, - EuiConfirmModal, - EuiSpacer, -} from '@elastic/eui'; - -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; -import { WzSearchBar, filtersToObject } from '../../../../../components/wz-search-bar'; -import { withUserPermissions } from '../../../../../components/common/hocs/withUserPermissions'; -import { WzUserPermissions } from '../../../../../react-services/wz-user-permissions'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; - - -import { - updateLoadingStatus, - updateFileContent, - updateIsProcessing, - updatePageIndex, - updateShowModal, - updateListItemsForRemove, - updateSortDirection, - updateSortField, - updateGroupDetail, -} from '../../../../../redux/actions/groupsActions'; - -import GroupsColums from './utils/columns-main'; - -class WzGroupsTable extends Component { - _isMounted = false; - - suggestions = []; //TODO: Fix suggestions without q search for API 4.0 - - constructor(props) { - super(props); - this.state = { - items: [], - pageSize: 10, - totalItems: 0, - filters: [], - }; - - this.groupsHandler = GroupsHandler; - } - - async componentDidMount() { - this._isMounted = true; - await this.getItems(); - } - - shouldComponentUpdate(nextProps, nextState) { - const { items, filters } = this.state; - const { isProcessing, showModal, isLoading } = this.props.state; - if (showModal !== nextProps.state.showModal) return true; - if (isProcessing !== nextProps.state.isProcessing) return true; - if (JSON.stringify(items) !== JSON.stringify(nextState.items)) return true; - if (JSON.stringify(filters) !== JSON.stringify(nextState.filters)) return true; - if (isLoading !== nextProps.state.isLoading) return true; - return false; - } - - async componentDidUpdate(prevProps, prevState) { - const { filters } = this.state; - if ((JSON.stringify(filters) !== JSON.stringify(prevState.filters)) || - /** - Is verifying that isProcessing is true and that it has changed its value, - since in the shouldComponentUpdate it is making it re-execute several times - each time a state changes, regardless of whether it is a change in isProcessing. - */ - ( - prevProps.state.isProcessing !== this.props.state.isProcessing && - this.props.state.isProcessing && - this._isMounted - ) - ) { - await this.getItems(); - } - } - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Loads the initial information - */ - async getItems() { - try { - this.props.updateLoadingStatus(true); - const rawItems = await this.groupsHandler.listGroups({ params: this.buildFilter() }); - const { - affected_items: affectedItem, - total_affected_items: totalAffectedItem - } = rawItems?.data?.data; - this.setState({ - items: affectedItem, - totalItems: totalAffectedItem, - }); - this.props.updateLoadingStatus(false); - this.props.state.isProcessing && this.props.updateIsProcessing(false); - - } catch (error) { - this.props.updateLoadingStatus(false); - this.props.state.isProcessing && this.props.updateIsProcessing(false); - const options = { - context: `${WzGroupsTable.name}.getItems`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error getting groups`, - }, - }; - getErrorOrchestrator().handleError(options); - } - - } - - buildFilter() { - const { pageIndex } = this.props.state; - const { pageSize, filters } = this.state; - const filter = { - ...filtersToObject(filters), - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - }; - - return filter; - } - - buildSortFilter() { - const { sortField, sortDirection } = this.props.state; - - const field = sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + field; - } - - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this._isMounted && this.setState({ pageSize }); - this.props.updatePageIndex(pageIndex); - this.props.updateSortDirection(sortDirection); - this.props.updateSortField(sortField); - this.props.updateIsProcessing(true); - }; - - render() { - const { filters } = this.state; - - this.groupsColumns = new GroupsColums(this.props); - const { isLoading, pageIndex, error, sortField, sortDirection } = this.props.state; - const { items, pageSize, totalItems } = this.state; - const columns = this.groupsColumns.columns; - const message = isLoading ? null : 'No results...'; - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [10, 25, 50, 100], - }; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - const getRowProps = (item) => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - className: 'customRowClass', - onClick: !WzUserPermissions.checkMissingUserPermissions( - [{ action: 'group:read', resource: `group:id:${item.name}` }], - this.props.userPermissions - ) - ? () => this.props.updateGroupDetail(item) - : undefined, - }; - }; - - if (error) { - return ; - } - const itemList = this.props.state.itemList; - return ( - - this._isMounted && this.setState({ filters })} - placeholder="Search group" - /> - - - {this.props.state.showModal ? ( - - this.props.updateShowModal(false)} - onConfirm={() => { - this.removeItems(itemList); - this.props.updateShowModal(false); - }} - cancelButtonText="Cancel" - confirmButtonText="Delete" - defaultFocusedButton="cancel" - buttonColor="danger" - > - - ) : null} - - ); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - - async removeItems(items) { - this.props.updateLoadingStatus(true); - const results = items.map(async (item, i) => { - await this.groupsHandler.deleteGroup(item.name); - }); - - Promise.all(results).then( - (completed) => { - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.showToast('success', 'Success', 'Deleted successfully', 3000); - }, - (error) => { - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.showToast('danger', 'Error', error, 3000); - } - ); - } -} - -const mapStateToProps = (state) => { - return { - state: state.groupsReducers, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateFileContent: (content) => dispatch(updateFileContent(content)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), - updatePageIndex: (pageIndex) => dispatch(updatePageIndex(pageIndex)), - updateShowModal: (showModal) => dispatch(updateShowModal(showModal)), - updateListItemsForRemove: (itemList) => dispatch(updateListItemsForRemove(itemList)), - updateSortDirection: (sortDirection) => dispatch(updateSortDirection(sortDirection)), - updateSortField: (sortField) => dispatch(updateSortField(sortField)), - updateGroupDetail: (itemDetail) => dispatch(updateGroupDetail(itemDetail)), - }; -}; - -export default compose( - connect(mapStateToProps, mapDispatchToProps), - withUserPermissions -)(WzGroupsTable); diff --git a/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js b/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js index 46bedb5a08..6174625d70 100644 --- a/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js +++ b/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js @@ -42,12 +42,14 @@ export default class GroupsFilesColumns { field: 'filename', name: 'File', align: 'left', + searchable: true, sortable: true }, { field: 'hash', name: 'Checksum', align: 'left', + searchable: true, sortable: true } ];