diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7c5b2a1b..e7b05882b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,18 +31,20 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Wazuh 4.8.1 -## Wazuh v4.8.0 - OpenSearch Dashboards 2.10.0 - Revision 02 +## Wazuh v4.8.0 - OpenSearch Dashboards 2.10.0 - Revision 03 ### Added - Support for Wazuh 4.8.0 -- Added the ability to check if there are available updates from the UI. [#6093](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6093) [#6256](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6256) +- Added the ability to check if there are available updates from the UI. [#6093](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6093) [#6256](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6256) [#6328](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6328) - Added remember server address check [#5791](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5791) - Added the ssl_agent_ca configuration to the SSL Settings form [#6083](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6083) -- Added global vulnerabilities dashboards [#5896](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5896) [#6179](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6179) [#6173](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6173) [#6147](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6147) [#6231](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6231) [#6246](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6246) +- Added global vulnerabilities dashboards [#5896](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5896) [#6179](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6179) [#6173](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6173) [#6147](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6147) [#6231](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6231) [#6246](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6246) [#6321](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6321) [#6338](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6338) [#6356](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6356) - Added an agent selector to the IT Hygiene application [#5840](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5840) - Added query results limit when the search exceed 10000 hits [#6106](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6106) -- Added a redirection button to Endpoint Summary from IT Hygiene application [6176](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6176) +- Added a redirection button to Endpoint Summary from IT Hygiene application [#6176](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6176) +- Added information icon with tooltip on the most active agent in the endpoint summary view [#6364](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6364) +- Added a dash with a tooltip in the server APIs table when the run as is disabled [#6354](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6354) ### Changed @@ -50,6 +52,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed dashboards. [#6035](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6035) - Change the display order of tabs in all modules. [#6067](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6067) - Upgraded the `axios` dependency to `1.6.1` [#5062](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5062) +- Changed the api configuration title in the Server APIs section. [#6373](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6373) +- Changed overview home top KPIs. [#6379](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6379) ### Fixed @@ -59,6 +63,16 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed exception in Inventory when agents don't have OS information [#6177](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6177) - Fixed pinned agent state in URL [#6177](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6177) - Fixed invalid date format in about and agent views [#6234](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6234) +- Fixed script to install agents on macOS when you have password to deploy [#6305](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6305) +- Fixed a problem with the address validation on Deploy New Agent [#6327](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6327) +- Fixed a typo in an abbreviation for Fully Qualified Domain Name [#6333](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6333) +- Fixed server statistics when cluster mode is disabled [#6352](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6352) +- Fixed unnecessary scrolling in Vulnerability Inventory table [#6345](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6345) +- Fixed wrong value at server stat Archives queue usage [#6342](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6342) +- Fixed the inventory data table when maximized and the docked menu [#6344](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6344) +- Fixed implicit filter close button in the search bar [#6346](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6346) +- Fixed the help menu, to be consistent and avoid duplication [#6374](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6374) +- Fixed the axis label visual bug from dashboards [#6378](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6378) ### Removed @@ -66,6 +80,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed the ability to configure the visibility of modules and removed `extensions.*` settings [#5840](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5840) - Removed the application menu in the IT Hygiene application [#6176](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6176) - Removed the implicit filter of WQL language of the search bar UI [#6174](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6174) +- Removed notice of old Discover deprecation [#6341](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6341) +- Removed compilation date field from the app [#6366](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6366) +- Removed WAZUH_REGISTRATION_SERVER from Windows agent deployment command [#6361](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6361) ## Wazuh v4.7.2 - OpenSearch Dashboards 2.8.0 - Revision 02 diff --git a/docker/imposter/agents/agent_manager.json b/docker/imposter/agents/agent_manager.json new file mode 100644 index 0000000000..444c7b82f7 --- /dev/null +++ b/docker/imposter/agents/agent_manager.json @@ -0,0 +1,37 @@ +{ + "data": { + "affected_items": [ + { + "os": { + "arch": "x86_64", + "codename": "stretch", + "major": "9", + "name": "Debian GNU/Linux", + "platform": "debian", + "uname": "Linux |ip-10-0-1-106 |4.9.0-9-amd64 |#1 SMP Debian 4.9.168-1+deb9u2 (2019-05-13) |x86_64", + "version": "9" + }, + "ip": "FE80:0034:0223:A000:0002:B3FF:0000:8329", + "configSum": "6f4293818ef64291ca53727fb9ab8958", + "mergedSum": "7976a83d1aebcca09bc14459b5518ed5", + "id": "000", + "registerIP": "any", + "dateAdd": "2022-08-25T16:25:53Z", + "disconnection_time": "2022-08-25T16:36:35Z", + "name": "Debian", + "status": "active", + "manager": "wazuh-manager-master-0", + "node_name": "master", + "group": ["default", "debian"], + "lastKeepAlive": "2022-09-12T08:48:40Z", + "version": "Wazuh v4.3.7", + "status_code": 0 + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All selected agents information was returned", + "error": 0 +} diff --git a/docker/imposter/agents/agents.js b/docker/imposter/agents/agents.js index b624a57725..1a3cdff7f5 100644 --- a/docker/imposter/agents/agents.js +++ b/docker/imposter/agents/agents.js @@ -12,6 +12,9 @@ switch (agentId) { case undefined: respond().withStatusCode(200).withFile('agents/agents.json'); break; + case '000': + respond().withStatusCode(200).withFile('agents/agent_manager.json'); + break; case '001': respond().withStatusCode(200).withFile('agents/agent_active_groups.json'); break; diff --git a/docker/imposter/cluster/configuration/wmodules_wmodules.json b/docker/imposter/cluster/configuration/wmodules_wmodules.json index b36ce8feac..711991c94d 100644 --- a/docker/imposter/cluster/configuration/wmodules_wmodules.json +++ b/docker/imposter/cluster/configuration/wmodules_wmodules.json @@ -22,6 +22,7 @@ "interval": 86400, "java_path": "wodles/java", "ciscat_path": "wodles/ciscat", + "ciscat_binary": "CIS-CAT.sh", "timeout": 1800 } }, @@ -59,114 +60,19 @@ } }, { - "vulnerability-detector": { + "vulnerability-detection": { "enabled": "yes", - "run_on_start": "yes", - "interval": 300, - "min_full_scan_interval": 21600, - "retry_interval": 30, - "providers": [ - { - "name": "canonical", - "version": "TRUSTY", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "canonical", - "version": "XENIAL", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "canonical", - "version": "BIONIC", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "canonical", - "version": "FOCAL", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "debian", - "version": "STRETCH", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "debian", - "version": "BUSTER", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "redhat", - "version": "5", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "redhat", - "version": "6", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "redhat", - "version": "7", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "redhat", - "version": "8", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "jredhat", - "update_interval": 3600, - "download_timeout": 0 - }, - { - "name": "alas", - "version": "Amazon-Linux", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "test", - "version": "test", - "url": "https://test.com", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "arch", - "update_interval": 3600, - "download_timeout": 300 - }, - { - "name": "nvd", - "update_from_year": 2010, - "update_interval": 3600, - "download_timeout": 300 - } - ] + "feed-update-interval": "60m" + } + }, + { + "wazuh_control": { + "enabled": "yes" + } + }, + { + "wazuh_control": { + "enabled": "yes" } }, { diff --git a/docker/imposter/manager/configuration.js b/docker/imposter/manager/configuration.js index 9b5a87219d..02984110f2 100644 --- a/docker/imposter/manager/configuration.js +++ b/docker/imposter/manager/configuration.js @@ -13,6 +13,12 @@ switch (pathConfiguration[0]) { .withStatusCode(200) .withFile('manager/configuration/monitor_reports.json'); + break; + case 'wmodules': + respond() + .withStatusCode(200) + .withFile('manager/configuration/monitor_reports.json'); + break; default: respond() diff --git a/docker/imposter/manager/configuration/wmodules_wmodules.json b/docker/imposter/manager/configuration/wmodules_wmodules.json new file mode 100644 index 0000000000..5d511bde41 --- /dev/null +++ b/docker/imposter/manager/configuration/wmodules_wmodules.json @@ -0,0 +1,105 @@ +{ + "data": { + "affected_items": [ + { + "wmodules": [ + { + "agent-upgrade": { + "enabled": "yes", + "max_threads": 8, + "chunk_size": 512 + } + }, + { + "task-manager": { + "enabled": "yes" + } + }, + { + "cis-cat": { + "disabled": "yes", + "scan-on-start": "yes", + "interval": 86400, + "java_path": "wodles/java", + "ciscat_path": "wodles/ciscat", + "ciscat_binary": "CIS-CAT.sh", + "timeout": 1800 + } + }, + { + "osquery": { + "disabled": "yes", + "run_daemon": "yes", + "add_labels": "yes", + "log_path": "/var/log/osquery/osqueryd.results.log", + "config_path": "/etc/osquery/osquery.conf" + } + }, + { + "syscollector": { + "disabled": "no", + "scan-on-start": "yes", + "interval": 3600, + "network": "yes", + "os": "yes", + "hardware": "yes", + "packages": "yes", + "ports": "yes", + "ports_all": "no", + "processes": "yes", + "sync_max_eps": 10 + } + }, + { + "sca": { + "interval": 43200, + "enabled": "yes", + "scan_on_start": "yes", + "skip_nfs": "yes", + "policies": ["/var/ossec/ruleset/sca/cis_ubuntu20-04.yml"] + } + }, + { + "vulnerability-detection": { + "enabled": "yes", + "feed-update-interval": "60m" + } + }, + { + "wazuh_control": { + "enabled": "yes" + } + }, + { + "wazuh_control": { + "enabled": "yes" + } + }, + { + "database": { + "sync_agents": "yes", + "real_time": "yes", + "interval": 60, + "max_queued_events": 0 + } + }, + { + "wazuh_download": { + "enabled": "yes" + } + }, + { + "wazuh_control": { + "enabled": "yes" + } + } + ] + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "Active configuration was successfully read", + "error": 0 +} diff --git a/docker/integrations/extra/dashboards/Splunk/wazuh-pci-dss b/docker/integrations/extra/dashboards/Splunk/wazuh-pci-dss index 705a40d0f0..681acc9193 100644 --- a/docker/integrations/extra/dashboards/Splunk/wazuh-pci-dss +++ b/docker/integrations/extra/dashboards/Splunk/wazuh-pci-dss @@ -3,7 +3,6 @@ "viz_9NIbkgTo": { "type": "splunk.bubble", "options": { - "backgroundColor": "#ffffff", "xAxisTitleText": "timestamp", "yAxisTitleText": "count" }, @@ -134,4 +133,4 @@ }, "description": "", "title": "wazuh-pci-dss-v1.0" -} +} \ No newline at end of file diff --git a/docker/integrations/extra/dashboards/Splunk/wazuh-security-events b/docker/integrations/extra/dashboards/Splunk/wazuh-security-events index 7ddd19103c..7d5d033ab0 100644 --- a/docker/integrations/extra/dashboards/Splunk/wazuh-security-events +++ b/docker/integrations/extra/dashboards/Splunk/wazuh-security-events @@ -56,7 +56,6 @@ "viz_R8LMR6U6": { "type": "splunk.singlevalueradial", "options": { - "backgroundColor": "#ffffff", "majorColor": "#bf0561" }, "dataSources": { @@ -294,4 +293,4 @@ }, "description": "", "title": "wazuh-security-events-v1.0" -} +} \ No newline at end of file diff --git a/docker/osd-dev/config/wazuh_cluster/wazuh_manager.conf b/docker/osd-dev/config/wazuh_cluster/wazuh_manager.conf deleted file mode 100755 index aff1af9d6c..0000000000 --- a/docker/osd-dev/config/wazuh_cluster/wazuh_manager.conf +++ /dev/null @@ -1,353 +0,0 @@ - - - yes - yes - no - no - no - smtp.example.wazuh.com - wazuh@example.wazuh.com - recipient@example.wazuh.com - 12 - alerts.log - 10m - 0 - - - - 3 - 12 - - - - - plain - - - - secure - 1514 - tcp - 131072 - - - - - no - yes - yes - yes - yes - yes - yes - yes - - - 43200 - - etc/rootcheck/rootkit_files.txt - etc/rootcheck/rootkit_trojans.txt - - yes - - - - yes - 1800 - 1d - yes - - wodles/java - wodles/ciscat - - - - - yes - yes - /var/log/osquery/osqueryd.results.log - /etc/osquery/osquery.conf - yes - - - - - no - 1h - yes - yes - yes - yes - yes - yes - yes - - - - 10 - - - - - yes - yes - 12h - yes - - - - no - 5m - 6h - yes - - - - no - trusty - xenial - bionic - focal - 1h - - - - - no - stretch - buster - bullseye - 1h - - - - - no - 5 - 6 - 7 - 8 - 1h - - - - - no - amazon-linux - amazon-linux-2 - 1h - - - - - no - 1h - - - - - yes - 1h - - - - - yes - 2010 - 1h - - - - - - - no - - - 43200 - - yes - - - yes - - - no - - - /etc,/usr/bin,/usr/sbin - /bin,/sbin,/boot - - - /etc/mtab - /etc/hosts.deny - /etc/mail/statistics - /etc/random-seed - /etc/random.seed - /etc/adjtime - /etc/httpd/logs - /etc/utmpx - /etc/wtmpx - /etc/cups/certs - /etc/dumpdates - /etc/svc/volatile - - - .log$|.swp$ - - - /etc/ssl/private.key - - yes - yes - yes - yes - - - 10 - - - 100 - - - - yes - 5m - 1h - 10 - - - - - - 127.0.0.1 - ^localhost.localdomain$ - 10.0.0.106 - - - - disable-account - disable-account - yes - - - - restart-wazuh - restart-wazuh - - - - firewall-drop - firewall-drop - yes - - - - host-deny - host-deny - yes - - - - route-null - route-null - yes - - - - win_route-null - route-null.exe - yes - - - - netsh - netsh.exe - yes - - - - - - - command - df -P - 360 - - - - full_command - netstat -tulpn | sed 's/\([[:alnum:]]\+\)\ \+[[:digit:]]\+\ \+[[:digit:]]\+\ \+\(.*\):\([[:digit:]]*\)\ \+\([0-9\.\:\*]\+\).\+\ \([[:digit:]]*\/[[:alnum:]\-]*\).*/\1 \2 == \3 == \4 \5/' | sort -k 4 -g | sed 's/ == \(.*\) ==/:\1/' | sed 1,2d - netstat listening ports - 360 - - - - full_command - last -n 20 - 360 - - - - - ruleset/decoders - ruleset/rules - 0215-policy_rules.xml - etc/lists/audit-keys - etc/lists/amazon/aws-eventnames - etc/lists/security-eventchannel - - - etc/decoders - etc/rules - - - - yes - 1 - 64 - 15m - - - - - no - 1515 - no - yes - no - HIGH:!ADH:!EXP:!MD5:!RC4:!3DES:!CAMELLIA:@STRENGTH - - no - etc/sslmanager.cert - etc/sslmanager.key - no - - - - wazuh - node01 - master - - 1516 - 0.0.0.0 - - NODE_IP - - no - yes - - - - - - - syslog - /var/ossec/logs/active-responses.log - - - diff --git a/docker/osd-dev/dev.yml b/docker/osd-dev/dev.yml index c610575eda..efdf6553bd 100755 --- a/docker/osd-dev/dev.yml +++ b/docker/osd-dev/dev.yml @@ -358,7 +358,6 @@ services: - API_PASSWORD=MyS3cr37P450r.*- volumes: - wm_certs:/etc/ssl/wazuh - - ./config/wazuh_cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf ports: - '514:514' - '1514:1514' diff --git a/plugins/main/common/api-info/endpoints.json b/plugins/main/common/api-info/endpoints.json index 11c29300f4..ac58e3d7d8 100644 --- a/plugins/main/common/api-info/endpoints.json +++ b/plugins/main/common/api-info/endpoints.json @@ -1181,7 +1181,6 @@ "socket", "syscheck", "syslog_output", - "vulnerability-detection", "indexer", "aws-s3", "azure-logs", @@ -4846,7 +4845,6 @@ "socket", "syscheck", "syslog_output", - "vulnerability-detection", "indexer", "aws-s3", "azure-logs", diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 8fcce9ea44..d1a9ea4fd3 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -815,7 +815,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Enable or disable the vulnerabilities index pattern health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, - defaultValue: true, + defaultValue: false, isConfigurableFromFile: true, isConfigurableFromUI: true, options: { diff --git a/plugins/main/common/wazuh-modules.ts b/plugins/main/common/wazuh-modules.ts index 35b0221bfe..c7b1307849 100644 --- a/plugins/main/common/wazuh-modules.ts +++ b/plugins/main/common/wazuh-modules.ts @@ -114,20 +114,20 @@ export const WAZUH_MODULES = { syscollector: { title: 'Inventory data', // This appId is not used, for consistency was added. - appId: 'it-hygiene', + appId: 'endpoint-summary', description: 'Applications, network configuration, open ports and processes running on your monitored systems.', }, stats: { title: 'Stats', // This appId is not used, for consistency was added. - appId: 'it-hygiene', + appId: 'endpoint-summary', description: 'Stats for agent and logcollector', }, configuration: { title: 'Configuration', // This appId is not used, for consistency was added. - appId: 'it-hygiene', + appId: 'endpoint-summary', description: 'Check the current agent configuration remotely applied by its group.', }, diff --git a/plugins/main/opensearch_dashboards.json b/plugins/main/opensearch_dashboards.json index ba297ced28..d2beb8d5a5 100644 --- a/plugins/main/opensearch_dashboards.json +++ b/plugins/main/opensearch_dashboards.json @@ -2,9 +2,7 @@ "id": "wazuh", "version": "4.9.0-00", "opensearchDashboardsVersion": "opensearchDashboards", - "configPath": [ - "wazuh" - ], + "configPath": ["wazuh"], "requiredPlugins": [ "navigation", "data", @@ -31,4 +29,4 @@ ], "server": true, "ui": true -} \ No newline at end of file +} diff --git a/plugins/main/package.json b/plugins/main/package.json index f047252e2f..9f1cc623d6 100644 --- a/plugins/main/package.json +++ b/plugins/main/package.json @@ -88,4 +88,4 @@ "redux-mock-store": "^1.5.4", "swagger-client": "^3.19.11" } -} \ No newline at end of file +} diff --git a/plugins/main/public/app.js b/plugins/main/public/app.js index 6fdb2de6be..f8e23658ea 100644 --- a/plugins/main/public/app.js +++ b/plugins/main/public/app.js @@ -48,7 +48,6 @@ import { updateCurrentPlatform } from './redux/actions/appStateActions'; import { WzAuthentication, loadAppConfig } from './react-services'; import { getAngularModule, getHttp } from './kibana-services'; -import { addHelpMenuToAppChrome } from './utils'; const app = getAngularModule(); @@ -78,7 +77,7 @@ app.run([ .then(item => { store.dispatch(updateCurrentPlatform(item)); }) - .catch(() => {}); + .catch(() => { }); // Init the process of refreshing the user's token when app start. checkPluginVersion().finally(WzAuthentication.refresh); @@ -102,8 +101,6 @@ app.run(function ($rootElement) { `); - // Add plugin help links as extension to plugin platform help menu - addHelpMenuToAppChrome(); // Bind deleteExistentToken on Log out component. $('.euiHeaderSectionItem__button, .euiHeaderSectionItemButton').on( diff --git a/plugins/main/public/components/add-modules-data/add-modules-data-main.tsx b/plugins/main/public/components/add-modules-data/add-modules-data-main.tsx index 9fdba4d3db..ee490a2033 100644 --- a/plugins/main/public/components/add-modules-data/add-modules-data-main.tsx +++ b/plugins/main/public/components/add-modules-data/add-modules-data-main.tsx @@ -177,6 +177,6 @@ class WzAddModulesData extends Component< export default compose( withGlobalBreadcrumb(props => { - return [{ text: sampleData.title }]; + return [{ text: sampleData.breadcrumbLabel }]; }), )(WzAddModulesData); diff --git a/plugins/main/public/components/agents/stats/agent-stats.tsx b/plugins/main/public/components/agents/stats/agent-stats.tsx index 4f4e8b462b..a77b125b56 100644 --- a/plugins/main/public/components/agents/stats/agent-stats.tsx +++ b/plugins/main/public/components/agents/stats/agent-stats.tsx @@ -46,7 +46,8 @@ import { UI_LOGGER_LEVELS, } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -import { itHygiene } from '../../../utils/applications'; +import { endpointSummary } from '../../../utils/applications'; +import { getCore } from '../../../kibana-services'; const tableColumns = [ { @@ -106,7 +107,10 @@ export const MainAgentStats = compose( withReduxProvider, withGlobalBreadcrumb(({ agent }) => [ { - text: itHygiene.title, + text: endpointSummary.breadcrumbLabel, + href: getCore().application.getUrlForApp(endpointSummary.id, { + path: `#/agents-preview`, + }), }, { agent }, { diff --git a/plugins/main/public/components/agents/syscollector/main.tsx b/plugins/main/public/components/agents/syscollector/main.tsx index a3234947cf..cdec38ef3e 100644 --- a/plugins/main/public/components/agents/syscollector/main.tsx +++ b/plugins/main/public/components/agents/syscollector/main.tsx @@ -18,7 +18,8 @@ import { } from '../../common/hocs'; import { SyscollectorInventory } from './inventory'; import { compose } from 'redux'; -import { itHygiene } from '../../../utils/applications'; +import { endpointSummary } from '../../../utils/applications'; +import { getCore } from '../../../kibana-services'; export const MainSyscollector = compose( withReduxProvider, @@ -26,7 +27,10 @@ export const MainSyscollector = compose( withGlobalBreadcrumb(({ agent }) => { return [ { - text: itHygiene.title, + text: endpointSummary.breadcrumbLabel, + href: getCore().application.getUrlForApp(endpointSummary.id, { + path: `#/agents-preview`, + }), }, { agent }, { diff --git a/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx b/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx index bd36df61c0..c7ce052e16 100644 --- a/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx +++ b/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx @@ -9,141 +9,164 @@ const COLLAPSE_LINE_LENGTH = 350; const DOT_PREFIX_RE = /(.).+?\./g; export type tDocViewerProps = { - flattened: any; - formatted: any; - mapping: any; - indexPattern: any; -} + flattened: any; + formatted: any; + mapping: any; + indexPattern: any; +}; /** * Convert a dot.notated.string into a short * version (d.n.string) */ -export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); +export const shortenDottedString = (input: string) => + input.replace(DOT_PREFIX_RE, '$1.'); export const getFieldTypeName = (type: string) => { - switch (type) { - case 'boolean': - return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { - defaultMessage: 'Boolean field', - }); - case 'conflict': - return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { - defaultMessage: 'Conflicting field', - }); - case 'date': - return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { - defaultMessage: 'Date field', - }); - case 'geo_point': - return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { - defaultMessage: 'Geo point field', - }); - case 'geo_shape': - return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { - defaultMessage: 'Geo shape field', - }); - case 'ip': - return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { - defaultMessage: 'IP address field', - }); - case 'murmur3': - return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { - defaultMessage: 'Murmur3 field', - }); - case 'number': - return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { - defaultMessage: 'Number field', - }); - case 'source': - // Note that this type is currently not provided, type for _source is undefined - return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { - defaultMessage: 'Source field', - }); - case 'string': - return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { - defaultMessage: 'String field', - }); - case 'nested': - return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { - defaultMessage: 'Nested field', - }); - default: - return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', { - defaultMessage: 'Unknown field', - }); - } -} + switch (type) { + case 'boolean': + return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { + defaultMessage: 'Boolean field', + }); + case 'conflict': + return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { + defaultMessage: 'Conflicting field', + }); + case 'date': + return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { + defaultMessage: 'Date field', + }); + case 'geo_point': + return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { + defaultMessage: 'Geo point field', + }); + case 'geo_shape': + return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { + defaultMessage: 'Geo shape field', + }); + case 'ip': + return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { + defaultMessage: 'IP address field', + }); + case 'murmur3': + return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { + defaultMessage: 'Murmur3 field', + }); + case 'number': + return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { + defaultMessage: 'Number field', + }); + case 'source': + // Note that this type is currently not provided, type for _source is undefined + return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { + defaultMessage: 'Source field', + }); + case 'string': + return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { + defaultMessage: 'String field', + }); + case 'nested': + return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { + defaultMessage: 'Nested field', + }); + default: + return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', { + defaultMessage: 'Unknown field', + }); + } +}; const DocViewer = (props: tDocViewerProps) => { - const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); - const { flattened, formatted, mapping, indexPattern } = props; + const [fieldRowOpen, setFieldRowOpen] = useState( + {} as Record, + ); + const { flattened, formatted, mapping, indexPattern } = props; - return (<> - {flattened && ( - - - {Object.keys(flattened) - .sort() - .map((field, index) => { - const value = String(formatted[field]); - const fieldMapping = mapping(field); - const isCollapsible = value.length > COLLAPSE_LINE_LENGTH; - const isCollapsed = isCollapsible && !fieldRowOpen[field]; - const valueClassName = classNames({ - // eslint-disable-next-line @typescript-eslint/naming-convention - osdDocViewer__value: true, - 'truncate-by-height': isCollapsible && isCollapsed, - }); - const isNestedField = - !indexPattern.fields.getByName(field) && - !!indexPattern.fields.getAll().find((patternField) => { - // We only want to match a full path segment - const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)'); - return nestedRootRegex.test(patternField.subType?.nested?.path ?? ''); - }); - const fieldType = isNestedField ? 'nested' : indexPattern.fields.getByName(field)?.type; - const typeName = getFieldTypeName(String(fieldType)); - const displayName = field; - const fieldIconProps = { fill: 'none', color: 'gray' } - const scripted = Boolean(fieldMapping?.scripted) + return ( + <> + {flattened && ( +
+ + {Object.keys(flattened) + .sort() + .map((field, index) => { + const value = String(formatted[field]); + const fieldMapping = mapping(field); + const isCollapsible = value.length > COLLAPSE_LINE_LENGTH; + const isCollapsed = isCollapsible && !fieldRowOpen[field]; + const valueClassName = classNames({ + // eslint-disable-next-line @typescript-eslint/naming-convention + osdDocViewer__value: true, + 'truncate-by-height': isCollapsible && isCollapsed, + }); + const isNestedField = + !indexPattern.fields.getByName(field) && + !!indexPattern.fields.getAll().find(patternField => { + // We only want to match a full path segment + const nestedRootRegex = new RegExp( + escapeRegExp(field) + '(\\.|$)', + ); + return nestedRootRegex.test( + patternField.subType?.nested?.path ?? '', + ); + }); + const fieldType = isNestedField + ? 'nested' + : indexPattern.fields.getByName(field)?.type; + const typeName = getFieldTypeName(String(fieldType)); + const displayName = field; + const fieldIconProps = { fill: 'none', color: 'gray' }; + const scripted = Boolean(fieldMapping?.scripted); - return ( - - - - - ); - })} - -
- - - - - - - {displayName} - - - - -
-
- )}) + return ( + + + + + + + + + {displayName} + + + + + +
+ + + ); + })} + + + )} + + ); }; -export default DocViewer; \ No newline at end of file +export default DocViewer; diff --git a/plugins/main/public/components/common/globalBreadcrumb/platformBreadcrumb.tsx b/plugins/main/public/components/common/globalBreadcrumb/platformBreadcrumb.tsx index 18fc1578af..1c5a5ee8b4 100644 --- a/plugins/main/public/components/common/globalBreadcrumb/platformBreadcrumb.tsx +++ b/plugins/main/public/components/common/globalBreadcrumb/platformBreadcrumb.tsx @@ -1,5 +1,5 @@ import { getCore } from '../../../kibana-services'; -import { itHygiene } from '../../../utils/applications'; +import { endpointSummary } from '../../../utils/applications'; export const setBreadcrumbs = (breadcrumbs, router) => { if (breadcrumbs === '' || breadcrumbs === undefined) { @@ -12,7 +12,7 @@ export const setBreadcrumbs = (breadcrumbs, router) => { 'euiLink euiLink--subdued osdBreadcrumbs wz-vertical-align-middle', onClick: ev => { ev.stopPropagation(); - getCore().application.navigateToApp(itHygiene.id, { + getCore().application.navigateToApp(endpointSummary.id, { path: `#/agents?tab=welcome&agent=${breadcrumb.agent.id}`, }); router.reload(); diff --git a/plugins/main/public/components/common/modules/events-enhance-discover-fields.ts b/plugins/main/public/components/common/modules/events-enhance-discover-fields.ts index 9c0a70771d..e5e5f5fc97 100644 --- a/plugins/main/public/components/common/modules/events-enhance-discover-fields.ts +++ b/plugins/main/public/components/common/modules/events-enhance-discover-fields.ts @@ -121,7 +121,7 @@ export const EventsEnhanceDiscoverCell = { ...buttonStyles, href: content => content !== '000' - ? getCore().application.getUrlForApp('it-hygiene', { + ? getCore().application.getUrlForApp('endpoints-summary', { path: `#/agents?tab=welcome&agent=${content}`, }) : undefined, @@ -131,7 +131,7 @@ export const EventsEnhanceDiscoverCell = { href: (content, rowData) => { const agentId = (((rowData || {})._source || {}).agent || {}).id; return agentId !== '000' - ? getCore().application.getUrlForApp('it-hygiene', { + ? getCore().application.getUrlForApp('endpoints-summary', { path: `#/agents?tab=welcome&agent=${agentId}`, }) : undefined; diff --git a/plugins/main/public/components/common/modules/main-agent.tsx b/plugins/main/public/components/common/modules/main-agent.tsx index d4b88b6d4a..6baa5d2ab9 100644 --- a/plugins/main/public/components/common/modules/main-agent.tsx +++ b/plugins/main/public/components/common/modules/main-agent.tsx @@ -25,10 +25,10 @@ import { AppState } from '../../../react-services/app-state'; import { ReportingService } from '../../../react-services/reporting'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { AgentInfo } from '../../common/welcome/agents-info'; -import { getAngularModule } from '../../../kibana-services'; +import { getAngularModule, getCore } from '../../../kibana-services'; import { compose } from 'redux'; import { withGlobalBreadcrumb } from '../hocs'; -import { itHygiene } from '../../../utils/applications'; +import { endpointSummary } from '../../../utils/applications'; export class MainModuleAgent extends Component { props!: { @@ -226,11 +226,22 @@ export class MainModuleAgent extends Component { export default compose( withGlobalBreadcrumb(({ agent, section }) => { if (section === 'welcome') { - return [{ text: itHygiene.title }, { text: agent.id }]; + return [ + { + text: endpointSummary.breadcrumbLabel, + href: getCore().application.getUrlForApp(endpointSummary.id, { + path: `#/agents-preview`, + }), + }, + { text: agent.id }, + ]; } else { return [ { - text: itHygiene.title, + text: endpointSummary.breadcrumbLabel, + href: getCore().application.getUrlForApp(endpointSummary.id, { + path: `#/agents-preview`, + }), }, { agent: agent }, { diff --git a/plugins/main/public/components/common/modules/modules-defaults.js b/plugins/main/public/components/common/modules/modules-defaults.js index 6cfbdc17bb..372fa8fdcd 100644 --- a/plugins/main/public/components/common/modules/modules-defaults.js +++ b/plugins/main/public/components/common/modules/modules-defaults.js @@ -181,12 +181,14 @@ export const ModulesDefaults = { { id: 'dashboard', name: 'Dashboard', - component: withModuleNotForAgent(DashboardVuls), + component: DashboardVuls, + buttons: [ButtonModuleExploreAgent], }, { id: 'inventory', name: 'Inventory', - component: withModuleNotForAgent(InventoryVuls), + component: InventoryVuls, + buttons: [ButtonModuleExploreAgent], }, { ...renderDiscoverTab(ALERTS_INDEX_PATTERN, vulnerabilitiesColumns), @@ -199,7 +201,7 @@ export const ModulesDefaults = { }, ], buttons: ['settings'], - availableFor: ['manager'], + availableFor: ['manager', 'agent'], }, mitre: { init: 'dashboard', diff --git a/plugins/main/public/components/common/modules/modules-helper.js b/plugins/main/public/components/common/modules/modules-helper.js index 4482664952..91ebc10f87 100644 --- a/plugins/main/public/components/common/modules/modules-helper.js +++ b/plugins/main/public/components/common/modules/modules-helper.js @@ -1,11 +1,13 @@ import { getAngularModule, getDataPlugin } from '../../../kibana-services'; +import { AppState } from '../../../react-services/app-state'; +import { FilterHandler } from '../../../utils/filter-handler'; export class ModulesHelper { static async getDiscoverScope() { const $injector = getAngularModule().$injector; const location = $injector.get('$location'); const initialTab = location.search().tab; - return new Promise((resolve) => { + return new Promise(resolve => { const checkExist = setInterval(() => { const app = getAngularModule(); if (app.discoverScope) { @@ -21,9 +23,11 @@ export class ModulesHelper { } static async cleanAvailableFields() { - const fields = document.querySelectorAll(`.dscFieldChooser .dscFieldList--unpopular li`); + const fields = document.querySelectorAll( + `.dscFieldChooser .dscFieldList--unpopular li`, + ); if (fields.length) { - fields.forEach((field) => { + fields.forEach(field => { const attr = field.getAttribute('data-attr-field'); if (attr.startsWith('_')) { field.style.display = 'none'; @@ -36,53 +40,133 @@ export class ModulesHelper { this.activeNoImplicitsFilters(); }; - static activeNoImplicitsFilters() { + static async activeNoImplicitsFilters(generatedImplicitFilters) { const { filterManager } = getDataPlugin().query; - const implicitFilters = filterManager.getFilters().filter((x) => { - return x.$state.isImplicit; - }); - if (!(implicitFilters || []).length) { + const filters = filterManager.getFilters(); + const implicitFilters = generatedImplicitFilters + ? generatedImplicitFilters + : filters.filter(filter => filter.$state.isImplicit); + + /* + Since the OSD filter definition does not include the "isImplicit" attribute that Wazuh adds, there may be cases where the "isImplicit" attribute is lost, since any action regarding filters that is done with the filterManager ( addFilters, setFilters, setGlobalFilters, setAppFilters) does a mapAndFlattenFilters mapping to the filters that removes any attributes that are not part of the filter definition. +If this case happens, the implicit filters are regenerated and the function is called again with the generated implicit filters. + */ + if (implicitFilters.length === 0) { + const generatedImplicitFilters = ModulesHelper.getImplicitFilter(); setTimeout(() => { - this.activeNoImplicitsFilters(); + this.activeNoImplicitsFilters(generatedImplicitFilters); }, 100); + return; } - // With the filter classes decide if they are from the module view or not + + this.processFilters(implicitFilters); + } + + static processFilters(filters) { const allFilters = $(`.globalFilterItem .euiBadge__childButton`); - for (let i = 0; i < allFilters.length; i++) { - const data = allFilters[i].attributes['data-test-subj']; - let found = false; - (implicitFilters || []).forEach((moduleFilter) => { - // Checks if the filter is already in use - // Check which of the filters are from the module view and which are not pinned filters - if (!moduleFilter.used) { - const objKey = moduleFilter.query?.match - ? Object.keys(moduleFilter.query.match)[0] - : moduleFilter.meta.key; - const objValue = moduleFilter.query?.match - ? moduleFilter.query.match[objKey].query - : moduleFilter.meta.value; - const key = `filter-key-${objKey}`; - const value = `filter-value-${objValue}`; - - const noExcludedValues = - !data.value.includes('filter-pinned') && !data.value.includes('filter-negated'); - const acceptedValues = data.value.includes(key) && data.value.includes(value); - - if (acceptedValues && noExcludedValues) { - found = true; - moduleFilter.used = true; - } + + allFilters.each((_index, filter) => { + const data = filter.attributes['data-test-subj']; + + /* With the filter classes decide if they are from the module view or not */ + const isImplicitFilter = this.checkFilterValues(data, filters); + + this.updateFilterState(isImplicitFilter, filter); + }); + } + + static checkFilterValues(data, filters) { + /* + Checks if the filter is already in use + Check which of the filters are from the module view and which are not pinned filters + */ + for (const moduleFilter of filters) { + if (!moduleFilter.used) { + const { objKey, objValue } = this.extractKeyAndValue(moduleFilter); + + const noExcludedValues = + !data.value.includes('filter-pinned') && + !data.value.includes('filter-negated'); + const acceptedValues = + data.value.includes(`filter-key-${objKey}`) && + data.value.includes(`filter-value-${objValue}`); + + if (acceptedValues && noExcludedValues) { + moduleFilter.used = true; + return true; } - }); - if (!found) { - $(allFilters[i]).siblings('.euiBadge__iconButton').removeClass('hide-close-button'); - $(allFilters[i]).off('click'); - } else { - $(allFilters[i]).siblings('.euiBadge__iconButton').addClass('hide-close-button'); - $(allFilters[i]).on('click', (ev) => { - ev.stopPropagation(); - }); } } + return false; + } + + static extractKeyAndValue(moduleFilter) { + const objKey = moduleFilter.query?.match + ? Object.keys(moduleFilter.query.match)[0] + : moduleFilter.meta.key; + const objValue = moduleFilter.query?.match + ? moduleFilter.query.match[objKey].query + : moduleFilter.meta.value; + + return { objKey, objValue }; + } + + static updateFilterState(isImplicitFilter, filter) { + const closeButton = $(filter).siblings('.euiBadge__iconButton'); + if (!isImplicitFilter) { + closeButton.removeClass('hide-close-button'); + $(filter).off('click'); + } else { + closeButton.addClass('hide-close-button'); + $(filter).on('click', ev => ev.stopPropagation()); + } + } + + static getImplicitFilter() { + const tabFilters = { + general: { group: '' }, + welcome: { group: '' }, + fim: { group: 'syscheck' }, + pm: { group: 'rootcheck' }, + vuls: { group: 'vulnerability-detector' }, + oscap: { group: 'oscap' }, + ciscat: { group: 'ciscat' }, + audit: { group: 'audit' }, + pci: { group: 'pci_dss' }, + gdpr: { group: 'gdpr' }, + hipaa: { group: 'hipaa' }, + nist: { group: 'nist' }, + tsc: { group: 'tsc' }, + aws: { group: 'amazon' }, + gcp: { group: 'gcp' }, + office: { group: 'office365' }, + virustotal: { group: 'virustotal' }, + osquery: { group: 'osquery' }, + sca: { group: 'sca' }, + docker: { group: 'docker' }, + github: { group: 'github' }, + }; + const filterHandler = new FilterHandler(AppState.getCurrentPattern()); + const filters = []; + const isCluster = AppState.getClusterInfo().status == 'enabled'; + filters.push( + filterHandler.managerQuery( + isCluster + ? AppState.getClusterInfo().cluster + : AppState.getClusterInfo().manager, + isCluster, + ), + ); + filters.push(filterHandler.pciQuery()); + filters.push(filterHandler.gdprQuery()); + filters.push(filterHandler.hipaaQuery()); + filters.push(filterHandler.nistQuery()); + filters.push(filterHandler.tscQuery()); + filters.push(filterHandler.mitreQuery()); + Object.keys(tabFilters).forEach(tab => { + filters.push(filterHandler.ruleGroupQuery(tabFilters[tab].group)); + }); + + return filters; } } diff --git a/plugins/main/public/components/common/modules/overview-current-section.tsx b/plugins/main/public/components/common/modules/overview-current-section.tsx index f83b4b2425..28f3eb91d6 100644 --- a/plugins/main/public/components/common/modules/overview-current-section.tsx +++ b/plugins/main/public/components/common/modules/overview-current-section.tsx @@ -29,7 +29,7 @@ class WzCurrentOverviewSection extends Component { const section = Applications.find( ({ id }) => getWzCurrentAppID() === id, - )?.title; + )?.breadcrumbLabel; if (section) { const breadcrumb = currentAgent.id diff --git a/plugins/main/public/components/common/search-bar/use-search-bar.test.ts b/plugins/main/public/components/common/search-bar/use-search-bar.test.ts index 4cef6433b7..82c1caccf7 100644 --- a/plugins/main/public/components/common/search-bar/use-search-bar.test.ts +++ b/plugins/main/public/components/common/search-bar/use-search-bar.test.ts @@ -1,7 +1,10 @@ import { renderHook } from '@testing-library/react-hooks'; import '@testing-library/jest-dom/extend-expect'; // osd dependencies -import { Start, dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { + Start, + dataPluginMock, +} from '../../../../../../src/plugins/data/public/mocks'; import { Filter, IndexPattern, @@ -13,6 +16,7 @@ import useSearchBar from './use-search-bar'; import { getDataPlugin } from '../../../kibana-services'; import * as timeFilterHook from '../hooks/use-time-filter'; import * as queryManagerHook from '../hooks/use-query'; +import { AppState } from '../../../react-services/app-state'; /** * Mocking Data Plugin @@ -41,7 +45,7 @@ mockedGetDataPlugin.mockImplementation( }, }, }, - } as Start) + } as Start), ); /////////////////////////////////////////////////////////// @@ -74,6 +78,9 @@ describe('[hook] useSearchBarConfiguration', () => { }); it('should return default app index pattern when not receiving a default index pattern', async () => { + jest + .spyOn(AppState, 'getCurrentPattern') + .mockImplementation(() => 'default-index-pattern'); jest .spyOn(mockDataPlugin.indexPatterns, 'getDefault') .mockResolvedValue(mockedDefaultIndexPatternData); @@ -95,6 +102,10 @@ describe('[hook] useSearchBarConfiguration', () => { id: exampleIndexPatternId, title: '', }; + jest + .spyOn(AppState, 'getCurrentPattern') + .mockImplementation(() => 'wazuh-alerts-*'); + jest.spyOn(AppState, 'setCurrentPattern').mockImplementation(jest.fn()); jest .spyOn(mockDataPlugin.indexPatterns, 'get') .mockResolvedValue(mockedIndexPatternData); @@ -114,6 +125,10 @@ describe('[hook] useSearchBarConfiguration', () => { it('should show an ERROR message and get the default app index pattern when not found the index pattern data by the ID received', async () => { const INDEX_NOT_FOUND_ERROR = new Error('Index Pattern not found'); + jest + .spyOn(AppState, 'getCurrentPattern') + .mockImplementation(() => 'default-index-pattern'); + jest.spyOn(AppState, 'setCurrentPattern').mockImplementation(jest.fn()); jest.spyOn(mockDataPlugin.indexPatterns, 'get').mockImplementation(() => { throw INDEX_NOT_FOUND_ERROR; }); @@ -156,6 +171,10 @@ describe('[hook] useSearchBarConfiguration', () => { }, }, ]; + jest + .spyOn(AppState, 'getCurrentPattern') + .mockImplementation(() => 'default-index-pattern'); + jest.spyOn(AppState, 'setCurrentPattern').mockImplementation(jest.fn()); jest .spyOn(mockDataPlugin.indexPatterns, 'getDefault') .mockResolvedValue(mockedDefaultIndexPatternData); @@ -184,6 +203,10 @@ describe('[hook] useSearchBarConfiguration', () => { id: exampleIndexPatternId, title: '', }; + jest + .spyOn(AppState, 'getCurrentPattern') + .mockImplementation(() => exampleIndexPatternId); + jest.spyOn(AppState, 'setCurrentPattern').mockImplementation(jest.fn()); jest .spyOn(mockDataPlugin.indexPatterns, 'get') .mockResolvedValue(mockedExampleIndexPatternData); @@ -204,4 +227,4 @@ describe('[hook] useSearchBarConfiguration', () => { ]); expect(result.current.searchBarProps.filters).toStrictEqual([]); }); -}); \ No newline at end of file +}); diff --git a/plugins/main/public/components/common/search-bar/use-search-bar.ts b/plugins/main/public/components/common/search-bar/use-search-bar.ts index 9a5f3e7dee..40a7488210 100644 --- a/plugins/main/public/components/common/search-bar/use-search-bar.ts +++ b/plugins/main/public/components/common/search-bar/use-search-bar.ts @@ -9,9 +9,11 @@ import { IndexPatternsContract, } from '../../../../../../src/plugins/data/public'; import { getDataPlugin } from '../../../kibana-services'; - import { useFilterManager, useQueryManager, useTimeFilter } from '../hooks'; import { AUTHORIZED_AGENTS } from '../../../../common/constants'; +import { AppState } from '../../../react-services/app-state'; +import { getSettingDefaultValue } from '../../../../common/services/settings'; +import { FilterStateStore } from '../../../../../../src/plugins/data/common'; // Input - types type tUseSearchBarCustomInputs = { @@ -34,11 +36,10 @@ type tUserSearchBarResponse = { * @param props * @returns */ -const useSearchBar = ( - props?: tUseSearchBarProps, -): tUserSearchBarResponse => { +const useSearchBar = (props?: tUseSearchBarProps): tUserSearchBarResponse => { // dependencies const SESSION_STORAGE_FILTERS_NAME = 'wazuh_persistent_searchbar_filters'; + const SESSION_STORAGE_PREV_FILTER_NAME = 'wazuh_persistent_current_filter'; const filterManager = useFilterManager().filterManager as FilterManager; const { filters } = useFilterManager(); const [query, setQuery] = props?.query @@ -51,25 +52,43 @@ const useSearchBar = ( useState(); useEffect(() => { + const prevPattern = + AppState.getCurrentPattern() || getSettingDefaultValue('pattern'); if (filters && filters.length > 0) { sessionStorage.setItem( SESSION_STORAGE_FILTERS_NAME, - JSON.stringify(filters), + JSON.stringify( + updatePrevFilters(filters, props?.defaultIndexPatternID), + ), ); } + sessionStorage.setItem(SESSION_STORAGE_PREV_FILTER_NAME, prevPattern); + AppState.setCurrentPattern(props?.defaultIndexPatternID); initSearchBar(); + /** - * When the component is disassembled, the original filters that arrived - * when the component was assembled are added. + * When the component is unmounted, the original filters that arrived + * when the component was mounted are added. + * Both when the component is mounted and unmounted, the index pattern is + * updated so that the pin action adds the agent with the correct index pattern. */ return () => { + const prevStoragePattern = sessionStorage.getItem( + SESSION_STORAGE_PREV_FILTER_NAME, + ); + AppState.setCurrentPattern(prevStoragePattern); + sessionStorage.removeItem(SESSION_STORAGE_PREV_FILTER_NAME); const storagePreviousFilters = sessionStorage.getItem( SESSION_STORAGE_FILTERS_NAME, ); if (storagePreviousFilters) { const previousFilters = JSON.parse(storagePreviousFilters); - const cleanedFilters = cleanFilters(previousFilters); + const cleanedFilters = cleanFilters( + previousFilters, + prevStoragePattern ?? prevPattern, + ); filterManager.setFilters(cleanedFilters); + sessionStorage.removeItem(SESSION_STORAGE_FILTERS_NAME); } }; }, []); @@ -107,6 +126,64 @@ const useSearchBar = ( } }; + const updatePrevFilters = ( + previousFilters: Filter[], + indexPattern?: string, + ) => { + const pinnedAgent = previousFilters.find( + (filter: Filter) => + filter.meta.key === 'agent.id' && !!filter?.$state?.isImplicit, + ); + if (!pinnedAgent) { + const url = window.location.href; + const regex = new RegExp('agentId=' + '[^&]*'); + const match = url.match(regex); + if (match && match[0]) { + const agentId = match[0].split('=')[1]; + const agentFilters = previousFilters.filter(x => { + return x.meta.key !== 'agent.id'; + }); + const insertPinnedAgent = { + meta: { + alias: null, + disabled: false, + key: 'agent.id', + negate: false, + params: { query: agentId }, + type: 'phrase', + index: indexPattern, + }, + query: { + match: { + 'agent.id': { + query: agentId, + type: 'phrase', + }, + }, + }, + $state: { store: 'appState', isImplicit: true }, + }; + agentFilters.push(insertPinnedAgent); + return agentFilters; + } + } + if (pinnedAgent) { + const agentFilters = previousFilters.filter(x => { + return x.meta.key !== 'agent.id'; + }); + agentFilters.push({ + ...pinnedAgent, + meta: { + ...pinnedAgent.meta, + index: indexPattern, + }, + $state: { store: FilterStateStore.APP_STATE, isImplicit: true }, + }); + return agentFilters; + } + return previousFilters; + }; + /** * Return filters from filters manager. * Additionally solve the known issue with the auto loaded agent.id filters from the searchbar @@ -115,26 +192,102 @@ const useSearchBar = ( */ const getFilters = () => { const originalFilters = filterManager ? filterManager.getFilters() : []; - return originalFilters.filter( + const pinnedAgent = originalFilters.find( + (filter: Filter) => + filter.meta.key === 'agent.id' && !!filter?.$state?.isImplicit, + ); + const mappedFilters = originalFilters.filter( (filter: Filter) => filter?.meta?.controlledBy !== AUTHORIZED_AGENTS && // remove auto loaded agent.id filters filter?.meta?.index === props?.defaultIndexPatternID, ); + + if (pinnedAgent) { + const agentFilters = mappedFilters.filter(x => { + return x.meta.key !== 'agent.id'; + }); + agentFilters.push({ + ...pinnedAgent, + meta: { + ...pinnedAgent.meta, + index: props?.defaultIndexPatternID, + }, + }); + return agentFilters; + } + return mappedFilters; }; /** * Return cleaned filters. * Clean the known issue with the auto loaded agent.id filters from the searchbar - * and filters those filters that are not related to the default index pattern + * and filters those filters that are not related to the default index pattern. + * This cleanup adjusts the index pattern of a pinned agent, if applicable. * @param previousFilters * @returns */ - const cleanFilters = (previousFilters: Filter[]) => { - return previousFilters.filter( + const cleanFilters = (previousFilters: Filter[], indexPattern?: string) => { + /** + * Verify if a pinned agent exists, identifying it by its meta.isImplicit attribute or by the agentId query param URL. + * We also compare the agent.id filter with the agentId query param because the OSD filter definition does not include the "isImplicit" attribute that Wazuh adds. + * There may be cases where the "isImplicit" attribute is lost, since any action regarding filters that is done with the + * filterManager ( addFilters, setFilters, setGlobalFilters, setAppFilters) + * does a mapAndFlattenFilters mapping to the filters that removes any attributes that are not part of the filter definition. + * */ + const mappedFilters = previousFilters.filter( (filter: Filter) => - filter?.meta?.controlledBy !== AUTHORIZED_AGENTS && + filter?.meta?.controlledBy !== AUTHORIZED_AGENTS && // remove auto loaded agent.id filters filter?.meta?.index !== props?.defaultIndexPatternID, ); + const pinnedAgent = mappedFilters.find( + (filter: Filter) => + filter.meta.key === 'agent.id' && !!filter?.$state?.isImplicit, + ); + + if (!pinnedAgent) { + const url = window.location.href; + const regex = new RegExp('agentId=' + '[^&]*'); + const match = url.match(regex); + if (match && match[0]) { + const agentId = match[0].split('=')[1]; + const agentFilters = mappedFilters.filter(x => { + return x.meta.key !== 'agent.id'; + }); + const insertPinnedAgent = { + meta: { + alias: null, + disabled: false, + key: 'agent.id', + negate: false, + params: { query: agentId }, + type: 'phrase', + index: indexPattern, + }, + query: { + match: { + 'agent.id': { + query: agentId, + type: 'phrase', + }, + }, + }, + $state: { store: 'appState', isImplicit: true }, + }; + agentFilters.push(insertPinnedAgent); + return agentFilters; + } + } + if (pinnedAgent) { + mappedFilters.push({ + ...pinnedAgent, + meta: { + ...pinnedAgent.meta, + index: indexPattern, + }, + $state: { store: FilterStateStore.APP_STATE, isImplicit: true }, + }); + } + return mappedFilters; }; /** @@ -158,7 +311,10 @@ const useSearchBar = ( */ if (storagePreviousFilters) { const previousFilters = JSON.parse(storagePreviousFilters); - const cleanedFilters = cleanFilters(previousFilters); + const cleanedFilters = cleanFilters( + previousFilters, + props?.defaultIndexPatternID, + ); filterManager.setFilters([...cleanedFilters, ...filters]); props?.onFiltersUpdated && @@ -179,7 +335,7 @@ const useSearchBar = ( props?.onQuerySubmitted && props?.onQuerySubmitted(payload); }, // its necessary to use saved queries. if not, the load saved query not work - useDefaultBehaviors: true + useDefaultBehaviors: true, }; return { diff --git a/plugins/main/public/components/common/welcome/agents-welcome.js b/plugins/main/public/components/common/welcome/agents-welcome.js index 53b0ec9bbf..cb4b63fb54 100644 --- a/plugins/main/public/components/common/welcome/agents-welcome.js +++ b/plugins/main/public/components/common/welcome/agents-welcome.js @@ -21,10 +21,12 @@ import { EuiFlexGrid, EuiButtonEmpty, EuiPage, + EuiPopover, EuiLoadingChart, EuiToolTip, EuiButtonIcon, EuiPageBody, + EuiLink, } from '@elastic/eui'; import { FimEventsTable, @@ -34,6 +36,7 @@ import { } from './components'; import { AgentInfo } from './agents-info'; import WzReduxProvider from '../../../redux/wz-redux-provider'; +import MenuAgent from './components/menu-agent'; import './welcome.scss'; import { WzDatePicker } from '../../../components/wz-date-picker/wz-date-picker'; import KibanaVis from '../../../kibana-integrations/kibana-vis'; @@ -51,6 +54,7 @@ import { getCore, getDataPlugin, } from '../../../kibana-services'; +import { hasAgentSupportModule } from '../../../react-services/wz-agents'; import { withErrorBoundary, withGlobalBreadcrumb, @@ -59,6 +63,7 @@ import { } from '../hocs'; import { compose } from 'redux'; import { API_NAME_AGENT_STATUS } from '../../../../common/constants'; +import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { PromptAgentNeverConnected, PromptNoSelectedAgent, @@ -67,8 +72,12 @@ import { connect } from 'react-redux'; import { WzButton } from '../buttons'; import { Applications, - itHygiene, + configurationAssessment, + fileIntegrityMonitoring, + endpointSummary, mitreAttack, + threatHunting, + malwareDetection, } from '../../../utils/applications'; import { RedirectAppLinks } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; @@ -89,7 +98,10 @@ export const AgentsWelcome = compose( withGlobalBreadcrumb(({ agent }) => { return [ { - text: itHygiene.title, + text: endpointSummary.breadcrumbLabel, + href: getCore().application.getUrlForApp(endpointSummary.id, { + path: `#/agents-preview`, + }), }, ...(agent?.name ? [ @@ -103,7 +115,25 @@ export const AgentsWelcome = compose( }), withGuard( props => !(props.agent && props.agent.id), - () => , + () => ( + <> + + You need to select an agent or return to + + + Endpoint summary + + + + } + /> + + ), ), withGuard( props => props.agent.status === API_NAME_AGENT_STATUS.NEVER_CONNECTED, @@ -112,9 +142,14 @@ export const AgentsWelcome = compose( )( class AgentsWelcome extends Component { _isMount = false; + sidebarSizeDefault; constructor(props) { super(props); + this.offset = 275; + + this.sidebarSizeDefault = 320; + this.state = { lastScans: [], isLoading: true, @@ -122,12 +157,43 @@ export const AgentsWelcome = compose( sortDirection: 'desc', actionAgents: true, // Hide actions agents selectedRequirement: 'pci', + menuAgent: [], + maxModules: 5, widthWindow: window.innerWidth, + isLocked: false, }; } updateWidth = () => { - this.setState({ widthWindow: window.innerWidth }); + let menuSize; + if (this.state.isLocked) { + menuSize = window.innerWidth - this.offset - this.sidebarSizeDefault; + } else { + menuSize = window.innerWidth - this.offset; + } + let maxModules = 5; + if (menuSize > 1400) { + maxModules = 5; + } else { + if (menuSize > 1250) { + maxModules = 4; + } else { + if (menuSize > 1100) { + maxModules = 3; + } else { + if (menuSize > 900) { + maxModules = 2; + } else { + maxModules = 1; + if (menuSize < 750) { + maxModules = null; + } + } + } + } + } + + this.setState({ maxModules: maxModules, widthWindow: window.innerWidth }); }; /* TODO: we should to create a unique Explore agent button instead @@ -152,6 +218,7 @@ export const AgentsWelcome = compose( /* WORKAROUND: ensure the $scope.agent is synced with the agent stored in Redux (this.props.agent). See agents.js controller. */ this.props.setAgent(this.props.agent); + this.updatePinnedApplications(); this.updateWidth(); const tabVisualizations = new TabVisualizations(); tabVisualizations.removeAll(); @@ -163,8 +230,10 @@ export const AgentsWelcome = compose( const $injector = getAngularModule().$injector; this.drawerLokedSubscribtion = getChrome() .getIsNavDrawerLocked$() - .subscribe(() => { - this.updateWidth(); + .subscribe(isLocked => { + this.setState({ isLocked }, () => { + this.updateWidth(); + }); }); this.router = $injector.get('$route'); this.location = $injector.get('$location'); @@ -188,26 +257,111 @@ export const AgentsWelcome = compose( this.drawerLokedSubscribtion?.unsubscribe(); } - renderEndpointsSummaryButton() { - const application = Applications.find( - ({ id }) => id === 'endpoints-summary', + updatePinnedApplications(applications) { + let pinnedApplications; + + if (applications) { + pinnedApplications = applications; + } else { + pinnedApplications = window.localStorage.getItem( + 'wz-menu-agent-apps-pinned', + ) + ? JSON.parse(window.localStorage.getItem('wz-menu-agent-apps-pinned')) + : [ + // Default pinned applications + threatHunting.id, + fileIntegrityMonitoring.id, + configurationAssessment.id, + mitreAttack.id, + malwareDetection.id, + ]; + } + + // Ensure the pinned applications are supported + pinnedApplications = pinnedApplications.filter(pinnedApplication => + Applications.some(({ id }) => id === pinnedApplication), + ); + + window.localStorage.setItem( + 'wz-menu-agent-apps-pinned', + JSON.stringify(pinnedApplications), ); + this.setState({ menuAgent: pinnedApplications }); + } + + renderModules() { return ( - - - {application.title} - - + + {this.state.menuAgent.map((applicationId, i) => { + const moduleID = Object.keys(WAZUH_MODULES).find( + key => WAZUH_MODULES[key]?.appId === applicationId, + ).appId; + if ( + i < this.state.maxModules && + hasAgentSupportModule(this.props.agent, moduleID) + ) { + return ( + + + + + { + Applications.find(({ id }) => id === applicationId) + .title + } +   + + + + + ); + } + })} + + + this.setState({ switchModule: !this.state.switchModule }) + } + > + More... + + } + isOpen={this.state.switchModule} + closePopover={() => this.setState({ switchModule: false })} + repositionOnScroll={false} + anchorPosition='downCenter' + > +
+ +
+ + this.updatePinnedApplications(applications) + } + closePopover={() => { + this.setState({ switchModule: false }); + }} + switchTab={module => this.props.switchTab(module)} + > +
+
+
+
+
+
); } @@ -224,9 +378,47 @@ export const AgentsWelcome = compose( > - - {this.renderEndpointsSummaryButton()} - + {(this.state.maxModules !== null && this.renderModules()) || ( + + + this.setState({ + switchModule: !this.state.switchModule, + }) + } + > + Applications + + } + isOpen={this.state.switchModule} + closePopover={() => this.setState({ switchModule: false })} + repositionOnScroll={false} + anchorPosition='downCenter' + > +
+ +
+ + this.updatePinnedApplications(applications) + } + closePopover={() => { + this.setState({ switchModule: false }); + }} + switchTab={module => this.props.switchTab(module)} + > +
+
+
+
+
+ )}
@@ -272,13 +464,13 @@ export const AgentsWelcome = compose( this.props.switchTab('syscollector', notNeedStatus) } className='wz-it-hygiene-header-button' - tooltip={{ - position: 'bottom', - content: 'Inventory data', - className: 'wz-it-hygiene-header-button-tooltip', - }} + tooltip={ + this.state.maxModules === null + ? { position: 'bottom', content: 'Inventory data' } + : undefined + } > - Inventory data + {this.state.maxModules !== null ? 'Inventory data' : ''} @@ -287,13 +479,13 @@ export const AgentsWelcome = compose( iconType='stats' onClick={() => this.props.switchTab('stats', notNeedStatus)} className='wz-it-hygiene-header-button' - tooltip={{ - position: 'bottom', - content: 'Stats', - className: 'wz-it-hygiene-header-button-tooltip', - }} + tooltip={ + this.state.maxModules === null + ? { position: 'bottom', content: 'Stats' } + : undefined + } > - Stats + {this.state.maxModules !== null ? 'Stats' : ''} @@ -304,13 +496,13 @@ export const AgentsWelcome = compose( this.props.switchTab('configuration', notNeedStatus) } className='wz-it-hygiene-header-button' - tooltip={{ - position: 'bottom', - content: 'Configuration', - className: 'wz-it-hygiene-header-button-tooltip', - }} + tooltip={ + this.state.maxModules === null + ? { position: 'bottom', content: 'Configuration' } + : undefined + } > - Configuration + {this.state.maxModules !== null ? 'Configuration' : ''} diff --git a/plugins/main/public/components/common/welcome/components/menu-agent.js b/plugins/main/public/components/common/welcome/components/menu-agent.js new file mode 100644 index 0000000000..f11013ad54 --- /dev/null +++ b/plugins/main/public/components/common/welcome/components/menu-agent.js @@ -0,0 +1,218 @@ +/* + * Wazuh app - React component for registering agents. + * 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 { + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSideNav, + EuiLink, +} from '@elastic/eui'; +import { connect } from 'react-redux'; +import { hasAgentSupportModule } from '../../../../react-services/wz-agents'; +import { + getAngularModule, + getCore, + getToasts, +} from '../../../../kibana-services'; +import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; +import { Applications, Categories } from '../../../../utils/applications'; +import { RedirectAppLinks } from '../../../../../../../src/plugins/opensearch_dashboards_react/public'; + +class WzMenuAgent extends Component { + constructor(props) { + super(props); + this.state = { + hoverAddFilter: '', + }; + + this.appCategories = Applications.reduce((categories, app) => { + const existingCategory = categories.find( + category => category.id === app.category, + ); + if (app.showInAgentMenu) { + if (existingCategory) { + existingCategory.apps.push(app); + } else { + const category = Categories.find( + category => app.category === category.id, + ); + categories.push({ + id: category.id, + label: Categories.find(category => app.category === category.id) + .label, + icon: category.euiIconType, + apps: [app], + }); + } + } + return categories; + }, []).sort((a, b) => { + return ( + Categories.find(category => a.id === category.id).order - + Categories.find(category => b.id === category.id).order + ); + }); + } + + componentDidMount() { + const $injector = getAngularModule().$injector; + this.router = $injector.get('$route'); + } + + clickMenuItem = appId => { + this.props.closePopover(); + // do not redirect if we already are in that tab + this.props.updateCurrentAgentData(this.props.isAgent); + this.router.reload(); + }; + + addToast({ color, title, text, time = 3000 }) { + getToasts().add({ title, text, toastLifeTimeMs: time, color }); + } + + createItems = items => { + return items + .filter(item => + hasAgentSupportModule(this.props.currentAgentData, item.id), + ) + .map(item => this.createItem(item)); + }; + + createItem = (item, data = {}) => { + // NOTE: Duplicate `name` values will cause `id` collisions. + return { + ...data, + id: item.id, + name: ( + { + this.setState({ hoverAddFilter: item.id }); + }} + onMouseLeave={() => { + this.setState({ hoverAddFilter: '' }); + }} + > + (!item.isTitle ? this.clickMenuItem(item.id) : null)} + style={{ cursor: !item.isTitle ? 'pointer' : 'normal' }} + > + + + {item.title} + + + + {this.state.hoverAddFilter === item.id && + !item.isTitle && + (this.props.pinnedApplications.length < 6 || item.isPin) && + (this.props.pinnedApplications.length > 1 || !item.isPin) && ( + + { + if ( + !item.isPin && + this.props.pinnedApplications.length < 6 + ) { + this.props.updatePinnedApplications([ + ...this.props.pinnedApplications, + item.id, + ]); + } else if ( + this.props.pinnedApplications.includes(item.id) + ) { + this.props.updatePinnedApplications([ + ...this.props.pinnedApplications.filter( + id => id !== item.id, + ), + ]); + } else { + this.addToast({ + title: + 'The limit of pinned applications has been reached', + color: 'danger', + }); + } + }} + color='primary' + type={ + this.props.pinnedApplications.includes(item.id) + ? 'pinFilled' + : 'pin' + } + aria-label='Next' + style={{ cursor: 'pointer' }} + /> + + )} + + ), + isSelected: this.props.currentTab === item.id, + }; + }; + + render() { + const items = this.appCategories.map(({ apps, ...rest }) => ({ + ...rest, + items: this.createItems( + apps.map(app => ({ + id: app.id, + title: app.title, + isPin: this.props.pinnedApplications.includes(app.id), + })), + ), + })); + + return ( +
+
+ + {items.map(item => ( + + , + items: item.items, + }, + ]} + style={{ padding: '4px 12px' }} + /> + + ))} + +
+
+ ); + } +} + +const mapStateToProps = state => { + return { + currentAgentData: state.appStateReducers.currentAgentData, + currentTab: state.appStateReducers.currentTab, + }; +}; + +const mapDispatchToProps = dispatch => ({ + updateCurrentAgentData: agentData => + dispatch(updateCurrentAgentData(agentData)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(WzMenuAgent); diff --git a/plugins/main/public/components/common/welcome/overview-welcome.js b/plugins/main/public/components/common/welcome/overview-welcome.js index 42afc33bbf..ffa23e69e4 100644 --- a/plugins/main/public/components/common/welcome/overview-welcome.js +++ b/plugins/main/public/components/common/welcome/overview-welcome.js @@ -33,7 +33,7 @@ import { compose } from 'redux'; import { Applications, Categories, - endpointSumary, + endpointSummary, overview, } from '../../../utils/applications'; import { getCore } from '../../../kibana-services'; @@ -66,7 +66,7 @@ export const OverviewWelcome = compose( withReduxProvider, withErrorBoundary, withGlobalBreadcrumb(props => { - return [{ text: overview.title }]; + return [{ text: overview.breadcrumbLabel }]; }), )( class OverviewWelcome extends Component { @@ -92,9 +92,9 @@ export const OverviewWelcome = compose( ]} iconType='plusInCircle' href={getCore().application.getUrlForApp( - endpointSumary.id, + endpointSummary.id, { - path: `#${endpointSumary.redirectTo()}deploy`, + path: `#${endpointSummary.redirectTo()}deploy`, }, )} > diff --git a/plugins/main/public/components/common/welcome/welcome.scss b/plugins/main/public/components/common/welcome/welcome.scss index dab373eee2..65dd40462b 100644 --- a/plugins/main/public/components/common/welcome/welcome.scss +++ b/plugins/main/public/components/common/welcome/welcome.scss @@ -44,41 +44,3 @@ span.statWithLink:hover { text-decoration: underline; } - -// Header buttons of IT Hygiene application - -// Sidebar is open and locked -body.euiBody--hasFlyout:not(.euiBody-hasOverlayMask) { - @media only screen and (max-width: 1345px) { - // Hide button text depending on the window size - .wz-it-hygiene-header-button .euiButtonEmpty__text { - display: none; - } - } - - @media only screen and (min-width: 1346px) { - // Hide the tooltip of button depending on the window size - .wz-it-hygiene-header-button-tooltip { - display: none; - } - } -} - -// Sidebar is closed -body:not(.euiBody--hasFlyout) { - @media only screen and (max-width: 1025px) { - // Hide button text depending on the window size - .wz-it-hygiene-header-button .euiButtonEmpty__text { - display: none; - } - } - - @media only screen and (min-width: 1026px) { - // Hide the tooltip of button depending on the window size - .wz-it-hygiene-header-button-tooltip { - display: none; - } - } -} - -// Header buttons of IT Hygiene application diff --git a/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx b/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx index cf8adb3ff7..494e2da137 100644 --- a/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx +++ b/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx @@ -37,7 +37,6 @@ import { withUserAuthorizationPrompt, withErrorBoundary, } from '../common/hocs'; -import { formatUIDate } from '../../../public/react-services/time-service'; import { compose } from 'redux'; import { UI_LOGGER_LEVELS, @@ -50,7 +49,7 @@ import { agentStatusColorByAgentStatus, agentStatusLabelByAgentStatus, } from '../../../common/services/wz_agent_status'; -import { endpointSumary, itHygiene } from '../../utils/applications'; +import { endpointSummary } from '../../utils/applications'; import { ShareAgent } from '../../factories/share-agent'; import { getCore } from '../../kibana-services'; import './endpoints-summary.scss'; @@ -59,7 +58,7 @@ import { RedirectAppLinks } from '../../../../../src/plugins/opensearch_dashboar export const EndpointsSummary = compose( withErrorBoundary, withReduxProvider, - withGlobalBreadcrumb([{ text: endpointSumary.title }]), + withGlobalBreadcrumb([{ text: endpointSummary.breadcrumbLabel }]), withUserAuthorizationPrompt([ [ { action: 'agent:read', resource: 'agent:id:*' }, @@ -295,7 +294,7 @@ export const EndpointsSummary = compose( > diff --git a/plugins/main/public/components/endpoints-summary/index.tsx b/plugins/main/public/components/endpoints-summary/index.tsx index fe25b7dd95..2d37a9c9d8 100644 --- a/plugins/main/public/components/endpoints-summary/index.tsx +++ b/plugins/main/public/components/endpoints-summary/index.tsx @@ -6,7 +6,7 @@ import { EuiProgress, } from '@elastic/eui'; import { EndpointsSummary } from './endpoints-summary'; -import { endpointSumary } from '../../utils/applications'; +import { endpointSummary } from '../../utils/applications'; import { withErrorBoundary, withReduxProvider, @@ -23,7 +23,7 @@ import { useGetTotalAgents } from './hooks'; export const MainEndpointsSummary = compose( withErrorBoundary, withReduxProvider, - withGlobalBreadcrumb([{ text: endpointSumary.title }]), + withGlobalBreadcrumb([{ text: endpointSummary.breadcrumbLabel }]), )(() => { const { isLoading, totalAgents, error } = useGetTotalAgents(); @@ -64,8 +64,8 @@ export const MainEndpointsSummary = compose( fill permissions={[{ action: 'agent:create', resource: '*:*:*' }]} iconType='plusInCircle' - href={getCore().application.getUrlForApp(endpointSumary.id, { - path: `#${endpointSumary.redirectTo()}deploy`, + href={getCore().application.getUrlForApp(endpointSummary.id, { + path: `#${endpointSummary.redirectTo()}deploy`, })} > Deploy new agent diff --git a/plugins/main/public/components/endpoints-summary/register-agent/components/command-output/command-output.tsx b/plugins/main/public/components/endpoints-summary/register-agent/components/command-output/command-output.tsx index 1a8f604ccf..3137ba5831 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/components/command-output/command-output.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/components/command-output/command-output.tsx @@ -84,14 +84,18 @@ export default function CommandOutput(props: ICommandSectionProps) { )}
- {showCommand && havePassword ? ( - - ) : null} + <> + + + + ) : ( + + )} ); diff --git a/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx b/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx index b973ec5b1e..45d29691ab 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx @@ -38,7 +38,7 @@ import { validateAgentName, } from '../../utils/validations'; import { compose } from 'redux'; -import { endpointSumary } from '../../../../../utils/applications'; +import { endpointSummary } from '../../../../../utils/applications'; import { getCore } from '../../../../../kibana-services'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; @@ -46,7 +46,7 @@ export const RegisterAgent = compose( withErrorBoundary, withReduxProvider, withGlobalBreadcrumb([ - { text: endpointSumary.title, href: `#${endpointSumary.redirectTo()}` }, + { text: endpointSummary.title, href: `#${endpointSummary.redirectTo()}` }, { text: 'Deploy new agent' }, ]), withUserAuthorizationPrompt([ @@ -184,9 +184,9 @@ export const RegisterAgent = compose( diff --git a/plugins/main/public/components/endpoints-summary/register-agent/services/register-agent-os-commands-services.test.ts b/plugins/main/public/components/endpoints-summary/register-agent/services/register-agent-os-commands-services.test.ts new file mode 100644 index 0000000000..cea94326b9 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/register-agent/services/register-agent-os-commands-services.test.ts @@ -0,0 +1,227 @@ +import { + getAllOptionals, + getAllOptionalsMacos, + getDEBAMD64InstallCommand, + getDEBARM64InstallCommand, + getLinuxStartCommand, + getMacOsInstallCommand, + getMacosStartCommand, + getRPMAMD64InstallCommand, + getRPMARM64InstallCommand, + getWindowsInstallCommand, + getWindowsStartCommand, + transformOptionalsParamatersMacOSCommand, +} from './register-agent-os-commands-services'; + +let test: any; + +beforeEach(() => { + test = { + optionals: { + agentGroups: "WAZUH_AGENT_GROUP='default'", + agentName: "WAZUH_AGENT_NAME='test'", + serverAddress: "WAZUH_MANAGER='1.1.1.1'", + wazuhPassword: "WAZUH_REGISTRATION_PASSWORD=''", + }, + urlPackage: 'https://test.com/agent.deb', + wazuhVersion: '4.8.0', + }; +}); + +describe('getAllOptionals', () => { + it('should return empty string if optionals is falsy', () => { + const result = getAllOptionals(null); + expect(result).toBe(''); + }); + + it('should return the correct paramsText', () => { + const optionals = { + serverAddress: 'localhost', + wazuhPassword: 'password', + agentGroups: 'group1', + agentName: 'agent1', + protocol: 'http', + }; + const result = getAllOptionals(optionals, 'linux'); + expect(result).toBe('localhost password group1 agent1 http '); + }); +}); + +describe('getDEBAMD64InstallCommand', () => { + it('should return the correct install command', () => { + const props = { + optionals: { + serverAddress: 'localhost', + wazuhPassword: 'password', + agentGroups: 'group1', + agentName: 'agent1', + protocol: 'http', + }, + urlPackage: 'https://example.com/package.deb', + wazuhVersion: '4.0.0', + }; + const result = getDEBAMD64InstallCommand(props); + expect(result).toBe( + 'wget https://example.com/package.deb && sudo localhost password group1 agent1 http dpkg -i ./wazuh-agent_4.0.0-1_amd64.deb', + ); + }); +}); + +describe('getDEBAMD64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb`; + const withAllOptionals = getDEBAMD64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb`; + const withServerAddresAndAgentGroupsOptions = + getDEBAMD64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getDEBARM64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb`; + const withAllOptionals = getDEBARM64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb`; + const withServerAddresAndAgentGroupsOptions = + getDEBARM64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getRPMAMD64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `curl -o wazuh-agent-4.8.0-1.x86_64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm`; + const withAllOptionals = getRPMAMD64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `curl -o wazuh-agent-4.8.0-1.x86_64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm`; + const withServerAddresAndAgentGroupsOptions = + getRPMAMD64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getRPMARM64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `curl -o wazuh-agent-4.8.0-1.aarch64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm`; + const withAllOptionals = getRPMARM64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `curl -o wazuh-agent-4.8.0-1.aarch64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm`; + const withServerAddresAndAgentGroupsOptions = + getRPMARM64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getLinuxStartCommand', () => { + it('returns the correct start command for Linux', () => { + const startCommand = getLinuxStartCommand({}); + const expectedCommand = + 'sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent'; + + expect(startCommand).toEqual(expectedCommand); + }); +}); + +// Windows + +describe('getWindowsInstallCommand', () => { + it('should return the correct install command', () => { + let expected = `Invoke-WebRequest -Uri ${test.urlPackage} -OutFile \${env.tmp}\\wazuh-agent; msiexec.exe /i \${env.tmp}\\wazuh-agent /q ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} `; + + const withAllOptionals = getWindowsInstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `Invoke-WebRequest -Uri ${test.urlPackage} -OutFile \${env.tmp}\\wazuh-agent; msiexec.exe /i \${env.tmp}\\wazuh-agent /q ${test.optionals.serverAddress} ${test.optionals.agentGroups} `; + const withServerAddresAndAgentGroupsOptions = + getWindowsInstallCommand(test); + + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getWindowsStartCommand', () => { + it('should return the correct start command', () => { + const expectedCommand = 'NET START WazuhSvc'; + + const result = getWindowsStartCommand({}); + + expect(result).toEqual(expectedCommand); + }); +}); + +// MacOS + +describe('getAllOptionalsMacos', () => { + it('should return empty string if optionals is falsy', () => { + const result = getAllOptionalsMacos(null); + expect(result).toBe(''); + }); + + it('should return the correct paramsValueList', () => { + const optionals = { + serverAddress: 'localhost', + agentGroups: 'group1', + agentName: 'agent1', + protocol: 'http', + wazuhPassword: 'password', + }; + const result = getAllOptionalsMacos(optionals); + expect(result).toBe('localhost && group1 && agent1 && http && password'); + }); +}); + +describe('transformOptionalsParamatersMacOSCommand', () => { + it('should transform the command correctly', () => { + const command = + "' serverAddress && agentGroups && agentName && protocol && wazuhPassword"; + const result = transformOptionalsParamatersMacOSCommand(command); + expect(result).toBe( + "' && serverAddress && agentGroups && agentName && protocol && wazuhPassword", + ); + }); +}); + +describe('getMacOsInstallCommand', () => { + it('should return the correct macOS installation script', () => { + let expected = `curl -so wazuh-agent.pkg ${test.urlPackage} && echo "${test.optionals.serverAddress} && ${test.optionals.agentGroups} && ${test.optionals.agentName} && ${test.optionals.wazuhPassword}\\n\" > /tmp/wazuh_envs && sudo installer -pkg ./wazuh-agent.pkg -target /`; + + const withAllOptionals = getMacOsInstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + expected = `curl -so wazuh-agent.pkg ${test.urlPackage} && echo "${test.optionals.serverAddress} && ${test.optionals.agentGroups}" > /tmp/wazuh_envs && sudo installer -pkg ./wazuh-agent.pkg -target /`; + + const withServerAddresAndAgentGroupsOptions = getMacOsInstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getMacosStartCommand', () => { + it('returns the correct start command for macOS', () => { + const startCommand = getMacosStartCommand({}); + expect(startCommand).toEqual('sudo /Library/Ossec/bin/wazuh-control start'); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/register-agent/services/register-agent-os-commands-services.tsx b/plugins/main/public/components/endpoints-summary/register-agent/services/register-agent-os-commands-services.tsx index bce1272426..f7d139e30b 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/services/register-agent-os-commands-services.tsx @@ -6,7 +6,7 @@ import { } from '../core/register-commands/types'; import { tOperatingSystem } from '../hooks/use-register-agent-commands.test'; -const getAllOptionals = ( +export const getAllOptionals = ( optionals: IOptionalParameters, osName?: tOperatingSystem['name'], ) => { @@ -25,25 +25,15 @@ const getAllOptionals = ( '', ); - if (osName && osName.toLowerCase() === 'windows' && optionals.serverAddress) { - // when os is windows we must to add wazuh registration server with server address - paramsText = - paramsText + - `WAZUH_REGISTRATION_SERVER=${optionals.serverAddress.replace( - 'WAZUH_MANAGER=', - '', - )} `; - } - return paramsText; }; -const getAllOptionalsMacos = ( +export const getAllOptionalsMacos = ( optionals: IOptionalParameters, ) => { // create paramNameOrderList, which is an array of the keys of optionals add interface const paramNameOrderList: (keyof IOptionalParameters)[] = - ['serverAddress', 'agentGroups', 'agentName', 'protocol']; + ['serverAddress', 'agentGroups', 'agentName', 'protocol', 'wazuhPassword']; if (!optionals) return ''; @@ -145,29 +135,33 @@ export const getMacOsInstallCommand = ( props: tOSEntryInstallCommand, ) => { const { optionals, urlPackage } = props; - // Set macOS installation script with environment variables - const optionalsText = optionals && getAllOptionalsMacos(optionals); - const macOSInstallationOptions = transformOptionalsParamatersMacOSCommand( - optionalsText || '', - ); - let wazuhPasswordParamWithValue = ''; - if (optionals?.wazuhPassword) { + + let optionalsForCommand = { ...optionals }; + if (optionalsForCommand?.wazuhPassword) { /** * We use the JSON.stringify to prevent that the scaped specials characters will be removed * and mantain the format of the password The JSON.stringify mantain the password format but adds " to wrap the characters */ const scapedPasswordLength = JSON.stringify( - optionals?.wazuhPassword, + optionalsForCommand?.wazuhPassword, ).length; // We need to remove the " added by JSON.stringify - wazuhPasswordParamWithValue = `${JSON.stringify( - optionals?.wazuhPassword, + optionalsForCommand.wazuhPassword = `${JSON.stringify( + optionalsForCommand?.wazuhPassword, ).substring(1, scapedPasswordLength - 1)}\\n`; } + + // Set macOS installation script with environment variables + const optionalsText = + optionalsForCommand && getAllOptionalsMacos(optionalsForCommand); + const macOSInstallationOptions = transformOptionalsParamatersMacOSCommand( + optionalsText || '', + ); + // If no variables are set, the echo will be empty const macOSInstallationSetEnvVariablesScript = macOSInstallationOptions - ? `echo "${macOSInstallationOptions}${wazuhPasswordParamWithValue}" > /tmp/wazuh_envs && ` + ? `echo "${macOSInstallationOptions}" > /tmp/wazuh_envs && ` : ``; // Merge environment variables with installation script diff --git a/plugins/main/public/components/endpoints-summary/register-agent/utils/register-agent-data.tsx b/plugins/main/public/components/endpoints-summary/register-agent/utils/register-agent-data.tsx index 09d52a8ecc..1a4d6ab883 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/utils/register-agent-data.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/utils/register-agent-data.tsx @@ -34,7 +34,7 @@ export const SERVER_ADDRESS_TEXTS = [ { title: 'Server address', subtitle: - 'This is the address the agent uses to communicate with the server. Enter an IP address or a fully qualified domain name (FDQN).', + 'This is the address the agent uses to communicate with the server. Enter an IP address or a fully qualified domain name (FQDN).', }, ]; diff --git a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx index df84f6d767..beca15c672 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx @@ -12,26 +12,42 @@ describe('Validations', () => { expect(result).toBeUndefined(); }); - it('should return undefined for a valid IP', () => { + it('should return undefined for a valid IPv4', () => { const validIP = '192.168.1.1'; const result = validateServerAddress(validIP); expect(result).toBeUndefined(); }); + it('should return undefined for a valid IPv6', () => { + const validIP = '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; + const result = validateServerAddress(validIP); + expect(result).toBeUndefined(); + }); + + it('should return an error message for an invalid IPv6', () => { + const invalidIPV6 = '2001:db8:85a3::8a2e:370:7334'; + const result = validateServerAddress(invalidIPV6); + expect(result).toBe( + 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', + ); + }); + + it('should return an error message for a compressed IPv6', () => { + const compressedIPV6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334:KL12'; + const result = validateServerAddress(compressedIPV6); + expect(result).toBe( + 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', + ); + }); + it('should return an error message for an invalid FQDN', () => { const invalidFQDN = 'example.'; const result = validateServerAddress(invalidFQDN); expect(result).toBe( - 'Each label must have a letter or number at the beginning. The maximum length is 63 characters.', + 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', ); }); - test('should return an error message for an invalid IP', () => { - const invalidIP = '999.999.999.999.999'; - const result = validateServerAddress(invalidIP); - expect(result).toBe('Not a valid IP'); - }); - test('should return undefined for an empty value', () => { const emptyValue = ''; const result = validateAgentName(emptyValue); diff --git a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx index c231d99c76..6d5c402739 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx @@ -1,41 +1,27 @@ -//IP: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 -// O ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 +//IPv4: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 +//IPv6: This is a set or eight hexadecimal expressions, each from 0000 to FFFF. 2001:0db8:85a3:0000:0000:8a2e:0370:7334 // FQDN: Maximum of 63 characters per label. // Can only contain numbers, letters and hyphens (-) // Labels cannot begin or end with a hyphen // Currently supports multilingual characters, i.e. letters not included in the English alphabet: e.g. á é í ó ú ü ñ. // Minimum 3 labels -export const validateServerAddress = (value: any) => { - const isFQDN = - /^(?!-)(?!.*--)(?!.*\d$)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){1,}$/; - const isIP = - /^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})$/; - const numbersAndPoints = /^[0-9.]+$/; - const areLettersNumbersAndColons = /^[a-zA-Z0-9:]+$/; - const letters = /[a-zA-Z]/; - const isFQDNFormatValid = isFQDN.test(value); - const isIPFormatValid = isIP.test(value); - const areNumbersAndPoints = numbersAndPoints.test(value); - const hasLetters = letters.test(value); - const hasPoints = value.includes('.'); +// A label can contain only numbers - let validation = undefined; - if (value.length === 0) { - return validation; - } else if (isFQDNFormatValid && value !== '') { - return validation; // FQDN valid - } else if (isIPFormatValid && value !== '') { - return validation; // IP valid - } else if (hasPoints && hasLetters && !isFQDNFormatValid) { - return (validation = - 'Each label must have a letter or number at the beginning. The maximum length is 63 characters.'); // FQDN invalid - } else if ( - (areNumbersAndPoints || areLettersNumbersAndColons) && - !isIPFormatValid +// Hostname: Maximum of 63 characters per label. Same rules as FQDN apply. + +export const validateServerAddress = (value: string) => { + const isFQDNOrHostname = + /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ](?:\.[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ]){0,}$/; + const isIPv6 = /^(?:[0-9a-fA-F]{4}:){7}[0-9a-fA-F]{4}$/; + + if ( + value.length > 255 || + (value.length > 0 && !isFQDNOrHostname.test(value) && !isIPv6.test(value)) ) { - return (validation = 'Not a valid IP'); // IP invalid + return 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'; } + return undefined; }; export const validateAgentName = (value: any) => { diff --git a/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap b/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap index 6f3a290170..6f16d19e0b 100644 --- a/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap +++ b/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap @@ -434,6 +434,7 @@ exports[`AgentsTable component Renders correctly to match the snapshot 1`] = ` data-test-subj="tableHeaderCell_version_6" role="columnheader" scope="col" + style="width:10%" >