diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df6835dd6..d3fc347ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new global error treatment (client-side) [#4163](https://github.com/wazuh/wazuh-kibana-app/pull/4163) - Added new CLI to generate API data from specification file [#5519](https://github.com/wazuh/wazuh-kibana-app/pull/5519) - Added specific RBAC permissions to Security section [#5551](https://github.com/wazuh/wazuh-kibana-app/pull/5551) +- Added Refresh and Export formatted button to panels in Agents > Inventory data [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) +- Added Refresh and Export formatted buttons to Management > Cluster > Nodes [#5491](https://github.com/wazuh/wazuh-kibana-app/pull/5491) ### Changed @@ -20,6 +22,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) +- Changed the search bar in Management (Rules, Decoders, CDB List, Groups, Cluster > Nodes) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}, Integrity monitoring > Inventory > Files, Integrity monitoring > Inventory > Registry), Agent Inventory data, Explore agent modal, Agents [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) [#5443](https://github.com/wazuh/wazuh-kibana-app/pull/5443) [#5444](https://github.com/wazuh/wazuh-kibana-app/pull/5444) [#5445](https://github.com/wazuh/wazuh-kibana-app/pull/5445) [#5447](https://github.com/wazuh/wazuh-kibana-app/pull/5447) [#5452](https://github.com/wazuh/wazuh-kibana-app/pull/5452) [#5491](https://github.com/wazuh/wazuh-kibana-app/pull/5491) ### Fixed @@ -65,6 +68,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 @@ -74,6 +80,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed method to perform redirection on agent table buttons [#5539](https://github.com/wazuh/wazuh-kibana-app/pull/5539) - Changed windows agent service name in the deploy agent wizard [#5538](https://github.com/wazuh/wazuh-kibana-app/pull/5538) +- Changed the requests to get the agent labels for the managers [#5687](https://github.com/wazuh/wazuh-kibana-app/pull/5687) ## Wazuh v4.5.0 - OpenSearch Dashboards 2.6.0 - Revision 01 diff --git a/docker/imposter/agents/configuration.js b/docker/imposter/agents/configuration.js new file mode 100644 index 0000000000..f1d3c93a34 --- /dev/null +++ b/docker/imposter/agents/configuration.js @@ -0,0 +1,15 @@ +var path = context.request.path; +var pathConfiguration = path.split('/'); +pathConfiguration.splice(0, 5); +console.log(pathConfiguration); +switch (pathConfiguration[0]) { + case 'labels': + respond() + .withStatusCode(200) + .withFile('agents/configuration/agent_labels.json'); + + break; + default: + respond().withStatusCode(200).withFile('agents/configuration/default.json'); + break; +} diff --git a/docker/imposter/agents/configuration/agent_labels.json b/docker/imposter/agents/configuration/agent_labels.json new file mode 100644 index 0000000000..a3bbe13481 --- /dev/null +++ b/docker/imposter/agents/configuration/agent_labels.json @@ -0,0 +1,12 @@ +{ + "data": { + "labels": [ + { + "value": "customLabel", + "key": "custom", + "hidden": "no" + } + ] + }, + "error": 0 +} diff --git a/docker/imposter/agents/configuration/default.json b/docker/imposter/agents/configuration/default.json new file mode 100644 index 0000000000..d97500d76f --- /dev/null +++ b/docker/imposter/agents/configuration/default.json @@ -0,0 +1,33 @@ +{ + "data": { + "client": { + "config-profile": "ubuntu, ubuntu20, ubuntu20.04", + "notify_time": 10, + "time-reconnect": 60, + "force_reconnect_interval": 0, + "ip_update_interval": 0, + "auto_restart": "yes", + "remote_conf": "yes", + "crypto_method": "aes", + "server": [ + { + "address": "nginx-lb/172.25.0.4", + "port": 1514, + "max_retries": 5, + "retry_interval": 10, + "protocol": "tcp" + } + ], + "enrollment": [ + { + "enabled": "yes", + "delay_after_enrollment": 20, + "port": 1515, + "ssl_cipher": "HIGH:!ADH:!EXP:!MD5:!RC4:!3DES:!CAMELLIA:@STRENGTH", + "auto_method": "no" + } + ] + } + }, + "error": 0 +} diff --git a/docker/imposter/cluster/configuration/agent_labels.json b/docker/imposter/cluster/configuration/agent_labels.json new file mode 100644 index 0000000000..52edc2ea1d --- /dev/null +++ b/docker/imposter/cluster/configuration/agent_labels.json @@ -0,0 +1,20 @@ +{ + "data": { + "affected_items": [ + { + "labels": [ + { + "value": "customLabel", + "key": "custom", + "hidden": "no" + } + ] + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Active configuration was successfully read in specified node.", + "error": 0 +} diff --git a/docker/imposter/manager/configuration.js b/docker/imposter/manager/configuration.js new file mode 100644 index 0000000000..9b5a87219d --- /dev/null +++ b/docker/imposter/manager/configuration.js @@ -0,0 +1,22 @@ +var path = context.request.path; +var pathConfiguration = path.split('/'); +pathConfiguration.splice(0, 4); +switch (pathConfiguration[0]) { + case 'labels': + respond() + .withStatusCode(200) + .withFile('manager/configuration/agent_labels.json'); + + break; + case 'reports': + respond() + .withStatusCode(200) + .withFile('manager/configuration/monitor_reports.json'); + + break; + default: + respond() + .withStatusCode(200) + .withFile('manager/configuration/default.json'); + break; +} diff --git a/docker/imposter/manager/configuration/agent_labels.json b/docker/imposter/manager/configuration/agent_labels.json new file mode 100644 index 0000000000..52edc2ea1d --- /dev/null +++ b/docker/imposter/manager/configuration/agent_labels.json @@ -0,0 +1,20 @@ +{ + "data": { + "affected_items": [ + { + "labels": [ + { + "value": "customLabel", + "key": "custom", + "hidden": "no" + } + ] + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Active configuration was successfully read in specified node.", + "error": 0 +} diff --git a/docker/imposter/manager/configuration/default.json b/docker/imposter/manager/configuration/default.json new file mode 100644 index 0000000000..614c20c2f8 --- /dev/null +++ b/docker/imposter/manager/configuration/default.json @@ -0,0 +1,35 @@ +{ + "data": { + "affected_items": [ + { + "global": { + "email_notification": "no", + "logall": "no", + "logall_json": "no", + "integrity_checking": 8, + "rootkit_detection": 8, + "host_information": 8, + "prelude_output": "no", + "zeromq_output": "no", + "jsonout_output": "yes", + "alerts_log": "yes", + "stats": 4, + "memory_size": 8192, + "white_list": [ + "127.0.0.1", + "80.58.61.250", + "80.58.61.254", + "localhost.localdomain" + ], + "rotate_interval": 0, + "max_output_size": 0 + } + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Active configuration was successfully read in specified node", + "error": 0 +} diff --git a/docker/imposter/manager/configuration/monitor_reports.json b/docker/imposter/manager/configuration/monitor_reports.json new file mode 100644 index 0000000000..a611e47fbe --- /dev/null +++ b/docker/imposter/manager/configuration/monitor_reports.json @@ -0,0 +1,16 @@ +{ + "data": { + "affected_items": [{ + "reports": [{ + "category": "syscheck", + "title": "Daily report: File changes", + "email_to": "example@test.com" + }] + }], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Could not read active configuration in specified node", + "error": 0 +} \ No newline at end of file diff --git a/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index 028dd20d3f..faffab6698 100755 --- a/docker/imposter/wazuh-config.yml +++ b/docker/imposter/wazuh-config.yml @@ -507,6 +507,9 @@ resources: # Get active configuration - method: GET path: /manager/configuration/{component}/{configuration} + response: + statusCode: 200 + scriptFile: manager/configuration.js # ===================================================== # # MITRE diff --git a/plugins/main/common/api-info/endpoints.json b/plugins/main/common/api-info/endpoints.json index 0691b9fb38..9323cdbaf6 100644 --- a/plugins/main/common/api-info/endpoints.json +++ b/plugins/main/common/api-info/endpoints.json @@ -265,7 +265,7 @@ }, { "name": ":configuration", - "description": "

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

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

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

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

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

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

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

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

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

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

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

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ComponentConfigurationTag
agentclient<client>
agentbuffer<client_buffer>
agentlabels<labels>
agentinternal<agent>, <monitord>, <remoted>
agentlessagentless<agentless>
analysisglobal<global>
analysisactive_response<active-response>
analysisalerts<alerts>
analysiscommand<command>
analysisrules<rule>
analysisdecoders<decoder>
analysisinternal<analysisd>
analysisrule_test<rule_test>
authauth<auth>
comactive-response<active-response>
comlogging<logging>
cominternal<execd>
comcluster<cluster>
csyslogcsyslog<csyslog_output>
integratorintegration<integration>
logcollectorlocalfile<localfile>
logcollectorsocket<socket>
logcollectorinternal<logcollector>
mailglobal<global><email...>
mailalerts<email_alerts>
mailinternal<maild>
monitorglobal<global>
monitorinternal<monitord>
monitorreports<reports>
requestglobal<global>
requestremote<remote>
requestinternal<remoted>
syschecksyscheck<syscheck>
syscheckrootcheck<rootcheck>
syscheckinternal<syscheck>, <rootcheck>
wazuh-dbinternal<wazuh_db>
wazuh-dbwdb<wdb>
wmoduleswmodules<wodle>
\n", "required": true, "schema": { "type": "string", @@ -9373,7 +9373,7 @@ "required": true, "schema": { "type": "string", - "format": "wazuh_path" + "format": "wpk_path" } }, { diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 403b9153e1..3917f85354 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -24,10 +24,10 @@ export const WAZUH_ALERTS_PREFIX = 'wazuh-alerts-'; export const WAZUH_ALERTS_PATTERN = 'wazuh-alerts-*'; // Job - Wazuh monitoring -export const WAZUH_INDEX_TYPE_MONITORING = "monitoring"; -export const WAZUH_MONITORING_PREFIX = "wazuh-monitoring-"; -export const WAZUH_MONITORING_PATTERN = "wazuh-monitoring-*"; -export const WAZUH_MONITORING_TEMPLATE_NAME = "wazuh-agent"; +export const WAZUH_INDEX_TYPE_MONITORING = 'monitoring'; +export const WAZUH_MONITORING_PREFIX = 'wazuh-monitoring-'; +export const WAZUH_MONITORING_PATTERN = 'wazuh-monitoring-*'; +export const WAZUH_MONITORING_TEMPLATE_NAME = 'wazuh-agent'; export const WAZUH_MONITORING_DEFAULT_INDICES_SHARDS = 1; export const WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS = 0; export const WAZUH_MONITORING_DEFAULT_CREATION = 'w'; @@ -36,9 +36,9 @@ export const WAZUH_MONITORING_DEFAULT_FREQUENCY = 900; export const WAZUH_MONITORING_DEFAULT_CRON_FREQ = '0 * * * * *'; // Job - Wazuh statistics -export const WAZUH_INDEX_TYPE_STATISTICS = "statistics"; -export const WAZUH_STATISTICS_DEFAULT_PREFIX = "wazuh"; -export const WAZUH_STATISTICS_DEFAULT_NAME = "statistics"; +export const WAZUH_INDEX_TYPE_STATISTICS = 'statistics'; +export const WAZUH_STATISTICS_DEFAULT_PREFIX = 'wazuh'; +export const WAZUH_STATISTICS_DEFAULT_NAME = 'statistics'; export const WAZUH_STATISTICS_PATTERN = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}-*`; export const WAZUH_STATISTICS_TEMPLATE_NAME = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}`; export const WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS = 1; @@ -60,7 +60,8 @@ export const WAZUH_SAMPLE_ALERT_PREFIX = 'wazuh-alerts-4.x-'; export const WAZUH_SAMPLE_ALERTS_INDEX_SHARDS = 1; export const WAZUH_SAMPLE_ALERTS_INDEX_REPLICAS = 0; export const WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY = 'security'; -export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = 'auditing-policy-monitoring'; +export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = + 'auditing-policy-monitoring'; export const WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION = 'threat-detection'; export const WAZUH_SAMPLE_ALERTS_DEFAULT_NUMBER_ALERTS = 3000; export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { @@ -74,7 +75,7 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { { apache: true, alerts: 2000 }, { web: true }, { windows: { service_control_manager: true }, alerts: 1000 }, - { github: true } + { github: true }, ], [WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING]: [ { rootcheck: true }, @@ -92,7 +93,8 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { }; // Security -export const WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY = 'OpenSearch Dashboards Security'; +export const WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY = + 'OpenSearch Dashboards Security'; export const WAZUH_SECURITY_PLUGINS = [ WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY, @@ -103,40 +105,49 @@ export const WAZUH_CONFIGURATION_CACHE_TIME = 10000; // time in ms; // Reserved ids for Users/Role mapping export const WAZUH_API_RESERVED_ID_LOWER_THAN = 100; -export const WAZUH_API_RESERVED_WUI_SECURITY_RULES = [ - 1, - 2 -]; +export const WAZUH_API_RESERVED_WUI_SECURITY_RULES = [1, 2]; // Wazuh data path const WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH = 'data'; export const WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH = path.join( __dirname, '../../../', - WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH + WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH, +); +export const WAZUH_DATA_ABSOLUTE_PATH = path.join( + WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, + 'wazuh', ); -export const WAZUH_DATA_ABSOLUTE_PATH = path.join(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, 'wazuh'); // Wazuh data path - config -export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'config'); -export const WAZUH_DATA_CONFIG_APP_PATH = path.join(WAZUH_DATA_CONFIG_DIRECTORY_PATH, 'wazuh.yml'); +export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'config', +); +export const WAZUH_DATA_CONFIG_APP_PATH = path.join( + WAZUH_DATA_CONFIG_DIRECTORY_PATH, + 'wazuh.yml', +); export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( WAZUH_DATA_CONFIG_DIRECTORY_PATH, - 'wazuh-registry.json' + 'wazuh-registry.json', ); // Wazuh data path - logs export const MAX_MB_LOG_FILES = 100; -export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'logs'); +export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'logs', +); export const WAZUH_DATA_LOGS_PLAIN_FILENAME = 'wazuhapp-plain.log'; export const WAZUH_DATA_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_DATA_LOGS_PLAIN_FILENAME + WAZUH_DATA_LOGS_PLAIN_FILENAME, ); export const WAZUH_DATA_LOGS_RAW_FILENAME = 'wazuhapp.log'; export const WAZUH_DATA_LOGS_RAW_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_DATA_LOGS_RAW_FILENAME + WAZUH_DATA_LOGS_RAW_FILENAME, ); // Wazuh data path - UI logs @@ -144,15 +155,21 @@ export const WAZUH_UI_LOGS_PLAIN_FILENAME = 'wazuh-ui-plain.log'; export const WAZUH_UI_LOGS_RAW_FILENAME = 'wazuh-ui.log'; export const WAZUH_UI_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_UI_LOGS_PLAIN_FILENAME + WAZUH_UI_LOGS_PLAIN_FILENAME, +); +export const WAZUH_UI_LOGS_RAW_PATH = path.join( + WAZUH_DATA_LOGS_DIRECTORY_PATH, + WAZUH_UI_LOGS_RAW_FILENAME, ); -export const WAZUH_UI_LOGS_RAW_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, WAZUH_UI_LOGS_RAW_FILENAME); // Wazuh data path - downloads -export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'downloads'); +export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'downloads', +); export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join( WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - 'reports' + 'reports', ); // Queue @@ -191,8 +208,8 @@ export enum WAZUH_MODULES_ID { CIS_CAT = 'ciscat', VIRUSTOTAL = 'virustotal', GDPR = 'gdpr', - GITHUB = 'github' -}; + GITHUB = 'github', +} export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { MANAGEMENT = 'management', @@ -209,19 +226,19 @@ export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { LOGS = 'logs', REPORTING = 'reporting', STATISTICS = 'statistics', -}; +} export enum WAZUH_MENU_TOOLS_SECTIONS_ID { API_CONSOLE = 'devTools', RULESET_TEST = 'logtest', -}; +} export enum WAZUH_MENU_SECURITY_SECTIONS_ID { USERS = 'users', ROLES = 'roles', POLICIES = 'policies', ROLES_MAPPING = 'roleMapping', -}; +} export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { SETTINGS = 'settings', @@ -232,13 +249,14 @@ export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { LOGS = 'logs', MISCELLANEOUS = 'miscellaneous', ABOUT = 'about', -}; +} export const AUTHORIZED_AGENTS = 'authorized-agents'; // Wazuh links export const WAZUH_LINK_GITHUB = 'https://github.com/wazuh'; -export const WAZUH_LINK_GOOGLE_GROUPS = 'https://groups.google.com/forum/#!forum/wazuh'; +export const WAZUH_LINK_GOOGLE_GROUPS = + 'https://groups.google.com/forum/#!forum/wazuh'; export const WAZUH_LINK_SLACK = 'https://wazuh.com/community/join-us-on-slack'; export const HEALTH_CHECK = 'health-check'; @@ -252,7 +270,8 @@ export const WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER = { from: 'now-24h', to: 'now', }; -export const PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER = 'timepicker:timeDefaults'; +export const PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER = + 'timepicker:timeDefaults'; // Default maxBuckets set by the app export const WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS = 200000; @@ -280,24 +299,30 @@ export const ASSETS_BASE_URL_PREFIX = '/plugins/wazuh/assets/'; export const ASSETS_PUBLIC_URL = '/plugins/wazuh/public/assets/'; // Reports -export const REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH = 'images/logo_reports.png'; +export const REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH = + 'images/logo_reports.png'; export const REPORTS_PRIMARY_COLOR = '#256BD1'; -export const REPORTS_PAGE_FOOTER_TEXT = 'Copyright © 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 export const PLUGIN_PLATFORM_NAME = 'Wazuh dashboard'; -export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = '/usr/share/wazuh-dashboard/data/wazuh/'; +export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = + '/usr/share/wazuh-dashboard/data/wazuh/'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'wazuh-dashboard'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = 'upgrade-guide'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = 'user-manual/wazuh-dashboard/troubleshooting.html'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = 'user-manual/wazuh-dashboard/config-file.html'; -export const PLUGIN_PLATFORM_URL_GUIDE = 'https://opensearch.org/docs/1.2/opensearch/index/'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = + 'upgrade-guide'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = + 'user-manual/wazuh-dashboard/troubleshooting.html'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = + 'user-manual/wazuh-dashboard/config-file.html'; +export const PLUGIN_PLATFORM_URL_GUIDE = + 'https://opensearch.org/docs/1.2/opensearch/index/'; export const PLUGIN_PLATFORM_URL_GUIDE_TITLE = 'OpenSearch guide'; export const PLUGIN_PLATFORM_REQUEST_HEADERS = { - 'osd-xsrf': 'kibana' + 'osd-xsrf': 'kibana', }; // Plugin app @@ -316,7 +341,7 @@ export const UI_COLOR_AGENT_STATUS = { [API_NAME_AGENT_STATUS.DISCONNECTED]: '#BD271E', [API_NAME_AGENT_STATUS.PENDING]: '#FEC514', [API_NAME_AGENT_STATUS.NEVER_CONNECTED]: '#646A77', - default: '#000000' + default: '#000000', } as const; export const UI_LABEL_NAME_AGENT_STATUS = { @@ -324,23 +349,23 @@ export const UI_LABEL_NAME_AGENT_STATUS = { [API_NAME_AGENT_STATUS.DISCONNECTED]: 'Disconnected', [API_NAME_AGENT_STATUS.PENDING]: 'Pending', [API_NAME_AGENT_STATUS.NEVER_CONNECTED]: 'Never connected', - default: 'Unknown' + default: 'Unknown', } as const; export const UI_ORDER_AGENT_STATUS = [ API_NAME_AGENT_STATUS.ACTIVE, API_NAME_AGENT_STATUS.DISCONNECTED, API_NAME_AGENT_STATUS.PENDING, - API_NAME_AGENT_STATUS.NEVER_CONNECTED -] + API_NAME_AGENT_STATUS.NEVER_CONNECTED, +]; export const AGENT_SYNCED_STATUS = { SYNCED: 'synced', NOT_SYNCED: 'not synced', -} +}; // Documentation -export const DOCUMENTATION_WEB_BASE_URL = "https://documentation.wazuh.com"; +export const DOCUMENTATION_WEB_BASE_URL = 'https://documentation.wazuh.com'; // Default Elasticsearch user name context export const ELASTIC_NAME = 'elastic'; @@ -360,62 +385,62 @@ export enum SettingCategory { STATISTICS, SECURITY, CUSTOMIZATION, -}; +} type TPluginSettingOptionsTextArea = { - maxRows?: number - minRows?: number - maxLength?: number + maxRows?: number; + minRows?: number; + maxLength?: number; }; type TPluginSettingOptionsSelect = { - select: { text: string, value: any }[] + select: { text: string; value: any }[]; }; type TPluginSettingOptionsEditor = { - editor: { - language: string - } + editor: { + language: string; + }; }; type TPluginSettingOptionsFile = { - file: { - type: 'image' - extensions?: string[] - size?: { - maxBytes?: number - minBytes?: number - } - recommended?: { - dimensions?: { - width: number, - height: number, - unit: string - } - } - store?: { - relativePathFileSystem: string - filename: string - resolveStaticURL: (filename: string) => string - } - } + file: { + type: 'image'; + extensions?: string[]; + size?: { + maxBytes?: number; + minBytes?: number; + }; + recommended?: { + dimensions?: { + width: number; + height: number; + unit: string; + }; + }; + store?: { + relativePathFileSystem: string; + filename: string; + resolveStaticURL: (filename: string) => string; + }; + }; }; type TPluginSettingOptionsNumber = { number: { - min?: number - max?: number - integer?: boolean - } + min?: number; + max?: number; + integer?: boolean; + }; }; type TPluginSettingOptionsSwitch = { switch: { values: { - disabled: { label?: string, value: any }, - enabled: { label?: string, value: any }, - } - } + disabled: { label?: string; value: any }; + enabled: { label?: string; value: any }; + }; + }; }; export enum EpluginSettingType { @@ -425,61 +450,63 @@ export enum EpluginSettingType { number = 'number', editor = 'editor', select = 'select', - filepicker = 'filepicker' -}; + filepicker = 'filepicker', +} export type TPluginSetting = { // Define the text displayed in the UI. - title: string + title: string; // Description. - description: string + description: string; // Category. - category: SettingCategory + category: SettingCategory; // Type. - type: EpluginSettingType + type: EpluginSettingType; // Default value. - defaultValue: any + defaultValue: any; // Default value if it is not set. It has preference over `default`. - defaultValueIfNotSet?: any + defaultValueIfNotSet?: any; // Configurable from the configuration file. - isConfigurableFromFile: boolean + isConfigurableFromFile: boolean; // Configurable from the UI (Settings/Configuration). - isConfigurableFromUI: boolean + isConfigurableFromUI: boolean; // Modify the setting requires running the plugin health check (frontend). - requiresRunningHealthCheck?: boolean + requiresRunningHealthCheck?: boolean; // Modify the setting requires reloading the browser tab (frontend). - requiresReloadingBrowserTab?: boolean + requiresReloadingBrowserTab?: boolean; // Modify the setting requires restarting the plugin platform to take effect. - requiresRestartingPluginPlatform?: boolean + requiresRestartingPluginPlatform?: boolean; // Define options related to the `type`. options?: - TPluginSettingOptionsEditor | - TPluginSettingOptionsFile | - TPluginSettingOptionsNumber | - TPluginSettingOptionsSelect | - TPluginSettingOptionsSwitch | - TPluginSettingOptionsTextArea + | TPluginSettingOptionsEditor + | TPluginSettingOptionsFile + | TPluginSettingOptionsNumber + | TPluginSettingOptionsSelect + | TPluginSettingOptionsSwitch + | TPluginSettingOptionsTextArea; // Transform the input value. The result is saved in the form global state of Settings/Configuration - uiFormTransformChangedInputValue?: (value: any) => any + uiFormTransformChangedInputValue?: (value: any) => any; // Transform the configuration value or default as initial value for the input in Settings/Configuration - uiFormTransformConfigurationValueToInputValue?: (value: any) => any + uiFormTransformConfigurationValueToInputValue?: (value: any) => any; // Transform the input value changed in the form of Settings/Configuration and returned in the `changed` property of the hook useForm - uiFormTransformInputValueToConfigurationValue?: (value: any) => any + uiFormTransformInputValueToConfigurationValue?: (value: any) => any; // Validate the value in the form of Settings/Configuration. It returns a string if there is some validation error. - validate?: (value: any) => string | undefined - // Validate function creator to validate the setting in the backend. It uses `schema` of the `@kbn/config-schema` package. - validateBackend?: (schema: any) => (value: unknown) => string | undefined + validate?: (value: any) => string | undefined; + // Validate function creator to validate the setting in the backend. It uses `schema` of the `@kbn/config-schema` package. + validateBackend?: (schema: any) => (value: unknown) => string | undefined; }; export type TPluginSettingWithKey = TPluginSetting & { key: TPluginSettingKey }; export type TPluginSettingCategory = { - title: string - description?: string - documentationLink?: string - renderOrder?: number + title: string; + description?: string; + documentationLink?: string; + renderOrder?: number; }; -export const PLUGIN_SETTINGS_CATEGORIES: { [category: number]: TPluginSettingCategory } = { +export const PLUGIN_SETTINGS_CATEGORIES: { + [category: number]: TPluginSettingCategory; +} = { [SettingCategory.HEALTH_CHECK]: { title: 'Health check', description: "Checks will be executed by the app's Healthcheck.", @@ -487,40 +514,45 @@ export const PLUGIN_SETTINGS_CATEGORIES: { [category: number]: TPluginSettingCat }, [SettingCategory.GENERAL]: { title: 'General', - description: "Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.", + description: + 'Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.', renderOrder: SettingCategory.GENERAL, }, [SettingCategory.EXTENSIONS]: { title: 'Initial display state of the modules of the new API host entries.', - description: "Extensions.", + description: 'Extensions.', }, [SettingCategory.SECURITY]: { title: 'Security', - description: "Application security options such as unauthorized roles.", + description: 'Application security options such as unauthorized roles.', renderOrder: SettingCategory.SECURITY, }, [SettingCategory.MONITORING]: { title: 'Task:Monitoring', - description: "Options related to the agent status monitoring job and its storage in indexes.", + description: + 'Options related to the agent status monitoring job and its storage in indexes.', renderOrder: SettingCategory.MONITORING, }, [SettingCategory.STATISTICS]: { title: 'Task:Statistics', - description: "Options related to the daemons manager monitoring job and their storage in indexes.", + description: + 'Options related to the daemons manager monitoring job and their storage in indexes.', renderOrder: SettingCategory.STATISTICS, }, [SettingCategory.CUSTOMIZATION]: { title: 'Custom branding', - description: "If you want to use custom branding elements such as logos, you can do so by editing the settings below.", + description: + 'If you want to use custom branding elements such as logos, you can do so by editing the settings below.', documentationLink: 'user-manual/wazuh-dashboard/white-labeling.html', renderOrder: SettingCategory.CUSTOMIZATION, - } + }, }; export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { - "alerts.sample.prefix": { - title: "Sample alerts prefix", - description: "Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.", + 'alerts.sample.prefix': { + title: 'Sample alerts prefix', + description: + 'Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_SAMPLE_ALERT_PREFIX, @@ -532,15 +564,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "checks.api": { - title: "API connection", - description: "Enable or disable the API health check when opening the app.", + 'checks.api': { + title: 'API connection', + description: 'Enable or disable the API health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -551,20 +594,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.fields": { - title: "Known fields", - description: "Enable or disable the known fields health check when opening the app.", + 'checks.fields': { + title: 'Known fields', + description: + 'Enable or disable the known fields health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -575,20 +621,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.maxBuckets": { - title: "Set max buckets to 200000", - description: "Change the default value of the plugin platform max buckets configuration.", + 'checks.maxBuckets': { + title: 'Set max buckets to 200000', + description: + 'Change the default value of the plugin platform max buckets configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -599,20 +648,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } + }, }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.metaFields": { - title: "Remove meta fields", - description: "Change the default value of the plugin platform metaField configuration.", + 'checks.metaFields': { + title: 'Remove meta fields', + description: + 'Change the default value of the plugin platform metaField configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -623,20 +675,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.pattern": { - title: "Index pattern", - description: "Enable or disable the index pattern health check when opening the app.", + 'checks.pattern': { + title: 'Index pattern', + description: + 'Enable or disable the index pattern health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -647,20 +702,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.setup": { - title: "API version", - description: "Enable or disable the setup health check when opening the app.", + 'checks.setup': { + title: 'API version', + description: + 'Enable or disable the setup health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -671,20 +729,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.template": { - title: "Index template", - description: "Enable or disable the template health check when opening the app.", + 'checks.template': { + title: 'Index template', + description: + 'Enable or disable the template health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -695,20 +756,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.timeFilter": { - title: "Set time filter to 24h", - description: "Change the default value of the plugin platform timeFilter configuration.", + 'checks.timeFilter': { + title: 'Set time filter to 24h', + description: + 'Change the default value of the plugin platform timeFilter configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -719,20 +783,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "cron.prefix": { - title: "Cron prefix", - description: "Define the index prefix of predefined jobs.", + 'cron.prefix': { + title: 'Cron prefix', + description: 'Define the index prefix of predefined jobs.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_PREFIX, @@ -743,15 +809,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.apis": { - title: "Includes APIs", - description: "Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.", + 'cron.statistics.apis': { + title: 'Includes APIs', + description: + 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', category: SettingCategory.STATISTICS, type: EpluginSettingType.editor, defaultValue: [], @@ -759,72 +837,87 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } + }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + }), + ); }, - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )})); - }, }, - "cron.statistics.index.creation": { - title: "Index creation", - description: "Define the interval in which a new index will be created.", + 'cron.statistics.index.creation': { + title: 'Index creation', + description: 'Define the interval in which a new index will be created.', category: SettingCategory.STATISTICS, type: EpluginSettingType.select, options: { select: [ { - text: "Hourly", - value: "h" + text: 'Hourly', + value: 'h', }, { - text: "Daily", - value: "d" + text: 'Daily', + value: 'd', }, { - text: "Weekly", - value: "w" + text: 'Weekly', + value: 'w', }, { - text: "Monthly", - value: "m" - } - ] + text: 'Monthly', + value: 'm', + }, + ], }, defaultValue: WAZUH_STATISTICS_DEFAULT_CREATION, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRunningHealthCheck: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "cron.statistics.index.name": { - title: "Index name", - description: "Define the name of the index in which the documents will be saved.", + 'cron.statistics.index.name': { + title: 'Index name', + description: + 'Define the name of the index in which the documents will be saved.', category: SettingCategory.STATISTICS, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_NAME, @@ -836,15 +929,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.index.replicas": { - title: "Index replicas", - description: "Define the number of replicas to use for the statistics indices.", + 'cron.statistics.index.replicas': { + title: 'Index replicas', + description: + 'Define the number of replicas to use for the statistics indices.', category: SettingCategory.STATISTICS, type: EpluginSettingType.number, defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, @@ -854,25 +959,30 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 0, - integer: true - } + integer: true, + }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number): string { + uiFormTransformConfigurationValueToInputValue: function ( + value: number, + ): string { return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value) - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "cron.statistics.index.shards": { - title: "Index shards", - description: "Define the number of shards to use for the statistics indices.", + 'cron.statistics.index.shards': { + title: 'Index shards', + description: + 'Define the number of shards to use for the statistics indices.', category: SettingCategory.STATISTICS, type: EpluginSettingType.number, defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, @@ -882,41 +992,46 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value) - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "cron.statistics.interval": { - title: "Interval", - description: "Define the frequency of task execution using cron schedule expressions.", + 'cron.statistics.interval': { + title: 'Interval', + description: + 'Define the frequency of task execution using cron schedule expressions.', category: SettingCategory.STATISTICS, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_CRON_FREQ, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRestartingPluginPlatform: true, - validate: function(value: string){ - return validateNodeCronInterval(value) ? undefined : "Interval is not valid." - }, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validate: function (value: string) { + return validateNodeCronInterval(value) + ? undefined + : 'Interval is not valid.'; + }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.status": { - title: "Status", - description: "Enable or disable the statistics tasks.", + 'cron.statistics.status': { + title: 'Status', + description: 'Enable or disable the statistics tasks.', category: SettingCategory.STATISTICS, type: EpluginSettingType.switch, defaultValue: WAZUH_STATISTICS_DEFAULT_STATUS, @@ -927,217 +1042,248 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "customization.enabled": { - title: "Status", - description: "Enable or disable the customization.", - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, + 'customization.enabled': { + title: 'Status', + description: 'Enable or disable the customization.', + category: SettingCategory.CUSTOMIZATION, + type: EpluginSettingType.switch, + defaultValue: true, + isConfigurableFromFile: true, + isConfigurableFromUI: true, requiresReloadingBrowserTab: true, - options: { - switch: { - values: { - disabled: {label: 'false', value: false}, - enabled: {label: 'true', value: true}, - } - } - }, - uiFormTransformChangedInputValue: function(value: boolean | string): boolean{ - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, - }, - "customization.logo.app": { - title: "App main logo", + options: { + switch: { + values: { + disabled: { label: 'false', value: false }, + enabled: { label: 'true', value: true }, + }, + }, + }, + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { + return Boolean(value); + }, + validate: SettingsValidator.isBoolean, + validateBackend: function (schema) { + return schema.boolean(); + }, + }, + 'customization.logo.app': { + title: 'App main logo', description: `This logo is used in the app main menu, at the top left corner.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.app', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 300, + height: 70, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.app', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.healthcheck": { - title: "Healthcheck logo", + 'customization.logo.healthcheck': { + title: 'Healthcheck logo', description: `This logo is displayed during the Healthcheck routine of the app.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.healthcheck', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 300, + height: 70, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.healthcheck', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.reports": { - title: "PDF reports logo", + 'customization.logo.reports': { + title: 'PDF reports logo', description: `This logo is used in the PDF reports generated by the app. It's placed at the top left corner of every page of the PDF.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', defaultValueIfNotSet: REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH, isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 190, - height: 40, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.reports', - resolveStaticURL: (filename: string) => `custom/images/${filename}` - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 190, + height: 40, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.reports', + resolveStaticURL: (filename: string) => `custom/images/${filename}`, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.sidebar": { - title: "Navigation drawer logo", + 'customization.logo.sidebar': { + title: 'Navigation drawer logo', description: `This is the logo for the app to display in the platform's navigation drawer, this is, the main sidebar collapsible menu.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, requiresReloadingBrowserTab: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 80, - height: 80, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.sidebar', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 80, + height: 80, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.sidebar', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.reports.footer": { - title: "Reports footer", - description: "Set the footer of the reports.", - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.textarea, - defaultValue: "", + 'customization.reports.footer': { + title: 'Reports footer', + description: 'Set the footer of the reports.', + category: SettingCategory.CUSTOMIZATION, + type: EpluginSettingType.textarea, + defaultValue: '', defaultValueIfNotSet: REPORTS_PAGE_FOOTER_TEXT, - isConfigurableFromFile: true, - isConfigurableFromUI: true, + isConfigurableFromFile: true, + isConfigurableFromUI: true, options: { maxRows: 2, maxLength: 50 }, validate: function (value) { return SettingsValidator.multipleLinesString({ maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength - })(value) + maxLength: this.options?.maxLength, + })(value); }, validateBackend: function (schema) { return schema.string({ validate: this.validate.bind(this) }); }, }, - "customization.reports.header": { - title: "Reports header", - description: "Set the header of the reports.", + 'customization.reports.header': { + title: 'Reports header', + description: 'Set the header of the reports.', category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.textarea, - defaultValue: "", + defaultValue: '', defaultValueIfNotSet: REPORTS_PAGE_HEADER_TEXT, isConfigurableFromFile: true, isConfigurableFromUI: true, @@ -1145,16 +1291,16 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validate: function (value) { return SettingsValidator.multipleLinesString({ maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength - })(value) - }, - validateBackend: function(schema){ - return schema.string({validate: this.validate.bind(this)}); - }, - }, - "disabled_roles": { - title: "Disable roles", - description: "Disabled the plugin visibility for users with the roles.", + maxLength: this.options?.maxLength, + })(value); + }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate.bind(this) }); + }, + }, + disabled_roles: { + title: 'Disable roles', + description: 'Disabled the plugin visibility for users with the roles.', category: SettingCategory.SECURITY, type: EpluginSettingType.editor, defaultValue: [], @@ -1162,62 +1308,74 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } + }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + }), + ); }, - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )})); - }, }, - "enrollment.dns": { - title: "Enrollment DNS", - description: "Specifies the Wazuh registration server, used for the agent enrollment.", + 'enrollment.dns': { + title: 'Enrollment DNS', + description: + 'Specifies the Wazuh registration server, used for the agent enrollment.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, validate: SettingsValidator.hasNoSpaces, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "enrollment.password": { - title: "Enrollment password", - description: "Specifies the password used to authenticate during the agent enrollment.", + 'enrollment.password': { + title: 'Enrollment password', + description: + 'Specifies the password used to authenticate during the agent enrollment.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: false, validate: SettingsValidator.isNotEmptyString, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "extensions.audit": { - title: "System auditing", - description: "Enable or disable the Audit tab on Overview and Agents.", + 'extensions.audit': { + title: 'System auditing', + description: 'Enable or disable the Audit tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1228,20 +1386,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.aws": { - title: "Amazon AWS", - description: "Enable or disable the Amazon (AWS) tab on Overview.", + 'extensions.aws': { + title: 'Amazon AWS', + description: 'Enable or disable the Amazon (AWS) tab on Overview.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1252,20 +1412,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.ciscat": { - title: "CIS-CAT", - description: "Enable or disable the CIS-CAT tab on Overview and Agents.", + 'extensions.ciscat': { + title: 'CIS-CAT', + description: 'Enable or disable the CIS-CAT tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1276,20 +1438,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.docker": { - title: "Docker listener", - description: "Enable or disable the Docker listener tab on Overview and Agents.", + 'extensions.docker': { + title: 'Docker listener', + description: + 'Enable or disable the Docker listener tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1300,20 +1465,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.gcp": { - title: "Google Cloud platform", - description: "Enable or disable the Google Cloud Platform tab on Overview.", + 'extensions.gcp': { + title: 'Google Cloud platform', + description: 'Enable or disable the Google Cloud Platform tab on Overview.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1324,20 +1491,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.gdpr": { - title: "GDPR", - description: "Enable or disable the GDPR tab on Overview and Agents.", + 'extensions.gdpr': { + title: 'GDPR', + description: 'Enable or disable the GDPR tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1348,20 +1517,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.github": { - title: "GitHub", - description: "Enable or disable the GitHub tab on Overview and Agents.", + 'extensions.github': { + title: 'GitHub', + description: 'Enable or disable the GitHub tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1372,20 +1543,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.hipaa": { - title: "HIPAA", - description: "Enable or disable the HIPAA tab on Overview and Agents.", + 'extensions.hipaa': { + title: 'HIPAA', + description: 'Enable or disable the HIPAA tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1396,20 +1569,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.nist": { - title: "NIST", - description: "Enable or disable the NIST 800-53 tab on Overview and Agents.", + 'extensions.nist': { + title: 'NIST', + description: + 'Enable or disable the NIST 800-53 tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1420,20 +1596,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.office": { - title: "Office 365", - description: "Enable or disable the Office 365 tab on Overview and Agents.", + 'extensions.office': { + title: 'Office 365', + description: 'Enable or disable the Office 365 tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1444,20 +1622,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.oscap": { - title: "OSCAP", - description: "Enable or disable the Open SCAP tab on Overview and Agents.", + 'extensions.oscap': { + title: 'OSCAP', + description: 'Enable or disable the Open SCAP tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1468,20 +1648,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.osquery": { - title: "Osquery", - description: "Enable or disable the Osquery tab on Overview and Agents.", + 'extensions.osquery': { + title: 'Osquery', + description: 'Enable or disable the Osquery tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1492,20 +1674,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.pci": { - title: "PCI DSS", - description: "Enable or disable the PCI DSS tab on Overview and Agents.", + 'extensions.pci': { + title: 'PCI DSS', + description: 'Enable or disable the PCI DSS tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1516,20 +1700,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.tsc": { - title: "TSC", - description: "Enable or disable the TSC tab on Overview and Agents.", + 'extensions.tsc': { + title: 'TSC', + description: 'Enable or disable the TSC tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1540,20 +1726,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.virustotal": { - title: "Virustotal", - description: "Enable or disable the VirusTotal tab on Overview and Agents.", + 'extensions.virustotal': { + title: 'Virustotal', + description: 'Enable or disable the VirusTotal tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1564,20 +1752,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "hideManagerAlerts": { - title: "Hide manager alerts", - description: "Hide the alerts of the manager in every dashboard.", + hideManagerAlerts: { + title: 'Hide manager alerts', + description: 'Hide the alerts of the manager in every dashboard.', category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: false, @@ -1589,20 +1779,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "ip.ignore": { - title: "Index pattern ignore", - description: "Disable certain index pattern names from being available in index pattern selector.", + 'ip.ignore': { + title: 'Index pattern ignore', + description: + 'Disable certain index pattern names from being available in index pattern selector.', category: SettingCategory.GENERAL, type: EpluginSettingType.editor, defaultValue: [], @@ -1610,43 +1803,74 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') - )})); - }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + SettingsValidator.noLiteralString('.', '..'), + SettingsValidator.noStartsWithString('-', '_', '+', '.'), + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + SettingsValidator.noLiteralString('.', '..'), + SettingsValidator.noStartsWithString('-', '_', '+', '.'), + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), + ), + }), + ); + }, }, - "ip.selector": { - title: "IP selector", - description: "Define if the user is allowed to change the selected index pattern directly from the top menu bar.", + 'ip.selector': { + title: 'IP selector', + description: + 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: true, @@ -1657,48 +1881,55 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "logs.level": { - title: "Log level", - description: "Logging level of the App.", + 'logs.level': { + title: 'Log level', + description: 'Logging level of the App.', category: SettingCategory.GENERAL, type: EpluginSettingType.select, options: { select: [ { - text: "Info", - value: "info" + text: 'Info', + value: 'info', }, { - text: "Debug", - value: "debug" - } - ] + text: 'Debug', + value: 'debug', + }, + ], }, - defaultValue: "info", + defaultValue: 'info', isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRestartingPluginPlatform: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "pattern": { - title: "Index pattern", - description: "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", + pattern: { + title: 'Index pattern', + description: + "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_ALERTS_PATTERN, @@ -1711,15 +1942,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "timeout": { - title: "Request timeout", - description: "Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.", + timeout: { + title: 'Request timeout', + description: + 'Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.', category: SettingCategory.GENERAL, type: EpluginSettingType.number, defaultValue: 20000, @@ -1728,61 +1970,69 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1500, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.creation": { - title: "Index creation", - description: "Define the interval in which a new wazuh-monitoring index will be created.", + 'wazuh.monitoring.creation': { + title: 'Index creation', + description: + 'Define the interval in which a new wazuh-monitoring index will be created.', category: SettingCategory.MONITORING, type: EpluginSettingType.select, options: { select: [ { - text: "Hourly", - value: "h" + text: 'Hourly', + value: 'h', }, { - text: "Daily", - value: "d" + text: 'Daily', + value: 'd', }, { - text: "Weekly", - value: "w" + text: 'Weekly', + value: 'w', }, { - text: "Monthly", - value: "m" - } - ] + text: 'Monthly', + value: 'm', + }, + ], }, defaultValue: WAZUH_MONITORING_DEFAULT_CREATION, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRunningHealthCheck: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "wazuh.monitoring.enabled": { - title: "Status", - description: "Enable or disable the wazuh-monitoring index creation and/or visualization.", + 'wazuh.monitoring.enabled': { + title: 'Status', + description: + 'Enable or disable the wazuh-monitoring index creation and/or visualization.', category: SettingCategory.MONITORING, type: EpluginSettingType.switch, defaultValue: WAZUH_MONITORING_DEFAULT_ENABLED, @@ -1794,20 +2044,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "wazuh.monitoring.frequency": { - title: "Frequency", - description: "Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.", + 'wazuh.monitoring.frequency': { + title: 'Frequency', + description: + 'Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_FREQUENCY, @@ -1817,25 +2070,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 60, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.pattern": { - title: "Index pattern", - description: "Default index pattern to use for Wazuh monitoring.", + 'wazuh.monitoring.pattern': { + title: 'Index pattern', + description: 'Default index pattern to use for Wazuh monitoring.', category: SettingCategory.MONITORING, type: EpluginSettingType.text, defaultValue: WAZUH_MONITORING_PATTERN, @@ -1847,15 +2102,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), ), - validateBackend: function(schema){ - return schema.string({minLength: 1, validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ minLength: 1, validate: this.validate }); + }, }, - "wazuh.monitoring.replicas": { - title: "Index replicas", - description: "Define the number of replicas to use for the wazuh-monitoring-* indices.", + 'wazuh.monitoring.replicas': { + title: 'Index replicas', + description: + 'Define the number of replicas to use for the wazuh-monitoring-* indices.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, @@ -1865,25 +2131,28 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 0, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.shards": { - title: "Index shards", - description: "Define the number of shards to use for the wazuh-monitoring-* indices.", + 'wazuh.monitoring.shards': { + title: 'Index shards', + description: + 'Define the number of shards to use for the wazuh-monitoring-* indices.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, @@ -1893,22 +2162,24 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, - } + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, + }, }; export type TPluginSettingKey = keyof typeof PLUGIN_SETTINGS; @@ -1969,12 +2240,22 @@ export enum HTTP_STATUS_CODES { GATEWAY_TIMEOUT = 504, HTTP_VERSION_NOT_SUPPORTED = 505, INSUFFICIENT_STORAGE = 507, - NETWORK_AUTHENTICATION_REQUIRED = 511 + NETWORK_AUTHENTICATION_REQUIRED = 511, } // Module Security configuration assessment export const MODULE_SCA_CHECK_RESULT_LABEL = { passed: 'Passed', failed: 'Failed', - 'not applicable': 'Not applicable' -} + 'not applicable': 'Not applicable', +}; + +// Search bar + +// This limits the results in the API request +export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT = 30; +// This limits the suggestions for the token of type value displayed in the search bar +export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT = 10; +/* Time in milliseconds to debounce the analysis of search bar. This mitigates some problems related +to changes running in parallel */ +export const SEARCH_BAR_DEBOUNCE_UPDATE_TIME = 400; diff --git a/plugins/main/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/agents/fim/inventory.tsx b/plugins/main/public/components/agents/fim/inventory.tsx index b468416cd2..e6bdf38716 100644 --- a/plugins/main/public/components/agents/fim/inventory.tsx +++ b/plugins/main/public/components/agents/fim/inventory.tsx @@ -12,7 +12,6 @@ import React, { Component } from 'react'; import { - EuiButtonEmpty, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -26,9 +25,8 @@ import { EuiTabs, EuiTitle, } from '@elastic/eui'; -import { FilterBar, InventoryTable, RegistryTable } from './inventory/'; +import { InventoryTable, RegistryTable } from './inventory/'; import { WzRequest } from '../../../react-services/wz-request'; -import exportCsv from '../../../react-services/wz-csv'; import { getToasts } from '../../../kibana-services'; import { ICustomBadges } from '../../wz-search-bar/components'; import { filtersToObject } from '../../wz-search-bar'; @@ -194,45 +192,20 @@ export class Inventory extends Component { const { isLoading } = this.state; if (tabs.length > 1) { return ( -
- - {tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - disabled={tab.disabled} - key={index}> - {tab.name} {isLoading === true && } - - ))} - - - - - - this.downloadCsv()}> - Export formatted - - - -
- ) - } else { - return ( - - - -

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

-
-
- - this.downloadCsv()}> - Export formatted - - -
- ) - } + + {tabs.map((tab, index) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.state.selectedTabId} + disabled={tab.disabled} + key={index}> + {tab.name} {isLoading === true && } + + ))} + + ); + }; + return null; } showToast = (color, title, time) => { @@ -243,44 +216,10 @@ export class Inventory extends Component { }); }; - async downloadCsv() { - const { filters } = this.state; - try { - const filtersObject = filtersToObject(filters); - const formatedFilters = Object.keys(filtersObject).map(key => ({name: key, value: filtersObject[key]})); - this.showToast('success', 'Your download should begin automatically...', 3000); - await exportCsv( - '/syscheck/' + this.props.agent.id, - [ - { name: 'type', value: this.state.selectedTabId === 'files' ? 'file' : this.state.selectedTabId }, - ...formatedFilters - ], - `fim-${this.state.selectedTabId}` - ); - } catch (error) { - const options: UIErrorLog = { - context: `${Inventory.name}.downloadCsv`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - renderTable() { const { filters, syscheck, selectedTabId, customBadges, totalItemsRegistry, totalItemsFile } = this.state; return ( -
- + <> {selectedTabId === 'files' && } -
- ) + + ); } noConfiguredMonitoring() { diff --git a/plugins/main/public/components/agents/fim/inventory/filterBar.tsx b/plugins/main/public/components/agents/fim/inventory/filterBar.tsx deleted file mode 100644 index 9830e33456..0000000000 --- a/plugins/main/public/components/agents/fim/inventory/filterBar.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import React, { Component } from 'react'; -import { getFilterValues } from './lib'; -import { IFilter, IWzSuggestItem, WzSearchBar } from '../../../../components/wz-search-bar'; -import { ICustomBadges } from '../../../wz-search-bar/components'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { formatUIDate } from '../../../../react-services/time-service'; - -export class FilterBar extends Component { - // TODO: Change the type - suggestions: { [key: string]: IWzSuggestItem[] } = { - files: [ - { - type: 'q', - label: 'file', - description: 'Name of the file', - operators: ['=', '!=', '~'], - values: async (value) => - getFilterValues('file', value, this.props.agent.id, { type: 'file' }), - }, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'perm', - description: 'Permissions of the file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('perm', value, this.props.agent.id), - }, - ] - : []), - { - type: 'q', - label: 'mtime', - description: 'Date the file was modified', - operators: ['=', '!=', '>', '<'], - values: async (value) => - getFilterValues('mtime', value, this.props.agent.id, {}, formatUIDate), - }, - { - type: 'q', - label: 'date', - description: 'Date of registration of the event', - operators: ['=', '!=', '>', '<'], - values: async (value) => - getFilterValues('date', value, this.props.agent.id, {}, formatUIDate), - }, - { - type: 'q', - label: 'uname', - description: 'Owner of the file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('uname', value, this.props.agent.id), - }, - { - type: 'q', - label: 'uid', - description: 'Id of the owner file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('uid', value, this.props.agent.id), - }, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'gname', - description: 'Name of the group owner file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('gname', value, this.props.agent.id), - }, - ] - : []), - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'gid', - description: 'Id of the group owner', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('gid', value, this.props.agent.id), - }, - ] - : []), - { - type: 'q', - label: 'md5', - description: 'md5 hash', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('md5', value, this.props.agent.id), - }, - { - type: 'q', - label: 'sha1', - description: 'sha1 hash', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('sha1', value, this.props.agent.id), - }, - { - type: 'q', - label: 'sha256', - description: 'sha256 hash', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('sha256', value, this.props.agent.id), - }, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' - ? [ - { - type: 'q', - label: 'inode', - description: 'Inode of the file', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('inode', value, this.props.agent.id), - }, - ] - : []), - { - type: 'q', - label: 'size', - description: 'Size of the file in Bytes', - values: async (value) => getFilterValues('size', value, this.props.agent.id), - }, - ], - registry: [ - { - type: 'q', - label: 'file', - description: 'Name of the registry_key', - operators: ['=', '!=', '~'], - values: async (value) => - getFilterValues('file', value, this.props.agent.id, { q: 'type=registry_key' }), - }, - ], - }; - - props!: { - onFiltersChange(filters: IFilter[]): void; - selectView: 'files' | 'registry'; - agent: { id: string; agentPlatform: string }; - onChangeCustomBadges?(customBadges: ICustomBadges[]): void; - customBadges?: ICustomBadges[]; - filters: IFilter[]; - }; - - render() { - const { onFiltersChange, selectView, filters } = this.props; - return ( - - - - - - ); - } -} diff --git a/plugins/main/public/components/agents/fim/inventory/index.ts b/plugins/main/public/components/agents/fim/inventory/index.ts index 484ed526bb..28a9313aaf 100644 --- a/plugins/main/public/components/agents/fim/inventory/index.ts +++ b/plugins/main/public/components/agents/fim/inventory/index.ts @@ -1,4 +1,2 @@ -export { FilterBar } from './filterBar'; - export { InventoryTable } from './table'; export { RegistryTable } from './registry-table' \ No newline at end of file diff --git a/plugins/main/public/components/agents/fim/inventory/registry-table.tsx b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx index a03ab38ae6..c25bec7bb4 100644 --- a/plugins/main/public/components/agents/fim/inventory/registry-table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx @@ -11,36 +11,26 @@ */ import React, { Component } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - Direction, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; -import { filtersToObject } from '../../../wz-search-bar'; import { formatUIDate } from '../../../../react-services/time-service'; -import { - UI_ERROR_SEVERITIES, - UIErrorLog, - UIErrorSeverity, - UILogLevel, -} from '../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; -import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { TableWzAPI } from '../../../common/tables'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../common/constants'; + +const searchBarWQLOptions = { + implicitQuery: { + query: 'type=registry_key', + conjunction: ';', + }, +}; + +const searchBarWQLFilters = { default: { q: 'type=registry_key' } }; export class RegistryTable extends Component { state: { syscheck: []; - error?: string; - pageIndex: number; - pageSize: number; - totalItems: number; - sortField: string; isFlyoutVisible: Boolean; - sortDirection: Direction; - isLoading: boolean; currentFile: { file: string; type: string; @@ -58,12 +48,6 @@ export class RegistryTable extends Component { this.state = { syscheck: [], - pageIndex: 0, - pageSize: 15, - totalItems: 0, - sortField: 'file', - sortDirection: 'asc', - isLoading: true, isFlyoutVisible: false, currentFile: { file: '', @@ -74,141 +58,83 @@ export class RegistryTable extends Component { } async componentDidMount() { - await this.getSyscheck(); const regex = new RegExp('file=' + '[^&]*'); const match = window.location.href.match(regex); - this.setState({ totalItems: this.props.totalItems }); if (match && match[0]) { const file = match[0].split('=')[1]; this.showFlyout(decodeURIComponent(file), true); } } - componentDidUpdate(prevProps) { - const { filters } = this.props; - if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }, this.getSyscheck); - } - } - closeFlyout() { this.setState({ isFlyoutVisible: false, currentFile: {} }); } async showFlyout(file, item, redirect = false) { - window.location.href = window.location.href.replace(new RegExp('&file=' + '[^&]*', 'g'), ''); + window.location.href = window.location.href.replace( + new RegExp('&file=' + '[^&]*', 'g'), + '', + ); let fileData = false; if (!redirect) { - fileData = this.state.syscheck.filter((item) => { + fileData = this.state.syscheck.filter(item => { return item.file === file; }); } else { - const response = await WzRequest.apiReq('GET', `/syscheck/${this.props.agent.id}`, { - params: { - file: file, + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + file: file, + }, }, - }); + ); fileData = ((response.data || {}).data || {}).affected_items || []; } - if (!redirect) window.location.href = window.location.href += `&file=${file}`; + if (!redirect) + window.location.href = window.location.href += `&file=${file}`; //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. const currentFile = { file, type: item.type, }; this.setState({ isFlyoutVisible: false }, () => - this.setState({ isFlyoutVisible: true, currentFile, syscheckItem: item }) + this.setState({ isFlyoutVisible: true, currentFile, syscheckItem: item }), ); } - async getSyscheck() { - const agentID = this.props.agent.id; - try { - const syscheck = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { - params: this.buildFilter(), - }); - - this.setState({ - syscheck: (((syscheck || {}).data || {}).data || {}).affected_items || {}, - totalItems: (((syscheck || {}).data || {}).data || {}).total_affected_items - 1, - isLoading: false, - error: undefined, - }); - } catch (error) { - this.setState({ error, isLoading: false }); - - const options: UIErrorLog = { - context: `${RegistryTable.name}.getSyscheck`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - - const field = sortField === 'os_name' ? '' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + field; - } - - buildFilter() { - const { pageIndex, pageSize } = this.state; - const filters = filtersToObject(this.props.filters); - - const filter = { - ...filters, - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - q: 'type=registry_key', - }; - - return filter; - } - - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this.setState( - { - pageIndex, - pageSize, - sortField, - sortDirection, - isLoading: true, - }, - () => this.getSyscheck() - ); - }; - columns() { return [ { field: 'file', name: 'Registry', sortable: true, + searchable: true, }, { field: 'mtime', - name: 'Last Modified', + name: ( + + Last Modified{' '} + + + ), sortable: true, width: '200px', render: formatUIDate, + searchable: false, }, ]; } renderRegistryTable() { - const getRowProps = (item) => { + const getRowProps = item => { const { file } = item; return { 'data-test-subj': `row-${file}`, @@ -216,44 +142,76 @@ export class RegistryTable extends Component { }; }; - const { - syscheck, - pageIndex, - pageSize, - totalItems, - sortField, - sortDirection, - isLoading, - error, - } = this.state; const columns = this.columns(); - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [15, 25, 50, 100], - }; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; return ( - [ + { label: 'file', description: 'filter by file' }, + { + label: 'mtime', + description: 'filter by modification time', + }, + ], + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + // Add the implicit query + q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + } + : { + q: `${searchBarWQLOptions.implicitQuery.query}`, + }), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + validate: { + value: ({ formattedValue, value: rawValue }, { field }) => { + const value = formattedValue ?? rawValue; + if (value) { + if (['mtime'].some(dateField => dateField === field)) { + return /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2}(.\d{1,6})?Z?)?$/.test( + value, + ) + ? undefined + : `"${value}" is not a expected format. Valid formats: YYYY-MM-DD, YYYY-MM-DD HH:mm:ss, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ.`; + } + } + }, + }, + }} + filters={searchBarWQLFilters} + showReload + downloadCsv={`fim-registry-${this.props.agent.id}`} + searchTable={true} rowProps={getRowProps} - sorting={sorting} - itemId="file" - isExpandable={true} - loading={isLoading} /> @@ -272,7 +230,7 @@ export class RegistryTable extends Component { item={this.state.syscheckItem} closeFlyout={() => this.closeFlyout()} type={this.state.currentFile.type} - view="inventory" + view='inventory' {...this.props} /> )} diff --git a/plugins/main/public/components/agents/fim/inventory/table.tsx b/plugins/main/public/components/agents/fim/inventory/table.tsx index dcb550a693..61de5e489a 100644 --- a/plugins/main/public/components/agents/fim/inventory/table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/table.tsx @@ -11,38 +11,26 @@ */ import React, { Component } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - Direction, - EuiOverlayMask, - EuiOutsideClickDetector, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; -import { filtersToObject, IFilter } from '../../../wz-search-bar'; import { formatUIDate } from '../../../../react-services/time-service'; -import { - UI_ERROR_SEVERITIES, - UIErrorLog, - UIErrorSeverity, - UILogLevel, -} from '../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; -import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { TableWzAPI } from '../../../common/tables'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../common/constants'; + +const searchBarWQLOptions = { + implicitQuery: { + query: 'type=file', + conjunction: ';', + }, +}; + +const searchBarWQLFilters = { default: { q: 'type=file' } }; export class InventoryTable extends Component { state: { syscheck: []; - error?: string; - pageIndex: number; - pageSize: number; - totalItems: number; - sortField: string; isFlyoutVisible: Boolean; - sortDirection: Direction; - isLoading: boolean; currentFile: { file: string; }; @@ -50,7 +38,7 @@ export class InventoryTable extends Component { }; props!: { - filters: IFilter[]; + filters: any; agent: any; items: []; totalItems: number; @@ -62,12 +50,6 @@ export class InventoryTable extends Component { this.state = { syscheck: props.items, - pageIndex: 0, - pageSize: 15, - totalItems: 0, - sortField: 'file', - sortDirection: 'asc', - isLoading: false, isFlyoutVisible: false, currentFile: { file: '', @@ -79,10 +61,9 @@ export class InventoryTable extends Component { async componentDidMount() { const regex = new RegExp('file=' + '[^&]*'); const match = window.location.href.match(regex); - this.setState({ totalItems: this.props.totalItems }); if (match && match[0]) { const file = match[0].split('=')[1]; - this.showFlyout(decodeURIComponent(file), true); + this.showFlyout(decodeURIComponent(file), true); // FIX: second parameter is the item. Why is this a boolean? } } @@ -91,103 +72,45 @@ export class InventoryTable extends Component { } async showFlyout(file, item, redirect = false) { - window.location.href = window.location.href.replace(new RegExp('&file=' + '[^&]*', 'g'), ''); - let fileData = false; + window.location.href = window.location.href.replace( + new RegExp('&file=' + '[^&]*', 'g'), + '', + ); + let fileData = false; // FIX: fileData variable is unused if (!redirect) { - fileData = this.state.syscheck.filter((item) => { + fileData = this.state.syscheck.filter(item => { return item.file === file; }); } else { - const response = await WzRequest.apiReq('GET', `/syscheck/${this.props.agent.id}`, { - params: { - file: file, + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + file: file, + }, }, - }); + ); fileData = ((response.data || {}).data || {}).affected_items || []; } - if (!redirect) window.location.href = window.location.href += `&file=${file}`; + if (!redirect) + window.location.href = window.location.href += `&file=${file}`; //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. this.setState({ isFlyoutVisible: false }, () => - this.setState({ isFlyoutVisible: true, currentFile: file, syscheckItem: item }) + this.setState({ + isFlyoutVisible: true, + currentFile: file, + syscheckItem: item, + }), ); } - async componentDidUpdate(prevProps) { - const { filters } = this.props; - if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }, this.getSyscheck); - } - } - - async getSyscheck() { - const agentID = this.props.agent.id; - try { - const syscheck = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { - params: this.buildFilter(), - }); - - this.props.onTotalItemsChange( + // TODO: connect to total items change on parent component + /* + tis.props.onTotalItemsChange( (((syscheck || {}).data || {}).data || {}).total_affected_items ); - - this.setState({ - syscheck: (((syscheck || {}).data || {}).data || {}).affected_items || {}, - totalItems: (((syscheck || {}).data || {}).data || {}).total_affected_items - 1, - isLoading: false, - error: undefined, - }); - } catch (error) { - this.setState({ error, isLoading: false }); - const options: UIErrorLog = { - context: `${InventoryTable.name}.getSyscheck`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - - const field = sortField === 'os_name' ? '' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + field; - } - - buildFilter() { - const { pageIndex, pageSize } = this.state; - const filters = filtersToObject(this.props.filters); - const filter = { - ...filters, - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - type: 'file', - }; - return filter; - } - - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this.setState( - { - pageIndex, - pageSize, - sortField, - sortDirection, - isLoading: true, - }, - () => this.getSyscheck() - ); - }; + */ columns() { let width; @@ -200,13 +123,25 @@ export class InventoryTable extends Component { name: 'File', sortable: true, width: '250px', + searchable: true, }, { field: 'mtime', - name: 'Last Modified', + name: ( + + Last Modified{' '} + + + ), sortable: true, width: '100px', render: formatUIDate, + searchable: false, }, { field: 'uname', @@ -214,6 +149,7 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'uid', @@ -221,6 +157,7 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'gname', @@ -228,6 +165,7 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'gid', @@ -235,63 +173,101 @@ export class InventoryTable extends Component { sortable: true, truncateText: true, width: `${width}`, + searchable: true, }, { field: 'size', name: 'Size', sortable: true, width: `${width}`, + searchable: true, }, ]; } renderFilesTable() { - const getRowProps = (item) => { + const getRowProps = item => { const { file } = item; return { 'data-test-subj': `row-${file}`, onClick: () => this.showFlyout(file, item), }; }; - - const { - syscheck, - pageIndex, - pageSize, - totalItems, - sortField, - sortDirection, - isLoading, - error, - } = this.state; const columns = this.columns(); - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [15, 25, 50, 100], - }; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; return ( - [ + { label: 'file', description: 'filter by file' }, + { label: 'gid', description: 'filter by group id' }, + { label: 'gname', description: 'filter by group name' }, + { + label: 'mtime', + description: 'filter by modification time', + }, + { label: 'size', description: 'filter by size' }, + { label: 'uname', description: 'filter by user name' }, + { label: 'uid', description: 'filter by user id' }, + ], + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscheck/${this.props.agent.id}`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + // Add the implicit query + q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + } + : { + q: `${searchBarWQLOptions.implicitQuery.query}`, + }), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + validate: { + value: ({ formattedValue, value: rawValue }, { field }) => { + const value = formattedValue ?? rawValue; + if (value) { + if (['mtime'].some(dateField => dateField === field)) { + return /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2}(.\d{1,6})?Z?)?$/.test( + value, + ) + ? undefined + : `"${value}" is not a expected format. Valid formats: YYYY-MM-DD, YYYY-MM-DD HH:mm:ss, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ.`; + } + } + }, + }, + }} + filters={searchBarWQLFilters} + showReload + downloadCsv={`fim-files-${this.props.agent.id}`} + searchTable={true} rowProps={getRowProps} - sorting={sorting} - itemId="file" - isExpandable={true} - loading={isLoading} /> @@ -301,7 +277,7 @@ export class InventoryTable extends Component { render() { const filesTable = this.renderFilesTable(); return ( -
+
{filesTable} {this.state.isFlyoutVisible && ( this.closeFlyout()} - type="file" - view="inventory" + type='file' + view='inventory' showViewInEvents={true} {...this.props} /> diff --git a/plugins/main/public/components/agents/sca/inventory.tsx b/plugins/main/public/components/agents/sca/inventory.tsx index 28b2a0c885..4540f2f0c1 100644 --- a/plugins/main/public/components/agents/sca/inventory.tsx +++ b/plugins/main/public/components/agents/sca/inventory.tsx @@ -61,7 +61,7 @@ type InventoryState = { loading: boolean; checksIsLoading: boolean; redirect: boolean; - filters: object[]; + filters: object; pageTableChecks: { pageIndex: number; pageSize?: number }; policies: object[]; checks: object[]; @@ -81,7 +81,7 @@ export class Inventory extends Component { itemIdToExpandedRowMap: {}, showMoreInfo: false, loading: false, - filters: [], + filters: {}, pageTableChecks: { pageIndex: 0 }, policies: [], checks: [], @@ -370,7 +370,7 @@ export class Inventory extends Component { buttonStat(text, field, value) { return ( - ); diff --git a/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx b/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx index 2ab8309e7e..5327d08fc2 100644 --- a/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx +++ b/plugins/main/public/components/agents/sca/inventory/agent-policies-table.tsx @@ -16,18 +16,18 @@ export default function SCAPoliciesTable(props: Props) { 'data-test-subj': `sca-row-${idx}`, className: 'customRowClass', onClick: rowProps ? () => rowProps(item) : null - } - } + }; + }; return ( <> - + /> ); } diff --git a/plugins/main/public/components/agents/sca/inventory/checks-table.tsx b/plugins/main/public/components/agents/sca/inventory/checks-table.tsx index 8f8641797f..e1c5d32279 100644 --- a/plugins/main/public/components/agents/sca/inventory/checks-table.tsx +++ b/plugins/main/public/components/agents/sca/inventory/checks-table.tsx @@ -2,7 +2,6 @@ import { EuiButtonIcon, EuiDescriptionList, EuiHealth } from '@elastic/eui'; import React, { Component } from 'react'; import { MODULE_SCA_CHECK_RESULT_LABEL } from '../../../../../common/constants'; import { TableWzAPI } from '../../../common/tables'; -import { IWzSuggestItem } from '../../../wz-search-bar'; import { ComplianceText, RuleText } from '../components'; import { getFilterValues } from './lib'; @@ -20,9 +19,42 @@ type State = { pageTableChecks: { pageIndex: 0 }; }; +const searchBarWQLFieldSuggestions = [ + { label: 'condition', description: 'filter by check condition' }, + { label: 'description', description: 'filter by check description' }, + { label: 'file', description: 'filter by check file' }, + { label: 'rationale', description: 'filter by check rationale' }, + { label: 'reason', description: 'filter by check reason' }, + { label: 'registry', description: 'filter by check registry' }, + { label: 'remediation', description: 'filter by check remediation' }, + { label: 'result', description: 'filter by check result' }, + { label: 'title', description: 'filter by check title' }, +]; + +const searchBarWQLOptions = { + searchTermFields: [ + 'command', + 'compliance.key', + 'compliance.value', + 'description', + 'directory', + 'file', + 'id', + 'title', + 'process', + 'registry', + 'rationale', + 'reason', + 'references', + 'remediation', + 'result', + 'rules.type', + 'rules.rule', + ], +}; + export class InventoryPolicyChecksTable extends Component { _isMount = false; - suggestions: IWzSuggestItem[] = []; columnsChecks: any; constructor(props) { super(props); @@ -31,108 +63,9 @@ export class InventoryPolicyChecksTable extends Component { agent, lookingPolicy, itemIdToExpandedRowMap: {}, - filters: filters || [], + filters: filters || '', pageTableChecks: { pageIndex: 0 }, }; - this.suggestions = [ - { - type: 'params', - label: 'condition', - description: 'Filter by check condition', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'condition', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'description', - description: 'Filter by check description', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'description', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'file', - description: 'Filter by check file', - operators: ['=', '!='], - values: (value) => - getFilterValues('file', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'registry', - description: 'Filter by check registry', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'registry', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'rationale', - description: 'Filter by check rationale', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'rationale', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'reason', - description: 'Filter by check reason', - operators: ['=', '!='], - values: (value) => - getFilterValues('reason', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'remediation', - description: 'Filter by check remediation', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'remediation', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'result', - description: 'Filter by check result', - operators: ['=', '!='], - values: (value) => - getFilterValues('result', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'title', - description: 'Filter by check title', - operators: ['=', '!='], - values: (value) => - getFilterValues('title', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - ]; this.columnsChecks = [ { field: 'id', @@ -149,7 +82,7 @@ export class InventoryPolicyChecksTable extends Component { { name: 'Target', truncateText: true, - render: (item) => ( + render: item => (
{item.file ? ( @@ -189,21 +122,25 @@ export class InventoryPolicyChecksTable extends Component { align: 'right', width: '40px', isExpander: true, - render: (item) => ( + render: item => ( this.toggleDetails(item)} - aria-label={this.state.itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} - iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + aria-label={ + this.state.itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand' + } + iconType={ + this.state.itemIdToExpandedRowMap[item.id] + ? 'arrowUp' + : 'arrowDown' + } /> ), }, ]; } - async componentDidMount() {} - async componentDidUpdate(prevProps) { - const { filters } = this.props + const { filters } = this.props; if (filters !== prevProps.filters) { this.setState({ filters: filters }); } @@ -217,7 +154,7 @@ export class InventoryPolicyChecksTable extends Component { * * @param item */ - toggleDetails = (item) => { + toggleDetails = item => { const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; if (itemIdToExpandedRowMap[item.id]) { @@ -228,7 +165,7 @@ export class InventoryPolicyChecksTable extends Component { checks += item.condition ? ` (Condition: ${item.condition})` : ''; const complianceText = item.compliance && item.compliance.length - ? item.compliance.map((el) => `${el.key}: ${el.value}`).join('\n') + ? item.compliance.map(el => `${el.key}: ${el.value}`).join('\n') : ''; const listItems = [ { @@ -260,10 +197,12 @@ export class InventoryPolicyChecksTable extends Component { description: , }, ]; - const itemsToShow = listItems.filter((x) => { + const itemsToShow = listItems.filter(x => { return x.description; }); - itemIdToExpandedRowMap[item.id] = ; + itemIdToExpandedRowMap[item.id] = ( + + ); } this.setState({ itemIdToExpandedRowMap }); }; @@ -306,28 +245,51 @@ export class InventoryPolicyChecksTable extends Component { }; }; + const { filters } = this.state; + const agentID = this.props?.agent?.id; + const scaPolicyID = this.props?.lookingPolicy?.policy_id; + return ( - <> - this.setState({ filters })} - tablePageSizeOptions={[10, 25, 50, 100]} - /> - + { + try { + return await getFilterValues( + field, + agentID, + scaPolicyID, + { + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + item => ({ label: item }), + ); + } catch (error) { + return []; + } + }, + }, + }} + /> ); } } diff --git a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts index 5d7844d49f..1804a72529 100644 --- a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts +++ b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts @@ -1,26 +1,29 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../common/constants'; import { WzRequest } from '../../../../../react-services/wz-request'; export async function getFilterValues( field: string, - value: string, agentId: string, policyId: string, filters: { [key: string]: string } = {}, - format = (item) => item) { + format = item => item, +) { const filter = { ...filters, distinct: true, select: field, - limit: 30, + sort: `+${field}`, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', `/sca/${agentId}/checks/${policyId}`, { - params: filter, - }); + const result = await WzRequest.apiReq( + 'GET', + `/sca/${agentId}/checks/${policyId}`, + { + params: filter, + }, + ); return ( - result?.data?.data?.affected_items?.map((item) => { + result?.data?.data?.affected_items?.map(item => { return format(item[field]); }) || [] ); diff --git a/plugins/main/public/components/agents/syscollector/columns/index.ts b/plugins/main/public/components/agents/syscollector/columns/index.ts index 233876ed88..fe2b7fb3b4 100644 --- a/plugins/main/public/components/agents/syscollector/columns/index.ts +++ b/plugins/main/public/components/agents/syscollector/columns/index.ts @@ -1,3 +1,7 @@ -export { processColumns } from './process-columns' -export { portsColumns } from './ports-columns' -export { packagesColumns } from './packages-columns' +export { netaddrColumns } from './netaddr-columns'; +export { netifaceColumns } from './netiface-columns'; +export { processColumns } from './process-columns'; +export { portsColumns } from './ports-columns'; +export { packagesColumns } from './packages-columns'; +export { windowsUpdatesColumns } from './windows-updates-columns'; + diff --git a/plugins/main/public/components/agents/syscollector/columns/netaddr-columns.ts b/plugins/main/public/components/agents/syscollector/columns/netaddr-columns.ts new file mode 100644 index 0000000000..59a8f96de2 --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/columns/netaddr-columns.ts @@ -0,0 +1,9 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + +export const netaddrColumns = [ + { field: 'iface', searchable: true, sortable: true }, + { field: 'address', searchable: true, sortable: true }, + { field: 'netmask', searchable: true, sortable: true }, + { field: 'proto', searchable: true, sortable: true }, + { field: 'broadcast', searchable: true, sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); \ No newline at end of file diff --git a/plugins/main/public/components/agents/syscollector/columns/netiface-columns.ts b/plugins/main/public/components/agents/syscollector/columns/netiface-columns.ts new file mode 100644 index 0000000000..f411c3d583 --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/columns/netiface-columns.ts @@ -0,0 +1,9 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + +export const netifaceColumns = [ + { field: 'name', searchable: true, sortable: true, }, + { field: 'mac', searchable: true, sortable: true }, + { field: 'state', searchable: true, name: 'State', sortable: true }, + { field: 'mtu', searchable: true, name: 'MTU', sortable: true }, + { field: 'type', searchable: true, name: 'Type', sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); \ No newline at end of file diff --git a/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts b/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts index 0310e58678..e7d1fffcfc 100644 --- a/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts +++ b/plugins/main/public/components/agents/syscollector/columns/packages-columns.ts @@ -1,31 +1,33 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + const windowsColumns = [ - { id: 'name' }, - { id: 'architecture', width: '10%' }, - { id: 'version' }, - { id: 'vendor', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'architecture', searchable: true, sortable: true, width: '10%' }, + { field: 'version', searchable: true, sortable: true }, + { field: 'vendor', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const linuxColumns = [ - { id: 'name' }, - { id: 'architecture', width: '10%' }, - { id: 'version' }, - { id: 'vendor', width: '30%' }, - { id: 'description', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'architecture', searchable: true, sortable: true, width: '10%' }, + { field: 'version', searchable: true, sortable: true }, + { field: 'vendor', searchable: true, sortable: true, width: '30%' }, + { field: 'description', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const MacColumns = [ - { id: 'name' }, - { id: 'version' }, - { id: 'format' }, - { id: 'location', width: '30%' }, - { id: 'description', width: '20%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'version', searchable: true, sortable: true }, + { field: 'format', searchable: true, sortable: true }, + { field: 'location', searchable: true, sortable: true, width: '30%' }, + { field: 'description', searchable: true, sortable: true, width: '20%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const FreebsdColumns = [ - { id: 'name' }, - { id: 'version' }, - { id: 'format' }, - { id: 'architecture', width: '20%' }, - { id: 'vendor', width: '20%' }, - { id: 'description', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true }, + { field: 'version', searchable: true, sortable: true }, + { field: 'format', searchable: true, sortable: true }, + { field: 'architecture', searchable: true, sortable: true, width: '20%' }, + { field: 'vendor', searchable: true, sortable: true, width: '20%' }, + { field: 'description', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); export const packagesColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts b/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts index c8e7462a19..5c1890e51f 100644 --- a/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts +++ b/plugins/main/public/components/agents/syscollector/columns/ports-columns.ts @@ -1,16 +1,18 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + const windowsColumns = [ - { id: 'process' }, - { id: 'local.ip', sortable: false }, - { id: 'local.port', sortable: false }, - { id: 'state' }, - { id: 'protocol' }, -]; + { field: 'process', searchable: true, sortable: true }, + { field: 'local.ip', searchable: true, sortable: false }, + { field: 'local.port', searchable: true, sortable: false }, + { field: 'state', searchable: true, sortable: true }, + { field: 'protocol', searchable: true, sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const defaultColumns = [ - { id: 'local.ip', sortable: false }, - { id: 'local.port', sortable: false }, - { id: 'state' }, - { id: 'protocol' }, -]; + { field: 'local.ip', searchable: true, sortable: false }, + { field: 'local.port', searchable: true, sortable: false }, + { field: 'state', searchable: true, sortable: true }, + { field: 'protocol', searchable: true, sortable: true }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); export const portsColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/agents/syscollector/columns/process-columns.ts b/plugins/main/public/components/agents/syscollector/columns/process-columns.ts index 01e5bff9e7..a21ee0447d 100644 --- a/plugins/main/public/components/agents/syscollector/columns/process-columns.ts +++ b/plugins/main/public/components/agents/syscollector/columns/process-columns.ts @@ -1,35 +1,37 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + const windowsColumns = [ - { id: 'name', width: '10%' }, - { id: 'pid' }, - { id: 'ppid' }, - { id: 'vm_size' }, - { id: 'priority' }, - { id: 'nlwp' }, - { id: 'cmd', width: '30%' }, -]; + { field: 'name', searchable: true, sortable: true, width: '10%' }, + { field: 'pid', searchable: true, sortable: true }, + { field: 'ppid', searchable: true, sortable: true }, + { field: 'vm_size', searchable: true, sortable: true }, + { field: 'priority', searchable: true, sortable: true }, + { field: 'nlwp', searchable: true, sortable: true }, + { field: 'cmd', searchable: true, sortable: true, width: '30%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const linuxColumns = [ - { id: 'name', width: '10%' }, - { id: 'euser' }, - { id: 'egroup' }, - { id: 'pid' }, - { id: 'ppid' }, - { id: 'cmd', width: '15%' }, - { id: 'argvs', width: '15%' }, - { id: 'vm_size' }, - { id: 'size' }, - { id: 'session' }, - { id: 'nice' }, - { id: 'state', width: '15%' }, -]; + { field: 'name', searchable: true, sortable: true, width: '10%' }, + { field: 'euser', searchable: true, sortable: true }, + { field: 'egroup', searchable: true, sortable: true }, + { field: 'pid', searchable: true, sortable: true }, + { field: 'ppid', searchable: true, sortable: true }, + { field: 'cmd', searchable: true, sortable: true, width: '15%' }, + { field: 'argvs', searchable: true, sortable: true, width: '15%' }, + { field: 'vm_size', searchable: true, sortable: true }, + { field: 'size', searchable: true, sortable: true }, + { field: 'session', searchable: true, sortable: true }, + { field: 'nice', searchable: true, sortable: true }, + { field: 'state', searchable: true, sortable: true, width: '15%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); const macColumns = [ - { id: 'name', width: '10%' }, - { id: 'euser' }, - { id: 'pid' }, - { id: 'ppid' }, - { id: 'vm_size' }, - { id: 'nice' }, - { id: 'state', width: '15%' }, -]; + { field: 'name', searchable: true, sortable: true, width: '10%' }, + { field: 'euser', searchable: true, sortable: true }, + { field: 'pid', searchable: true, sortable: true }, + { field: 'ppid', searchable: true, sortable: true }, + { field: 'vm_size', searchable: true, sortable: true }, + { field: 'nice', searchable: true, sortable: true }, + { field: 'state', searchable: true, sortable: true, width: '15%' }, +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); export const processColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/agents/syscollector/columns/windows-updates-columns.ts b/plugins/main/public/components/agents/syscollector/columns/windows-updates-columns.ts new file mode 100644 index 0000000000..815cae842d --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/columns/windows-updates-columns.ts @@ -0,0 +1,5 @@ +import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; + +export const windowsUpdatesColumns = [ + { field: 'hotfix', searchable: true, sortable: true} +].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); \ No newline at end of file diff --git a/plugins/main/public/components/agents/syscollector/components/syscollector-table.tsx b/plugins/main/public/components/agents/syscollector/components/syscollector-table.tsx deleted file mode 100644 index e5e5b0056f..0000000000 --- a/plugins/main/public/components/agents/syscollector/components/syscollector-table.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React, { useState } from 'react'; -import { - EuiPanel, - EuiFlexGroup, - EuiButtonEmpty, - EuiFlexItem, - EuiText, - EuiLoadingSpinner, - EuiFieldSearch, - EuiHorizontalRule, - EuiIcon, - EuiBasicTable, -} from '@elastic/eui'; -import { useApiRequest } from '../../../common/hooks/useApiRequest'; -import { KeyEquivalence } from '../../../../../common/csv-key-equivalence'; -import { AppState } from '../../../../react-services/app-state'; - -export function SyscollectorTable({ tableParams }) { - const [params, setParams] = useState<{ - limit: number; - offset: number; - select: string; - q?: string; - }>({ - limit: 10, - offset: 0, - select: tableParams.columns.map(({ id }) => id).join(','), - }); - const [pageIndex, setPageIndex] = useState(0); - const [searchBarValue, setSearchBarValue] = useState(''); - const [pageSize, setPageSize] = useState(10); - const [sortField, setSortField] = useState(''); - const [timerDelaySearch, setTimerDelaySearch] = useState(); - const [sortDirection, setSortDirection] = useState(''); - const [loading, data, error] = useApiRequest('GET', tableParams.path, params); - - const onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - setPageIndex(pageIndex); - setPageSize(pageSize); - setSortField(sortField); - setSortDirection(sortDirection); - const field = sortField === 'os_name' ? '' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - const newParams = { - ...params, - limit: pageSize, - offset: Math.floor((pageIndex * pageSize) / params.limit) * params.limit, - ...(!!field ? { sort: `${direction}${field}` } : {}), - }; - - setParams(newParams); - }; - - const buildColumns = () => { - return (tableParams.columns || []).map(item => { - return { - field: item.id, - name: KeyEquivalence[item.id] || item.id, - sortable: typeof item.sortable !== 'undefined' ? item.sortable : true, - width: item.width || undefined, - }; - }); - }; - - const columns = buildColumns(); - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: data.total_affected_items || 0, - pageSizeOptions: [10, 25, 50], - }; - - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - const onChange = e => { - const value = e.target.value; - setSearchBarValue(value); - timerDelaySearch && clearTimeout(timerDelaySearch); - const timeoutId = setTimeout(() => { - const { q, ...rest } = params; - const newParams = { - ...rest, - ...(value - ? { - q: tableParams.columns - .map(({ id }) => `${id}~${value}`) - .join(','), - } - : {}), - }; - setParams(newParams); - setPageIndex(0); - }, 400); - setTimerDelaySearch(timeoutId); - }; - - const getTotal = () => { - if (loading) - return ( - <> - {'( '} - - {' )'} - - ); - else return `(${data.total_affected_items})`; - }; - - const downloadCsv = async () => { - await AppState.downloadCsv( - tableParams.path, - tableParams.exportFormatted, - !!params.q ? [{ name: 'q', value: params.q }] : [], - ); - }; - - return ( - - - - - {' '} - {' '} -  {' '} - - {tableParams.title} {tableParams.hasTotal ? getTotal() : ''} - {' '} - - - - - {tableParams.searchBar && ( - - - - - - )} - - - - - - {tableParams.exportFormatted && tableParams.columns && ( - - - - - Download CSV - - - - )} - - ); -} diff --git a/plugins/main/public/components/agents/syscollector/inventory.tsx b/plugins/main/public/components/agents/syscollector/inventory.tsx index ef9cf66de2..9fbe6a1e43 100644 --- a/plugins/main/public/components/agents/syscollector/inventory.tsx +++ b/plugins/main/public/components/agents/syscollector/inventory.tsx @@ -10,20 +10,35 @@ * Find more information about this on the LICENSE file. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiEmptyPrompt, EuiButton, EuiFlexGroup, EuiFlexItem, EuiCallOut, - EuiLink + EuiLink, + EuiPanel, } from '@elastic/eui'; import { InventoryMetrics } from './components/syscollector-metrics'; -import { SyscollectorTable } from './components/syscollector-table'; -import { processColumns, portsColumns, packagesColumns } from './columns'; -import { API_NAME_AGENT_STATUS } from '../../../../common/constants'; +import { + netaddrColumns, + netifaceColumns, + processColumns, + portsColumns, + packagesColumns, + windowsUpdatesColumns, +} from './columns'; +import { + API_NAME_AGENT_STATUS, + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, +} from '../../../../common/constants'; import { webDocumentationLink } from '../../../../common/services/web_documentation'; +import { TableWzAPI } from '../../common/tables'; +import { WzRequest } from '../../../react-services'; +import { get as getLodash } from 'lodash'; + +const sortFieldSuggestion = (a, b) => (a.label > b.label ? 1 : -1); export function SyscollectorInventory({ agent }) { if (agent && agent.status === API_NAME_AGENT_STATUS.NEVER_CONNECTED) { @@ -33,7 +48,7 @@ export function SyscollectorInventory({ agent }) { style={{ marginTop: 20 }} title={

Agent has never connected.

} body={ - + <>

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

{agent && agent.status === API_NAME_AGENT_STATUS.DISCONNECTED && ( @@ -104,89 +104,359 @@ export function SyscollectorInventory({ agent }) { - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return netifaceColumns + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/netiface`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return portsColumns[soPlatform] + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/ports`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return netaddrColumns + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/netaddr`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + {agent && agent.os && agent.os.platform === 'windows' && ( - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return windowsUpdatesColumns + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/hotfixes`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map( + item => ({ + label: getLodash(item, field), + }), + ); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + )} - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return packagesColumns[soPlatform] + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/packages`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> + - + + field) + .join(',')}`} + searchTable + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + searchBarWQL={{ + suggestions: { + field(currentValue) { + return processColumns[soPlatform] + .map(item => ({ + label: item.field, + description: `filter by ${item.name}`, + })) + .sort(sortFieldSuggestion); + }, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/syscollector/${agent.id}/processes`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} + /> +
diff --git a/plugins/main/public/components/agents/vuls/inventory.tsx b/plugins/main/public/components/agents/vuls/inventory.tsx index c9f796e3aa..4476d2a56f 100644 --- a/plugins/main/public/components/agents/vuls/inventory.tsx +++ b/plugins/main/public/components/agents/vuls/inventory.tsx @@ -59,7 +59,7 @@ interface TitleColors { export class Inventory extends Component { _isMount = false; state: { - filters: []; + filters: object; isLoading: boolean; isLoadingStats: boolean; customBadges: ICustomBadges[]; @@ -82,7 +82,7 @@ export class Inventory extends Component { isLoading: true, isLoadingStats: true, customBadges: [], - filters: [], + filters: {}, stats: [ { title: 0, @@ -167,12 +167,9 @@ export class Inventory extends Component { } buildFilterQuery(field = '', selectedItem = '') { - return [ - { - field: 'q', - value: `${field}=${selectedItem}`, - }, - ]; + return { + q: `${field}=${selectedItem}` + }; } async loadAgent() { @@ -220,7 +217,7 @@ export class Inventory extends Component { textAlign='center' isLoading={isLoadingStats} title={ - + item) { - +export async function getFilterValues( + field, + agentId, + filters = {}, + format = item => item, +) { const filter = { ...filters, distinct: true, select: field, - limit: 30, + sort: `+${field}`, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { params: filter }); - return result?.data?.data?.affected_items?.map((item) => { return format(item[field]) }) || []; + const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { + params: filter, + }); + return ( + result?.data?.data?.affected_items?.map(item => { + return format(item[field]); + }) || [] + ); } export async function getLastScan(agentId: string = '000') { const response = await WzRequest.apiReq( 'GET', `/vulnerability/${agentId}/last_scan`, - {} + {}, ); return response?.data?.data?.affected_items[0] || {}; } diff --git a/plugins/main/public/components/agents/vuls/inventory/table.tsx b/plugins/main/public/components/agents/vuls/inventory/table.tsx index 2c5c604905..d3bb882ed3 100644 --- a/plugins/main/public/components/agents/vuls/inventory/table.tsx +++ b/plugins/main/public/components/agents/vuls/inventory/table.tsx @@ -11,86 +11,34 @@ */ import React, { Component } from 'react'; -import { Direction } from '@elastic/eui'; import { FlyoutDetail } from './flyout'; -import { filtersToObject, IFilter, IWzSuggestItem } from '../../../wz-search-bar'; import { TableWzAPI } from '../../../../components/common/tables'; import { getFilterValues } from './lib'; import { formatUIDate } from '../../../../react-services/time-service'; +import { EuiIconTip } from '@elastic/eui'; + +const searchBarWQLOptions = { + searchTermFields: [ + 'name', + 'cve', + 'version', + 'architecture', + 'severity', + 'cvss2_score', + 'cvss3_score', + ], +}; export class InventoryTable extends Component { state: { error?: string; - pageIndex: number; - pageSize: number; - sortField: string; isFlyoutVisible: Boolean; - sortDirection: Direction; isLoading: boolean; currentItem: {}; }; - suggestions: IWzSuggestItem[] = [ - { - type: 'q', - label: 'name', - description: 'Filter by package ID', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('name', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cve', - description: 'Filter by CVE ID', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cve', value, this.props.agent.id), - }, - { - type: 'q', - label: 'version', - description: 'Filter by CVE version', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('version', value, this.props.agent.id), - }, - { - type: 'q', - label: 'architecture', - description: 'Filter by architecture', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('architecture', value, this.props.agent.id), - }, - { - type: 'q', - label: 'severity', - description: 'Filter by Severity', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('severity', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cvss2_score', - description: 'Filter by CVSS2', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cvss2_score', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cvss3_score', - description: 'Filter by CVSS3', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cvss3_score', value, this.props.agent.id), - }, - { - type: 'q', - label: 'detection_time', - description: 'Filter by Detection Time', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('detection_time', value, this.props.agent.id), - }, - ]; - props!: { - filters: IFilter[]; + filters: string; agent: any; items: []; onFiltersChange: Function; @@ -100,11 +48,6 @@ export class InventoryTable extends Component { super(props); this.state = { - pageIndex: 0, - pageSize: 15, - sortField: 'name', - sortDirection: 'asc', - isLoading: false, isFlyoutVisible: false, currentItem: {}, }; @@ -117,36 +60,10 @@ export class InventoryTable extends Component { async showFlyout(item, redirect = false) { //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. this.setState({ isFlyoutVisible: false }, () => - this.setState({ isFlyoutVisible: true, currentItem: item }) + this.setState({ isFlyoutVisible: true, currentItem: item }), ); } - async componentDidUpdate(prevProps) { - const { filters } = this.props; - if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }); - } - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + sortField; - } - - buildFilter() { - const { pageIndex, pageSize } = this.state; - const filters = filtersToObject(this.props.filters); - const filter = { - ...filters, - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - }; - return filter; - } - columns() { let width; (((this.props.agent || {}).os || {}).platform || false) === 'windows' @@ -199,7 +116,17 @@ export class InventoryTable extends Component { }, { field: 'detection_time', - name: 'Detection Time', + name: ( + + Detection Time{' '} + + + ), sortable: true, width: `100px`, render: formatUIDate, @@ -208,7 +135,7 @@ export class InventoryTable extends Component { } renderTable() { - const getRowProps = (item) => { + const getRowProps = item => { const id = `${item.name}-${item.cve}-${item.architecture}-${item.version}-${item.severity}-${item.cvss2_score}-${item.cvss3_score}-${item.detection_time}`; return { 'data-test-subj': `row-${id}`, @@ -217,7 +144,6 @@ export class InventoryTable extends Component { }; const { error } = this.state; - const { filters, onFiltersChange } = this.props; const columns = this.columns(); const selectFields = `select=${[ 'cve', @@ -232,33 +158,71 @@ export class InventoryTable extends Component { 'condition', 'updated', 'published', - 'external_references' + 'external_references', ].join(',')}`; + const agentID = this.props.agent.id; + return ( ({ + mapResponseItem={item => ({ ...item, // Some vulnerability data could not contain the external_references field. // This causes the rendering of them can crash when opening the flyout with the details. // So, we ensure the fields are defined with the expected data structure. - external_references: Array.isArray(item?.external_references) + external_references: Array.isArray(item?.external_references) ? item?.external_references - : [] + : [], })} error={error} - downloadCsv={true} - filters={filters} - onFiltersChange={onFiltersChange} + searchTable + downloadCsv + showReload tablePageSizeOptions={[10, 25, 50, 100]} + filters={this.props.filters} + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + { + label: 'architecture', + description: 'filter by architecture', + }, + { label: 'cve', description: 'filter by CVE ID' }, + { label: 'cvss2_score', description: 'filter by CVSS2' }, + { label: 'cvss3_score', description: 'filter by CVSS3' }, + { + label: 'detection_time', + description: 'filter by detection time', + }, + { label: 'name', description: 'filter by package name' }, + { label: 'severity', description: 'filter by severity' }, + { label: 'version', description: 'filter by CVE version' }, + ]; + }, + value: async (currentValue, { field }) => { + try { + return await getFilterValues( + field, + agentID, + { + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + label => ({ label }), + ); + } catch (error) { + return []; + } + }, + }, + }} /> ); } @@ -266,7 +230,7 @@ export class InventoryTable extends Component { render() { const table = this.renderTable(); return ( -
+
{table} {this.state.isFlyoutVisible && ( this.closeFlyout()} - type="vulnerability" - view="inventory" + type='vulnerability' + view='inventory' showViewInEvents={true} outsideClickCloses={true} {...this.props} diff --git a/plugins/main/public/components/common/hooks/index.ts b/plugins/main/public/components/common/hooks/index.ts index 944998ea1a..e3ce7584c7 100644 --- a/plugins/main/public/components/common/hooks/index.ts +++ b/plugins/main/public/components/common/hooks/index.ts @@ -27,3 +27,4 @@ export * from './use_async_action'; export * from './use_async_action_run_on_start'; export { useEsSearch } from './use-es-search'; export { useValueSuggestion, IValueSuggestion } from './use-value-suggestion'; +export * from './use-state-storage'; diff --git a/plugins/main/public/components/common/hooks/use-state-storage.ts b/plugins/main/public/components/common/hooks/use-state-storage.ts new file mode 100644 index 0000000000..3251ea1be5 --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-state-storage.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; + +function transformValueToStorage(value: any){ + return typeof value !== 'string' ? JSON.stringify(value) : value; +}; + +function transformValueFromStorage(value: any){ + return typeof value === 'string' ? JSON.parse(value) : value; +}; + +export function useStateStorage(initialValue: any, storageSystem?: 'sessionStorage' | 'localStorage', storageKey?: string){ + const [state, setState] = useState( + (storageSystem && storageKey && window?.[storageSystem]?.getItem(storageKey)) + ? transformValueFromStorage(window?.[storageSystem]?.getItem(storageKey)) + : initialValue + ); + + function setStateStorage(value: any){ + setState((state) => { + const formattedValue = typeof value === 'function' + ? value(state) + : value; + + storageSystem && storageKey && window?.[storageSystem]?.setItem(storageKey, transformValueToStorage(formattedValue)); + return formattedValue; + }); + }; + + return [state, setStateStorage]; +}; diff --git a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index 0c57fd5b26..7b13e589a4 100644 --- a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -6,6 +6,17 @@ exports[`Table With Search Bar component renders correctly to match the snapshot reload={[Function]} rowProps={[Function]} searchBarSuggestions={Array []} + searchBarWQL={ + Object { + "options": Object { + "searchTermFields": Array [], + }, + "suggestions": Object { + "field": [Function], + "value": [Function], + }, + } + } tableColumns={ Array [ Object { @@ -44,222 +55,494 @@ exports[`Table With Search Bar component renders correctly to match the snapshot } tableProps={Object {}} > - - -
- + + WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS +
- + + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ + } + inputRef={ + Object { + "current": , + } + } + isPopoverOpen={false} + onChange={[Function]} + onClosePopover={[Function]} + onInputChange={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Search" + suggestions={Array []} + value="" + > +
+ + WQL + } - status="unchanged" - suggestions={Array []} - value="" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" > + + SYNTAX OPTIONS +
- + style={ + Object { + "width": "350px", } - sendValue={[Function]} - status="unchanged" - suggestions={Array []} - value="" - > -
- - } - value="" - /> - } - isOpen={false} - panelPaddingSize="none" + } + > + + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ + } + inputRef={ + Object { + "current": , + } + } + isPopoverOpen={false} + onChange={[Function]} + onClosePopover={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Search" + sendValue={[Function]} + status="unchanged" + suggestions={Array []} + value="" + > +
+ - [Function] - + WQL + } - buttonRef={[Function]} - className="euiInputPopover euiInputPopover--fullWidth" closePopover={[Function]} - display="block" + display="inlineBlock" hasArrow={true} - id="popover" isOpen={false} - ownFocus={false} - panelPaddingSize="none" - panelRef={[Function]} + ownFocus={true} + panelPaddingSize="m" > - + SYNTAX OPTIONS + +
-
+ WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + inputRef={ + Object { + "current": , + } + } + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Search" + value="" + /> + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiInputPopover--fullWidth" + closePopover={[Function]} + display="block" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS + +
+ + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + inputRef={ + Object { + "current": , + } + } + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Search" + value="" > -
+ WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS + +
+ + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + isLoading={false} > - -
- + + + + - } - value="" + /> +
+ + WQL + + } + className="euiFormControlLayout__append" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + key="0/.0" + ownFocus={true} + panelPaddingSize="m" + > +
- - } +
-
- -
- -
- - - - -
-
- - -
- -
-
- -
- + + + WQL + + + + + +
+
+ +
+ + +
+ +
-
-
- + + +
-
+
-
-
+ + diff --git a/plugins/main/public/components/common/tables/components/export-table-csv.tsx b/plugins/main/public/components/common/tables/components/export-table-csv.tsx index 01486f31e6..d4bc30b2dc 100644 --- a/plugins/main/public/components/common/tables/components/export-table-csv.tsx +++ b/plugins/main/public/components/common/tables/components/export-table-csv.tsx @@ -15,7 +15,6 @@ import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -import { filtersToObject } from '../../../wz-search-bar/'; import exportCsv from '../../../../react-services/wz-csv'; import { getToasts } from '../../../../kibana-services'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; @@ -34,8 +33,7 @@ export function ExportTableCsv({ endpoint, totalItems, filters, title }) { const downloadCsv = async () => { try { - const filtersObject = filtersToObject(filters); - const formatedFilters = Object.keys(filtersObject).map(key => ({name: key, value: filtersObject[key]})); + const formatedFilters = Object.entries(filters).map(([name, value]) => ({name, value})); showToast('success', 'Your download should begin automatically...', 3000); await exportCsv( endpoint, diff --git a/plugins/main/public/components/common/tables/table-default.tsx b/plugins/main/public/components/common/tables/table-default.tsx index 97e3d50974..ed068e3d23 100644 --- a/plugins/main/public/components/common/tables/table-default.tsx +++ b/plugins/main/public/components/common/tables/table-default.tsx @@ -111,9 +111,8 @@ export function TableDefault({ hidePerPageOptions }; return ( - <> ({...rest}))} items={items} loading={loading} pagination={tablePagination} @@ -122,6 +121,5 @@ export function TableDefault({ rowProps={rowProps} {...tableProps} /> - ); } diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx index 263b98b51b..60d83b8a52 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx @@ -63,6 +63,10 @@ const columns = [ }, ]; +const searchBarWQLOptions = { + searchTermFields: [] +} + const tableProps = { onSearch: () => {}, tableColumns: columns, @@ -73,6 +77,17 @@ const tableProps = { reload: () => {}, searchBarSuggestions: [], rowProps: () => {}, + searchBarWQL: { + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return []; + }, + value: async (currentValue, { field }) => { + return []; + }, + }, + } }; describe('Table With Search Bar component', () => { diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.tsx index 6804dcb1a8..1f60167d3a 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.tsx @@ -10,18 +10,92 @@ * Find more information about this on the LICENSE file. */ -import React, { useState, useEffect, useRef } from 'react'; -import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { EuiBasicTable, EuiBasicTableProps, EuiSpacer } from '@elastic/eui'; import _ from 'lodash'; -import { WzSearchBar } from '../../wz-search-bar/'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { SearchBar, SearchBarProps } from '../../search-bar'; -export function TableWithSearchBar({ +export interface ITableWithSearcHBarProps { + /** + * Function to fetch the data + */ + onSearch: ( + endpoint: string, + filters: Record, + pagination: { pageIndex: number; pageSize: number }, + sorting: { sort: { field: string; direction: string } }, + ) => Promise<{ items: any[]; totalItems: number }>; + /** + * Properties for the search bar + */ + searchBarProps?: Omit< + SearchBarProps, + 'defaultMode' | 'modes' | 'onSearch' | 'input' + >; + /** + * Columns for the table + */ + tableColumns: EuiBasicTableProps['columns'] & { + composeField?: string[]; + searchable?: string; + show?: boolean; + }; + /** + * Table row properties for the table + */ + rowProps?: EuiBasicTableProps['rowProps']; + /** + * Table page size options + */ + tablePageSizeOptions?: number[]; + /** + * Table initial sorting direction + */ + tableInitialSortingDirection?: 'asc' | 'desc'; + /** + * Table initial sorting field + */ + tableInitialSortingField?: string; + /** + * Table properties + */ + tableProps?: Omit< + EuiBasicTableProps, + | 'columns' + | 'items' + | 'loading' + | 'pagination' + | 'sorting' + | 'onChange' + | 'rowProps' + >; + /** + * Refresh the fetch of data + */ + reload?: number; + /** + * API endpoint + */ + endpoint: string; + /** + * Search bar properties for WQL + */ + searchBarWQL?: any; + /** + * Visible fields + */ + selectedFields: string[]; + /** + * API request filters + */ + filters?: any; +} + +export function TableWithSearchBar({ onSearch, - searchBarSuggestions, - searchBarPlaceholder = 'Filter or search', searchBarProps = {}, tableColumns, rowProps, @@ -32,25 +106,44 @@ export function TableWithSearchBar({ reload, endpoint, ...rest -}) { +}: ITableWithSearcHBarProps) { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState(rest.filters || []); + const [filters, setFilters] = useState(rest.filters || {}); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: tablePageSizeOptions[0], }); - const [sorting, setSorting] = useState({ sort: { field: tableInitialSortingField, direction: tableInitialSortingDirection, }, }); + const [refresh, setRefresh] = useState(Date.now()); const isMounted = useRef(false); + const searchBarWQLOptions = useMemo( + () => ({ + searchTermFields: tableColumns + .filter( + ({ field, searchable }) => + searchable && rest.selectedFields.includes(field), + ) + .map(({ field, composeField }) => [composeField || field].flat()) + .flat(), + ...(rest?.searchBarWQL?.options || {}), + }), + [rest?.searchBarWQL?.options, rest?.selectedFields], + ); + + function updateRefresh() { + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + setRefresh(Date.now()); + } + function tableOnChange({ page = {}, sort = {} }) { if (isMounted.current) { const { index: pageIndex, size: pageSize } = page; @@ -73,44 +166,53 @@ export function TableWithSearchBar({ // We don't want to set the pagination state because there is another effect that has this dependency // and will cause the effect is triggered (redoing the onSearch function). if (isMounted.current) { - // Reset the page index when the endpoint changes. + // Reset the page index when the endpoint or reload changes. // This will cause that onSearch function is triggered because to changes in pagination in the another effect. - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + updateRefresh(); } }, [endpoint, reload]); - useEffect(function () { - (async () => { - try { - setLoading(true); - const { items, totalItems } = await onSearch(endpoint, filters, pagination, sorting); - setItems(items); - setTotalItems(totalItems); - } catch (error) { - setItems([]); - setTotalItems(0); - const options = { - context: `${TableWithSearchBar.name}.useEffect`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error fetching items`, - }, - }; - getErrorOrchestrator().handleError(options); - } - setLoading(false); - })(); - }, [filters, pagination, sorting]); + useEffect( + function () { + (async () => { + try { + setLoading(true); + const { items, totalItems } = await onSearch( + endpoint, + filters, + pagination, + sorting, + ); + setItems(items); + setTotalItems(totalItems); + } catch (error) { + setItems([]); + setTotalItems(0); + const options = { + context: `${TableWithSearchBar.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error fetching items`, + }, + }; + getErrorOrchestrator().handleError(options); + } + setLoading(false); + })(); + }, + [filters, pagination, sorting, refresh], + ); useEffect(() => { // This effect is triggered when the component is mounted because of how to the useEffect hook works. // We don't want to set the filters state because there is another effect that has this dependency // and will cause the effect is triggered (redoing the onSearch function). if (isMounted.current && !_.isEqual(rest.filters, filters)) { - setFilters(rest.filters || []); + setFilters(rest.filters || {}); + updateRefresh(); } }, [rest.filters]); @@ -128,17 +230,33 @@ export function TableWithSearchBar({ }; return ( <> - { + // Set the query, reset the page index and update the refresh + setFilters(apiQuery); + updateRefresh(); + }} /> - + ({ ...rest }), + )} items={items} loading={loading} pagination={tablePagination} diff --git a/plugins/main/public/components/common/tables/table-wz-api.tsx b/plugins/main/public/components/common/tables/table-wz-api.tsx index cb46407446..fc11c05c42 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.tsx @@ -18,8 +18,11 @@ import { EuiFlexItem, EuiText, EuiButtonEmpty, + EuiSpacer, + EuiToolTip, + EuiIcon, + EuiCheckboxGroup, } from '@elastic/eui'; -import { filtersToObject } from '../../wz-search-bar'; import { TableWithSearchBar } from './table-with-search-bar'; import { TableDefault } from './table-default'; import { WzRequest } from '../../../react-services/wz-request'; @@ -27,6 +30,7 @@ import { ExportTableCsv } from './components/export-table-csv'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { useStateStorage } from '../hooks'; /** * Search input custom filter button @@ -37,6 +41,11 @@ interface CustomFilterButton { value: string; } +const getFilters = filters => { + const { default: defaultFilters, ...restFilters } = filters; + return Object.keys(restFilters).length ? restFilters : defaultFilters; +}; + export function TableWzAPI({ actionButtons, ...rest @@ -44,7 +53,7 @@ export function TableWzAPI({ actionButtons?: ReactNode | ReactNode[]; title?: string; description?: string; - downloadCsv?: boolean; + downloadCsv?: boolean | string; searchTable?: boolean; endpoint: string; buttonOptions?: CustomFilterButton[]; @@ -54,17 +63,35 @@ export function TableWzAPI({ reload?: boolean; }) { const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState([]); + const [filters, setFilters] = useState({}); const [isLoading, setIsLoading] = useState(false); - const onFiltersChange = (filters) => - typeof rest.onFiltersChange === 'function' ? rest.onFiltersChange(filters) : null; + const onFiltersChange = filters => + typeof rest.onFiltersChange === 'function' + ? rest.onFiltersChange(filters) + : null; /** * Changing the reloadFootprint timestamp will trigger reloading the table */ const [reloadFootprint, setReloadFootprint] = useState(rest.reload || 0); - const onSearch = useCallback(async function (endpoint, filters, pagination, sorting) { + const [selectedFields, setSelectedFields] = useStateStorage( + rest.tableColumns.some(({ show }) => show) + ? rest.tableColumns.filter(({ show }) => show).map(({ field }) => field) + : rest.tableColumns.map(({ field }) => field), + rest?.saveStateStorage?.system, + rest?.saveStateStorage?.key + ? `${rest?.saveStateStorage?.key}-visible-fields` + : undefined, + ); + const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); + + const onSearch = useCallback(async function ( + endpoint, + filters, + pagination, + sorting, + ) { try { const { pageIndex, pageSize } = pagination; const { field, direction } = sorting.sort; @@ -72,7 +99,7 @@ export function TableWzAPI({ setFilters(filters); onFiltersChange(filters); const params = { - ...filtersToObject(filters), + ...getFilters(filters), offset: pageIndex * pageSize, limit: pageSize, sort: `${direction === 'asc' ? '+' : '-'}${field}`, @@ -85,23 +112,25 @@ export function TableWzAPI({ ).data; setIsLoading(false); setTotalItems(totalItems); - return { items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, totalItems }; + return { + items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, + totalItems, + }; } catch (error) { setIsLoading(false); setTotalItems(0); - const options = { - context: `${TableWithSearchBar.name}.useEffect`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error searching items`, - }, - }; - getErrorOrchestrator().handleError(options); + if (error?.name) { + /* This replaces the error name. The intention is that an AxiosError + doesn't appear in the toast message. + TODO: This should be managed by the service that does the request instead of only changing + the name in this case. + */ + error.name = 'RequestError'; + } + throw error; } - }, []); + }, + []); const renderActionButtons = ( <> @@ -130,58 +159,116 @@ export function TableWzAPI({ const ReloadButton = ( - triggerReload()} - > + triggerReload()}> Refresh ); const header = ( - - - {rest.title && ( - -

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

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

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

+
+ )} + {rest.description && ( + {rest.description} )} +
+ + + {/* Render optional custom action button */} + {renderActionButtons} + {/* Render optional reload button */} + {rest.showReload && ReloadButton} + {/* Render optional export to CSV button */} + {rest.downloadCsv && ( + + )} + {rest.showFieldSelector && ( + + + setIsOpenFieldSelector(state => !state)} + > + + + + + )} + + +
+ {isOpenFieldSelector && ( + + + ({ + id: item.field, + label: item.name, + checked: selectedFields.includes(item.field), + }))} + onChange={optionID => { + setSelectedFields(state => { + if (state.includes(optionID)) { + if (state.length > 1) { + return state.filter(field => field !== optionID); + } + return state; + } + return [...state, optionID]; + }); + }} + className='columnsSelectedCheckboxs' + idToSelectedMap={{}} + /> + -
-
+ )} + + ); + + const tableColumns = rest.tableColumns.filter(({ field }) => + selectedFields.includes(field), ); const table = rest.searchTable ? ( - + ) : ( - + ); return ( <> {header} + {rest.description && } {table} ); diff --git a/plugins/main/public/components/management/cluster/node-list.tsx b/plugins/main/public/components/management/cluster/node-list.tsx index 580c747d14..20dc51dd42 100644 --- a/plugins/main/public/components/management/cluster/node-list.tsx +++ b/plugins/main/public/components/management/cluster/node-list.tsx @@ -1,4 +1,4 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import { EuiPanel, EuiFlexGroup, @@ -6,80 +6,64 @@ import { EuiToolTip, EuiButtonIcon, EuiTitle, - EuiInMemoryTable, - EuiFieldSearch, } from '@elastic/eui'; -import { WzRequest } from '../../../react-services/wz-request'; import { withErrorBoundary } from '../../common/hocs'; +import { TableWzAPI } from '../../common/tables'; +import { WzRequest } from '../../../react-services'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../common/constants'; + +const searchBarWQLFieldSuggestions = [ + { label: 'ip', description: 'filter by IP address' }, + { label: 'name', description: 'filter by name' }, + { label: 'type', description: 'filter by type' }, + { label: 'version', description: 'filter by version' }, +]; export const NodeList = withErrorBoundary( class NodeList extends Component { constructor(props) { super(props); - this.state = { - nodes: [], - loading: false, - }; - } - async componentDidMount() { - this.search(); - } - - async search(searchTerm = false) { - let params = {}; - if (searchTerm) { - params.search = searchTerm; - } - this.setState({ loading: true }); - try { - const request = await WzRequest.apiReq('GET', '/cluster/nodes', { - params, - }); - this.setState({ - nodes: (((request || {}).data || {}).data || {}).affected_items || [], - loading: false, - }); - } catch (error) { - this.setState({ loading: false }); - } - } - render() { - const columns = [ + this.columns = [ { field: 'name', name: 'Name', + searchable: true, sortable: true, + truncateText: true, }, { field: 'version', name: 'Version', + searchable: true, sortable: true, }, { field: 'ip', name: 'IP address', + searchable: true, sortable: true, }, { field: 'type', name: 'Type', + searchable: true, sortable: true, }, ]; - - const sorting = { - sort: { - field: 'name', - direction: 'asc', - }, + this.state = { + nodes: [], + loading: false, }; + } + + render() { return ( - + -

Nodes

+

Cluster nodes

@@ -100,24 +84,51 @@ export const NodeList = withErrorBoundary(
- this.search(e)} - isClearable={true} - fullWidth={true} - aria-label='Filter' - /> - - - - - { + try { + const response = await WzRequest.apiReq( + 'GET', + '/cluster/nodes', + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map( + item => ({ + label: item[field], + }), + ); + } catch (error) { + return []; + } + }, + }, + }} + tableProps={{ + tableLayout: 'auto', + }} /> diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap b/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap index ce40ecc0ea..adf70d0cc4 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap @@ -163,40 +163,51 @@ exports[`Module Mitre Att&ck intelligence container should render the component />
-
+
-
+
-
+
-
+
+ +
+
-
-
- -
+ + + WQL + + +
diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx index 5e64bf018e..7eaa3c39dd 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx @@ -29,7 +29,7 @@ export const ModuleMitreAttackIntelligence = compose( const [selectedResource, setSelectedResource] = useState(MitreAttackResources[0].id); const [searchTermAllResources, setSearchTermAllResources] = useState(''); const searchTermAllResourcesLastSearch = useRef(''); - const [resourceFilters, setResourceFilters] = useState([]); + const [resourceFilters, setResourceFilters] = useState({}); const searchTermAllResourcesUsed = useRef(false); const searchTermAllResourcesAction = useAsyncAction( async (searchTerm) => { @@ -37,11 +37,23 @@ export const ModuleMitreAttackIntelligence = compose( searchTermAllResourcesUsed.current = true; searchTermAllResourcesLastSearch.current = searchTerm; const limitResults = 5; + const fields = ['name', 'description', 'external_id']; return ( await Promise.all( MitreAttackResources.map(async (resource) => { const response = await WzRequest.apiReq('GET', resource.apiEndpoint, { - params: { search: searchTerm, limit: limitResults }, + params: { + ...( + searchTerm + ? { + q: fields + .map(key => `${key}~${searchTerm}`) + .join(',') + } + : {} + ), + limit: limitResults + } }); return { id: resource.id, @@ -53,9 +65,18 @@ export const ModuleMitreAttackIntelligence = compose( response?.data?.data?.total_affected_items && response?.data?.data?.total_affected_items > limitResults && (() => { - setResourceFilters([ - { field: 'search', value: searchTermAllResourcesLastSearch.current }, - ]); + setResourceFilters({ + ...( + searchTermAllResourcesLastSearch.current + ? { + q: fields + .map(key => `${key}~${searchTermAllResourcesLastSearch.current}`) + .join(',') + } + : {} + ) + } + ); setSelectedResource(resource.id); }), }; @@ -76,7 +97,7 @@ export const ModuleMitreAttackIntelligence = compose( const onSelectResource = useCallback( (resourceID) => { - setResourceFilters([]); + setResourceFilters({}); setSelectedResource((prevSelectedResource) => prevSelectedResource === resourceID && searchTermAllResourcesUsed.current ? null diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx index 9dbf394150..fe311cfbea 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -21,7 +21,7 @@ import { getErrorOrchestrator } from '../../../react-services/common-services'; export const ModuleMitreAttackIntelligenceResource = ({ label, - searchBarSuggestions, + searchBar, apiEndpoint, tableColumnsCreator, initialSortingField, @@ -64,7 +64,7 @@ export const ModuleMitreAttackIntelligenceResource = ({ getErrorOrchestrator().handleError(options); } }; - + const tableColumns = useMemo(() => tableColumnsCreator(setDetails), []); const closeFlyout = useCallback(() => { @@ -78,11 +78,13 @@ export const ModuleMitreAttackIntelligenceResource = ({ title={label} tableColumns={tableColumns} tableInitialSortingField={initialSortingField} - searchBarPlaceholder={`Search in ${label}`} - searchBarSuggestions={searchBarSuggestions} endpoint={apiEndpoint} tablePageSizeOptions={[10, 15, 25, 50, 100]} filters={resourceFilters} + searchBarWQL={{ + options: searchBar.wql.options, + suggestions: searchBar.wql.suggestions, + }} /> {details && ( )} - - ) + + ); }; diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx index cc63a43d78..bf90d04a65 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx @@ -16,19 +16,31 @@ import { Markdown } from '../../common/util'; import { formatUIDate } from '../../../react-services'; import React from 'react'; import { EuiLink } from '@elastic/eui'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + UI_LOGGER_LEVELS, +} from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) => async (input: string) => { - try{ - const response = await WzRequest.apiReq('GET', endpoint, {}); - return response?.data?.data.affected_items - .map(item => item[field]) - .filter(item => item && item.toLowerCase().includes(input.toLowerCase())) - .sort() - .slice(0,9) - }catch(error){ +const getMitreAttackIntelligenceSuggestions = async ( + endpoint: string, + field: string, + currentValue: string, +) => { + try { + const params = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const response = await WzRequest.apiReq('GET', endpoint, { params }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { const options = { context: `${ModuleMitreAttackIntelligenceResource.name}.getMitreItemToRedirect`, level: UI_LOGGER_LEVELS.ERROR, @@ -43,62 +55,74 @@ const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) }; getErrorOrchestrator().handleError(options); return []; - }; + } }; -function buildResource(label: string, labelResource: string){ +function buildResource(label: string) { const id = label.toLowerCase(); const endpoint: string = `/mitre/${id}`; + const fieldsMitreAttactResource = [ + { field: 'description', name: 'description' }, + { field: 'external_id', name: 'external ID' }, + { field: 'name', name: 'name' }, + ]; return { label: label, id, - searchBarSuggestions: [ - { - type: 'q', - label: 'description', - description: `${labelResource} description`, - operators: ['~'], - values: (input) => input ? [input] : [] - }, - { - type: 'q', - label: 'name', - description: `${labelResource} name`, - operators: ['=', '!='], - values: getMitreAttackIntelligenceSuggestions(endpoint, 'name') + searchBar: { + wql: { + options: { + searchTermFields: fieldsMitreAttactResource.map(({ field }) => field), + }, + suggestions: { + field(currentValue) { + return fieldsMitreAttactResource.map(({ field, name }) => ({ + label: field, + description: `filter by ${name}`, + })); + }, + value: async (currentValue, { field }) => { + try { + return await getMitreAttackIntelligenceSuggestions( + endpoint, + field, + currentValue, + ); + } catch (error) { + return []; + } + }, + }, }, - { - type: 'q', - label: 'external_id', - description: `${labelResource} ID`, - operators: ['=', '!='], - values: getMitreAttackIntelligenceSuggestions(endpoint, 'external_id') - } - ], + }, apiEndpoint: endpoint, fieldName: 'name', initialSortingField: 'name', - tableColumnsCreator: (openResourceDetails) => [ + tableColumnsCreator: openResourceDetails => [ { field: 'external_id', name: 'ID', width: '12%', - render: (value, item) => openResourceDetails(item)}>{value} + render: (value, item) => ( + openResourceDetails(item)}>{value} + ), }, { field: 'name', name: 'Name', sortable: true, width: '30%', - render: (value, item) => openResourceDetails(item)}>{value} + render: (value, item) => ( + openResourceDetails(item)}>{value} + ), }, { field: 'description', name: 'Description', sortable: true, - render: (value) => value ? : '', - truncateText: true - } + render: value => (value ? : ''), + truncateText: true, + }, ], mitreFlyoutHeaderProperties: [ { @@ -107,34 +131,30 @@ function buildResource(label: string, labelResource: string){ }, { label: 'Name', - id: 'name' + id: 'name', }, { label: 'Created Time', id: 'created_time', - render: (value) => value ? ( - formatUIDate(value) - ) : '' + render: value => (value ? formatUIDate(value) : ''), }, { label: 'Modified Time', id: 'modified_time', - render: (value) => value ? ( - formatUIDate(value) - ) : '' + render: value => (value ? formatUIDate(value) : ''), }, { label: 'Version', - id: 'mitre_version' + id: 'mitre_version', }, ], - } -}; + }; +} export const MitreAttackResources = [ - buildResource('Groups', 'Group'), - buildResource('Mitigations', 'Mitigation'), - buildResource('Software', 'Software'), - buildResource('Tactics', 'Tactic'), - buildResource('Techniques', 'Technique') + buildResource('Groups'), + buildResource('Mitigations'), + buildResource('Software'), + buildResource('Tactics'), + buildResource('Techniques'), ]; diff --git a/plugins/main/public/components/search-bar/index.tsx b/plugins/main/public/components/search-bar/index.tsx index 4a82d5d360..d0538739de 100644 --- a/plugins/main/public/components/search-bar/index.tsx +++ b/plugins/main/public/components/search-bar/index.tsx @@ -9,21 +9,22 @@ import { EuiSelect, EuiText, EuiFlexGroup, - EuiFlexItem + EuiFlexItem, } from '@elastic/eui'; import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; import _ from 'lodash'; import { ISearchBarModeWQL } from './query-language/wql'; +import { SEARCH_BAR_DEBOUNCE_UPDATE_TIME } from '../../../common/constants'; -export interface SearchBarProps{ +export interface SearchBarProps { defaultMode?: string; modes: ISearchBarModeWQL[]; onChange?: (params: any) => void; onSearch: (params: any) => void; - buttonsRender?: () => React.ReactNode + buttonsRender?: () => React.ReactNode; input?: string; -}; +} export const SearchBar = ({ defaultMode, @@ -54,12 +55,16 @@ export const SearchBar = ({ output: undefined, }); // Cache the previous output - const queryLanguageOutputRunPreviousOutput = useRef(queryLanguageOutputRun.output); + const queryLanguageOutputRunPreviousOutput = useRef( + queryLanguageOutputRun.output, + ); // Controls when the suggestion popover is open/close const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = useState(false); // Reference to the input const inputRef = useRef(); + // Debounce update timer + const debounceUpdateSearchBarTimer = useRef(); // Handler when searching const _onSearch = (output: any) => { @@ -79,55 +84,69 @@ export const SearchBar = ({ } }; - const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); + const selectedQueryLanguageParameters = modes.find( + ({ id }) => id === queryLanguage.id, + ); useEffect(() => { // React to external changes and set the internal input text. Use the `transformInput` of // the query language in use - rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformInput && setInput( - searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( - rest.input, - { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - } - ), - ); + rest.input && + searchBarQueryLanguages[queryLanguage.id]?.transformInput && + setInput( + searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( + rest.input, + { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + ), + ); }, [rest.input]); useEffect(() => { (async () => { // Set the query language output - const queryLanguageOutput = await searchBarQueryLanguages[queryLanguage.id].run(input, { - onSearch: _onSearch, - setInput, - closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), - openSuggestionPopover: () => setIsOpenSuggestionPopover(true), - setQueryLanguageConfiguration: (configuration: any) => - setQueryLanguage(state => ({ - ...state, - configuration: - configuration?.(state.configuration) || configuration, - })), - setQueryLanguageOutput: setQueryLanguageOutputRun, - inputRef, - queryLanguage: { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - }, - }); - queryLanguageOutputRunPreviousOutput.current = { - ...queryLanguageOutputRun.output - }; - setQueryLanguageOutputRun(queryLanguageOutput); + debounceUpdateSearchBarTimer.current && + clearTimeout(debounceUpdateSearchBarTimer.current); + // Debounce the updating of the search bar state + debounceUpdateSearchBarTimer.current = setTimeout(async () => { + const queryLanguageOutput = await searchBarQueryLanguages[ + queryLanguage.id + ].run(input, { + onSearch: _onSearch, + setInput, + closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), + openSuggestionPopover: () => setIsOpenSuggestionPopover(true), + setQueryLanguageConfiguration: (configuration: any) => + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), + setQueryLanguageOutput: setQueryLanguageOutputRun, + inputRef, + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + }); + queryLanguageOutputRunPreviousOutput.current = { + ...queryLanguageOutputRun.output, + }; + setQueryLanguageOutputRun(queryLanguageOutput); + }, SEARCH_BAR_DEBOUNCE_UPDATE_TIME); })(); }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); useEffect(() => { - onChange - // Ensure the previous output is different to the new one - && !_.isEqual(queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current) - && onChange(queryLanguageOutputRun.output); + onChange && + // Ensure the previous output is different to the new one + !_.isEqual( + queryLanguageOutputRun.output, + queryLanguageOutputRunPreviousOutput.current, + ) && + onChange(queryLanguageOutputRun.output); }, [queryLanguageOutputRun.output]); const onQueryLanguagePopoverSwitch = () => @@ -163,7 +182,7 @@ export const SearchBar = ({ closePopover={onQueryLanguagePopoverSwitch} > SYNTAX OPTIONS -
+
{searchBarQueryLanguages[queryLanguage.id].description} @@ -173,7 +192,8 @@ export const SearchBar = ({
) => { + onChange={( + event: React.ChangeEvent, + ) => { const queryLanguageID: string = event.target.value; setQueryLanguage({ id: queryLanguageID, @@ -214,16 +236,28 @@ export const SearchBar = ({ } {...queryLanguageOutputRun.searchBarProps} + {...(queryLanguageOutputRun.searchBarProps?.onItemClick + ? { + onItemClick: + queryLanguageOutputRun.searchBarProps?.onItemClick(input), + } + : {})} /> ); - return rest.buttonsRender || queryLanguageOutputRun.filterButtons - ? ( - - {searchBar} - {rest.buttonsRender && {rest.buttonsRender()}} - {queryLanguageOutputRun.filterButtons && {queryLanguageOutputRun.filterButtons}} - - ) - : searchBar; + return rest.buttonsRender || queryLanguageOutputRun.filterButtons ? ( + + {searchBar} + {rest.buttonsRender && ( + {rest.buttonsRender()} + )} + {queryLanguageOutputRun.filterButtons && ( + + {queryLanguageOutputRun.filterButtons} + + )} + + ) : ( + searchBar + ); }; diff --git a/plugins/main/public/components/search-bar/query-language/aql.test.tsx b/plugins/main/public/components/search-bar/query-language/aql.test.tsx index a5f7c7d36c..3c6a57caf3 100644 --- a/plugins/main/public/components/search-bar/query-language/aql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/aql.test.tsx @@ -15,27 +15,25 @@ describe('SearchBar component', () => { field(currentValue) { return []; }, - value(currentValue, { previousField }){ + value(currentValue, { previousField }) { return []; }, }, - } + }, ], /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, - onSearch: () => {} + onSearch: () => {}, /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly to match the snapshot of query language', async () => { - const wrapper = render( - - ); + const wrapper = render(); await waitFor(() => { - const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); + const elementImplicitQuery = wrapper.container.querySelector( + '.euiCodeBlock__code', + ); expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); expect(wrapper.container).toMatchSnapshot(); }); @@ -45,32 +43,32 @@ describe('SearchBar component', () => { describe('Query language - AQL', () => { // Tokenize the input it.each` - input | tokens - ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - `(`Tokenizer API input $input`, ({input, tokens}) => { + input | tokens + ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + `(`Tokenizer API input $input`, ({ input, tokens }) => { expect(tokenizer(input)).toEqual(tokens); }); @@ -127,79 +125,87 @@ describe('Query language - AQL', () => { // When a suggestion is clicked, change the input text it.each` - AQL | clikedSuggestion | changedInput - ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} - ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} - ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} - ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} - ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} - ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';'}} | ${'field=value;'} - ${'field=value;'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value;field2'} - ${'field=value;field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value;field2>'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field=with spaces'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field=with "spaces'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="value'} - ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} - ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} - ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} - ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} - ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} - ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} - ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ','}} | ${'(field=value,'} - ${'(field=value,'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value,field2'} - ${'(field=value,field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value,field2~'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value,field2>value2'} - ${'(field=value,field2>value2'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value,field2>value3'} - ${'(field=value,field2>value2'} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value,field2>value2)'} - `('click suggestion - AQL $AQL => $changedInput', async ({AQL: currentInput, clikedSuggestion, changedInput}) => { - // Mock input - let input = currentInput; + AQL | clikedSuggestion | changedInput + ${''} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'field'} + ${'field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field2'} + ${'field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'field='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field='} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!=' }} | ${'field!='} + ${'field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'field=value2'} + ${'field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';' }} | ${'field=value;'} + ${'field=value;'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value;field2'} + ${'field=value;field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'field=value;field2>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces' }} | ${'field=with spaces'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${'field=with "spaces'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${'field="value'} + ${''} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '(' }} | ${'('} + ${'('} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'(field'} + ${'(field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field2'} + ${'(field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'(field='} + ${'(field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'(field=value'} + ${'(field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value2'} + ${'(field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: ',' }} | ${'(field=value,'} + ${'(field=value,'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value,field2'} + ${'(field=value,field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value,field2~'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value,field2>value3'} + ${'(field=value,field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value,field2>value2)'} + `( + 'click suggestion - AQL "$AQL" => "$changedInput"', + async ({ AQL: currentInput, clikedSuggestion, changedInput }) => { + // Mock input + let input = currentInput; - const qlOutput = await AQL.run(input, { - setInput: (value: string): void => { input = value; }, - queryLanguage: { - parameters: { - implicitQuery: '', - suggestions: { - field: () => ([]), - value: () => ([]) - } - } - } - }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); - expect(input).toEqual(changedInput); - }); + const qlOutput = await AQL.run(input, { + setInput: (value: string): void => { + input = value; + }, + queryLanguage: { + parameters: { + implicitQuery: '', + suggestions: { + field: () => [], + value: () => [], + }, + }, + }, + }); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); + expect(input).toEqual(changedInput); + }, + ); // Transform the external input in UQL (Unified Query Language) to QL it.each` - UQL | AQL - ${''} | ${''} - ${'field'} | ${'field'} - ${'field='} | ${'field='} - ${'field!='} | ${'field!='} - ${'field>'} | ${'field>'} - ${'field<'} | ${'field<'} - ${'field~'} | ${'field~'} - ${'field=value'} | ${'field=value'} - ${'field=value;'} | ${'field=value;'} - ${'field=value;field2'} | ${'field=value;field2'} - ${'field="'} | ${'field="'} - ${'field=with spaces'} | ${'field=with spaces'} - ${'field=with "spaces'} | ${'field=with "spaces'} - ${'('} | ${'('} - ${'(field'} | ${'(field'} - ${'(field='} | ${'(field='} - ${'(field=value'} | ${'(field=value'} - ${'(field=value,'} | ${'(field=value,'} - ${'(field=value,field2'} | ${'(field=value,field2'} - ${'(field=value,field2>'} | ${'(field=value,field2>'} - ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} - ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} - `('Transform the external input UQL to QL - UQL $UQL => $AQL', async ({UQL, AQL: changedInput}) => { - expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); - }); + UQL | AQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value;'} + ${'field=value;field2'} | ${'field=value;field2'} + ${'field="'} | ${'field="'} + ${'field=with spaces'} | ${'field=with spaces'} + ${'field=with "spaces'} | ${'field=with "spaces'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value,'} + ${'(field=value,field2'} | ${'(field=value,field2'} + ${'(field=value,field2>'} | ${'(field=value,field2>'} + ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} + `( + 'Transform the external input UQL to QL - UQL $UQL => $AQL', + async ({ UQL, AQL: changedInput }) => { + expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); + }, + ); }); diff --git a/plugins/main/public/components/search-bar/query-language/aql.tsx b/plugins/main/public/components/search-bar/query-language/aql.tsx index 8c898af3e2..68d1292a23 100644 --- a/plugins/main/public/components/search-bar/query-language/aql.tsx +++ b/plugins/main/public/components/search-bar/query-language/aql.tsx @@ -71,28 +71,27 @@ const suggestionMappingLanguageTokenType = { /** * Creator of intermediate interface of EuiSuggestItem - * @param type - * @returns + * @param type + * @returns */ -function mapSuggestionCreator(type: ITokenType ){ - return function({...params}){ +function mapSuggestionCreator(type: ITokenType) { + return function ({ ...params }) { return { type, - ...params + ...params, }; }; -}; +} const mapSuggestionCreatorField = mapSuggestionCreator('field'); const mapSuggestionCreatorValue = mapSuggestionCreator('value'); - /** * Tokenize the input string. Returns an array with the tokens. * @param input * @returns */ -export function tokenizer(input: string): ITokens{ +export function tokenizer(input: string): ITokens { // API regular expression // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 // self.query_regex = re.compile( @@ -118,44 +117,50 @@ export function tokenizer(input: string): ITokens{ // completed. This helps to tokenize the query and manage when the input is not completed. // A ( character. '(?\\()?' + - // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added an optional find - // Operator: looks for '=', '!=', '<', '>' or '~'. - // This seems to be a bug because is not searching the literal valid operators. - // I guess the operator is validated after the regular expression matches - `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find - // Value: A string. - '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find - // A ) character. - '(?\\))?' + - `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, - 'g' + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys( + language.tokens.operator_compare.literal, + )}]{1,2})?` + // Added an optional find + // Value: A string. + '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find + // A ) character. + '(?\\))?' + + `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, + 'g', ); - return [ - ...input.matchAll(re)] - .map( - ({groups}) => Object.entries(groups) - .map(([key, value]) => ({ - type: key.startsWith('operator_group') ? 'operator_group' : key, - value}) - ) - ).flat(); -}; + return [...input.matchAll(re)] + .map(({ groups }) => + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') ? 'operator_group' : key, + value, + })), + ) + .flat(); +} type QLOptionSuggestionEntityItem = { - description?: string - label: string + description?: string; + label: string; }; -type QLOptionSuggestionEntityItemTyped = - QLOptionSuggestionEntityItem - & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; +type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { + type: + | 'operator_group' + | 'field' + | 'operator_compare' + | 'value' + | 'conjunction'; +}; type SuggestItem = QLOptionSuggestionEntityItem & { - type: { iconType: string, color: string } + type: { iconType: string; color: string }; }; type QLOptionSuggestionHandler = ( @@ -179,15 +184,11 @@ type optionsQL = { * @param tokenType token type to search * @returns */ -function getLastTokenWithValue( - tokens: ITokens -): IToken | undefined { +function getLastTokenWithValue(tokens: ITokens): IToken | undefined { // Reverse the tokens array and use the Array.protorype.find method const shallowCopyArray = Array.from([...tokens]); const shallowCopyArrayReversed = shallowCopyArray.reverse(); - const tokenFound = shallowCopyArrayReversed.find( - ({ value }) => value, - ); + const tokenFound = shallowCopyArrayReversed.find(({ value }) => value); return tokenFound; } @@ -218,7 +219,10 @@ function getLastTokenWithValueByType( * @param options * @returns */ -export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { +export async function getSuggestions( + tokens: ITokens, + options: optionsQL, +): Promise { if (!tokens.length) { return []; } @@ -227,40 +231,42 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi const lastToken = getLastTokenWithValue(tokens); // If it can't get a token with value, then returns fields and open operator group - if(!lastToken?.type){ - return [ + if (!lastToken?.type) { + return [ // fields ...(await options.suggestions.field()).map(mapSuggestionCreatorField), { type: 'operator_group', label: '(', description: language.tokens.operator_group.literal['('], - } + }, ]; - }; + } switch (lastToken.type) { case 'field': return [ // fields that starts with the input but is not equals - ...(await options.suggestions.field()).filter( - ({ label }) => - label.startsWith(lastToken.value) && label !== lastToken.value, - ).map(mapSuggestionCreatorField), + ...(await options.suggestions.field()) + .filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ) + .map(mapSuggestionCreatorField), // operators if the input field is exact ...((await options.suggestions.field()).some( ({ label }) => label === lastToken.value, ) ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }), - ), - ] + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] : []), ]; break; @@ -281,14 +287,17 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi operator => operator === lastToken.value, ) ? [ - ...(await options.suggestions.value(undefined, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })).map(mapSuggestionCreatorValue), - ] + ...( + await options.suggestions.value(undefined, { + previousField: getLastTokenWithValueByType(tokens, 'field')! + .value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(mapSuggestionCreatorValue), + ] : []), ]; break; @@ -296,22 +305,24 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi return [ ...(lastToken.value ? [ - { - type: 'function_search', - label: 'Search', - description: 'run the search query', - }, - ] + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + ] : []), - ...(await options.suggestions.value(lastToken.value, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })).map(mapSuggestionCreatorValue), + ...( + await options.suggestions.value(lastToken.value, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( - ([ conjunction, description]) => ({ + ([conjunction, description]) => ({ type: 'conjunction', label: conjunction, description, @@ -342,8 +353,10 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi conjunction => conjunction === lastToken.value, ) ? [ - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ] + ...(await options.suggestions.field()).map( + mapSuggestionCreatorField, + ), + ] : []), { type: 'operator_group', @@ -381,16 +394,18 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi /** * Transform the suggestion object to the expected object by EuiSuggestItem - * @param param0 - * @returns + * @param param0 + * @returns */ -export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ - const { type, ...rest} = suggestion; +export function transformSuggestionToEuiSuggestItem( + suggestion: QLOptionSuggestionEntityItemTyped, +): SuggestItem { + const { type, ...rest } = suggestion; return { type: { ...suggestionMappingLanguageTokenType[type] }, - ...rest + ...rest, }; -}; +} /** * Transform the suggestion object to the expected object by EuiSuggestItem @@ -398,24 +413,26 @@ export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggesti * @returns */ function transformSuggestionsToEuiSuggestItem( - suggestions: QLOptionSuggestionEntityItemTyped[] + suggestions: QLOptionSuggestionEntityItemTyped[], ): SuggestItem[] { return suggestions.map(transformSuggestionToEuiSuggestItem); -}; +} /** * Get the output from the input * @param input * @returns */ -function getOutput(input: string, options: {implicitQuery?: string} = {}) { - const unifiedQuery = `${options?.implicitQuery ?? ''}${options?.implicitQuery ? `(${input})` : input}`; +function getOutput(input: string, options: { implicitQuery?: string } = {}) { + const unifiedQuery = `${options?.implicitQuery ?? ''}${ + options?.implicitQuery ? `(${input})` : input + }`; return { language: AQL.id, query: unifiedQuery, - unifiedQuery + unifiedQuery, }; -}; +} export const AQL = { id: 'aql', @@ -436,21 +453,24 @@ export const AQL = { // Props that will be used by the EuiSuggest component // Suggestions suggestions: transformSuggestionsToEuiSuggestItem( - await getSuggestions(tokens, params.queryLanguage.parameters) + await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: item => { + onItemClick: currentInput => item => { // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action - params.onSearch(getOutput(input, params.queryLanguage.parameters)); + params.onSearch( + getOutput(currentInput, params.queryLanguage.parameters), + ); } else { // When the clicked item has another iconType const lastToken: IToken = getLastTokenWithValue(tokens); // if the clicked suggestion is of same type of last token if ( - lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType ) { // replace the value of last token lastToken.value = item.label; @@ -462,15 +482,17 @@ export const AQL = { )[0], value: item.label, }); - }; + } // Change the input - params.setInput(tokens - .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join('')); + params.setInput( + tokens + .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); } }, prepend: params.queryLanguage.parameters.implicitQuery ? ( @@ -512,7 +534,7 @@ export const AQL = { // This causes when using the Search suggestion, the suggestion popover can be closed. // If this is disabled, then the suggestion popover is open after a short time for this // use case. - disableFocusTrap: true + disableFocusTrap: true, }, output: getOutput(input, params.queryLanguage.parameters), }; diff --git a/plugins/main/public/components/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/query-language/wql.test.tsx index 4de5de790b..2d2a1b3171 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -303,9 +303,9 @@ describe('Query language - WQL', () => { ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value or field2~'} ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value or field2>value2'} ${'(field=value or field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value or field2>value3'} - ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2)'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2 )'} `( - 'click suggestion - WQL $WQL => $changedInput', + 'click suggestion - WQL "$WQL" => "$changedInput"', async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { // Mock input let input = currentInput; @@ -324,7 +324,7 @@ describe('Query language - WQL', () => { }, }, }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); expect(input).toEqual(changedInput); }, ); diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 9df7dbbf01..539f90a076 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -7,7 +7,10 @@ import { EuiCode, } from '@elastic/eui'; import { tokenizer as tokenizerUQL } from './aql'; -import { PLUGIN_VERSION } from '../../../../common/constants'; +import { + PLUGIN_VERSION, + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT, +} from '../../../../common/constants'; /* UI Query language https://documentation.wazuh.com/current/user-manual/api/queries.html @@ -99,10 +102,14 @@ const suggestionMappingLanguageTokenType = { * @returns */ function mapSuggestionCreator(type: ITokenType) { - return function ({ ...params }) { + return function ({ label, ...params }) { return { type, ...params, + /* WORKAROUND: ensure the label is a string. If it is not a string, an warning is + displayed in the console related to prop types + */ + ...(typeof label !== 'undefined' ? { label: String(label) } : {}), }; }; } @@ -314,6 +321,37 @@ function getTokenNearTo( ); } +/** + * It returns the regular expression that validate the token of type value + * @returns The regular expression + */ +function getTokenValueRegularExpression() { + return new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', + ); +} + +/** + * It filters the values that matche the validation regular expression and returns the first items + * defined by SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT constant. + * @param suggestions Suggestions provided by the suggestions.value method of each instance of the + * search bar + * @returns + */ +function filterTokenValueSuggestion( + suggestions: QLOptionSuggestionEntityItemTyped[], +) { + return suggestions + .filter(({ label }: QLOptionSuggestionEntityItemTyped) => { + const re = getTokenValueRegularExpression(); + return re.test(label); + }) + .slice(0, SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT); +} + /** * Get the suggestions from the tokens * @param tokens @@ -407,11 +445,18 @@ export async function getSuggestions( operator => operator === lastToken.value, ) ? [ - ...( + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( await options.suggestions.value(undefined, { field, operatorCompare, - }) + }), ).map(mapSuggestionCreatorValue), ] : []), @@ -441,11 +486,18 @@ export async function getSuggestions( }, ] : []), - ...( + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( await options.suggestions.value(lastToken.formattedValue, { field, operatorCompare, - }) + }), ).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( ([conjunction, description]) => ({ @@ -685,12 +737,7 @@ function getOutput(input: string, options: OptionsQL) { * @returns */ function validateTokenValue(token: IToken): string | undefined { - const re = new RegExp( - // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', - ); + const re = getTokenValueRegularExpression(); const value = token.formattedValue ?? token.value; const match = value.match(re); @@ -959,7 +1006,7 @@ export const WQL = { error: validationStrict, }; - const onSearch = () => { + const onSearch = output => { if (output?.error) { params.setQueryLanguageOutput(state => ({ ...state, @@ -972,6 +1019,7 @@ export const WQL = { description: error, })), ), + isInvalid: true, }, })); } else { @@ -1024,7 +1072,7 @@ export const WQL = { : await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: item => { + onItemClick: currentInput => item => { // There is an error, clicking on the item does nothing if (item.type.iconType === 'alert') { return; @@ -1032,7 +1080,18 @@ export const WQL = { // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action - onSearch(); + // Get the tokens from the input + const tokens: ITokens = tokenizer(currentInput); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(currentInput, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); } else { // When the clicked item has another iconType const lastToken: IToken | undefined = getLastTokenDefined(tokens); @@ -1051,10 +1110,15 @@ export const WQL = { : item.label; } else { // add a whitespace for conjunction + // add a whitespace for grouping operator ) !/\s$/.test(input) && (item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType || - lastToken?.type === 'conjunction') && + lastToken?.type === 'conjunction' || + (item.type.iconType === + suggestionMappingLanguageTokenType.operator_group + .iconType && + item.label === ')')) && tokens.push({ type: 'whitespace', value: ' ', @@ -1134,7 +1198,19 @@ export const WQL = { // Define the handler when the a key is pressed while the input is focused onKeyPress: event => { if (event.key === 'Enter') { - onSearch(); + // Get the tokens from the input + const input = event.currentTarget.value; + const tokens: ITokens = tokenizer(input); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); } }, }, diff --git a/plugins/main/public/controllers/agent/components/agents-preview.js b/plugins/main/public/controllers/agent/components/agents-preview.js index 31ed799603..ef052c0745 100644 --- a/plugins/main/public/controllers/agent/components/agents-preview.js +++ b/plugins/main/public/controllers/agent/components/agents-preview.js @@ -197,7 +197,7 @@ export const AgentsPreview = compose( } removeFilters() { - this._isMount && this.setState({ agentTableFilters: [] }); + this._isMount && this.setState({ agentTableFilters: {} }); } showAgent(agent) { @@ -207,7 +207,7 @@ export const AgentsPreview = compose( filterAgentByStatus(status) { this._isMount && this.setState({ - agentTableFilters: [{ field: 'q', value: `status=${status}` }], + agentTableFilters: { q: `id!=000;status=${status}` }, }); } onRenderComplete() { diff --git a/plugins/main/public/controllers/agent/components/agents-table.js b/plugins/main/public/controllers/agent/components/agents-table.js index 09141ac9d6..853af6a40c 100644 --- a/plugins/main/public/controllers/agent/components/agents-table.js +++ b/plugins/main/public/controllers/agent/components/agents-table.js @@ -14,41 +14,37 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { - EuiBasicTable, EuiButton, - EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip, - EuiTitle, - EuiSpacer, - EuiCallOut, - EuiCheckboxGroup, - EuiIcon, + EuiIconTip, } from '@elastic/eui'; -import { getToasts } from '../../../kibana-services'; import { AppNavigate } from '../../../react-services/app-navigate'; import { GroupTruncate } from '../../../components/common/util'; -import { - WzSearchBar, - filtersToObject, -} from '../../../components/wz-search-bar'; -import { getAgentFilterValues } from '../../../controllers/management/components/management/groups/get-agents-filters-values'; import { WzButtonPermissions } from '../../../components/common/permissions/button'; import { formatUIDate } from '../../../react-services/time-service'; import { withErrorBoundary } from '../../../components/common/hocs'; import { API_NAME_AGENT_STATUS, - UI_LOGGER_LEVELS, UI_ORDER_AGENT_STATUS, AGENT_SYNCED_STATUS, + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, } from '../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; import { AgentStatus } from '../../../components/agents/agent_status'; import { AgentSynced } from '../../../components/agents/agent-synced'; +import { TableWzAPI } from '../../../components/common/tables'; +import { WzRequest } from '../../../react-services/wz-request'; +import { get as getLodash } from 'lodash'; + +const searchBarWQLOptions = { + implicitQuery: { + query: 'id!=000', + conjunction: ';', + }, +}; export const AgentsTable = withErrorBoundary( class AgentsTable extends Component { @@ -56,285 +52,43 @@ export const AgentsTable = withErrorBoundary( constructor(props) { super(props); this.state = { - agents: [], - isLoading: false, - pageIndex: 0, - pageSize: 15, - sortDirection: 'asc', - sortField: 'id', - totalItems: 0, - selectedItems: [], - allSelected: false, - purgeModal: false, - isFilterColumnOpen: false, - filters: sessionStorage.getItem('agents_preview_selected_options') - ? JSON.parse( - sessionStorage.getItem('agents_preview_selected_options'), - ) - : [], - }; - this.suggestions = [ - { - type: 'q', - label: 'status', - description: 'Filter by agent connection status', - operators: ['=', '!='], - values: UI_ORDER_AGENT_STATUS, - }, - { - type: 'q', - label: 'group_config_status', - description: 'Filter by agent synced configuration status', - operators: ['=', '!='], - values: [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED], - }, - { - type: 'q', - label: 'os.platform', - description: 'Filter by operating system platform', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('os.platform', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'ip', - description: 'Filter by agent IP address', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('ip', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'name', - description: 'Filter by agent name', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('name', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'id', - description: 'Filter by agent id', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('id', value, { q: 'id!=000' }), + filters: { + default: { q: 'id!=000' }, + ...(sessionStorage.getItem('wz-agents-overview-table-filter') + ? JSON.parse( + sessionStorage.getItem('wz-agents-overview-table-filter'), + ) + : {}), }, - { - type: 'q', - label: 'group', - description: 'Filter by agent group', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('group', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'node_name', - description: 'Filter by node name', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('node_name', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'manager', - description: 'Filter by manager', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('manager', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'version', - description: 'Filter by agent version', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('version', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'configSum', - description: 'Filter by agent config sum', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('configSum', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'mergedSum', - description: 'Filter by agent merged sum', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('mergedSum', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'dateAdd', - description: 'Filter by add date', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('dateAdd', value, { q: 'id!=000' }), - }, - { - type: 'q', - label: 'lastKeepAlive', - description: 'Filter by last keep alive', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('lastKeepAlive', value, { q: 'id!=000' }), - }, - ]; - this.downloadCsv.bind(this); + reloadTable: 0, + }; } - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this._isMount && - this.setState({ - pageIndex, - pageSize, - sortField, - sortDirection, - }); - }; - async componentDidMount() { this._isMount = true; - await this.getItems(); } componentWillUnmount() { this._isMount = false; - if (sessionStorage.getItem('agents_preview_selected_options')) { - sessionStorage.removeItem('agents_preview_selected_options'); + if (sessionStorage.getItem('wz-agents-overview-table-filter')) { + sessionStorage.removeItem('wz-agents-overview-table-filter'); } } async reloadAgents() { - await this.getItems(); + this.setState({ reloadTable: Date.now() }); await this.props.reload(); } - async componentDidUpdate(prevProps, prevState) { + async componentDidUpdate(prevProps) { if ( - !_.isEqual(prevState.filters, this.state.filters) || - prevState.pageIndex !== this.state.pageIndex || - prevState.pageSize !== this.state.pageSize || - prevState.sortField !== this.state.sortField || - prevState.sortDirection !== this.state.sortDirection - ) { - await this.getItems(); - } else if ( - !_.isEqual(prevProps.filters, this.props.filters) && - this.props.filters && - this.props.filters.length + // TODO: external filters + !_.isEqual(prevProps.filters, this.props.filters) ) { - this.setState({ filters: this.props.filters, pageIndex: 0 }); - this.props.removeFilters(); + this.setState({ filters: this.props.filters }); } } - async getItems() { - try { - this._isMount && this.setState({ isLoading: true }); - const selectFieldsList = this.defaultColumns - .filter(field => field.field != 'actions') - .map(field => field.field.replace('os_', 'os.')); // "os_name" subfield should be specified as 'os.name' - const selectFields = [ - ...selectFieldsList, - 'os.platform', - 'os.uname', - 'os.version', - ].join(','); // Add version and uname fields to render the OS icon and version in the table - - const rawAgents = await this.props.wzReq('GET', '/agents', { - params: { ...this.buildFilter(), select: selectFields }, - }); - const formatedAgents = ( - ((rawAgents || {}).data || {}).data || {} - ).affected_items.map(this.formatAgent.bind(this)); - - this._isMount && - this.setState({ - agents: formatedAgents, - totalItems: (((rawAgents || {}).data || {}).data || {}) - .total_affected_items, - isLoading: false, - }); - } catch (error) { - const options = { - context: `${AgentsTable.name}.getItems`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Could not get the agents list`, - }, - }; - getErrorOrchestrator().handleError(options); - this.setState({ isLoading: false }); - } - } - - buildFilter() { - const { pageIndex, pageSize, filters } = this.state; - - const filter = { - ...filtersToObject(filters), - offset: pageIndex * pageSize || 0, - limit: pageSize, - sort: this.buildSortFilter(), - }; - filter.q = !filter.q ? `id!=000` : `id!=000;${filter.q}`; - - return filter; - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - - const field = sortField === 'os_name' ? 'os.name,os.version' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + field; - } - - buildQFilter() { - const { q } = this.state; - return q === '' ? `id!=000` : `id!=000;${q}`; - } - - formatAgent(agent) { - const agentVersion = - agent.version !== undefined ? agent.version.split(' ')[1] : '-'; - const node_name = - agent.node_name && agent.node_name !== 'unknown' - ? agent.node_name - : '-'; - - return { - id: agent.id, - name: agent.name, - ip: agent.ip, - status: agent.status, - group_config_status: agent.group_config_status, - group: agent?.group || '-', - os_name: agent, - version: agentVersion, - node_name: node_name, - dateAdd: agent.dateAdd ? formatUIDate(agent.dateAdd) : '-', - lastKeepAlive: agent.lastKeepAlive - ? formatUIDate(agent.lastKeepAlive) - : '-', - actions: agent, - upgrading: false, - }; - } - actionButtonsRender(agent) { return (
@@ -405,110 +159,6 @@ export const AgentsTable = withErrorBoundary( ); } - reloadAgent = () => { - this._isMount && - this.setState({ - isLoading: true, - }); - this.props.reload(); - }; - - downloadCsv = () => { - const filters = this.buildFilter(); - const formatedFilters = Object.keys(filters) - .filter(field => !['limit', 'offset', 'sort'].includes(field)) - .map(field => ({ name: field, value: filters[field] })); - this.props.downloadCsv(formatedFilters); - }; - - openColumnsFilter = () => { - this.setState({ - isFilterColumnOpen: !this.state.isFilterColumnOpen, - }); - }; - - formattedButton() { - return ( - <> - - - Export formatted - - - - - - - - - - - ); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - - callOutRender() { - const { selectedItems, pageSize, allSelected, totalItems } = this.state; - - if (selectedItems.length === 0) { - return; - } else if (selectedItems.length === pageSize) { - return ( -
- - - - - { - this._isMount && - this.setState(prevState => ({ - allSelected: !prevState.allSelected, - })); - }} - > - {allSelected - ? `Clear all agents selection (${totalItems})` - : `Select all agents (${totalItems})`} - - - - - -
- ); - } - } - - getTableColumnsSelected() { - return ( - JSON.parse(window.localStorage.getItem('columnsSelectedTableAgent')) || - [] - ); - } - - setTableColumnsSelected(data) { - window.localStorage.setItem( - 'columnsSelectedTableAgent', - JSON.stringify(data), - ); - } - // Columns with the property truncateText: true won't wrap the text // This is added to prevent the wrap because of the table-layout: auto defaultColumns = [ @@ -517,18 +167,21 @@ export const AgentsTable = withErrorBoundary( name: 'ID', sortable: true, show: true, + searchable: true, }, { field: 'name', name: 'Name', sortable: true, show: true, + searchable: true, }, { field: 'ip', name: 'IP address', sortable: true, show: true, + searchable: true, }, { field: 'group', @@ -536,37 +189,64 @@ export const AgentsTable = withErrorBoundary( sortable: true, show: true, render: groups => (groups !== '-' ? this.renderGroups(groups) : '-'), + searchable: true, }, { - field: 'os_name', + field: 'os.name,os.version', + composeField: ['os.name', 'os.version'], name: 'Operating system', sortable: true, show: true, - render: this.addIconPlatformRender, + render: (field, agentData) => this.addIconPlatformRender(agentData), + searchable: true, }, { field: 'node_name', name: 'Cluster node', sortable: true, show: true, + searchable: true, }, { field: 'version', name: 'Version', sortable: true, show: true, + searchable: true, }, { field: 'dateAdd', - name: 'Registration date', + name: ( + + Registration date{' '} + + + ), sortable: true, show: false, + searchable: false, }, { field: 'lastKeepAlive', - name: 'Last keep alive', + name: ( + + Last keep alive{' '} + + + ), sortable: true, show: false, + searchable: false, }, { field: 'status', @@ -580,6 +260,7 @@ export const AgentsTable = withErrorBoundary( labelProps={{ className: 'hide-agent-status' }} /> ), + searchable: true, }, { field: 'group_config_status', @@ -587,6 +268,7 @@ export const AgentsTable = withErrorBoundary( sortable: true, show: false, render: synced => , + searchable: true, }, { align: 'right', @@ -594,133 +276,11 @@ export const AgentsTable = withErrorBoundary( field: 'actions', name: 'Actions', show: true, - render: agent => this.actionButtonsRender(agent), + render: (field, agentData) => this.actionButtonsRender(agentData), + searchable: false, }, ]; - columns() { - const selectedColumns = this.getTableColumnsSelected(); - - if (selectedColumns.length != 0) { - const newSelectedColumns = []; - selectedColumns.forEach(item => { - if (item.show) { - const column = this.defaultColumns.find( - column => column.field === item.field, - ); - newSelectedColumns.push(column); - } - }); - return newSelectedColumns; - } else { - const fieldColumns = this.defaultColumns.map(item => { - return { - field: item.field, - name: item.name, - show: item.show, - }; - }); - this.setTableColumnsSelected(fieldColumns); - return fieldColumns; - } - } - - headRender() { - const formattedButton = this.formattedButton(); - return ( -
- - - - - {!!this.state.totalItems && ( - -

Agents ({this.state.totalItems})

-
- )} -
-
-
- - this.props.addingNewAgent()} - > - Deploy new agent - - - {formattedButton} -
- -
- ); - } - - filterBarRender() { - return ( - - - - this.setState({ filters, pageIndex: 0 }) - } - placeholder='Filter or search agent' - /> - - - this.reloadAgents()} - > - Refresh - - - - ); - } - - selectColumnsRender() { - const columnsSelected = this.getTableColumnsSelected(); - - const onChange = optionId => { - let item = columnsSelected.find(item => item.field === optionId); - item.show = !item.show; - this.setTableColumnsSelected(columnsSelected); - this.forceUpdate(); - }; - - const options = () => { - return columnsSelected.map(item => { - return { - id: item.field, - label: item.name, - checked: item.show, - }; - }); - }; - - return this.state.isFilterColumnOpen ? ( - - - - - - ) : ( - '' - ); - } - tableRender() { const getRowProps = item => { const { id } = item; @@ -746,50 +306,201 @@ export const AgentsTable = withErrorBoundary( }; }; - const { - pageIndex, - pageSize, - totalItems, - agents, - sortField, - sortDirection, - isLoading, - } = this.state; - const columns = this.columns(); - const pagination = - totalItems > 15 - ? { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [15, 25, 50, 100], - } - : false; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - // The EuiBasicTable tableLayout is set to "auto" to improve the use of empty space in the component. // Previously the tableLayout is set to "fixed" with percentage width for each column, but the use of space was not optimal. // Important: If all the columns have the truncateText property set to true, the table cannot adjust properly when the viewport size is small. return ( - this.props.addingNewAgent()} + > + Deploy new agent + , + ]} + endpoint='/agents' + tableColumns={this.defaultColumns} + tableInitialSortingField='id' + tablePageSizeOptions={[10, 25, 50, 100]} + reload={this.state.reloadTable} + mapResponseItem={item => { + return { + ...item, + ...(item.ip ? { ip: item.ip } : { ip: '-' }), + ...(typeof item.dateAdd === 'string' + ? { dateAdd: formatUIDate(item.dateAdd) } + : { dateAdd: '-' }), + ...(typeof item.lastKeepAlive === 'string' + ? { lastKeepAlive: formatUIDate(item.lastKeepAlive) } + : { lastKeepAlive: '-' }), + ...(item.node_name !== 'unknown' + ? { node_name: item.node_name } + : { node_name: '-' }), + /* + The agent version contains the Wazuh word, this gets the string starting with + v + */ + ...(typeof item.version === 'string' + ? { version: item.version.match(/(v\d.+)/)?.[1] } + : { version: '-' }), + }; + }} rowProps={getRowProps} - cellProps={getCellProps} - noItemsMessage='No agents found' - {...(pagination && { pagination })} + filters={this.state.filters} + downloadCsv + showReload + showFieldSelector + searchTable + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + { + label: 'dateAdd', + description: 'filter by registration date', + }, + { label: 'id', description: 'filter by id' }, + { label: 'ip', description: 'filter by IP address' }, + { label: 'group', description: 'filter by group' }, + { + label: 'group_config_status', + description: 'filter by group configuration status', + }, + { + label: 'lastKeepAlive', + description: 'filter by last keep alive', + }, + { label: 'manager', description: 'filter by manager' }, + { label: 'name', description: 'filter by name' }, + { + label: 'node_name', + description: 'filter by cluster name', + }, + { + label: 'os.name', + description: 'filter by operating system name', + }, + { + label: 'os.platform', + description: 'filter by operating platform', + }, + { + label: 'os.version', + description: 'filter by operating system version', + }, + { label: 'status', description: 'filter by status' }, + { label: 'version', description: 'filter by version' }, + ]; + }, + value: async (currentValue, { field }) => { + try { + switch (field) { + case 'status': + return UI_ORDER_AGENT_STATUS.map(status => ({ + label: status, + })); + case 'group_config_status': + return [ + AGENT_SYNCED_STATUS.SYNCED, + AGENT_SYNCED_STATUS.NOT_SYNCED, + ].map(label => ({ + label, + })); + default: { + const response = await WzRequest.apiReq( + 'GET', + '/agents', + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + } + : { + q: `${searchBarWQLOptions.implicitQuery.query}`, + }), + }, + }, + ); + if (field === 'group') { + /* the group field is returned as an string[], + example: ['group1', 'group2'] + + Due the API request done to get the distinct values for the groups is + not returning the exepected values, as workaround, the values are + extracted in the frontend using the returned results. + + This API request to get the distint values of groups doesn't + return the unique values for the groups, else the unique combination + of groups. + */ + return response?.data?.data.affected_items + .map(item => getLodash(item, field)) + .flat() + .filter( + (item, index, array) => + array.indexOf(item) === index, + ) + .sort() + .map(group => ({ label: group })); + } + return response?.data?.data.affected_items.map( + item => ({ + label: getLodash(item, field), + }), + ); + } + } + } catch (error) { + return []; + } + }, + }, + validate: { + value: ({ formattedValue, value: rawValue }, { field }) => { + const value = formattedValue ?? rawValue; + if (value) { + if (['dateAdd', 'lastKeepAlive'].includes(field)) { + return /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2}(.\d{1,6})?Z?)?$/.test( + value, + ) + ? undefined + : `"${value}" is not a expected format. Valid formats: YYYY-MM-DD, YYYY-MM-DD HH:mm:ss, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ.`; + } + } + }, + }, + }} + searchBarProps={{ + buttonsRender: () => ( + this.reloadAgents()} + > + Refresh + + ), + }} + saveStateStorage={{ + system: 'localStorage', + key: 'wz-agents-overview-table', + }} + tableProps={{ + tableLayout: 'auto', + getCellProps, + }} /> @@ -797,25 +508,16 @@ export const AgentsTable = withErrorBoundary( } filterGroupBadge = group => { - const { filters } = this.state; - let auxFilters = filters.map( - filter => filter.value.match(/group=(.*S?)/)[1], - ); - if (filters.length > 0) { - !auxFilters.includes(group) - ? this.setState({ - filters: [...filters, { field: 'q', value: `group=${group}` }], - }) - : false; - } else { - this.setState({ - filters: [...filters, { field: 'q', value: `group=${group}` }], - }); - } + this.setState({ + filters: { + default: { q: 'id!=000' }, + q: `id!=000;group=${group}`, + }, + }); }; renderGroups(groups) { - return ( + return Array.isArray(groups) ? ( - ); + ) : undefined; } render() { - const title = this.headRender(); - const filter = this.filterBarRender(); - const selectColumnsRender = this.selectColumnsRender(); const table = this.tableRender(); - const callOut = this.callOutRender(); - let renderPurgeModal, loadItems; return (
- {filter} - - - {title} - {loadItems} - {callOut} - {selectColumnsRender} - {table} - {renderPurgeModal} - + {table}
); } 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/cdblists/components/cdblists-table.tsx b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx index d49792d817..11f4fc4854 100644 --- a/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx +++ b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx @@ -13,12 +13,22 @@ import React, { useState } from 'react'; import { TableWzAPI } from '../../../../../../components/common/tables'; import { getToasts } from '../../../../../../kibana-services'; -import { resourceDictionary, ResourcesConstants, ResourcesHandler } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesConstants, + ResourcesHandler, +} from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + UI_LOGGER_LEVELS, +} from '../../../../../../../common/constants'; -import { SECTION_CDBLIST_SECTION, SECTION_CDBLIST_KEY } from '../../common/constants'; +import { + SECTION_CDBLIST_SECTION, + SECTION_CDBLIST_KEY, +} from '../../common/constants'; import CDBListsColumns from './columns'; import { withUserPermissions } from '../../../../../../components/common/hocs/withUserPermissions'; @@ -29,43 +39,50 @@ import { AddNewFileButton, AddNewCdbListButton, UploadFilesButton, -} from '../../common/actions-buttons' +} from '../../common/actions-buttons'; +import { WzRequest } from '../../../../../../react-services'; + +const searchBarWQLOptions = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/lists', + label: 'Custom lists', + }, + ], +}; function CDBListsTable(props) { - const [filters, setFilters] = useState([]); const [showingFiles, setShowingFiles] = useState(false); const [tableFootprint, setTableFootprint] = useState(0); const resourcesHandler = new ResourcesHandler(ResourcesConstants.LISTS); - const updateFilters = (filters) => { - setFilters(filters); - } - const toggleShowFiles = () => { setShowingFiles(!showingFiles); - } - + }; const getColumns = () => { const cdblistsColumns = new CDBListsColumns({ removeItems: removeItems, state: { section: SECTION_CDBLIST_KEY, - defaultItems: [] - }, ...props + defaultItems: [], + }, + ...props, }).columns; const columns = cdblistsColumns[SECTION_CDBLIST_KEY]; return columns; - } + }; /** * Columns and Rows properties */ - const getRowProps = (item) => { + const getRowProps = item => { const { id, name } = item; - const getRequiredPermissions = (item) => { + const getRequiredPermissions = item => { const { permissionResource } = resourceDictionary[SECTION_CDBLIST_KEY]; return [ { @@ -80,17 +97,17 @@ function CDBListsTable(props) { className: 'customRowClass', onClick: !WzUserPermissions.checkMissingUserPermissions( getRequiredPermissions(item), - props.userPermissions + props.userPermissions, ) - ? async (ev) => { - const result = await resourcesHandler.getFileContent(item.filename); - const file = { - name: item.filename, - content: result, - path: item.relative_dirname, - }; - updateListContent(file); - } + ? async ev => { + const result = await resourcesHandler.getFileContent(item.filename); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + updateListContent(file); + } : undefined, }; }; @@ -98,13 +115,13 @@ function CDBListsTable(props) { /** * Remove files method */ - const removeItems = async (items) => { + const removeItems = async items => { try { const results = items.map(async (item, i) => { await resourcesHandler.deleteFile(item.filename || item.name); }); - Promise.all(results).then((completed) => { + Promise.all(results).then(completed => { setTableFootprint(Date.now()); getToasts().add({ color: 'success', @@ -126,7 +143,7 @@ function CDBListsTable(props) { }; getErrorOrchestrator().handleError(options); } - } + }; const { updateRestartClusterManager, updateListContent } = props; const columns = getColumns(); @@ -153,38 +170,63 @@ function CDBListsTable(props) { { updateRestartClusterManager && updateRestartClusterManager() }} + onSuccess={() => { + updateRestartClusterManager && updateRestartClusterManager(); + }} />, ]; - - return ( -
+
{ + try { + const response = await WzRequest.apiReq('GET', '/lists', { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} endpoint={'/lists'} isExpandable={true} rowProps={getRowProps} - downloadCsv={true} - showReload={true} - filters={filters} - onFiltersChange={updateFilters} + downloadCsv + showReload tablePageSizeOptions={[10, 25, 50, 100]} />
); - } - -export default compose( - withUserPermissions -)(CDBListsTable); +export default compose(withUserPermissions)(CDBListsTable); diff --git a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js index d0a9b448ae..49db124470 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js +++ b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-labels.js @@ -27,18 +27,18 @@ import { webDocumentationLink } from '../../../../../../../common/services/web_d const columns = [ { field: 'key', name: 'Label key' }, { field: 'value', name: 'Label value' }, - { field: 'hidden', name: 'Hidden' } + { field: 'hidden', name: 'Hidden' }, ]; const helpLinks = [ { text: 'Agent labels', - href: webDocumentationLink('user-manual/capabilities/labels.html') + href: webDocumentationLink('user-manual/agents/labels.html'), }, { text: 'Labels reference', - href: webDocumentationLink('user-manual/reference/ossec-conf/labels.html') - } + href: webDocumentationLink('user-manual/reference/ossec-conf/labels.html'), + }, ]; class WzConfigurationAlertsLabels extends Component { @@ -49,71 +49,34 @@ class WzConfigurationAlertsLabels extends Component { const { currentConfig, agent, wazuhNotReadyYet } = this.props; return ( - {currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] && - isString( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] - ) && ( + {currentConfig['agent-labels'] && + isString(currentConfig['agent-labels']) && ( )} - {currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] && - !isString( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] - ) && - !hasSize( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ].labels - ) && } + {currentConfig['agent-labels'] && + !isString(currentConfig['agent-labels']) && + !hasSize(currentConfig['agent-labels'].labels) && ( + + )} {wazuhNotReadyYet && - (!currentConfig || - !currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ]) && } - {currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] && - !isString( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ] - ) && - hasSize( - currentConfig[ - agent && agent.id !== '000' ? 'agent-labels' : 'analysis-labels' - ].labels - ) ? ( + (!currentConfig || !currentConfig['agent-labels']) && ( + + )} + {currentConfig['agent-labels'] && + !isString(currentConfig['agent-labels']) && + hasSize(currentConfig['agent-labels'].labels) ? ( ) : null} @@ -123,7 +86,7 @@ class WzConfigurationAlertsLabels extends Component { } const mapStateToProps = state => ({ - wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet + wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, }); export default connect(mapStateToProps)(WzConfigurationAlertsLabels); @@ -132,15 +95,15 @@ const sectionsAgent = [{ component: 'agent', configuration: 'labels' }]; export const WzConfigurationAlertsLabelsAgent = compose( connect(mapStateToProps), - withWzConfig(sectionsAgent) + withWzConfig(sectionsAgent), )(WzConfigurationAlertsLabels); WzConfigurationAlertsLabels.propTypes = { // currentConfig: PropTypes.object.isRequired, - wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) + wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; WzConfigurationAlertsLabelsAgent.propTypes = { // currentConfig: PropTypes.object.isRequired, - wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) + wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; diff --git a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js index 704a38befc..c72e0b4cca 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js +++ b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts.js @@ -14,7 +14,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import WzTabSelector, { - WzTabSelectorTab + WzTabSelectorTab, } from '../util-components/tab-selector'; import withWzConfig from '../util-hocs/wz-config'; import WzConfigurationAlertsGeneral from './alerts-general'; @@ -34,19 +34,19 @@ class WzConfigurationAlerts extends Component { return ( - + - + - + - + - + @@ -57,22 +57,22 @@ class WzConfigurationAlerts extends Component { const sections = [ { component: 'analysis', configuration: 'alerts' }, - { component: 'analysis', configuration: 'labels' }, + { component: 'agent', configuration: 'labels' }, { component: 'mail', configuration: 'alerts' }, { component: 'monitor', configuration: 'reports' }, - { component: 'csyslog', configuration: 'csyslog' } + { component: 'csyslog', configuration: 'csyslog' }, ]; const mapStateToProps = state => ({ - wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet + wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, }); WzConfigurationAlerts.propTypes = { // currentConfig: PropTypes.object.isRequired, - wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) + wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; export default compose( withWzConfig(sections), - connect(mapStateToProps) + connect(mapStateToProps), )(WzConfigurationAlerts); diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx b/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx index 32ee7ef1b2..727dc9b4fb 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx @@ -95,6 +95,12 @@ export default class DecodersColumns { align: 'left', sortable: true, }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + }, { name: 'Actions', align: 'left', diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index fbd4ed6999..d81f0e825c 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -1,44 +1,141 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../../common/constants'; import { WzRequest } from '../../../../../../react-services/wz-request'; -const decodersItems = [ - { - type: 'params', - label: 'filename', - description: 'Filters the decoders by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/decoders/files', filter); - return (((result || {}).data || {}).data || {}).affected_items.map((item) => { return item.filename }); - }, +const decodersItems = { + field(currentValue) { + return [ + { label: 'details.order', description: 'filter by program name' }, + { label: 'details.program_name', description: 'filter by program name' }, + { label: 'filename', description: 'filter by filename' }, + { label: 'name', description: 'filter by name' }, + { label: 'relative_dirname', description: 'filter by relative path' }, + ]; }, - { - type: 'params', - label: 'relative_dirname', - description: 'Path of the decoders files.', - values: async () => { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'decoder_dir' + value: async (currentValue, { field }) => { + try { + switch (field) { + case 'details.order': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return ( + result?.data?.data?.affected_items + // There are some affected items that doesn't return any value for the selected property + ?.filter(item => typeof item?.details?.order === 'string') + ?.map(item => ({ + label: item?.details?.order, + })) + ); + } + case 'details.program_name': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + // FIX: this breaks the search bar component because returns a non-string value. + return result?.data?.data?.affected_items + ?.filter(item => typeof item?.details?.program_name === 'string') + .map(item => ({ + label: item?.details?.program_name, + })); + } + case 'filename': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } + case 'name': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); } + case 'relative_dirname': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); + } + default: { + return []; + } + } + } catch (error) { + return []; + } + }, +}; + +const decodersFiles = { + field(currentValue) { + return [ + { label: 'filename', description: 'filter by filename' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + ]; + }, + value: async (currentValue, { field }) => { + try { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders/files', { + params: filter, }); - return (((result || {}).data || {}).data || {}).affected_items[0].ruleset.decoder_dir; + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; } }, - { - type: 'params', - label: 'status', - description: 'Filters the decoders by status.', - values: ['enabled', 'disabled'] - } -]; +}; const apiSuggestsItems = { items: decodersItems, - files: [], + files: decodersFiles, }; -export default apiSuggestsItems; \ No newline at end of file +export default apiSuggestsItems; diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx index 10930cc7bc..1cb105449b 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx @@ -39,41 +39,67 @@ import { import apiSuggestsItems from './decoders-suggestions'; +const searchBarWQLOptions = { + searchTermFields: [ + 'details.order', + 'details.program_name', + 'filename', + 'name', + 'relative_dirname', + ], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/decoders', + label: 'Custom decoders', + }, + ], +}; + +const searchBarWQLOptionsFiles = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], +}; + /*************************************** * Render tables */ const FilesTable = ({ actionButtons, - buttonOptions, columns, searchBarSuggestions, filters, - updateFilters, reload, }) => ( ); const DecodersFlyoutTable = ({ actionButtons, - buttonOptions, columns, searchBarSuggestions, getRowProps, @@ -88,20 +114,21 @@ const DecodersFlyoutTable = ({ <> {isFlyoutVisible && ( @@ -134,15 +161,6 @@ export default compose(withUserPermissions)(function DecodersTable({ const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - // Table custom filter options - const buttonOptions = [ - { - label: 'Custom decoders', - field: 'relative_dirname', - value: 'etc/decoders', - }, - ]; - const updateFilters = filters => { setFilters(filters); }; @@ -278,17 +296,14 @@ export default compose(withUserPermissions)(function DecodersTable({ {showingFiles ? ( ) : ( - this.setNewFiltersAndBack([ - { field: 'filename', value: file }, - ]) + this.setNewFiltersAndBack({ q: `filename=${file}` }) } >  {file} @@ -157,9 +155,7 @@ export default class WzDecoderInfo extends Component { - this.setNewFiltersAndBack([ - { field: 'relative_dirname', value: path }, - ]) + this.setNewFiltersAndBack({ q: `relative_dirname=${path}` }) } >  {path} @@ -359,7 +355,7 @@ export default class WzDecoderInfo extends Component { {currentDecoder?.filename && ( { - if (!this.props.state.isProcessing) { - this.props.updateLoadingStatus(false); - clearInterval(this.refreshTimeoutId); - } - }, 100); - } showManageAgents() { const { itemDetail } = this.props.state; @@ -105,117 +42,6 @@ class WzGroupsActionButtonsAgents extends Component { this.props.updateShowAddAgents(true); } - closePopover() { - this.setState({ - isPopoverOpen: false, - msg: false, - newGroupName: '', - }); - } - - clearGroupName() { - this.setState({ - newGroupName: '', - }); - } - - onChangeNewGroupName = (e) => { - this.setState({ - newGroupName: e.target.value, - }); - }; - - /** - * Looking for the input element to bind the keypress event, once the input is found the interval is clear - */ - bindEnterToInput() { - try { - const interval = setInterval(async () => { - const input = document.getElementsByClassName('groupNameInput'); - if (input.length) { - const i = input[0]; - if (!i.onkeypress) { - i.onkeypress = async (e) => { - if (e.which === 13) { - await this.createGroup(); - } - }; - } - clearInterval(interval); - } - }, 150); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsAgents.name}.bindEnterToInput`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.message || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - async createGroup() { - try { - this.props.updateLoadingStatus(true); - await this.groupsHandler.saveGroup(this.state.newGroupName); - this.showToast('success', 'Success', 'The group has been created successfully', 2000); - this.clearGroupName(); - - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.closePopover(); - } catch (error) { - this.props.updateLoadingStatus(false); - throw new Error(error); - } - } - - /** - * Generates a CSV - */ - async generateCsv() { - try { - this.setState({ generatingCsv: true }); - const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store - await this.exportCsv(`/groups/${this.props.state.itemDetail.name}/agents`, filters, 'Groups'); - this.showToast( - 'success', - 'Success', - 'CSV. Your download should begin automatically...', - 2000 - ); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsAgents.name}.generateCsv`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error when exporting the CSV file: ${error.message || error}`, - }, - }; - getErrorOrchestrator().handleError(options); - } - this.setState({ generatingCsv: false }); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - render() { // Add new group button const manageAgentsButton = ( @@ -237,30 +63,11 @@ class WzGroupsActionButtonsAgents extends Component { type="group" /> ); - // Export button - const exportCSVButton = ( - await this.generateCsv()} - isLoading={this.state.generatingCsv} - > - Export formatted - - ); - - // Refresh - const refreshButton = ( - await this.refresh()}> - Refresh - - ); return ( {manageAgentsButton} {exportPDFButton} - {exportCSVButton} - {refreshButton} ); } @@ -274,10 +81,7 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), updateShowAddAgents: (showAddAgents) => dispatch(updateShowAddAgents(showAddAgents)), - updateReload: () => dispatch(updateReload()), }; }; diff --git a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js index f32424dc95..2236648697 100644 --- a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js +++ b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-files.js @@ -16,88 +16,24 @@ import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { connect } from 'react-redux'; import { - updateLoadingStatus, - updateIsProcessing, updateFileContent, } from '../../../../../redux/actions/groupsActions'; -import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; import { ExportConfiguration } from '../../../../agent/components/export-configuration'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { ReportingService } from '../../../../../react-services/reporting'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzGroupsActionButtonsFiles extends Component { - _isMounted = false; - constructor(props) { super(props); this.reportingService = new ReportingService(); - this.state = { - generatingCsv: false, - isPopoverOpen: false, - newGroupName: '', - }; - this.exportCsv = exportCsv; - this.groupsHandler = GroupsHandler; this.refreshTimeoutId = null; } - componentDidMount() { - this._isMounted = true; - if (this._isMounted) this.bindEnterToInput(); - } - - componentDidUpdate() { - this.bindEnterToInput(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Refresh the items - */ - async refresh() { - try { - this.props.updateIsProcessing(true); - this.onRefreshLoading(); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsFiles.name}.refresh`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - onRefreshLoading() { - clearInterval(this.refreshTimeoutId); - - this.props.updateLoadingStatus(true); - this.refreshTimeoutId = setInterval(() => { - if (!this.props.state.isProcessing) { - this.props.updateLoadingStatus(false); - clearInterval(this.refreshTimeoutId); - } - }, 100); - } - autoFormat = (xml) => { var reg = /(>)\s*(<)(\/*)/g; var wsexp = / *(.*) +\n/g; @@ -173,117 +109,6 @@ class WzGroupsActionButtonsFiles extends Component { this.props.updateFileContent(file); } - closePopover() { - this.setState({ - isPopoverOpen: false, - msg: false, - newGroupName: '', - }); - } - - clearGroupName() { - this.setState({ - newGroupName: '', - }); - } - - onChangeNewGroupName = (e) => { - this.setState({ - newGroupName: e.target.value, - }); - }; - - /** - * Looking for the input element to bind the keypress event, once the input is found the interval is clear - */ - bindEnterToInput() { - try { - const interval = setInterval(async () => { - const input = document.getElementsByClassName('groupNameInput'); - if (input.length) { - const i = input[0]; - if (!i.onkeypress) { - i.onkeypress = async (e) => { - if (e.which === 13) { - await this.createGroup(); - } - }; - } - clearInterval(interval); - } - }, 150); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsFiles.name}.bindEnterToInput`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - async createGroup() { - try { - this.props.updateLoadingStatus(true); - await this.groupsHandler.saveGroup(this.state.newGroupName); - this.showToast('success', 'Success', 'The group has been created successfully', 2000); - this.clearGroupName(); - - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.closePopover(); - } catch (error) { - this.props.updateLoadingStatus(false); - throw new Error(error); - } - } - - /** - * Generates a CSV - */ - async generateCsv() { - try { - this.setState({ generatingCsv: true }); - const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store - await this.exportCsv(`/groups/${this.props.state.itemDetail.name}/files`, filters, 'Groups'); - this.showToast( - 'success', - 'Success', - 'CSV. Your download should begin automatically...', - 2000 - ); - } catch (error) { - const options = { - context: `${WzGroupsActionButtonsFiles.name}.generateCsv`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error when exporting the CSV file: ${error.message || error}`, - }, - }; - getErrorOrchestrator().handleError(options); - } - this.setState({ generatingCsv: false }); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - render() { // Add new group button const groupConfigurationButton = ( @@ -313,30 +138,11 @@ class WzGroupsActionButtonsFiles extends Component { type="group" /> ); - // Export button - const exportCSVButton = ( - await this.generateCsv()} - isLoading={this.state.generatingCsv} - > - Export formatted - - ); - - // Refresh - const refreshButton = ( - await this.refresh()}> - Refresh - - ); return ( {groupConfigurationButton} {exportPDFButton} - {exportCSVButton} - {refreshButton} ); } @@ -350,8 +156,6 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), updateFileContent: (content) => dispatch(updateFileContent(content)), }; }; diff --git a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js index d286ab652c..9e6b7cc061 100644 --- a/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js +++ b/plugins/main/public/controllers/management/components/management/groups/actions-buttons-main.js @@ -9,28 +9,18 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; // Eui components import { EuiFlexItem, - EuiButtonEmpty, EuiPopover, EuiFormRow, EuiFieldText, - EuiSpacer, EuiFlexGroup, - EuiButton, } from '@elastic/eui'; -import { connect } from 'react-redux'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; -import { - updateLoadingStatus, - updateIsProcessing, -} from '../../../../../redux/actions/groupsActions'; - -import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; import { getToasts } from '../../../../../kibana-services'; import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; @@ -45,14 +35,10 @@ class WzGroupsActionButtons extends Component { super(props); this.state = { - generatingCsv: false, isPopoverOpen: false, newGroupName: '', }; - this.exportCsv = exportCsv; - this.groupsHandler = GroupsHandler; - this.refreshTimeoutId = null; } componentDidMount() { @@ -68,41 +54,6 @@ class WzGroupsActionButtons extends Component { this._isMounted = false; } - /** - * Refresh the items - */ - async refresh() { - try { - this.props.updateIsProcessing(true); - this.onRefreshLoading(); - } catch (error) { - const options = { - context: `${WzGroupsActionButtons.name}.refresh`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.message || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - onRefreshLoading() { - clearInterval(this.refreshTimeoutId); - - this.props.updateLoadingStatus(true); - this.refreshTimeoutId = setInterval(() => { - if (!this.props.state.isProcessing) { - this.props.updateLoadingStatus(false); - clearInterval(this.refreshTimeoutId); - } - }, 100); - } - togglePopover() { if (this.state.isPopoverOpen) { this.closePopover(); @@ -169,13 +120,11 @@ class WzGroupsActionButtons extends Component { async createGroup() { try { if (this.isOkNameGroup(this.state.newGroupName)) { - this.props.updateLoadingStatus(true); - await this.groupsHandler.saveGroup(this.state.newGroupName); + await GroupsHandler.saveGroup(this.state.newGroupName); this.showToast('success', 'Success', 'The group has been created successfully', 2000); this.clearGroupName(); - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); + this.props.reloadTable(); this.closePopover(); } } catch (error) { @@ -192,42 +141,10 @@ class WzGroupsActionButtons extends Component { }, }; getErrorOrchestrator().handleError(options); - this.props.updateLoadingStatus(false); throw new Error(error); } } - /** - * Generates a CSV - */ - async generateCsv() { - try { - this.setState({ generatingCsv: true }); - const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store - await this.exportCsv('/groups', filters, 'Groups'); - this.showToast( - 'success', - 'Success', - 'CSV. Your download should begin automatically...', - 2000 - ); - } catch (error) { - const options = { - context: `${WzGroupsActionButtons.name}.generateCsv`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error when exporting the CSV file: ${error.message || error}`, - }, - }; - getErrorOrchestrator().handleError(options); - } - this.setState({ generatingCsv: false }); - } - showToast = (color, title, text, time) => { getToasts().add({ color: color, @@ -255,78 +172,41 @@ class WzGroupsActionButtons extends Component { ); - // Export button - const exportButton = ( - await this.generateCsv()} - isLoading={this.state.generatingCsv} - > - Export formatted - - ); - - // Refresh - const refreshButton = ( - await this.refresh()}> - Refresh - - ); - return ( - - - this.closePopover()} - > - - - - - - - - { - await this.createGroup(); - }} - > - Save new group - - - - - - {exportButton} - {refreshButton} - + this.closePopover()} + > + + + + + + + + { + await this.createGroup(); + }} + > + Save new group + + + + ); } } -const mapStateToProps = (state) => { - return { - state: state.groupsReducers, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(WzGroupsActionButtons); +export default WzGroupsActionButtons; diff --git a/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js b/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js index e7c4a11aba..f1b6225e9e 100644 --- a/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js +++ b/plugins/main/public/controllers/management/components/management/groups/group-agents-table.js @@ -9,7 +9,8 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { connect } from 'react-redux'; import GroupsHandler from './utils/groups-handler'; @@ -25,171 +26,80 @@ import { updateSortFieldAgents, updateReload, } from '../../../../../redux/actions/groupsActions'; -import { EuiCallOut } from '@elastic/eui'; -import { getAgentFilterValues } from './get-agents-filters-values'; import { TableWzAPI } from '../../../../../components/common/tables'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { WzButtonPermissionsModalConfirm } from '../../../../../components/common/buttons'; import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, UI_LOGGER_LEVELS, UI_ORDER_AGENT_STATUS, } from '../../../../../../common/constants'; +import { get as getLodash } from 'lodash'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; +import { AgentStatus } from '../../../../../components/agents/agent_status'; +import { WzRequest } from '../../../../../react-services'; class WzGroupAgentsTable extends Component { _isMounted = false; constructor(props) { super(props); - this.suggestions = [ - { - type: 'q', - label: 'status', - description: 'Filter by agent connection status', - operators: ['=', '!='], - values: UI_ORDER_AGENT_STATUS, - }, - { - type: 'q', - label: 'os.platform', - description: 'Filter by operating system platform', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('os.platform', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'ip', - description: 'Filter by agent IP address', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('ip', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'name', - description: 'Filter by agent name', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('name', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'id', - description: 'Filter by agent id', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('id', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'node_name', - description: 'Filter by node name', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('node_name', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'manager', - description: 'Filter by manager', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('manager', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'version', - description: 'Filter by agent version', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('version', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'configSum', - description: 'Filter by agent config sum', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('configSum', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - { - type: 'q', - label: 'mergedSum', - description: 'Filter by agent merged sum', - operators: ['=', '!='], - values: async value => - getAgentFilterValues('mergedSum', value, { - q: `group=${this.props.state.itemDetail.name}`, - }), - }, - //{ type: 'q', label: 'dateAdd', description: 'Filter by add date', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('dateAdd', value, {q: `group=${this.props.state.itemDetail.name}`})}, - //{ type: 'q', label: 'lastKeepAlive', description: 'Filter by last keep alive', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('lastKeepAlive', value, {q: `group=${this.props.state.itemDetail.name}`})}, - ]; - this.groupsHandler = GroupsHandler; this.columns = [ { field: 'id', name: 'Id', align: 'left', + searchable: true, sortable: true, }, { field: 'name', name: 'Name', align: 'left', + searchable: true, sortable: true, }, { field: 'ip', name: 'IP address', - sortable: true, - show: true, - }, - { - field: 'status', - name: 'Status', align: 'left', + searchable: true, sortable: true, }, { - field: 'os.name', - name: 'Operating system name', + field: 'os.name,os.version', + composeField: ['os.name', 'os.version'], + name: 'Operating system', align: 'left', + searchable: true, sortable: true, + render: (field, agentData) => this.addIconPlatformRender(agentData), }, { - field: 'os.version', - name: 'Operating system version', + field: 'version', + name: 'Version', align: 'left', + searchable: true, sortable: true, }, { - field: 'version', - name: 'Version', + field: 'status', + name: 'Status', align: 'left', + searchable: true, sortable: true, + render: status => ( + + ), }, { name: 'Actions', align: 'left', + searchable: false, render: item => { return (
@@ -251,20 +161,105 @@ class WzGroupAgentsTable extends Component { }, }, ]; + + this.searchBar = { + wql: { + suggestionFields: [ + { label: 'id', description: `filter by ID` }, + { label: 'ip', description: `filter by IP address` }, + { label: 'name', description: `filter by Name` }, + { label: 'os.name', description: `filter by Operating system name` }, + { + label: 'os.version', + description: `filter by Operating system version`, + }, + { label: 'status', description: `filter by Status` }, + { label: 'version', description: `filter by Version` }, + ], + }, + }; } componentWillUnmount() { this._isMounted = false; } + + addIconPlatformRender(agent) { + let icon = ''; + const os = agent?.os || {}; + + if ((os?.uname || '').includes('Linux')) { + icon = 'linux'; + } else if (os?.platform === 'windows') { + icon = 'windows'; + } else if (os?.platform === 'darwin') { + icon = 'apple'; + } + const os_name = `${agent?.os?.name || ''} ${agent?.os?.version || ''}`; + + return ( + + + + {' '} + {os_name.trim() || '-'} + + ); + } + render() { const { error } = this.props.state; + const groupName = this.props.state?.itemDetail?.name; + const searchBarSuggestionsFields = this.searchBar.wql.suggestionFields; if (!error) { return ( searchBarSuggestionsFields, + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq( + 'GET', + `/groups/${groupName}/agents`, + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } catch (error) { + return []; + } + }, + }, + }} + mapResponseItem={item => ({ + ...item, + ...(item.ip ? { ip: item.ip } : { ip: '-' }), + ...(typeof item.version === 'string' + ? { version: item.version.match(/(v\d.+)/)?.[1] } + : { version: '-' }), + })} + showReload + downloadCsv={`agents-group-${groupName}`} reload={this.props.state.reload} searchTable={true} tableProps={{ tableLayout: 'auto' }} @@ -289,9 +284,7 @@ class WzGroupAgentsTable extends Component { this.props.updateLoadingStatus(true); try { await Promise.all( - items.map(item => - this.groupsHandler.deleteAgent(item.id, itemDetail.name), - ), + items.map(item => GroupsHandler.deleteAgent(item.id, itemDetail.name)), ); this.props.updateIsProcessing(true); this.props.updateLoadingStatus(false); diff --git a/plugins/main/public/controllers/management/components/management/groups/group-detail.js b/plugins/main/public/controllers/management/components/management/groups/group-detail.js index 99ca3e3fc9..bbfc161651 100644 --- a/plugins/main/public/controllers/management/components/management/groups/group-detail.js +++ b/plugins/main/public/controllers/management/components/management/groups/group-detail.js @@ -83,40 +83,13 @@ class WzGroupDetail extends Component { renderAgents() { return ( - - - - - From here you can list and manage your agents - - - - - - - - - + ); } renderFiles() { return ( - - - - - From here you can list and see your group files, also, you can - edit the group configuration - - - - - - - - - + ); } @@ -142,7 +115,7 @@ class WzGroupDetail extends Component { - +

{itemDetail.name}

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

Groups

-
-
-
-
- -
- - - - From here you can list and check your groups, its agents and - files. - - - - - - - - + [ + { label: 'name', description: 'filter by name' }, + { label: 'count', description: 'filter by count' }, + { + label: 'configSum', + description: 'filter by configuration checksum', + }, + ], + value: async (currentValue, { field }) => { + try { + const response = await WzRequest.apiReq('GET', '/groups', { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { q: `${field}~${currentValue}` } + : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} + rowProps={getRowProps} + endpoint={'/groups'} + downloadCsv={true} + showReload={true} + tablePageSizeOptions={[10, 25, 50, 100]} + />
+ {this.props.state.showModal ? ( + + this.props.updateShowModal(false)} + onConfirm={() => { + this.removeItems(this.props.state.itemList); + this.props.updateShowModal(false); + }} + cancelButtonText='Cancel' + confirmButtonText='Delete' + defaultFocusedButton='cancel' + buttonColor='danger' + > + + ) : null}
); } @@ -71,14 +339,22 @@ export class WzGroupsOverview extends Component { const mapStateToProps = state => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; +const mapDispatchToProps = dispatch => ({ + updateShowModal: showModal => dispatch(updateShowModal(showModal)), + updateListItemsForRemove: itemList => + dispatch(updateListItemsForRemove(itemList)), + updateGroupDetail: itemDetail => dispatch(updateGroupDetail(itemDetail)), + updateFileContent: content => dispatch(updateFileContent(content)), +}); export default compose( - withUserAuthorizationPrompt([{action: 'group:read', resource: 'group:id:*'}]), - connect( - mapStateToProps - ), + withUserAuthorizationPrompt([ + { action: 'group:read', resource: 'group:id:*' }, + ]), + connect(mapStateToProps, mapDispatchToProps), + withUserPermissions, )(WzGroupsOverview); diff --git a/plugins/main/public/controllers/management/components/management/groups/groups-table.js b/plugins/main/public/controllers/management/components/management/groups/groups-table.js deleted file mode 100644 index 421fcbe1ad..0000000000 --- a/plugins/main/public/controllers/management/components/management/groups/groups-table.js +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Wazuh app - React component for groups main table. - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component, Fragment } from 'react'; -import { - EuiBasicTable, - EuiCallOut, - EuiOverlayMask, - EuiConfirmModal, - EuiSpacer, -} from '@elastic/eui'; - -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; -import { WzSearchBar, filtersToObject } from '../../../../../components/wz-search-bar'; -import { withUserPermissions } from '../../../../../components/common/hocs/withUserPermissions'; -import { WzUserPermissions } from '../../../../../react-services/wz-user-permissions'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; - - -import { - updateLoadingStatus, - updateFileContent, - updateIsProcessing, - updatePageIndex, - updateShowModal, - updateListItemsForRemove, - updateSortDirection, - updateSortField, - updateGroupDetail, -} from '../../../../../redux/actions/groupsActions'; - -import GroupsColums from './utils/columns-main'; - -class WzGroupsTable extends Component { - _isMounted = false; - - suggestions = []; //TODO: Fix suggestions without q search for API 4.0 - - constructor(props) { - super(props); - this.state = { - items: [], - pageSize: 10, - totalItems: 0, - filters: [], - }; - - this.groupsHandler = GroupsHandler; - } - - async componentDidMount() { - this._isMounted = true; - await this.getItems(); - } - - shouldComponentUpdate(nextProps, nextState) { - const { items, filters } = this.state; - const { isProcessing, showModal, isLoading } = this.props.state; - if (showModal !== nextProps.state.showModal) return true; - if (isProcessing !== nextProps.state.isProcessing) return true; - if (JSON.stringify(items) !== JSON.stringify(nextState.items)) return true; - if (JSON.stringify(filters) !== JSON.stringify(nextState.filters)) return true; - if (isLoading !== nextProps.state.isLoading) return true; - return false; - } - - async componentDidUpdate(prevProps, prevState) { - const { filters } = this.state; - if ((JSON.stringify(filters) !== JSON.stringify(prevState.filters)) || - /** - Is verifying that isProcessing is true and that it has changed its value, - since in the shouldComponentUpdate it is making it re-execute several times - each time a state changes, regardless of whether it is a change in isProcessing. - */ - ( - prevProps.state.isProcessing !== this.props.state.isProcessing && - this.props.state.isProcessing && - this._isMounted - ) - ) { - await this.getItems(); - } - } - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Loads the initial information - */ - async getItems() { - try { - this.props.updateLoadingStatus(true); - const rawItems = await this.groupsHandler.listGroups({ params: this.buildFilter() }); - const { - affected_items: affectedItem, - total_affected_items: totalAffectedItem - } = rawItems?.data?.data; - this.setState({ - items: affectedItem, - totalItems: totalAffectedItem, - }); - this.props.updateLoadingStatus(false); - this.props.state.isProcessing && this.props.updateIsProcessing(false); - - } catch (error) { - this.props.updateLoadingStatus(false); - this.props.state.isProcessing && this.props.updateIsProcessing(false); - const options = { - context: `${WzGroupsTable.name}.getItems`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, - store: true, - error: { - error: error, - message: error.message || error, - title: `Error getting groups`, - }, - }; - getErrorOrchestrator().handleError(options); - } - - } - - buildFilter() { - const { pageIndex } = this.props.state; - const { pageSize, filters } = this.state; - const filter = { - ...filtersToObject(filters), - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - }; - - return filter; - } - - buildSortFilter() { - const { sortField, sortDirection } = this.props.state; - - const field = sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + field; - } - - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this._isMounted && this.setState({ pageSize }); - this.props.updatePageIndex(pageIndex); - this.props.updateSortDirection(sortDirection); - this.props.updateSortField(sortField); - this.props.updateIsProcessing(true); - }; - - render() { - const { filters } = this.state; - - this.groupsColumns = new GroupsColums(this.props); - const { isLoading, pageIndex, error, sortField, sortDirection } = this.props.state; - const { items, pageSize, totalItems } = this.state; - const columns = this.groupsColumns.columns; - const message = isLoading ? null : 'No results...'; - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [10, 25, 50, 100], - }; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - const getRowProps = (item) => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - className: 'customRowClass', - onClick: !WzUserPermissions.checkMissingUserPermissions( - [{ action: 'group:read', resource: `group:id:${item.name}` }], - this.props.userPermissions - ) - ? () => this.props.updateGroupDetail(item) - : undefined, - }; - }; - - if (error) { - return ; - } - const itemList = this.props.state.itemList; - return ( - - this._isMounted && this.setState({ filters })} - placeholder="Search group" - /> - - - {this.props.state.showModal ? ( - - this.props.updateShowModal(false)} - onConfirm={() => { - this.removeItems(itemList); - this.props.updateShowModal(false); - }} - cancelButtonText="Cancel" - confirmButtonText="Delete" - defaultFocusedButton="cancel" - buttonColor="danger" - > - - ) : null} - - ); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - - async removeItems(items) { - this.props.updateLoadingStatus(true); - const results = items.map(async (item, i) => { - await this.groupsHandler.deleteGroup(item.name); - }); - - Promise.all(results).then( - (completed) => { - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.showToast('success', 'Success', 'Deleted successfully', 3000); - }, - (error) => { - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.showToast('danger', 'Error', error, 3000); - } - ); - } -} - -const mapStateToProps = (state) => { - return { - state: state.groupsReducers, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), - updateFileContent: (content) => dispatch(updateFileContent(content)), - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), - updatePageIndex: (pageIndex) => dispatch(updatePageIndex(pageIndex)), - updateShowModal: (showModal) => dispatch(updateShowModal(showModal)), - updateListItemsForRemove: (itemList) => dispatch(updateListItemsForRemove(itemList)), - updateSortDirection: (sortDirection) => dispatch(updateSortDirection(sortDirection)), - updateSortField: (sortField) => dispatch(updateSortField(sortField)), - updateGroupDetail: (itemDetail) => dispatch(updateGroupDetail(itemDetail)), - }; -}; - -export default compose( - connect(mapStateToProps, mapDispatchToProps), - withUserPermissions -)(WzGroupsTable); diff --git a/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js b/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js index 46bedb5a08..6174625d70 100644 --- a/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js +++ b/plugins/main/public/controllers/management/components/management/groups/utils/columns-files.js @@ -42,12 +42,14 @@ export default class GroupsFilesColumns { field: 'filename', name: 'File', align: 'left', + searchable: true, sortable: true }, { field: 'hash', name: 'Checksum', align: 'left', + searchable: true, sortable: true } ]; diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx b/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx index 7b43eea337..8b0d25e718 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx @@ -142,6 +142,13 @@ export default class RulesetColumns { align: 'left', sortable: true, }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + width: '10%', + }, { name: 'Actions', align: 'left', diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts index 84e4115185..aa6486bdb4 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts @@ -1,137 +1,208 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../../common/constants'; import { WzRequest } from '../../../../../../react-services/wz-request'; -const rulesItems = [ - { - type: 'params', - label: 'status', - description: 'Filters the rules by status.', - values: ['enabled', 'disabled'] +const rulesItems = { + field(currentValue) { + return [ + { label: 'id', description: 'filter by ID' }, + { label: 'filename', description: 'filter by filename' }, + { label: 'gdpr', description: 'filter by GDPR requirement' }, + { label: 'gpg13', description: 'filter by GPG requirement' }, + { label: 'groups', description: 'filter by group' }, + { label: 'hipaa', description: 'filter by HIPAA requirement' }, + { label: 'level', description: 'filter by level' }, + { label: 'mitre', description: 'filter by MITRE ATT&CK requirement' }, + { label: 'nist_800_53', description: 'filter by NIST requirement' }, + { label: 'pci_dss', description: 'filter by PCI DSS requirement' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + { label: 'status', description: 'filter by status' }, + { label: 'tsc', description: 'filter by TSC requirement' }, + ]; }, - { - type: 'params', - label: 'group', - description: 'Filters the rules by group', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/rules/groups', filter); - return result?.data?.data?.affected_items; - }, - }, - { - type: 'params', - label: 'level', - description: 'Filters the rules by level', - values: [...Array(16).keys()] - }, - { - type: 'params', - label: 'filename', - description: 'Filters the rules by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => { return item.filename }); - }, - }, - { - type: 'params', - label: 'relative_dirname', - description: 'Path of the rules files', - values: async () => { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'rule_dir' + value: async (currentValue, { field }) => { + try { + switch (field) { + case 'id': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `id~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules', { + params: filter, + }); + return result?.data?.data?.affected_items.map(label => ({ + label: label[field], + })); } - }); - return result?.data?.data?.affected_items?.[0].ruleset.rule_dir; - } - }, - { - type: 'params', - label: 'hipaa', - description: 'Filters the rules by HIPAA requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/hipaa', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'gdpr', - description: 'Filters the rules by GDPR requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gdpr', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'nist-800-53', - description: 'Filters the rules by NIST requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/nist-800-53', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'gpg13', - description: 'Filters the rules by GPG requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gpg13', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'pci_dss', - description: 'Filters the rules by PCI DSS requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/pci_dss', {}); - return result?.data?.data?.affected_items; + case 'status': { + return ['enabled', 'disabled'].map(label => ({ label })); + } + case 'groups': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/groups', { + params: filter, + }); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'level': { + return [...Array(16).keys()].map(label => ({ label })); + } + case 'filename': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } + case 'relative_dirname': { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules', { + params: filter, + }); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); + } + case 'hipaa': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/hipaa', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'gdpr': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/gdpr', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'nist_800_53': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/nist-800-53', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'gpg13': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/gpg13', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'pci_dss': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/pci_dss', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'tsc': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/tsc', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + case 'mitre': { + const filter = { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/mitre', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); + } + default: + return []; + } + } catch (error) { + return []; } }, - { - type: 'params', - label: 'tsc', - description: 'Filters the rules by TSC requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/tsc', {}); - return result?.data?.data?.affected_items; - } +}; + +const rulesFiles = { + field(currentValue) { + return [ + { label: 'filename', description: 'filter by filename' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + ]; }, - { - type: 'params', - label: 'mitre', - description: 'Filters the rules by MITRE requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/mitre', {}); - return result?.data?.data?.affected_items; + value: async (currentValue, { field }) => { + try { + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; } - } -]; -const rulesFiles = [ - { - type: 'params', - label: 'filename', - description: 'Filters the rules by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => { return item.filename }); - }, }, -]; +}; const apiSuggestsItems = { items: rulesItems, diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx index 229637f7a1..6ebfa3980a 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx @@ -39,6 +39,41 @@ import { import apiSuggestsItems from './ruleset-suggestions'; +const searchBarWQLOptions = { + searchTermFields: [ + 'id', + 'description', + 'filename', + 'gdpr', + 'gpg13', + 'groups', + 'level', + 'mitre', + 'nist_800_53', + 'pci_dss', + 'relative_dirname', + 'tsc', + ], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], +}; + +const searchBarWQLOptionsFiles = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], +}; + /*************************************** * Render tables */ @@ -54,19 +89,20 @@ const FilesTable = ({ ); @@ -88,20 +124,21 @@ const RulesFlyoutTable = ({ <> {isFlyoutVisible && ( diff --git a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx index 866921553c..a95208846b 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx @@ -323,7 +323,7 @@ export default class WzRuleInfo extends Component { > - this.setNewFiltersAndBack([{ field: 'rule_ids', value: id }]) + this.setNewFiltersAndBack({ q: `id=${id}` }) } > {id} @@ -340,7 +340,7 @@ export default class WzRuleInfo extends Component { > - this.setNewFiltersAndBack([{ field: 'level', value: level }]) + this.setNewFiltersAndBack({ q: `level=${level}` }) } > {level} @@ -354,9 +354,7 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([ - { field: 'filename', value: file }, - ]) + this.setNewFiltersAndBack({ q: `filename=${file}` }) } > {file} @@ -370,9 +368,7 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([ - { field: 'relative_dirname', value: path }, - ]) + this.setNewFiltersAndBack({ q: `relative_dirname=${path}` }) } > {path} @@ -486,7 +482,7 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([{ field: 'group', value: group }]) + this.setNewFiltersAndBack({ q: `groups=${group}` }) } > - this.setNewFiltersAndBack([{ field: key, value: element }]) + this.setNewFiltersAndBack({ q: `${key}=${element}` }) } > @@ -628,9 +624,9 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([ - { field: 'mitre', value: this.state.mitreIds[index] }, - ]) + this.setNewFiltersAndBack({ + q: `mitre=${this.state.mitreIds[index]}`, + }) } > @@ -857,7 +853,7 @@ export default class WzRuleInfo extends Component { {this.state.currentRuleInfo?.filename && ( { - return field !== undefined ? field : '-'; +const searchBarWQLOptions = { + implicitQuery: { + query: 'id!=000', + conjunction: ';', + }, }; export class AgentSelectionTable extends Component { constructor(props) { super(props); this.state = { - itemIdToSelectedMap: {}, - itemIdToOpenActionsPopoverMap: {}, - sortedColumn: 'title', - itemsPerPage: 10, - pageIndex: 0, - totalItems: 0, - isLoading: false, - sortDirection: 'asc', - sortField: 'id', - agents: [], - selectedOptions: [], - filters: [] + filters: { default: { q: 'id!=000' } }, }; this.columns = [ { - id: 'id', - label: 'ID', - alignment: LEFT_ALIGNMENT, + field: 'id', + name: 'ID', width: '60px', - mobileOptions: { - show: true, - }, - isSortable: true, + searchable: true, + sortable: true, }, { - id: 'name', - label: 'Name', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: true, - }, - isSortable: true + field: 'name', + name: 'Name', + searchable: true, + sortable: true, }, { - id: 'group', - label: 'Group', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: false, - }, - isSortable: true, - render: groups => this.renderGroups(groups) + field: 'group', + name: 'Group', + sortable: true, + searchable: true, + render: groups => this.renderGroups(groups), }, { - id: 'version', - label: 'Version', + field: 'version', + name: 'Version', width: '80px', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: true, - }, - isSortable: true, + searchable: true, + sortable: true, }, { - id: 'os', - label: 'Operating system', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: false, - }, - isSortable: true, - render: os => this.addIconPlatformRender(os) + field: 'os.name,os.version', + composeField: ['os.name', 'os.version'], + name: 'Operating system', + sortable: true, + searchable: true, + render: (field, agentData) => this.addIconPlatformRender(agentData), }, { - id: 'status', - label: 'Status', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: true, - }, - isSortable: true, + field: 'status', + name: 'Status', + searchable: true, + sortable: true, width: 'auto', - render: status => , + render: status => ( + + ), }, ]; - this.suggestions = [ - { type: 'q', label: 'status', description: 'Filter by agent connection status', operators: ['=', '!=',], values: UI_ORDER_AGENT_STATUS }, - { type: 'q', label: 'os.platform', description: 'Filter by operating system platform', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('os.platform', value, { q: 'id!=000'})}, - { type: 'q', label: 'ip', description: 'Filter by agent IP address', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('ip', value, { q: 'id!=000'})}, - { type: 'q', label: 'name', description: 'Filter by agent name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('name', value, { q: 'id!=000'})}, - { type: 'q', label: 'id', description: 'Filter by agent id', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('id', value, { q: 'id!=000'})}, - { type: 'q', label: 'group', description: 'Filter by agent group', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('group', value, { q: 'id!=000'})}, - { type: 'q', label: 'node_name', description: 'Filter by node name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('node_name', value, { q: 'id!=000'})}, - { type: 'q', label: 'manager', description: 'Filter by manager', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('manager', value, { q: 'id!=000'})}, - { type: 'q', label: 'version', description: 'Filter by agent version', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('version', value, { q: 'id!=000'})}, - { type: 'q', label: 'configSum', description: 'Filter by agent config sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('configSum', value, { q: 'id!=000'})}, - { type: 'q', label: 'mergedSum', description: 'Filter by agent merged sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('mergedSum', value, { q: 'id!=000'})}, - { type: 'q', label: 'dateAdd', description: 'Filter by add date', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('dateAdd', value, { q: 'id!=000'})}, - { type: 'q', label: 'lastKeepAlive', description: 'Filter by last keep alive', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('lastKeepAlive', value, { q: 'id!=000'})}, - ]; - } - - onChangeItemsPerPage = async itemsPerPage => { - this._isMounted && this.setState({ itemsPerPage }, async () => await this.getItems()); - }; - - onChangePage = async pageIndex => { - this._isMounted && this.setState({ pageIndex }, async () => await this.getItems()); - }; - - async componentDidMount() { - this._isMounted = true; - const tmpSelectedAgents = {}; - if(!store.getState().appStateReducers.currentAgentData.id){ - tmpSelectedAgents[store.getState().appStateReducers.currentAgentData.id] = true; - } - this._isMounted && this.setState({itemIdToSelectedMap: this.props.selectedAgents}); - await this.getItems(); - } - - componentWillUnmount(){ - this._isMounted = false; - } - - async componentDidUpdate(prevProps, prevState) { - if(!(_.isEqual(prevState.filters,this.state.filters))){ - await this.getItems(); - } - } - - getArrayFormatted(arrayText) { - try { - const stringText = arrayText.toString(); - const splitString = stringText.split(','); - return splitString.join(', '); - } catch (error) { - const options = { - context: `${AgentSelectionTable.name}.getArrayFormatted`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - - getErrorOrchestrator().handleError(options); - return arrayText; - } - } - - async getItems() { - try { - this._isMounted && this.setState({ isLoading: true }); - const rawData = await WzRequest.apiReq('GET', '/agents', { params: this.buildFilter() }); - const data = (((rawData || {}).data || {}).data || {}).affected_items; - const totalItems = (((rawData || {}).data || {}).data || {}).total_affected_items; - const formattedData = data.map((item, id) => { - return { - id: item.id, - name: item.name, - version: item.version !== undefined ? item.version.split(' ')[1] : '-', - os: item.os || '-', - status: item.status, - group: item.group || '-', - }; - }); - this._isMounted && this.setState({ agents: formattedData, totalItems, isLoading: false }); - } catch (error) { - this._isMounted && this.setState({ isLoading: false }); - const options = { - context: `${AgentSelectionTable.name}.getItems`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - - getErrorOrchestrator().handleError(options); - } - } - - buildFilter() { - const { itemsPerPage, pageIndex, filters } = this.state; - const filter = { - ...filtersToObject(filters), - offset: (pageIndex * itemsPerPage) || 0, - limit: pageIndex * itemsPerPage + itemsPerPage, - ...this.buildSortFilter() - }; - filter.q = !filter.q ? `id!=000` : `id!=000;${filter.q}`; - return filter; - } - - buildSortFilter() { - const { sortDirection, sortField } = this.state; - const sortFilter = {}; - if (sortField) { - const direction = sortDirection === 'asc' ? '+' : '-'; - sortFilter['sort'] = direction + (sortField === 'os'? 'os.name,os.version' : sortField); - } - - return sortFilter; - } - - onSort = async prop => { - const sortField = prop; - const sortDirection = - this.state.sortField === prop && this.state.sortDirection === 'asc' - ? 'desc' - : this.state.sortDirection === 'asc' - ? 'desc' - : 'asc'; - - this._isMounted && this.setState({ sortField, sortDirection }, async () => await this.getItems()); - }; - - toggleItem = itemId => { - this._isMounted && this.setState(previousState => { - const newItemIdToSelectedMap = { - [itemId]: !previousState.itemIdToSelectedMap[itemId], - }; - - return { - itemIdToSelectedMap: newItemIdToSelectedMap, - }; - }); - }; - - toggleAll = () => { - const allSelected = this.areAllItemsSelected(); - const newItemIdToSelectedMap = {}; - this.state.agents.forEach(item => (newItemIdToSelectedMap[item.id] = !allSelected)); - this._isMounted && this.setState({ - itemIdToSelectedMap: newItemIdToSelectedMap, - }); - }; - - isItemSelected = itemId => { - return this.state.itemIdToSelectedMap[itemId]; - }; - - areAllItemsSelected = () => { - const indexOfUnselectedItem = this.state.agents.findIndex(item => !this.isItemSelected(item.id)); - return indexOfUnselectedItem === -1; - }; - - areAnyRowsSelected = () => { - return ( - Object.keys(this.state.itemIdToSelectedMap).findIndex(id => { - return this.state.itemIdToSelectedMap[id]; - }) !== -1 - ); - }; - - togglePopover = itemId => { - this._isMounted && this.setState(previousState => { - const newItemIdToOpenActionsPopoverMap = { - ...previousState.itemIdToOpenActionsPopoverMap, - [itemId]: !previousState.itemIdToOpenActionsPopoverMap[itemId], - }; - - return { - itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, - }; - }); - }; - - closePopover = itemId => { - // only update the state if this item's popover is open - if (this.isPopoverOpen(itemId)) { - this._isMounted && this.setState(previousState => { - const newItemIdToOpenActionsPopoverMap = { - ...previousState.itemIdToOpenActionsPopoverMap, - [itemId]: false, - }; - - return { - itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, - }; - }); - } - }; - - isPopoverOpen = itemId => { - return this.state.itemIdToOpenActionsPopoverMap[itemId]; - }; - - renderSelectAll = mobile => { - if (!this.state.isLoading && this.state.agents.length) { - return ( - - ); - } - }; - - getTableMobileSortItems() { - const items = []; - this.columns.forEach(column => { - if (column.isCheckbox || !column.isSortable) { - return; - } - items.push({ - name: column.label, - key: column.id, - onSort: this.onSort.bind(this, column.id), - isSorted: this.state.sortField === column.id, - isSortAscending: this.state.sortDirection === 'asc', - }); - }); - return items.length ? items : null; - } - - renderHeaderCells() { - const headers = []; - - this.columns.forEach((column, columnIndex) => { - if (column.isCheckbox) { - headers.push( - - - ); - } else { - headers.push( - - {column.label} - - ); - } - }); - return headers.length ? headers : null; - } - - renderRows() { - const renderRow = item => { - const cells = this.columns.map(column => { - const cell = item[column.id]; - - let child; - - if (column.isCheckbox) { - return ( - - {}} - type="inList" - /> - - ); - } - - if (column.render) { - child = column.render(item[column.id]); - } else { - child = cell; - } - - return ( - - {child} - - ); - }); - - return ( - await this.selectAgentAndApply(item.id)} - hasActions={true} - > - {cells} - - ); - }; - - const rows = []; - - for ( - let itemIndex = (this.state.pageIndex * this.state.itemsPerPage) % this.state.itemsPerPage; - itemIndex < - ((this.state.pageIndex * this.state.itemsPerPage) % this.state.itemsPerPage) + - this.state.itemsPerPage && this.state.agents[itemIndex]; - itemIndex++ - ) { - const item = this.state.agents[itemIndex]; - rows.push(renderRow(item)); - } - - return rows; } - renderFooterCells() { - const footers = []; - - const items = this.state.agents; - const pagination = { - pageIndex: this.state.pageIndex, - pageSize: this.state.itemsPerPage, - totalItemCount: this.state.totalItems, - pageSizeOptions: [10, 25, 50, 100] - }; - - this.columns.forEach(column => { - const footer = this.getColumnFooter(column, { items, pagination }); - if (column.mobileOptions && column.mobileOptions.only) { - return; // exclude columns that only exist for mobile headers - } - - if (footer) { - footers.push( - - {footer} - - ); - } else { - footers.push( - - {undefined} - - ); - } - }); - return footers; - } - - getColumnFooter = (column, { items, pagination }) => { - if (column.footer === null) { - return null; - } - if (column.footer) { - return column.footer; - } - - return undefined; - }; - - async onQueryChange(result) { - this._isMounted && - this.setState({ isLoading: true, ...result }, async () => { - await this.getItems(); - }); - } - - getSelectedItems(){ - return Object.keys(this.state.itemIdToSelectedMap).filter(x => { - return (this.state.itemIdToSelectedMap[x] === true) - }) - } - - unselectAgents(){ - this._isMounted && this.setState({itemIdToSelectedMap: {}}); + unselectAgents() { store.dispatch(updateCurrentAgentData({})); this.props.removeAgentsFilter(); } - getSelectedCount(){ - return this.getSelectedItems().length; - } - - async selectAgentAndApply(agentID){ - try{ - const data = await WzRequest.apiReq('GET', '/agents', { params: { q: 'id=' + agentID}}); - const formattedData = data.data.data.affected_items[0] //TODO: do it correctly + async selectAgentAndApply(agentID) { + try { + const data = await WzRequest.apiReq('GET', '/agents', { + params: { q: 'id=' + agentID }, + }); + const formattedData = data?.data?.data?.affected_items?.[0]; store.dispatch(updateCurrentAgentData(formattedData)); this.props.updateAgentSearch([agentID]); - } catch(error) { + } catch (error) { store.dispatch(updateCurrentAgentData({})); this.props.removeAgentsFilter(true); const options = { @@ -546,52 +115,42 @@ export class AgentSelectionTable extends Component { } } - showContextMenu(id){ - this._isMounted && this.setState({contextMenuId: id}) - } + addIconPlatformRender(agent) { + let icon = ''; + const os = agent?.os || {}; - addIconPlatformRender(os) { - if(typeof os === "string" ){ return os}; - let icon = false; - - if (((os || {}).uname || '').includes('Linux')) { + if ((os?.uname || '').includes('Linux')) { icon = 'linux'; - } else if ((os || {}).platform === 'windows') { + } else if (os?.platform === 'windows') { icon = 'windows'; - } else if ((os || {}).platform === 'darwin') { + } else if (os?.platform === 'darwin') { icon = 'apple'; } - const os_name = - checkField((os || {}).name) + - ' ' + - checkField((os || {}).version); + const os_name = `${agent?.os?.name || ''} ${agent?.os?.version || ''}`; + return ( - - {' '} - {os_name === '--' ? '-' : os_name} - + + + + {' '} + {os_name.trim() || '-'} + ); } - filterGroupBadge = (group) => { - const { filters } = this.state; - let auxFilters = filters.map( filter => filter.value.match(/group=(.*S?)/)[1] ); - if (filters.length > 0) { - !auxFilters.includes(group) ? - this.setState({ - filters: [...filters, {field: "q", value: `group=${group}`}], - }) : false; - } else { - this.setState({ - filters: [...filters, {field: "q", value: `group=${group}`}], - }) - } - } + filterGroupBadge = group => { + this.setState({ + filters: { + default: { q: 'id!=000' }, + q: `group=${group}`, + }, + }); + }; - renderGroups(groups){ + renderGroups(groups) { return Array.isArray(groups) ? ( - ) : groups + {...this.props} + /> + ) : ( + groups + ); } render() { - const pagination = { - pageIndex: this.state.pageIndex, - pageSize: this.state.itemsPerPage, - totalItemCount: this.state.totalItems, - pageCount: - this.state.totalItems % this.state.itemsPerPage === 0 - ? this.state.totalItems / this.state.itemsPerPage - : parseInt(this.state.totalItems / this.state.itemsPerPage) + 1, - }; const selectedAgent = store.getState().appStateReducers.currentAgentData; + const getRowProps = (item, idx) => { + return { + 'data-test-subj': `explore-agent-${idx}`, + className: 'customRowClass', + onClick: () => this.selectAgentAndApply(item.id), + }; + }; + return (
- - - this.setState({filters, pageIndex: 0})} - placeholder="Filter or search agent" - /> - - - {selectedAgent && Object.keys(selectedAgent).length > 0 && ( - + {/* agent name (agent id) Unpin button right aligned, require justifyContent="flexEnd" in the EuiFlexGroup */} - - + + {selectedAgent.name} ({selectedAgent.id}) - + this.unselectAgents()} - iconType="pinFilled" - aria-label="unpin agent" + iconType='pinFilled' + aria-label='unpin agent' /> - + )} - - - - - - - - - - - {this.renderHeaderCells()} - {(this.state.agents.length && ( - - {this.renderRows()} - - )) || ( - - - - {this.state.isLoading ? 'Loading agents' : 'No results found'} - - - - )} - - - - - { + return { + ...item, + /* + The agent version contains the Wazuh word, this get the string starting with + v + */ + ...(typeof item.version === 'string' + ? { version: item.version.match(/(v\d.+)/)?.[1] } + : { version: '-' }), + }; + }} + rowProps={getRowProps} + filters={this.state.filters} + searchTable + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + { label: 'id', description: 'filter by id' }, + { label: 'group', description: 'filter by group' }, + { label: 'name', description: 'filter by name' }, + { + label: 'os.name', + description: 'filter by operating system name', + }, + { + label: 'os.version', + description: 'filter by operating system version', + }, + { label: 'status', description: 'filter by status' }, + { label: 'version', description: 'filter by version' }, + ]; + }, + value: async (currentValue, { field }) => { + try { + switch (field) { + case 'status': + return UI_ORDER_AGENT_STATUS.map(status => ({ + label: status, + })); + default: { + const response = await WzRequest.apiReq( + 'GET', + '/agents', + { + params: { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + } + : { + q: `${searchBarWQLOptions.implicitQuery.query}`, + }), + }, + }, + ); + if (field === 'group') { + /* the group field is returned as an string[], + example: ['group1', 'group2'] + + Due the API request done to get the distinct values for the groups is + not returning the exepected values, as workaround, the values are + extracted in the frontend using the returned results. + + This API request to get the distint values of groups doesn't + return the unique values for the groups, else the unique combination + of groups. + */ + return response?.data?.data.affected_items + .map(item => getLodash(item, field)) + .flat() + .filter( + (item, index, array) => + array.indexOf(item) === index, + ) + .sort() + .map(group => ({ label: group })); + } + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } + } + } catch (error) { + return []; + } + }, + }, + }} />
); diff --git a/plugins/main/public/controllers/overview/components/stats.js b/plugins/main/public/controllers/overview/components/stats.js index a9b1786250..0f8a7bfab4 100644 --- a/plugins/main/public/controllers/overview/components/stats.js +++ b/plugins/main/public/controllers/overview/components/stats.js @@ -33,11 +33,11 @@ export const Stats = withErrorBoundary (class Stats extends Component { goToAgents(status) { if(status){ sessionStorage.setItem( - 'agents_preview_selected_options', - JSON.stringify([{field: 'q', value: `status=${status}`}]) + 'wz-agents-overview-table-filter', + JSON.stringify({q: `id!=000;status=${status}`}) ); - }else if(sessionStorage.getItem('agents_preview_selected_options')){ - sessionStorage.removeItem('agents_preview_selected_options'); + }else if(sessionStorage.getItem('wz-agents-overview-table-filter')){ + sessionStorage.removeItem('wz-agents-overview-table-filter'); } window.location.href = '#/agents-preview'; } diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss index 55dd4092fa..a71216b6a6 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss @@ -18,10 +18,6 @@ margin-right: 10px; } -.euiCard__content .euiCard__titleButton { - text-decoration: none !important; -} - .cardText { font-style: normal; font-weight: 700; @@ -56,4 +52,4 @@ .cardsCallOut { margin-top: 16px; -} +} \ No newline at end of file diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.scss b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss index 337cc41298..005ccb6379 100644 --- a/plugins/main/public/controllers/register-agent/containers/steps/steps.scss +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss @@ -6,50 +6,51 @@ letter-spacing: 0.6px; flex-direction: row; } -} - -.stepSubtitleServerAddress { - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 24px; - margin-bottom: 9px; -} - -.stepSubtitle { - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 24px; - margin-bottom: 20px; -} - -.titleAndIcon { - display: flex; - flex-direction: row; -} - -.warningForAgentName { - margin-top: 10px; -} - -.euiToolTipAnchor { - margin-left: 7px; -} - -.subtitleAgentName { - flex-direction: 'row'; - font-style: 'normal'; - font-weight: 700; - font-size: '12px'; - line-height: '20px'; - color: '#343741'; -} - -.euiStep__titleWrapper { - align-items: center; -} - -.euiButtonEmpty .euiButtonEmpty__content { - padding: 0; -} + + + .stepSubtitleServerAddress { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 9px; + } + + .stepSubtitle { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 20px; + } + + .titleAndIcon { + display: flex; + flex-direction: row; + } + + .warningForAgentName { + margin-top: 10px; + } + + .euiToolTipAnchor { + margin-left: 7px; + } + + .subtitleAgentName { + flex-direction: 'row'; + font-style: 'normal'; + font-weight: 700; + font-size: '12px'; + line-height: '20px'; + color: '#343741'; + } + + .euiStep__titleWrapper { + align-items: center; + } + + .euiButtonEmpty .euiButtonEmpty__content { + padding: 0; + } +} \ No newline at end of file diff --git a/plugins/main/public/react-services/reporting.js b/plugins/main/public/react-services/reporting.js index b74ed090ff..4fa1dee29d 100644 --- a/plugins/main/public/react-services/reporting.js +++ b/plugins/main/public/react-services/reporting.js @@ -89,13 +89,15 @@ export class ReportingService { } const appliedFilters = await this.visHandlers.getAppliedFilters(syscollectorFilters); - + const dataplugin = await getDataPlugin(); + const serverSideQuery = dataplugin.query.getOpenSearchQuery(); const array = await this.vis2png.checkArray(visualizationIDList); const browserTimezone = moment.tz.guess(true); const data = { array, + serverSideQuery, // Used for applying the same filters on the server side requests filters: appliedFilters.filters, time: appliedFilters.time, searchBar: appliedFilters.searchBar, diff --git a/plugins/main/server/controllers/wazuh-reporting.ts b/plugins/main/server/controllers/wazuh-reporting.ts index 51cd76ea6f..5a13636cd1 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -36,7 +36,7 @@ interface AgentsFilter { } export class WazuhReportingCtrl { - constructor() {} + constructor() { } /** * This do format to filters * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability @@ -70,22 +70,21 @@ export class WazuhReportingCtrl { const { negate, key, value, params, type } = filters[i].meta; str += `${negate ? 'NOT ' : ''}`; str += `${key}: `; - str += `${ - type === 'range' - ? `${params.gte}-${params.lt}` - : type === 'phrases' - ? '(' + params.join(" OR ") + ')' - : type === 'exists' - ? '*' - : !!value - ? value - : (params || {}).query - }`; + str += `${type === 'range' + ? `${params.gte}-${params.lt}` + : type === 'phrases' + ? '(' + params.join(" OR ") + ')' + : type === 'exists' + ? '*' + : !!value + ? value + : (params || {}).query + }`; str += `${i === len - 1 ? '' : ' AND '}`; } if (searchBar) { - str += ` AND (${ searchBar})`; + str += ` AND (${searchBar})`; } agentsFilter.agentsText = agentsList.map((filter) => filter.meta.value).join(','); @@ -211,8 +210,8 @@ export class WazuhReportingCtrl { plainData[key] = Array.isArray(data[key]) && typeof data[key][0] !== 'object' ? data[key].map((x) => { - return typeof x === 'object' ? JSON.stringify(x) : x + '\n'; - }) + return typeof x === 'object' ? JSON.stringify(x) : x + '\n'; + }) : data[key]; } else if (Array.isArray(data[key]) && typeof data[key][0] === 'object') { tableData[key] = data[key]; @@ -229,7 +228,7 @@ export class WazuhReportingCtrl { title: (section.options || {}).hideHeader ? '' : (section.tabs || [])[tab] || - (section.isGroupConfig ? ((section.labels || [])[0] || [])[tab] : ''), + (section.isGroupConfig ? ((section.labels || [])[0] || [])[tab] : ''), columns: ['', ''], type: 'config', rows: this.getConfigRows(plainData, (section.labels || [])[0]), @@ -247,10 +246,10 @@ export class WazuhReportingCtrl { typeof x[key] !== 'object' ? x[key] : Array.isArray(x[key]) - ? x[key].map((x) => { + ? x[key].map((x) => { return x + '\n'; }) - : JSON.stringify(x[key]) + : JSON.stringify(x[key]) ); } while (row.length < columns.length) { @@ -291,6 +290,7 @@ export class WazuhReportingCtrl { browserTimezone, searchBar, filters, + serverSideQuery, time, tables, section, @@ -327,7 +327,7 @@ export class WazuhReportingCtrl { apiId, new Date(from).getTime(), new Date(to).getTime(), - sanitizedFilters, + serverSideQuery, agentsFilter, indexPatternTitle, agents @@ -356,7 +356,7 @@ export class WazuhReportingCtrl { } catch (error) { return ErrorResponse(error.message || error, 5029, 500, response); } - },({body:{ agents }, params: { moduleID }}) => `wazuh-module-${agents ? `agents-${agents}` : 'overview'}-${moduleID}-${this.generateReportTimestamp()}.pdf`) + }, ({ body: { agents }, params: { moduleID } }) => `wazuh-module-${agents ? `agents-${agents}` : 'overview'}-${moduleID}-${this.generateReportTimestamp()}.pdf`) /** * Create a report for the groups @@ -365,7 +365,7 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsGroups = this.checkReportsUserDirectoryIsValidRouteDecorator(async( + createReportsGroups = this.checkReportsUserDirectoryIsValidRouteDecorator(async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory @@ -486,10 +486,10 @@ export class WazuhReportingCtrl { typeof x[key] !== 'object' ? x[key] : Array.isArray(x[key]) - ? x[key].map((x) => { + ? x[key].map((x) => { return x + '\n'; }) - : JSON.stringify(x[key]) + : JSON.stringify(x[key]) ); }); return row; @@ -613,7 +613,7 @@ export class WazuhReportingCtrl { log('reporting:createReportsGroups', error.message || error); return ErrorResponse(error.message || error, 5029, 500, response); } - }, ({params: { groupID }}) => `wazuh-group-configuration-${groupID}-${this.generateReportTimestamp()}.pdf`) + }, ({ params: { groupID } }) => `wazuh-group-configuration-${groupID}-${this.generateReportTimestamp()}.pdf`) /** * Create a report for the agents @@ -622,7 +622,7 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsAgentsConfiguration = this.checkReportsUserDirectoryIsValidRouteDecorator( async ( + createReportsAgentsConfiguration = this.checkReportsUserDirectoryIsValidRouteDecorator(async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory @@ -745,10 +745,10 @@ export class WazuhReportingCtrl { typeof x[key] !== 'object' ? x[key] : Array.isArray(x[key]) - ? x[key].map((x) => { + ? x[key].map((x) => { return x + '\n'; }) - : JSON.stringify(x[key]) + : JSON.stringify(x[key]) ); }); return row; @@ -775,13 +775,13 @@ export class WazuhReportingCtrl { } else { /*INTEGRITY MONITORING MONITORED DIRECTORIES */ if (conf.matrix) { - const {directories,diff,synchronization,file_limit,...rest} = agentConfig[agentConfigKey]; + const { directories, diff, synchronization, file_limit, ...rest } = agentConfig[agentConfigKey]; tables.push( ...this.getConfigTables(rest, section, idx), - ...(diff && diff.disk_quota ? this.getConfigTables(diff.disk_quota, {tabs:['Disk quota']}, 0 ): []), - ...(diff && diff.file_size ? this.getConfigTables(diff.file_size, {tabs:['File size']}, 0 ): []), - ...(synchronization ? this.getConfigTables(synchronization, {tabs:['Synchronization']}, 0 ): []), - ...(file_limit ? this.getConfigTables(file_limit, {tabs:['File limit']}, 0 ): []), + ...(diff && diff.disk_quota ? this.getConfigTables(diff.disk_quota, { tabs: ['Disk quota'] }, 0) : []), + ...(diff && diff.file_size ? this.getConfigTables(diff.file_size, { tabs: ['File size'] }, 0) : []), + ...(synchronization ? this.getConfigTables(synchronization, { tabs: ['Synchronization'] }, 0) : []), + ...(file_limit ? this.getConfigTables(file_limit, { tabs: ['File limit'] }, 0) : []), ); let diffOpts = []; Object.keys(section.opts).forEach((x) => { @@ -860,7 +860,7 @@ export class WazuhReportingCtrl { log('reporting:createReportsAgentsConfiguration', error.message || error); return ErrorResponse(error.message || error, 5029, 500, response); } - }, ({ params: { agentID }}) => `wazuh-agent-configuration-${agentID}-${this.generateReportTimestamp()}.pdf`) + }, ({ params: { agentID } }) => `wazuh-agent-configuration-${agentID}-${this.generateReportTimestamp()}.pdf`) /** * Create a report for the agents @@ -869,14 +869,14 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsAgentsInventory = this.checkReportsUserDirectoryIsValidRouteDecorator( async ( + createReportsAgentsInventory = this.checkReportsUserDirectoryIsValidRouteDecorator(async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory ) => { try { log('reporting:createReportsAgentsInventory', `Report started`, 'info'); - const { searchBar, filters, time, indexPatternTitle, apiId } = request.body; + const { searchBar, filters, time, indexPatternTitle, apiId, serverSideQuery } = request.body; const { agentID } = request.params; const { from, to } = time || {}; // Init @@ -924,18 +924,18 @@ export class WazuhReportingCtrl { columns: agentOs === 'windows' ? [ - { id: 'name', label: 'Name' }, - { id: 'architecture', label: 'Architecture' }, - { id: 'version', label: 'Version' }, - { id: 'vendor', label: 'Vendor' }, - ] + { id: 'name', label: 'Name' }, + { id: 'architecture', label: 'Architecture' }, + { id: 'version', label: 'Version' }, + { id: 'vendor', label: 'Vendor' }, + ] : [ - { id: 'name', label: 'Name' }, - { id: 'architecture', label: 'Architecture' }, - { id: 'version', label: 'Version' }, - { id: 'vendor', label: 'Vendor' }, - { id: 'description', label: 'Description' }, - ], + { id: 'name', label: 'Name' }, + { id: 'architecture', label: 'Architecture' }, + { id: 'version', label: 'Version' }, + { id: 'vendor', label: 'Vendor' }, + { id: 'description', label: 'Description' }, + ], }, }, { @@ -946,17 +946,17 @@ export class WazuhReportingCtrl { columns: agentOs === 'windows' ? [ - { id: 'name', label: 'Name' }, - { id: 'cmd', label: 'CMD' }, - { id: 'priority', label: 'Priority' }, - { id: 'nlwp', label: 'NLWP' }, - ] + { id: 'name', label: 'Name' }, + { id: 'cmd', label: 'CMD' }, + { id: 'priority', label: 'Priority' }, + { id: 'nlwp', label: 'NLWP' }, + ] : [ - { id: 'name', label: 'Name' }, - { id: 'euser', label: 'Effective user' }, - { id: 'nice', label: 'Priority' }, - { id: 'state', label: 'State' }, - ], + { id: 'name', label: 'Name' }, + { id: 'euser', label: 'Effective user' }, + { id: 'nice', label: 'Priority' }, + { id: 'state', label: 'State' }, + ], }, mapResponseItems: (item) => agentOs === 'windows' ? item : { ...item, state: ProcessEquivalence[item.state] }, @@ -969,18 +969,18 @@ export class WazuhReportingCtrl { columns: agentOs === 'windows' ? [ - { id: 'local_ip', label: 'Local IP address' }, - { id: 'local_port', label: 'Local port' }, - { id: 'process', label: 'Process' }, - { id: 'state', label: 'State' }, - { id: 'protocol', label: 'Protocol' }, - ] + { id: 'local_ip', label: 'Local IP address' }, + { id: 'local_port', label: 'Local port' }, + { id: 'process', label: 'Process' }, + { id: 'state', label: 'State' }, + { id: 'protocol', label: 'Protocol' }, + ] : [ - { id: 'local_ip', label: 'Local IP address' }, - { id: 'local_port', label: 'Local port' }, - { id: 'state', label: 'State' }, - { id: 'protocol', label: 'Protocol' }, - ], + { id: 'local_ip', label: 'Local IP address' }, + { id: 'local_port', label: 'Local port' }, + { id: 'state', label: 'State' }, + { id: 'protocol', label: 'Protocol' }, + ], }, mapResponseItems: (item) => ({ ...item, @@ -1062,6 +1062,15 @@ export class WazuhReportingCtrl { }; if (time) { + // Add Vulnerability Detector filter to the Server Side Query + serverSideQuery?.bool?.must?.push?.({ + match_phrase: { + "rule.groups": { + query: "vulnerability-detector" + } + } + }); + await extendedInformation( context, printer, @@ -1070,7 +1079,7 @@ export class WazuhReportingCtrl { apiId, from, to, - sanitizedFilters + ' AND rule.groups: "vulnerability-detector"', + serverSideQuery, agentsFilter, indexPatternTitle, agentID @@ -1095,7 +1104,7 @@ export class WazuhReportingCtrl { log('reporting:createReportsAgents', error.message || error); return ErrorResponse(error.message || error, 5029, 500, response); } - }, ({params: { agentID }}) => `wazuh-agent-inventory-${agentID}-${this.generateReportTimestamp()}.pdf`) + }, ({ params: { agentID } }) => `wazuh-agent-inventory-${agentID}-${this.generateReportTimestamp()}.pdf`) /** * Fetch the reports list @@ -1194,21 +1203,21 @@ export class WazuhReportingCtrl { log('reporting:deleteReportByName', error.message || error); return ErrorResponse(error.message || error, 5032, 500, response); } - },(request) => request.params.name) + }, (request) => request.params.name) - checkReportsUserDirectoryIsValidRouteDecorator(routeHandler, reportFileNameAccessor){ + checkReportsUserDirectoryIsValidRouteDecorator(routeHandler, reportFileNameAccessor) { return (async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory ) => { - try{ + try { const { username, hashUsername } = await context.wazuh.security.getCurrentUser(request, context); const userReportsDirectoryPath = path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername); const filename = reportFileNameAccessor(request); const pathFilename = path.join(userReportsDirectoryPath, filename); log('reporting:checkReportsUserDirectoryIsValidRouteDecorator', `Checking the user ${username}(${hashUsername}) can do actions in the reports file: ${pathFilename}`, 'debug'); - if(!pathFilename.startsWith(userReportsDirectoryPath) || pathFilename.includes('../')){ + if (!pathFilename.startsWith(userReportsDirectoryPath) || pathFilename.includes('../')) { log('security:reporting:checkReportsUserDirectoryIsValidRouteDecorator', `User ${username}(${hashUsername}) tried to access to a non user report file: ${pathFilename}`, 'warn'); return response.badRequest({ body: { @@ -1217,15 +1226,15 @@ export class WazuhReportingCtrl { }); }; log('reporting:checkReportsUserDirectoryIsValidRouteDecorator', 'Checking the user can do actions in the reports file', 'debug'); - return await routeHandler.bind(this)({...context, wazuhEndpointParams: { hashUsername, filename, pathFilename }}, request, response); - }catch(error){ + return await routeHandler.bind(this)({ ...context, wazuhEndpointParams: { hashUsername, filename, pathFilename } }, request, response); + } catch (error) { log('reporting:checkReportsUserDirectoryIsValidRouteDecorator', error.message || error); return ErrorResponse(error.message || error, 5040, 500, response); } }) } - private generateReportTimestamp(){ + private generateReportTimestamp() { return `${(Date.now() / 1000) | 0}`; } } diff --git a/plugins/main/server/lib/reporting/base-query.ts b/plugins/main/server/lib/reporting/base-query.ts index 09d1f35f50..7e67e541d8 100644 --- a/plugins/main/server/lib/reporting/base-query.ts +++ b/plugins/main/server/lib/reporting/base-query.ts @@ -9,45 +9,28 @@ * * Find more information about this on the LICENSE file. */ + +import { cloneDeep } from 'lodash'; + export function Base(pattern: string, filters: any, gte: number, lte: number, allowedAgentsFilter: any = null) { + const clonedFilter = cloneDeep(filters); + clonedFilter?.bool?.must?.push?.({ + range: { + timestamp: { + gte: gte, + lte: lte, + format: 'epoch_millis' + } + } + }); const base = { - // index: pattern, - from: 0, size: 500, aggs: {}, sort: [], script_fields: {}, - query: { - bool: { - must: [ - { - query_string: { - query: filters, - analyze_wildcard: true, - default_field: '*' - } - }, - { - range: { - timestamp: { - gte: gte, - lte: lte, - format: 'epoch_millis' - } - } - } - ], - must_not: [] - } - } + query: clonedFilter }; - //Add allowed agents filter - if(allowedAgentsFilter?.query?.bool){ - base.query.bool.minimum_should_match = allowedAgentsFilter.query.bool.minimum_should_match; - base.query.bool.should = allowedAgentsFilter.query.bool.should; - } - return base; } diff --git a/plugins/main/server/lib/reporting/extended-information.ts b/plugins/main/server/lib/reporting/extended-information.ts index a533abff0b..377ba9408c 100644 --- a/plugins/main/server/lib/reporting/extended-information.ts +++ b/plugins/main/server/lib/reporting/extended-information.ts @@ -24,7 +24,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; * @param {Array} ids ids of agents * @param {String} apiId API id */ - export async function buildAgentsTable(context, printer: ReportPrinter, agentIDs: string[], apiId: string, groupID: string = '') { +export async function buildAgentsTable(context, printer: ReportPrinter, agentIDs: string[], apiId: string, groupID: string = '') { const dateFormat = await context.core.uiSettings.client.get('dateFormat'); if ((!agentIDs || !agentIDs.length) && !groupID) return; log('reporting:buildAgentsTable', `${agentIDs.length} agents for API ${apiId}`, 'info'); @@ -32,7 +32,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; let agentsData = []; if (groupID) { let totalAgentsInGroup = null; - do{ + do { const { data: { data: { affected_items, total_affected_items } } } = await context.wazuh.api.client.asCurrentUser.request( 'GET', `/groups/${groupID}/agents`, @@ -46,7 +46,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; ); !totalAgentsInGroup && (totalAgentsInGroup = total_affected_items); agentsData = [...agentsData, ...affected_items]; - }while(agentsData.length < totalAgentsInGroup); + } while (agentsData.length < totalAgentsInGroup); } else { for (const agentID of agentIDs) { try { @@ -72,7 +72,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; } } - if(agentsData.length){ + if (agentsData.length) { // Print a table with agent/s information printer.addSimpleTable({ columns: [ @@ -96,7 +96,7 @@ import { getSettingDefaultValue } from '../../../common/services/settings'; } }), }); - }else if(!agentsData.length && groupID){ + } else if (!agentsData.length && groupID) { // For group reports when there is no agents in the group printer.addContent({ text: 'There are no agents in this group.', @@ -135,12 +135,12 @@ export async function extendedInformation( filters, allowedAgentsFilter, pattern = getSettingDefaultValue('pattern'), - agent = null + agent = null, ) { try { log( 'reporting:extendedInformation', - `Section ${section} and tab ${tab}, API is ${apiId}. From ${from} to ${to}. Filters ${filters}. Index pattern ${pattern}`, + `Section ${section} and tab ${tab}, API is ${apiId}. From ${from} to ${to}. Filters ${JSON.stringify(filters)}. Index pattern ${pattern}`, 'info' ); if (section === 'agents' && !agent) { @@ -181,7 +181,7 @@ export async function extendedInformation( return count ? `${count} of ${totalAgents} agents have ${vulnerabilitiesLevel.toLocaleLowerCase()} vulnerabilities.` : undefined; - } catch (error) {} + } catch (error) { } }) ) ).filter((vulnerabilitiesResponse) => vulnerabilitiesResponse); diff --git a/plugins/main/server/lib/reporting/gdpr-request.ts b/plugins/main/server/lib/reporting/gdpr-request.ts index e058804be2..26fa191c99 100644 --- a/plugins/main/server/lib/reporting/gdpr-request.ts +++ b/plugins/main/server/lib/reporting/gdpr-request.ts @@ -28,10 +28,6 @@ export const topGDPRRequirements = async ( allowedAgentsFilter, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.gdpr: exists')) { - const [head, tail] = filters.split('AND rule.gdpr: exists'); - filters = head + tail; - }; try { const base = {}; @@ -50,12 +46,6 @@ export const topGDPRRequirements = async ( } }); - base.query.bool.must.push({ - exists: { - field: 'rule.gdpr' - } - }); - const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, body: base @@ -86,10 +76,6 @@ export const getRulesByRequirement = async ( requirement, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.gdpr: exists')) { - const [head, tail] = filters.split('AND rule.gdpr: exists'); - filters = head + tail; - }; try { const base = {}; @@ -119,8 +105,13 @@ export const getRulesByRequirement = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + ` AND rule.gdpr: "${requirement}"`; + base.query.bool.filter.push({ + match_phrase: { + 'rule.gdpr': { + query: requirement + } + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, diff --git a/plugins/main/server/lib/reporting/pci-request.ts b/plugins/main/server/lib/reporting/pci-request.ts index 811d615561..65a39755c2 100644 --- a/plugins/main/server/lib/reporting/pci-request.ts +++ b/plugins/main/server/lib/reporting/pci-request.ts @@ -28,9 +28,6 @@ export const topPCIRequirements = async ( allowedAgentsFilter, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.pci_dss: exists')) { - filters = filters.replace('AND rule.pci_dss: exists', ''); - }; try { const base = {}; @@ -49,12 +46,6 @@ export const topPCIRequirements = async ( } }); - base.query.bool.must.push({ - exists: { - field: 'rule.pci_dss' - } - }); - const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, body: base @@ -100,9 +91,6 @@ export const getRulesByRequirement = async ( requirement, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.pci_dss: exists')) { - filters = filters.replace('AND rule.pci_dss: exists', ''); - }; try { const base = {}; @@ -132,11 +120,13 @@ export const getRulesByRequirement = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND rule.pci_dss: "' + - requirement + - '"'; + base.query.bool.filter.push({ + match_phrase: { + 'rule.pci_dss': { + query: requirement + } + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, @@ -154,7 +144,7 @@ export const getRulesByRequirement = async ( ) { return accum; }; - accum.push({ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key}); + accum.push({ ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key }); return accum; }, []); } catch (error) { diff --git a/plugins/main/server/lib/reporting/rootcheck-request.ts b/plugins/main/server/lib/reporting/rootcheck-request.ts index 0eede80de9..8318bbc22a 100644 --- a/plugins/main/server/lib/reporting/rootcheck-request.ts +++ b/plugins/main/server/lib/reporting/rootcheck-request.ts @@ -46,9 +46,11 @@ export const top5RootkitsDetected = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND "rootkit" AND "detected"'; + base.query?.bool?.must?.push({ + query_string: { + query: '"rootkit" AND "detected"' + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, @@ -97,9 +99,11 @@ export const agentsWithHiddenPids = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND "process" AND "hidden"'; + base.query?.bool?.must?.push({ + query_string: { + query: '"process" AND "hidden"' + } + }); // "aggregations": { "1": { "value": 1 } } const response = await context.core.opensearch.client.asCurrentUser.search({ @@ -126,7 +130,7 @@ export const agentsWithHiddenPids = async ( * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability * @returns {Array} */ -export const agentsWithHiddenPorts = async( +export const agentsWithHiddenPorts = async ( context, gte, lte, @@ -147,8 +151,11 @@ export const agentsWithHiddenPorts = async( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + ' AND "port" AND "hidden"'; + base.query?.bool?.must?.push({ + query_string: { + query: '"port" AND "hidden"' + } + }); // "aggregations": { "1": { "value": 1 } } const response = await context.core.opensearch.client.asCurrentUser.search({ diff --git a/plugins/main/server/lib/reporting/tsc-request.ts b/plugins/main/server/lib/reporting/tsc-request.ts index aa59d6f6bc..2d03c804b8 100644 --- a/plugins/main/server/lib/reporting/tsc-request.ts +++ b/plugins/main/server/lib/reporting/tsc-request.ts @@ -12,14 +12,14 @@ import { Base } from './base-query'; import { getSettingDefaultValue } from '../../../common/services/settings'; - /** - * Returns top 5 TSC requirements - * @param {Number} context Endpoint context - * @param {Number} gte Timestamp (ms) from - * @param {Number} lte Timestamp (ms) to - * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability - * @returns {Array} - */ +/** + * Returns top 5 TSC requirements + * @param {Number} context Endpoint context + * @param {Number} gte Timestamp (ms) from + * @param {Number} lte Timestamp (ms) to + * @param {String} filters E.g: cluster.name: wazuh AND rule.groups: vulnerability + * @returns {Array} + */ export const topTSCRequirements = async ( context, gte, @@ -28,9 +28,6 @@ export const topTSCRequirements = async ( allowedAgentsFilter, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.tsc: exists')) { - filters = filters.replace('AND rule.tsc: exists', ''); - }; try { const base = {}; @@ -49,12 +46,6 @@ export const topTSCRequirements = async ( } }); - base.query.bool.must.push({ - exists: { - field: 'rule.tsc' - } - }); - const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, body: base @@ -100,9 +91,6 @@ export const getRulesByRequirement = async ( requirement, pattern = getSettingDefaultValue('pattern') ) => { - if (filters.includes('rule.tsc: exists')) { - filters = filters.replace('AND rule.tsc: exists', ''); - }; try { const base = {}; @@ -132,11 +120,13 @@ export const getRulesByRequirement = async ( } }); - base.query.bool.must[0].query_string.query = - base.query.bool.must[0].query_string.query + - ' AND rule.tsc: "' + - requirement + - '"'; + base.query.bool.filter.push({ + match_phrase: { + 'rule.tsc': { + query: requirement + } + } + }); const response = await context.core.opensearch.client.asCurrentUser.search({ index: pattern, @@ -155,7 +145,7 @@ export const getRulesByRequirement = async ( ) { return accum; }; - accum.push({ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key}); + accum.push({ ruleID: bucket['3'].buckets[0].key, ruleDescription: bucket.key }); return accum; }, []); } catch (error) { diff --git a/plugins/main/server/routes/wazuh-reporting.test.ts b/plugins/main/server/routes/wazuh-reporting.test.ts index 034377cbeb..45a9482c24 100644 --- a/plugins/main/server/routes/wazuh-reporting.test.ts +++ b/plugins/main/server/routes/wazuh-reporting.test.ts @@ -10,20 +10,23 @@ import { WazuhReportingRoutes } from './wazuh-reporting'; import { WazuhUtilsCtrl } from '../controllers/wazuh-utils/wazuh-utils'; import md5 from 'md5'; import path from 'path'; -import { createDataDirectoryIfNotExists, createDirectoryIfNotExists } from '../lib/filesystem'; +import { + createDataDirectoryIfNotExists, + createDirectoryIfNotExists, +} from '../lib/filesystem'; import { WAZUH_DATA_CONFIG_APP_PATH, WAZUH_DATA_CONFIG_DIRECTORY_PATH, WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, WAZUH_DATA_LOGS_DIRECTORY_PATH, WAZUH_DATA_ABSOLUTE_PATH, - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH + WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, } from '../../common/constants'; import { execSync } from 'child_process'; import fs from 'fs'; jest.mock('../lib/reporting/extended-information', () => ({ - extendedInformation: jest.fn() + extendedInformation: jest.fn(), })); const USER_NAME = 'admin'; const loggingService = loggingSystemMock.create(); @@ -31,18 +34,19 @@ const logger = loggingService.get(); const context = { wazuh: { security: { - getCurrentUser: (request) => { + getCurrentUser: request => { // x-test-username header doesn't exist when the platform or plugin are running. // It is used to generate the output of this method so we can simulate the user // that does the request to the endpoint and is expected by the endpoint handlers // of the plugin. const username = request.headers['x-test-username']; - return { username, hashUsername: md5(username) } - } - } - } + return { username, hashUsername: md5(username) }; + }, + }, + }, }; -const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, context); +const enhanceWithContext = (fn: (...args: any[]) => any) => + fn.bind(null, context); let server, innerServer; // BEFORE ALL @@ -71,12 +75,24 @@ beforeAll(async () => { } as any; server = new HttpServer(loggingService, 'tests'); const router = new Router('', logger, enhanceWithContext); - const { registerRouter, server: innerServerTest, ...rest } = await server.setup(config); + const { + registerRouter, + server: innerServerTest, + ...rest + } = await server.setup(config); innerServer = innerServerTest; // Mock decorator - jest.spyOn(WazuhUtilsCtrl.prototype as any, 'routeDecoratorProtectedAdministratorRoleValidToken') - .mockImplementation((handler) => async (...args) => handler(...args)); + jest + .spyOn( + WazuhUtilsCtrl.prototype as any, + 'routeDecoratorProtectedAdministratorRoleValidToken', + ) + .mockImplementation( + handler => + async (...args) => + handler(...args), + ); // Register routes WazuhUtilsRoutes(router); @@ -124,11 +140,21 @@ describe('[endpoint] GET /reports', () => { // Create directories and file/s within directory. directories.forEach(({ username, files }) => { const hashUsername = md5(username); - createDirectoryIfNotExists(path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername)); + createDirectoryIfNotExists( + path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername), + ); if (files) { Array.from(Array(files).keys()).forEach(indexFile => { - console.log('Generating', username, indexFile) - fs.closeSync(fs.openSync(path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername, `report_${indexFile}.pdf`), 'w')); + fs.closeSync( + fs.openSync( + path.join( + WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, + hashUsername, + `report_${indexFile}.pdf`, + ), + 'w', + ), + ); }); } }); @@ -139,13 +165,16 @@ describe('[endpoint] GET /reports', () => { execSync(`rm -rf ${WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH}`); }); - it.each(directories)('get reports of $username. status response: $responseStatus', async ({ username, files }) => { - const response = await supertest(innerServer.listener) - .get(`/reports`) - .set('x-test-username', username) - .expect(200); - expect(response.body.reports).toHaveLength(files); - }); + it.each(directories)( + 'get reports of $username. status response: $responseStatus', + async ({ username, files }) => { + const response = await supertest(innerServer.listener) + .get(`/reports`) + .set('x-test-username', username) + .expect(200); + expect(response.body.reports).toHaveLength(files); + }, + ); }); describe('[endpoint] PUT /utils/configuration', () => { @@ -174,16 +203,33 @@ describe('[endpoint] PUT /utils/configuration', () => { // expectedMD5 variable is a verified md5 of a report generated with this header and footer // If any of the parameters is changed this variable should be updated with the new md5 it.each` - footer | header | responseStatusCode | expectedMD5 | tab - ${null} | ${null} | ${200} | ${'7b6fa0e2a5911880d17168800c173f89'} | ${'pm'} - ${'Custom\nFooter'} | ${'info@company.com\nFake Avenue 123'}| ${200} | ${'51b268066bb5107e5eb0a9d791a89d0c'} | ${'general'} - ${''} | ${''} | ${200} | ${'23d5e0eedce38dc6df9e98e898628f68'} | ${'fim'} - ${'Custom Footer'} | ${null} | ${200} | ${'2b16be2ea88d3891cda7acb6075826d9'} | ${'aws'} - ${null} | ${'Custom Header'} | ${200} | ${'91e30564f157942718afdd97db3b4ddf'} | ${'gcp'} -`(`Set custom report header and footer - Verify PDF output`, async ({footer, header, responseStatusCode, expectedMD5, tab}) => { - + footer | header | responseStatusCode | expectedMD5 | tab + ${null} | ${null} | ${200} | ${'a261be6b2e5fb18bb7434ee46a01e174'} | ${'pm'} + ${'Custom\nFooter'} | ${'info@company.com\nFake Avenue 123'} | ${200} | ${'51b268066bb5107e5eb0a9d791a89d0c'} | ${'general'} + ${''} | ${''} | ${200} | ${'8e8fbd90e08b810f700fcafbfdcdf638'} | ${'fim'} + ${'Custom Footer'} | ${null} | ${200} | ${'2b16be2ea88d3891cda7acb6075826d9'} | ${'aws'} + ${null} | ${'Custom Header'} | ${200} | ${'4a55136aaf8b5f6b544a03fe46917552'} | ${'gcp'} + `( + `Set custom report header and footer - Verify PDF output`, + async ({ footer, header, responseStatusCode, expectedMD5, tab }) => { // Mock PDF report parameters - const reportBody = { "array": [], "filters": [], "time": { "from": '2022-10-01T09:59:40.825Z', "to": '2022-10-04T09:59:40.825Z' }, "searchBar": "", "tables": [], "tab": tab, "section": "overview", "agents": false, "browserTimezone": "Europe/Madrid", "indexPatternTitle": "wazuh-alerts-*", "apiId": "default" }; + const reportBody = { + array: [], + serverSideQuery: [], + filters: [], + time: { + from: '2022-10-01T09:59:40.825Z', + to: '2022-10-04T09:59:40.825Z', + }, + searchBar: '', + tables: [], + tab: tab, + section: 'overview', + agents: false, + browserTimezone: 'Europe/Madrid', + indexPatternTitle: 'wazuh-alerts-*', + apiId: 'default', + }; // Define custom configuration const configurationBody = {}; @@ -203,10 +249,18 @@ describe('[endpoint] PUT /utils/configuration', () => { .expect(responseStatusCode); if (typeof footer == 'string') { - expect(responseConfig.body?.data?.updatedConfiguration?.['customization.reports.footer']).toMatch(configurationBody['customization.reports.footer']); + expect( + responseConfig.body?.data?.updatedConfiguration?.[ + 'customization.reports.footer' + ], + ).toMatch(configurationBody['customization.reports.footer']); } if (typeof header == 'string') { - expect(responseConfig.body?.data?.updatedConfiguration?.['customization.reports.header']).toMatch(configurationBody['customization.reports.header']); + expect( + responseConfig.body?.data?.updatedConfiguration?.[ + 'customization.reports.header' + ], + ).toMatch(configurationBody['customization.reports.header']); } } @@ -216,16 +270,19 @@ describe('[endpoint] PUT /utils/configuration', () => { .set('x-test-username', USER_NAME) .send(reportBody) .expect(200); - const fileName = responseReport.body?.message.match(/([A-Z-0-9]*\.pdf)/gi)[0]; + const fileName = + responseReport.body?.message.match(/([A-Z-0-9]*\.pdf)/gi)[0]; const userPath = md5(USER_NAME); const reportPath = `${WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH}/${userPath}/${fileName}`; const PDFbuffer = fs.readFileSync(reportPath); const PDFcontent = PDFbuffer.toString('utf8'); - const content = PDFcontent - .replace(/\[<[a-z0-9].+> <[a-z0-9].+>\]/gi, '') - .replace(/(obj\n\(D:[0-9].+Z\)\nendobj)/gi, ''); + const content = PDFcontent.replace( + /\[<[a-z0-9].+> <[a-z0-9].+>\]/gi, + '', + ).replace(/(obj\n\(D:[0-9].+Z\)\nendobj)/gi, ''); const PDFmd5 = md5(content); expect(PDFmd5).toBe(expectedMD5); - }); + }, + ); }); diff --git a/plugins/main/server/routes/wazuh-reporting.ts b/plugins/main/server/routes/wazuh-reporting.ts index 946e73ac5d..7f78a27458 100644 --- a/plugins/main/server/routes/wazuh-reporting.ts +++ b/plugins/main/server/routes/wazuh-reporting.ts @@ -55,30 +55,31 @@ export function WazuhReportingRoutes(router: IRouter) { ]); router.post({ - path: '/reports/modules/{moduleID}', - validate: { - body: schema.object({ - array: schema.any(), - browserTimezone: schema.string(), - filters: schema.maybe(schema.any()), - agents: schema.maybe(schema.oneOf([agentIDValidation, schema.boolean()])), - components: schema.maybe(schema.any()), - searchBar: schema.maybe(schema.string()), - section: schema.maybe(schema.string()), - tab: schema.string(), - tables: schema.maybe(schema.any()), - time: schema.oneOf([schema.object({ - from: schema.string(), - to: schema.string() - }), schema.string()]), - indexPatternTitle: schema.string(), - apiId: schema.string() - }), - params: schema.object({ - moduleID: moduleIDValidation - }) - } - }, + path: '/reports/modules/{moduleID}', + validate: { + body: schema.object({ + array: schema.any(), + browserTimezone: schema.string(), + serverSideQuery: schema.maybe(schema.any()), + filters: schema.maybe(schema.any()), + agents: schema.maybe(schema.oneOf([agentIDValidation, schema.boolean()])), + components: schema.maybe(schema.any()), + searchBar: schema.maybe(schema.string()), + section: schema.maybe(schema.string()), + tab: schema.string(), + tables: schema.maybe(schema.any()), + time: schema.oneOf([schema.object({ + from: schema.string(), + to: schema.string() + }), schema.string()]), + indexPatternTitle: schema.string(), + apiId: schema.string() + }), + params: schema.object({ + moduleID: moduleIDValidation + }) + } + }, (context, request, response) => ctrl.createReportsModules(context, request, response) ); @@ -124,6 +125,7 @@ export function WazuhReportingRoutes(router: IRouter) { body: schema.object({ array: schema.any(), browserTimezone: schema.string(), + serverSideQuery: schema.maybe(schema.any()), filters: schema.maybe(schema.any()), agents: schema.maybe(schema.oneOf([schema.string(), schema.boolean()])), components: schema.maybe(schema.any()), @@ -148,33 +150,33 @@ export function WazuhReportingRoutes(router: IRouter) { // Fetch specific report router.get({ - path: '/reports/{name}', - validate: { - params: schema.object({ - name: ReportFilenameValidation - }) - } - }, + path: '/reports/{name}', + validate: { + params: schema.object({ + name: ReportFilenameValidation + }) + } + }, (context, request, response) => ctrl.getReportByName(context, request, response) ); // Delete specific report router.delete({ - path: '/reports/{name}', - validate: { - params: schema.object({ - name: ReportFilenameValidation - }) - } - }, + path: '/reports/{name}', + validate: { + params: schema.object({ + name: ReportFilenameValidation + }) + } + }, (context, request, response) => ctrl.deleteReportByName(context, request, response) ) // Fetch the reports list router.get({ - path: '/reports', - validate: false - }, + path: '/reports', + validate: false + }, (context, request, response) => ctrl.getReports(context, request, response) ); }