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