diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acbffcf11..c083522167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,24 +2,28 @@ All notable changes to the Wazuh app project will be documented in this file. -## Wazuh v5.0.0 - OpenSearch Dashboards 2.11.0 - Revision 00 +## Wazuh v5.0.0 - OpenSearch Dashboards 2.12.0 - Revision 00 ### Added - Support for Wazuh 5.0.0 -## Wazuh v4.9.0 - OpenSearch Dashboards 2.11.0 - Revision 00 +## Wazuh v4.9.0 - OpenSearch Dashboards 2.12.0 - Revision 00 ### Added - Support for Wazuh 4.9.0 - Added AngularJS dependencies [#6145](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6145) +- Added edit groups action to Endpoints Summary [#6250](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6250) +- Added global actions add agents to groups and remove agents from groups to Endpoints Summary [#6274](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6274) ### Changed -- Removed embedded discover [#6120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6120) [#6235](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6235) [#6254](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6254) [#6285](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6285) [#6290](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6290) [#6275](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6275) [#6287](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) +- Removed embedded discover [#6120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6120) [#6235](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6235) [#6254](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6254) [#6285](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6285) [#6288](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6288) [#6290](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6290) [#6289](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6289) [#6286](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6286) [#6275](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6275) [#6287](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6297](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6297) [#6287](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6291](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6459](https://github.com/wazuh/wazuh-dashboard-plugins/pull/#6459) - Develop logic of a new index for the fim module [#6227](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6227) - Allow editing groups for an agent from Endpoints Summary [#6250](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6250) +- Changed the usage of the endpoint GET /groups/{group_id}/files/{file_name} [#6385](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6385) +- Refactoring and redesign endpoints summary visualizations [#6268](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6268) ### Fixed @@ -37,7 +41,7 @@ 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 03 +## Wazuh v4.8.0 - OpenSearch Dashboards 2.10.0 - Revision 05 ### Added @@ -45,21 +49,21 @@ All notable changes to the Wazuh app project will be documented in this file. - 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) [#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 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) [#6396](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6396) [#6399](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6399) [#6405](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6405) [#6410](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6410) [#6424](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6424) [#6422](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6422) [#6429](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6429) [#6448](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6448) - 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 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 information icon with tooltip on the most active agent in the endpoint summary view [#6364](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6364) [#6421](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6421) - 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 -- Moved the plugin menu to platform applications into the side menu [#5840](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5840) [#6226](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6226) [#6244](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6244) +- Moved the plugin menu to platform applications into the side menu [#5840](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5840) [#6226](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6226) [#6244](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6244) [#6176](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6176) [#6423](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6423) - 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) +- Upgraded the `axios` dependency to `1.6.1` [#6114](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6114) - 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) +- Changed overview home top KPIs. [#6379](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6379) [#6408](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6408) ### Fixed @@ -79,17 +83,27 @@ All notable changes to the Wazuh app project will be documented in this file. - 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) +- Fixed a error pop-up spawn in MITRE ATT&CK [#6431](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6431) ### Removed - Removed the `disabled_roles` and `customization.logo.sidebar` settings [#5840](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5840) - 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.3 - OpenSearch Dashboards 2.8.0 - Revision 02 + +### Added + +- Support for Wazuh 4.7.3 + +### Fixed + +- Fixed CDB List import file feature [#6458](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6458) + ## Wazuh v4.7.2 - OpenSearch Dashboards 2.8.0 - Revision 02 ### Added diff --git a/docker/imposter/agents/agent_distinct.js b/docker/imposter/agents/agent_distinct.js index 158a6e5c2d..f2b237bc07 100644 --- a/docker/imposter/agents/agent_distinct.js +++ b/docker/imposter/agents/agent_distinct.js @@ -1,56 +1,323 @@ if (!String.prototype.includes) { - String.prototype.includes = function(search, start) { - 'use strict'; - - if (search instanceof RegExp) { - throw TypeError('first argument must not be a RegExp'); - } - if (start === undefined) { start = 0; } - return this.indexOf(search, start) !== -1; - }; -} + String.prototype.includes = function (search, start) { + 'use strict'; -function generateResponse(items, field, search){ - return { - data: { - affected_items: items.filter(function(item){ - return search ? item.includes(search) : true; - }).map(function(item){ - var obj = {}; - obj[field] = item - return obj; - }), - total_affected_items: 5, - total_failed_items: 0, - failed_items: [] - }, - message: "All selected agents information was returned", - error: 0 + if (search instanceof RegExp) { + throw TypeError('first argument must not be a RegExp'); + } + if (start === undefined) { + start = 0; + } + return this.indexOf(search, start) !== -1; }; +} + +var fields = context.request.queryParams.fields; + +/* Based on agents.json */ +var originalResponse = { + data: { + affected_items: [ + { + os: { + arch: 'x86_64', + major: '2', + name: 'Amazon Linux', + platform: 'amzn', + uname: + 'Linux |wazuh-manager-master-0 |4.14.114-105.126.amzn2.x86_64 |#1 SMP Tue May 7 02:26:40 UTC 2019 |x86_64', + version: '2', + }, + group: [ + 'default', + 'test', + 'test2', + 'test3', + 'test4', + 'test5', + 'test6', + 'test7', + 'test8', + 'test9', + 'test10', + ], + ip: 'FE80:0034:0223:A000:0002:B3FF:0000:8329', + id: '000', + registerIP: 'FE80:0034:0223:A000:0002:B3FF:0000:8329', + dateAdd: '2022-08-25T16:17:46Z', + name: 'wazuh-manager-master-0', + status: 'active', + manager: 'wazuh-manager-master-0', + node_name: 'master', + lastKeepAlive: '9999-12-31T23:59:59Z', + version: 'Wazuh v4.4.0', + group_config_status: 'synced', + status_code: 0, + count: 1, + }, + { + os: { + arch: 'x86_64', + major: '2', + name: 'Amazon Linux', + platform: 'amzn', + uname: + 'Linux |wazuh-manager-master-0 |4.14.114-105.126.amzn2.x86_64 |#1 SMP Tue May 7 02:26:40 UTC 2019 |x86_64', + version: '2', + }, + group: ['default', 'test', 'test2', 'test3', 'test4', 'test5'], + ip: 'FE80:1234:2223:A000:2202:B3FF:FE1E:8329', + id: '001', + registerIP: 'FE80:1234:2223:A000:2202:B3FF:FE1E:8329', + dateAdd: '2022-08-25T16:17:46Z', + name: 'wazuh-manager-master-0', + status: 'active', + manager: 'wazuh-manager-master-0', + node_name: 'master', + lastKeepAlive: '9999-12-31T23:59:59Z', + version: 'Wazuh v4.4.0', + group_config_status: 'not synced', + status_code: 0, + count: 1, + }, + { + os: { + arch: 'x86_64', + major: '2', + name: 'Amazon Linux', + platform: 'amzn', + uname: + 'Linux |wazuh-manager-master-0 |4.14.114-105.126.amzn2.x86_64 |#1 SMP Tue May 7 02:26:40 UTC 2019 |x86_64', + version: '2', + }, + group: ['default', 'test', 'test2'], + ip: '127.0.0.1', + id: '002', + registerIP: '127.0.0.1', + dateAdd: '2022-08-25T16:17:46Z', + name: 'wazuh-manager-master-0', + status: 'active', + manager: 'wazuh-manager-master-0', + node_name: 'master', + lastKeepAlive: '9999-12-31T23:59:59Z', + version: 'Wazuh v4.5.0', + group_config_status: 'synced', + status_code: 0, + count: 1, + }, + { + os: { + build: '19045', + major: '10', + minor: '0', + name: 'Microsoft Windows 10 Home Single Language', + platform: 'windows', + uname: 'Microsoft Windows 10 Home Single Language', + version: '10.0.19045', + }, + disconnection_time: '2023-03-14T04:37:42Z', + manager: 'test.com', + status: 'disconnected', + name: 'disconnected-agent', + dateAdd: '1970-01-01T00:00:00Z', + group: ['default', 'test'], + lastKeepAlive: '2023-03-14T04:20:51Z', + node_name: 'node01', + registerIP: 'any', + id: '003', + version: 'Wazuh v4.3.10', + ip: '111.111.1.111', + mergedSum: 'e669d89eba52f6897060fc65a45300ac', + configSum: '97fccbb67e250b7c80aadc8d0dc59abe', + group_config_status: 'not synced', + status_code: 1, + count: 1, + }, + { + status: 'never_connected', + name: 'never_connected_agent', + dateAdd: '2023-03-14T09:44:11Z', + node_name: 'unknown', + registerIP: 'any', + id: '004', + ip: 'any', + group_config_status: 'not synced', + status_code: 4, + count: 1, + }, + { + os: { + arch: 'x86_64', + major: '2', + name: 'macOS High Sierra', + platform: 'darwin', + uname: + 'macOS High Sierra |wazuh-manager-master-0 |4.14.114-105.126.amzn2.x86_64 |#1 SMP Tue May 7 02:26:40 UTC 2019 |x86_64', + version: '2', + }, + ip: '127.0.0.1', + id: '005', + group: ['default'], + registerIP: '127.0.0.1', + dateAdd: '2022-08-25T16:17:46Z', + name: 'macOS High Sierra agent', + status: 'disconnected', + manager: 'wazuh-manager-master-0', + node_name: 'master', + lastKeepAlive: '9999-12-31T23:59:59Z', + version: 'Wazuh v4.5.0', + group_config_status: 'synced', + status_code: 2, + count: 1, + }, + { + os: { + name: 'Ubuntu', + platform: 'ubuntu', + uname: + 'Linux |f288f4c59dbc |5.19.0-35-generic |#36~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb 17 15:17:25 UTC 2 |x86_64', + version: '18.04.6 LTS', + }, + group_config_status: 'not synced', + status_code: 0, + ip: '172.19.0.27', + status: 'pending', + name: 'Pending agent', + group: ['default'], + node_name: 'master-node', + version: 'Wazuh v4.4.0', + lastKeepAlive: '2023-03-16T15:15:05+00:00', + id: '006', + dateAdd: '2023-03-16T15:14:47+00:00', + count: 1, + }, + { + status: 'never_connected', + name: 'never_connected_agent-2', + dateAdd: '2023-03-14T09:44:11Z', + node_name: 'unknown', + registerIP: 'any', + id: '007', + ip: 'any', + group_config_status: 'not synced', + status_code: 5, + count: 1, + }, + { + status: 'never_connected', + name: 'never_connected_agent-3', + dateAdd: '2023-03-14T09:44:11Z', + node_name: 'unknown', + registerIP: 'any', + id: '008', + ip: 'any', + group_config_status: 'not synced', + status_code: 1, + count: 1, + }, + { + status: 'never_connected', + name: 'never_connected_agent-4', + dateAdd: '2023-03-14T09:44:11Z', + node_name: 'unknown', + registerIP: 'any', + id: '009', + ip: 'any', + group_config_status: 'not synced', + status_code: 2, + count: 1, + }, + { + os: { + build: '19045', + major: '10', + minor: '0', + name: 'Microsoft Windows 10 Home Single Language', + platform: 'windows', + uname: 'Microsoft Windows 10 Home Single Language', + version: '10.0.19045', + }, + disconnection_time: '2023-03-14T04:37:42Z', + manager: 'test.com', + status: 'disconnected', + name: 'disconnected-agent-2', + dateAdd: '1970-01-01T00:00:00Z', + group: ['default', 'test'], + lastKeepAlive: '2023-03-14T04:20:51Z', + node_name: 'node01', + registerIP: 'any', + id: '010', + version: 'Wazuh v4.3.10', + ip: '111.111.1.111', + mergedSum: 'e669d89eba52f6897060fc65a45300ac', + configSum: '97fccbb67e250b7c80aadc8d0dc59abe', + group_config_status: 'not synced', + count: 1, + }, + ], + total_affected_items: 5, + total_failed_items: 0, + failed_items: [], + }, + message: 'All selected agents information was returned', + error: 0, }; -var mock = { - 'configSum': ['97fccbb67e250b7c80aadc8d0dc59abc', '97fccbb67e250b7c80aadc8d0dc59abd', '97fccbb67e250b7c80aadc8d0dc59abf', '97fccbb67e250b7c80aadc8d0dc59abe'], - 'dateAdd': ['2022-08-25T16:17:46Z', '2022-08-25T17:17:46Z', '2022-08-25T18:17:46Z'], - 'id': ['001', '002','003','004','005'], - 'ip': ['127.0.0.1', '127.0.0.2','127.0.0.3','127.0.0.4','127.0.0.5'], - 'group': ['default', 'windows', 'linux', 'rhel', 'arch'], - 'group_config_status': ['not synced', 'synced'], - 'lastKeepAlive': ['2022-08-25T16:17:46Z', '2022-08-25T17:17:46Z', '2022-08-25T18:17:46Z'], - 'manager': ['test.com', 'test2.com'], - 'mergedSum': ['e669d89eba52f6897060fc65a45300ac', 'e669d89eba52f6897060fc65a45300ad', 'e669d89eba52f6897060fc65a45300ae', 'e669d89eba52f6897060fc65a45300af'], - 'name': ['linux-agent', 'windows-agent'], - 'node_name': ['node01', 'node02', 'node03'], - 'os.platform': ['ubuntu', 'windows', 'darwin', 'amzn'], - 'status': ['active', 'disconnected', 'pending', 'never_connected'], - 'version': ['4.3.10', '4.4.0'] +var selectedFields = fields.split(','); + +var combinationsCount = {}; + +originalResponse.data.affected_items.forEach(function (agent) { + var combinationKey = selectedFields + .map(function (field) { + if (field.includes('.')) { + var subfields = field.split('.'); + return agent[subfields[0]] !== undefined + ? agent[subfields[0]][subfields[1]] + : 'unknown'; + } + return agent[field]; + }) + .join(','); + + if (!combinationsCount[combinationKey]) { + combinationsCount[combinationKey] = { count: 0 }; + selectedFields.forEach(function (field) { + if (field.includes('.')) { + var subfields = field.split('.'); + if (!combinationsCount[combinationKey][subfields[0]]) { + combinationsCount[combinationKey][subfields[0]] = {}; + } + combinationsCount[combinationKey][subfields[0]][subfields[1]] = + agent[subfields[0]] && agent[subfields[0]][subfields[1]] + ? agent[subfields[0]][subfields[1]] + : 'unknown'; + } else { + combinationsCount[combinationKey][field] = agent[field]; + } + }); + } + combinationsCount[combinationKey].count += agent.count; +}); + +var transformedResponse = { + data: { + affected_items: [], + total_affected_items: 0, + total_failed_items: 0, + failed_items: [], + }, + message: 'All selected agents information was returned', + error: 0, }; -var field = context.request.queryParams.fields; -var search = context.request.queryParams.search; +for (var key in combinationsCount) { + if (combinationsCount.hasOwnProperty(key)) { + transformedResponse.data.affected_items.push(combinationsCount[key]); + } +} -var responseJSON = generateResponse(mock[field], field, search); +transformedResponse.data.total_affected_items = + transformedResponse.data.affected_items.length; -respond() - .withStatusCode(200) - .withData(JSON.stringify(responseJSON)) \ No newline at end of file +respond().withStatusCode(200).withData(JSON.stringify(transformedResponse)); diff --git a/docker/imposter/agents/group_files.js b/docker/imposter/agents/group_files.js new file mode 100644 index 0000000000..dd4dd0e3de --- /dev/null +++ b/docker/imposter/agents/group_files.js @@ -0,0 +1,10 @@ +var raw_param = context.request.queryParams; + +switch (raw_param.raw) { + case 'true': + respond().withStatusCode(200).withFile('agents/group_files_raw.xml'); + break; + default: + respond().withStatusCode(200).withFile('agents/group_files_default.json'); + break; +} diff --git a/docker/imposter/agents/group_files_default.json b/docker/imposter/agents/group_files_default.json new file mode 100644 index 0000000000..4a07fe87d9 --- /dev/null +++ b/docker/imposter/agents/group_files_default.json @@ -0,0 +1,27 @@ +{ + "data": { + "vars": "None", + "controls": [ + { + "name": "CIS - Testing against the CIS Debian Linux Benchmark v1.", + "cis": [], + "pci": [], + "condition": "all required", + "reference": "CIS_Debian_Benchmark_v1.0pdf", + "checks": [ + "f:/etc/debian_version;", + "f:/proc/sys/kernel/ostype -> Linux;" + ] + }, + { + "name": "CIS - Debian Linux - 1.4 - Robust partition scheme - /tmp is not on its own partition", + "cis": [], + "pci": [], + "condition": "any", + "reference": "https://benchmarks.cisecurity.org/tools2/linux/CIS_Debian_Benchmark_v1.0.pdf", + "checks": ["f:/etc/fstab -> !r:/tmp;"] + } + ] + }, + "error": 0 +} diff --git a/docker/imposter/agents/group_files_raw.xml b/docker/imposter/agents/group_files_raw.xml new file mode 100644 index 0000000000..5f12e3c920 --- /dev/null +++ b/docker/imposter/agents/group_files_raw.xml @@ -0,0 +1,3 @@ + + + diff --git a/docker/imposter/manager/configuration.js b/docker/imposter/manager/configuration.js index 02984110f2..d85926ec49 100644 --- a/docker/imposter/manager/configuration.js +++ b/docker/imposter/manager/configuration.js @@ -17,7 +17,7 @@ switch (pathConfiguration[0]) { case 'wmodules': respond() .withStatusCode(200) - .withFile('manager/configuration/monitor_reports.json'); + .withFile('manager/configuration/wmodules_wmodules.json'); break; default: diff --git a/docker/imposter/security/security-actions.json b/docker/imposter/security/security-actions.json index 88ba661fa8..ded985ae2b 100644 --- a/docker/imposter/security/security-actions.json +++ b/docker/imposter/security/security-actions.json @@ -142,8 +142,8 @@ "GET /groups/{group_id}/agents", "GET /groups/{group_id}/configuration", "GET /groups/{group_id}/files", - "GET /groups/{group_id}/files/{file_name}/json", - "GET /groups/{group_id}/files/{file_name}/xml", + "GET /groups/{group_id}/files/{file_name}", + "GET /groups/{group_id}/files/{file_name}?raw=true", "GET /overview/agents" ] }, diff --git a/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index 7b6a91548d..dd53ec3723 100755 --- a/docker/imposter/wazuh-config.yml +++ b/docker/imposter/wazuh-config.yml @@ -394,11 +394,10 @@ resources: # Get a file in group - method: GET - path: /groups/{group_id}/files/{file_name}/json - - # Get a file in group - - method: GET - path: /groups/{group_id}/files/{file_name}/xml + path: /groups/{group_id}/files/{file_name} + response: + statusCode: 200 + scriptFile: agents/group_files.js # ===================================================== # # LISTS diff --git a/docker/osd-dev/dev.sh b/docker/osd-dev/dev.sh index 33ae08a32f..9a03af5bee 100755 --- a/docker/osd-dev/dev.sh +++ b/docker/osd-dev/dev.sh @@ -13,6 +13,8 @@ os_versions=( '2.9.0' '2.10.0' '2.11.0' + '2.11.1' + '2.12.0' ) osd_versions=( @@ -28,8 +30,8 @@ osd_versions=( '2.9.0' '2.10.0' '2.11.0' - '4.6.0' - '4.7.0' + '2.11.1' + '2.12.0' ) wzs_version=( diff --git a/docker/osd-dev/dev.yml b/docker/osd-dev/dev.yml index efdf6553bd..c13e118fe0 100755 --- a/docker/osd-dev/dev.yml +++ b/docker/osd-dev/dev.yml @@ -204,8 +204,8 @@ services: mkdir -p /etc/filebeat echo admin | filebeat keystore add username --stdin --force echo ${PASSWORD}| filebeat keystore add password --stdin --force - curl -so /etc/filebeat/wazuh-template.json https://raw.githubusercontent.com/wazuh/wazuh/4.3/extensions/elasticsearch/7.x/wazuh-template.json - curl -s https://packages.wazuh.com/4.x/filebeat/wazuh-filebeat-0.4.tar.gz | tar -xvz -C /usr/share/filebeat/module + curl -so /etc/filebeat/wazuh-template.json https://raw.githubusercontent.com/wazuh/wazuh/v4.7.2/extensions/elasticsearch/7.x/wazuh-template.json + curl -s https://packages.wazuh.com/4.x/filebeat/wazuh-filebeat-0.3.tar.gz | tar -xvz -C /usr/share/filebeat/module # copy filebeat to preserve correct permissions without # affecting host filesystem cp /tmp/filebeat.yml /usr/share/filebeat/filebeat.yml diff --git a/plugins/main/common/api-info/security-actions.json b/plugins/main/common/api-info/security-actions.json index 71dce7a6a7..bb247b7912 100644 --- a/plugins/main/common/api-info/security-actions.json +++ b/plugins/main/common/api-info/security-actions.json @@ -219,6 +219,7 @@ "GET /groups/{group_id}/configuration", "GET /groups/{group_id}/files", "GET /groups/{group_id}/files/{file_name}", + "GET /groups/{group_id}/files/{file_name}?raw=true", "GET /overview/agents" ] }, diff --git a/plugins/main/common/config-equivalences.js b/plugins/main/common/config-equivalences.js deleted file mode 100644 index 529a0346b2..0000000000 --- a/plugins/main/common/config-equivalences.js +++ /dev/null @@ -1,219 +0,0 @@ -import { ASSETS_PUBLIC_URL, PLUGIN_PLATFORM_NAME } from './constants'; - -export const configEquivalences = { - pattern: - "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", - 'customization.logo.app': `Set the name of the app logo stored at ${ASSETS_PUBLIC_URL}. It is used while the user is logging into Wazuh API.`, - 'customization.logo.healthcheck': `Set the name of the health-check logo stored at ${ASSETS_PUBLIC_URL}`, - 'customization.logo.reports': `Set the name of the reports logo (.png) stored at ${ASSETS_PUBLIC_URL}`, - 'checks.pattern': - 'Enable or disable the index pattern health check when opening the app.', - 'checks.template': - 'Enable or disable the template health check when opening the app.', - 'checks.api': 'Enable or disable the API health check when opening the app.', - 'checks.setup': - 'Enable or disable the setup health check when opening the app.', - 'checks.fields': - 'Enable or disable the known fields health check when opening the app.', - 'checks.metaFields': `Change the default value of the ${PLUGIN_PLATFORM_NAME} metaField configuration`, - 'checks.timeFilter': `Change the default value of the ${PLUGIN_PLATFORM_NAME} timeFilter configuration`, - 'checks.maxBuckets': `Change the default value of the ${PLUGIN_PLATFORM_NAME} max buckets configuration`, - timeout: - 'Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.', - 'ip.selector': - 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', - 'ip.ignore': - 'Disable certain index pattern names from being available in index pattern selector from the Wazuh app.', - 'wazuh.monitoring.enabled': - 'Enable or disable the wazuh-monitoring index creation and/or visualization.', - 'wazuh.monitoring.frequency': - 'Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.', - 'wazuh.monitoring.shards': - 'Define the number of shards to use for the wazuh-monitoring-* indices.', - 'wazuh.monitoring.replicas': - 'Define the number of replicas to use for the wazuh-monitoring-* indices.', - 'wazuh.monitoring.creation': - 'Define the interval in which a new wazuh-monitoring index will be created.', - 'wazuh.monitoring.pattern': - 'Default index pattern to use for Wazuh monitoring.', - hideManagerAlerts: 'Hide the alerts of the manager in every dashboard.', - 'enrollment.dns': - 'Specifies the Wazuh registration server, used for the agent enrollment.', - 'enrollment.password': - 'Specifies the password used to authenticate during the agent enrollment.', - 'cron.prefix': 'Define the index prefix of predefined jobs.', - 'cron.statistics.status': 'Enable or disable the statistics tasks.', - 'cron.statistics.apis': - 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', - 'cron.statistics.interval': - 'Define the frequency of task execution using cron schedule expressions.', - 'cron.statistics.index.name': - 'Define the name of the index in which the documents will be saved.', - 'cron.statistics.index.creation': - 'Define the interval in which a new index will be created.', - 'cron.statistics.index.shards': - 'Define the number of shards to use for the statistics indices.', - 'cron.statistics.index.replicas': - 'Define the number of replicas to use for the statistics indices.', - 'alerts.sample.prefix': - 'Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.', - 'vulnerabilities.pattern': - 'Default index pattern to use for vulnerabilities.', -}; - -export const nameEquivalence = { - pattern: 'Index pattern', - 'customization.logo.app': 'Logo App', - 'customization.logo.healthcheck': 'Logo Health Check', - 'customization.logo.reports': 'Logo Reports', - 'checks.pattern': 'Index pattern', - 'checks.template': 'Index template', - 'checks.api': 'API connection', - 'checks.setup': 'API version', - 'checks.fields': 'Known fields', - 'checks.metaFields': 'Remove meta fields', - 'checks.timeFilter': 'Set time filter to 24h', - 'checks.maxBuckets': 'Set max buckets to 200000', - timeout: 'Request timeout', - 'ip.selector': 'IP selector', - 'ip.ignore': 'IP ignore', - 'wazuh.monitoring.enabled': 'Status', - 'wazuh.monitoring.frequency': 'Frequency', - 'wazuh.monitoring.shards': 'Index shards', - 'wazuh.monitoring.replicas': 'Index replicas', - 'wazuh.monitoring.creation': 'Index creation', - 'wazuh.monitoring.pattern': 'Index pattern', - hideManagerAlerts: 'Hide manager alerts', - 'enrollment.dns': 'Enrollment DNS', - 'cron.prefix': 'Cron prefix', - 'cron.statistics.status': 'Status', - 'cron.statistics.apis': 'Includes apis', - 'cron.statistics.interval': 'Interval', - 'cron.statistics.index.name': 'Index name', - 'cron.statistics.index.creation': 'Index creation', - 'cron.statistics.index.shards': 'Index shards', - 'cron.statistics.index.replicas': 'Index replicas', - 'alerts.sample.prefix': 'Sample alerts prefix', - 'vulnerabilities.pattern': 'Index pattern', - 'checks.vulnerabilities.pattern': 'Vulnerabilities index pattern', - 'fim.pattern': 'Index pattern', - 'checks.fim.pattern': 'Fim index pattern', -}; - -const HEALTH_CHECK = 'Health Check'; -const GENERAL = 'General'; -const SECURITY = 'Security'; -const MONITORING = 'Monitoring'; -const STATISTICS = 'Statistics'; -const VULNERABILITIES = 'Vulnerabilities'; -const CUSTOMIZATION = 'Logo Customization'; -export const categoriesNames = [ - HEALTH_CHECK, - GENERAL, - SECURITY, - MONITORING, - STATISTICS, - VULNERABILITIES, - CUSTOMIZATION, -]; - -export const categoriesEquivalence = { - pattern: GENERAL, - 'customization.logo.app': CUSTOMIZATION, - 'customization.logo.healthcheck': CUSTOMIZATION, - 'customization.logo.reports': CUSTOMIZATION, - 'checks.pattern': HEALTH_CHECK, - 'checks.template': HEALTH_CHECK, - 'checks.api': HEALTH_CHECK, - 'checks.setup': HEALTH_CHECK, - 'checks.fields': HEALTH_CHECK, - 'checks.metaFields': HEALTH_CHECK, - 'checks.timeFilter': HEALTH_CHECK, - 'checks.maxBuckets': HEALTH_CHECK, - timeout: GENERAL, - 'ip.selector': GENERAL, - 'ip.ignore': GENERAL, - 'wazuh.monitoring.enabled': MONITORING, - 'wazuh.monitoring.frequency': MONITORING, - 'wazuh.monitoring.shards': MONITORING, - 'wazuh.monitoring.replicas': MONITORING, - 'wazuh.monitoring.creation': MONITORING, - 'wazuh.monitoring.pattern': MONITORING, - hideManagerAlerts: GENERAL, - 'enrollment.dns': GENERAL, - 'cron.prefix': GENERAL, - 'cron.statistics.status': STATISTICS, - 'cron.statistics.apis': STATISTICS, - 'cron.statistics.interval': STATISTICS, - 'cron.statistics.index.name': STATISTICS, - 'cron.statistics.index.creation': STATISTICS, - 'cron.statistics.index.shards': STATISTICS, - 'cron.statistics.index.replicas': STATISTICS, - 'alerts.sample.prefix': GENERAL, - 'vulnerabilities.pattern': VULNERABILITIES, - 'checks.vulnerabilities.pattern': HEALTH_CHECK, -}; - -const TEXT = 'text'; -const NUMBER = 'number'; -const LIST = 'list'; -const BOOLEAN = 'boolean'; -const ARRAY = 'array'; -const INTERVAL = 'interval'; - -export const formEquivalence = { - pattern: { type: TEXT }, - 'customization.logo.app': { type: TEXT }, - 'customization.logo.healthcheck': { type: TEXT }, - 'customization.logo.reports': { type: TEXT }, - 'checks.pattern': { type: BOOLEAN }, - 'checks.template': { type: BOOLEAN }, - 'checks.api': { type: BOOLEAN }, - 'checks.setup': { type: BOOLEAN }, - 'checks.fields': { type: BOOLEAN }, - 'checks.metaFields': { type: BOOLEAN }, - 'checks.timeFilter': { type: BOOLEAN }, - 'checks.maxBuckets': { type: BOOLEAN }, - timeout: { type: NUMBER }, - 'ip.selector': { type: BOOLEAN }, - 'ip.ignore': { type: ARRAY }, - 'wazuh.monitoring.enabled': { type: BOOLEAN }, - 'wazuh.monitoring.frequency': { type: NUMBER }, - 'wazuh.monitoring.shards': { type: NUMBER }, - 'wazuh.monitoring.replicas': { type: NUMBER }, - 'wazuh.monitoring.creation': { - type: LIST, - params: { - options: [ - { text: 'Hourly', value: 'h' }, - { text: 'Daily', value: 'd' }, - { text: 'Weekly', value: 'w' }, - { text: 'Monthly', value: 'm' }, - ], - }, - }, - 'wazuh.monitoring.pattern': { type: TEXT }, - hideManagerAlerts: { type: BOOLEAN }, - 'enrollment.dns': { type: TEXT }, - 'cron.prefix': { type: TEXT }, - 'cron.statistics.status': { type: BOOLEAN }, - 'cron.statistics.apis': { type: ARRAY }, - 'cron.statistics.interval': { type: INTERVAL }, - 'cron.statistics.index.name': { type: TEXT }, - 'cron.statistics.index.creation': { - type: LIST, - params: { - options: [ - { text: 'Hourly', value: 'h' }, - { text: 'Daily', value: 'd' }, - { text: 'Weekly', value: 'w' }, - { text: 'Monthly', value: 'm' }, - ], - }, - }, - 'cron.statistics.index.shards': { type: NUMBER }, - 'cron.statistics.index.replicas': { type: NUMBER }, - 'alerts.sample.prefix': { type: TEXT }, - 'vulnerabilities.pattern': { type: TEXT }, - 'checks.vulnerabilities.pattern': { type: BOOLEAN }, -}; diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index d1a9ea4fd3..0d30df7489 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -51,6 +51,10 @@ export const WAZUH_STATISTICS_DEFAULT_CRON_FREQ = '0 */5 * * * *'; // Wazuh vulnerabilities export const WAZUH_VULNERABILITIES_PATTERN = 'wazuh-states-vulnerabilities'; export const WAZUH_INDEX_TYPE_VULNERABILITIES = 'vulnerabilities'; +export const VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER = { + enabled: 'wazuh.cluster.name', + disabled: 'wazuh.manager.name', +}; // Wazuh fim export const WAZUH_FIM_PATTERN = 'wazuh-states-fim'; @@ -230,6 +234,7 @@ export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { } export const AUTHORIZED_AGENTS = 'authorized-agents'; +export const DATA_SOURCE_FILTER_CONTROLLED_EXCLUDE_SERVER = 'exclude-server'; // Wazuh links export const WAZUH_LINK_GITHUB = 'https://github.com/wazuh'; @@ -809,60 +814,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return schema.boolean(); }, }, - 'checks.vulnerabilities.pattern': { - title: 'Vulnerabilities index pattern', - description: - 'Enable or disable the vulnerabilities index pattern health check when opening the app.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: false, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.fim.pattern': { - title: 'Fim index pattern', - description: - 'Enable or disable the fim index pattern health check when opening the app.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, 'cron.prefix': { title: 'Cron prefix', description: 'Define the index prefix of predefined jobs.', diff --git a/plugins/main/common/plugin-settings.test.ts b/plugins/main/common/plugin-settings.test.ts index 959f5f9d7d..27b39a1240 100644 --- a/plugins/main/common/plugin-settings.test.ts +++ b/plugins/main/common/plugin-settings.test.ts @@ -34,8 +34,6 @@ describe('[settings] Input validation', () => { ${'checks.template'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} ${'checks.timeFilter'} | ${true} | ${undefined} ${'checks.timeFilter'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'checks.vulnerabilities.pattern'} | ${true} | ${undefined} - ${'checks.vulnerabilities.pattern'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} ${'cron.prefix'} | ${'test'} | ${undefined} ${'cron.prefix'} | ${'test space'} | ${'No whitespaces allowed.'} ${'cron.prefix'} | ${''} | ${'Value can not be empty.'} diff --git a/plugins/main/opensearch_dashboards.json b/plugins/main/opensearch_dashboards.json index 1134b02ed2..1aa9b714ce 100644 --- a/plugins/main/opensearch_dashboards.json +++ b/plugins/main/opensearch_dashboards.json @@ -20,8 +20,7 @@ "opensearchDashboardsUtils", "opensearchDashboardsLegacy", "wazuhCheckUpdates", - "wazuhCore", - "wazuhEndpoints" + "wazuhCore" ], "optionalPlugins": [ "security", diff --git a/plugins/main/package.json b/plugins/main/package.json index 9d2382293d..e110c26dde 100644 --- a/plugins/main/package.json +++ b/plugins/main/package.json @@ -3,7 +3,7 @@ "version": "5.0.0", "revision": "00", "pluginPlatform": { - "version": "2.11.0" + "version": "2.12.0" }, "description": "Wazuh dashboard", "keywords": [ diff --git a/plugins/main/public/components/agents/__snapshots__/agent-status.test.tsx.snap b/plugins/main/public/components/agents/__snapshots__/agent-status.test.tsx.snap index 6253e66ffd..e4b3c3708c 100644 --- a/plugins/main/public/components/agents/__snapshots__/agent-status.test.tsx.snap +++ b/plugins/main/public/components/agents/__snapshots__/agent-status.test.tsx.snap @@ -26,7 +26,11 @@ exports[`AgentStatus component Renders status indicator with the its color and t viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" - /> + > + +
+ > + +
`; diff --git a/plugins/main/public/components/agents/__snapshots__/agent-synced.test.tsx.snap b/plugins/main/public/components/agents/__snapshots__/agent-synced.test.tsx.snap index de773fa3b7..21088099b7 100644 --- a/plugins/main/public/components/agents/__snapshots__/agent-synced.test.tsx.snap +++ b/plugins/main/public/components/agents/__snapshots__/agent-synced.test.tsx.snap @@ -23,7 +23,11 @@ Array [ viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" - /> + > + +
+ > + +
- - + > + + + Start: @@ -459,7 +463,7 @@ exports[`AgentStatTable component Renders correctly to match the snapshot 1`] = size="m" type="importAction" > - - + > + + +
-
-

- Network interfaces - - (0) - -

-
- + Network interfaces + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
- -
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + - - - - + + + + + + +
+
+ + No items found + +
+
- + + + +
+
+
+
+
-
- - - - - - - - - - + + + + +
+
+
+
-
- - -
-
+
+
+

+ Network ports + + (0) + +

+
+
+ +
+
+
-
- - - - +
-
- -
-
- +
- No items found - +
+ +
+
+
+ +
+
+
-
-
-
-
-
-
-
-
-
-

- Network ports - - (0) - -

-
-
-
-
- -
-
- +
+
-
-
-
-
+
-
-
+
+
- -
+ class="euiFlexItem euiFlexItem--flexGrowZero" + />
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + - - - - - - - - -
-
-
-
-
-
-
-
-
-
+
- - - - +
+
+ + No items found + +
+
- - - - - - - - - - - - - - -
-
- - - Local port - - - - - - Local IP address - - - - - - -
-
- - No items found - -
-
@@ -845,377 +885,397 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Network settings - - (0) - -

-
- + Network settings + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
- +
+
+
+
+
+
+ + + + + + +
+
+ + + - - - - - - - - - - - - + - + - + + + + + - - - - - - -
-
- - - - - Address - - - - - + - - Netmask - - - - - + - - Protocol - - - - - +
+
- Broadcast - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -1231,185 +1291,262 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Packages - - (0) - -

-
- + Packages + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - - + - + - + + + + + - - - - - - -
-
- - - - - Version - - - - - + - - Format - - - - - + - - Location - - - - - +
+
- Description - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -1619,185 +1699,263 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Processes - - (0) - -

-
- + Processes + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - + - - + - + - + - + + + + + - - - - - - -
-
- - - Name - - - - - - + - - Effective user - - - - - - - + - - Parent PID - - - - - + - - VM size - - - - - + - - Priority - - - - - +
+
- State - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -2177,360 +2277,396 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Network interfaces - - (0) - -

-
- + Network interfaces + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - + - - + - + + + + + - - - - - - -
-
- - - Name - - - - - - + - - MAC - - - - - - - + - - MTU - - - - - +
+
- Type - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -2543,1084 +2679,780 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Network ports - - (0) - -

-
- + Network ports + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
- + + + + Sorting + + + +
+
-
-
- - - - - - + + - + - + - - - - - - - -
-
- - - Local port - - - - - - Local IP address - - - - - +
+
- Process - - - - - + + Local port + + + - PID - - - - - State + + Process + - - - - - Protocol + + PID + - - -
-
- +
- No items found - - - -
-
-
-
-
-
-
-
-
-
-
-

- Network settings - - (0) - -

-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
+ + + State + + + + + -
-
-
-
+ + + + + + +
+ + No items found + +
+ + + +
+
+
+
+
+
+
-
-
+
-
-
-
- -
-
+ Network settings + + (0) + +
-
- - - - - - - - - - - - - - - -
-
+
+
-
- - - - +
-
- -
-
- - No items found - -
-
-
-
-
-
-
-
-
-
-
-
-

- Packages - - (0) - -

-
-
-
-
- -
-
- +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - - + - + - + + + + + - - - - - - -
-
- - - - - Architecture - - - - - + - - Version - - - - - + - - Vendor - - - - - +
+
- Description - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -3636,532 +3468,976 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Processes - - (0) - -

-
- + Packages + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
+
+
+ +
+
+
+
+
+
+ + + + - - - - - + + + + + + + +
+
- - - - - - - - - - - + - + - + - + + + + + + + +
-
- - - Name - - - - - - + - - Effective user - - - - - + - - Effective group - - - - - + - - PID - - - - + + Description + + + +
+
+ + No items found + +
+
+ + + + + + + +
+
+
+
+
+
+
+
+
- -
+ + + +
+
+
-
+
-
+ + + + +
+
+
+
+
- -
+ +
+
+ +
+
+ + + + + + +
+
+
+
+
- -
- + + + + + + + + + + - + - + + + + + + + + + - - - - - - -
+
- - Session - - - - - + - - Priority - - - - - + + + + + + + + + + + + + + + + + - - State - - - -
-
- + + Priority + + + + +
- No items found - - - -
+ +
+
+ + No items found + +
+
+
+
@@ -4299,185 +4575,262 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Network interfaces - - (0) - -

-
- + Network interfaces + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - - + - + - + + + + + - - - - - - -
-
- - - - - MAC - - - - - + - - State - - - - - + - - MTU - - - - - +
+
- Type - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -4682,347 +4978,367 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Network ports - - (0) - -

-
- + Network ports + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
- + + + + Sorting + + + +
+
-
-
- - - - - - + + - - - - - - - - -
-
- - - Local port - - - - - - Local IP address - - - -
+
- Process + + Local port + - - - - - State + + Local IP address + - - - - - - Protocol - - - -
-
- + + Process + + + + +
- No items found - - - -
+ + + + + + + + + + +
+ + No items found + +
+ + + + +
+
@@ -5039,185 +5355,262 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Network settings - - (0) - -

-
- + Network settings + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - - + - + - + + + + + - - - - - - -
-
- - - - - Address - - - - - + - - Netmask - - - - - + - - Protocol - - - - - +
+
- Broadcast - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -5422,185 +5758,262 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Windows updates - - (0) - -

-
- + Windows updates + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + +
+
- - - - +
+
+ + No items found + +
+
- - - - - - - - - - - -
-
- -
-
- - No items found - -
-
@@ -5712,185 +6068,262 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Packages - - (0) - -

-
- + Packages + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - - + - + + + + + - - - - - - -
-
- - - - - Architecture - - - - - + - - Version - - - - - +
+
- Vendor - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
@@ -6076,185 +6452,263 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = ` class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow wz-agent-inventory-panel" >
-
-

- Processes - - (0) - -

-
- + Processes + + (0) + + +
+
- +
+
+ + + + + Export formatted + + + +
+
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+ + - - - - - - - - - - - - + - + - + - + - + + + + + - - - - - - -
-
- - - - - PID - - - - - + - - Parent PID - - - - - + - - VM size - - - - - + - - Priority - - - - - + - - NLWP - - - - - +
+
- Command - - - - -
-
- - No items found - -
-
+ + No items found + + + +
+
+
diff --git a/plugins/main/public/components/common/charts/visualizations/basic.tsx b/plugins/main/public/components/common/charts/visualizations/basic.tsx index 3897ae1788..2e48e5e734 100644 --- a/plugins/main/public/components/common/charts/visualizations/basic.tsx +++ b/plugins/main/public/components/common/charts/visualizations/basic.tsx @@ -1,23 +1,32 @@ -import React, { useCallback, useState } from "react"; -import { ChartLegend } from "./legend"; +import React, { useCallback, useState } from 'react'; +import { ChartLegend } from './legend'; import { ChartDonut, ChartDonutProps } from '../charts/donut'; -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiSelect, EuiSpacer } from '@elastic/eui'; -import { useAsyncActionRunOnStart } from "../../hooks"; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingChart, + EuiText, + EuiSelect, + EuiSpacer, +} from '@elastic/eui'; +import { useAsyncActionRunOnStart } from '../../hooks'; +import './visualizations.scss'; export type VisualizationBasicProps = ChartDonutProps & { - type: 'donut', - size: number | string | { width: number | string, height: number | string } - showLegend?: boolean - isLoading?: boolean - noDataTitle?: string - noDataMessage?: string | (() => React.node) - errorTitle?: string - errorMessage?: string | (() => React.node) - error?: { message: string } -} + type: 'donut'; + size: number | string | { width: number | string; height: number | string }; + showLegend?: boolean; + isLoading?: boolean; + noDataTitle?: string; + noDataMessage?: string | (() => React.node); + errorTitle?: string; + errorMessage?: string | (() => React.node); + error?: { message: string }; +}; const chartTypes = { - 'donut': ChartDonut + donut: ChartDonut, }; /** @@ -34,131 +43,165 @@ export const VisualizationBasic = ({ noDataMessage, errorTitle = 'Error', errorMessage, - error + error, }: VisualizationBasicProps) => { - const { width, height } = typeof size === 'object' ? size : { width: size, height: size }; + const { width, height } = + typeof size === 'object' ? size : { width: size, height: size }; let visualization = null; if (isLoading) { visualization = ( -
- +
+
- ) + ); } else if (errorMessage || error?.message) { visualization = ( {errorTitle}} body={errorMessage || error?.message} /> - ) + ); } else if (!data || (Array.isArray(data) && !data.length)) { visualization = ( {noDataTitle}} - body={typeof noDataMessage === 'function' ? noDataMessage() : noDataMessage} + body={ + typeof noDataMessage === 'function' ? noDataMessage() : noDataMessage + } /> - ) + ); } else { const Chart = chartTypes[type]; - const chartFlexStyle = { - alignItems: 'flex-end', - paddingRight: '1em' - } - const legendFlexStyle = { - height: '100%', - paddingLeft: '1em' - } visualization = ( - - + + {showLegend && ( - + ({ ...rest, labelColor: color, color: 'text' }))} + data={data.map(({ color, ...rest }) => ({ + ...rest, + labelColor: color, + color: 'text', + }))} /> )} - ) + ); } return ( -
+
{visualization}
- ) - -} + ); +}; type VisualizationBasicWidgetProps = VisualizationBasicProps & { - onFetch: (...dependencies) => any[] - onFetchDependencies?: any[] -} + onFetch: (...dependencies) => any[]; + onFetchDependencies?: any[]; +}; /** * Component that fetch the data and renders the visualization using the visualization basic component */ -export const VisualizationBasicWidget = ({ onFetch, onFetchDependencies, ...rest }: VisualizationBasicWidgetProps) => { - const { running, ...restAsyncAction } = useAsyncActionRunOnStart(onFetch, onFetchDependencies); +export const VisualizationBasicWidget = ({ + onFetch, + onFetchDependencies, + ...rest +}: VisualizationBasicWidgetProps) => { + const { running, ...restAsyncAction } = useAsyncActionRunOnStart( + onFetch, + onFetchDependencies, + ); - return -} + return ( + + ); +}; type VisualizationBasicWidgetSelectorProps = VisualizationBasicWidgetProps & { - selectorOptions: { value: any, text: string }[] - title?: string - onFetchExtraDependencies?: any[] -} + selectorOptions: { value: any; text: string }[]; + title?: string; + onFetchExtraDependencies?: any[]; +}; /** - * Renders a visualization that has a selector to change the resource to fetch data and display it. Use the visualization basic. + * Renders a visualization that has a selector to change the resource to fetch data and display it. Use the visualization basic. */ -export const VisualizationBasicWidgetSelector = ({ selectorOptions, title, onFetchExtraDependencies, ...rest }: VisualizationBasicWidgetSelectorProps) => { - const [selectedOption, setSelectedOption] = useState(selectorOptions[0].value); +export const VisualizationBasicWidgetSelector = ({ + selectorOptions, + title, + onFetchExtraDependencies, + ...rest +}: VisualizationBasicWidgetSelectorProps) => { + const [selectedOption, setSelectedOption] = useState( + selectorOptions[0].value, + ); - const onChange = useCallback((e) => setSelectedOption(e.target.value)); + const onChange = useCallback(e => setSelectedOption(e.target.value)); return ( <> - + {title && ( -

-

{title}

+

+ +

{title}

+

)}
-
option.value === selectedOption)) - : rest.noDataMessage - } - : {} - )} - onFetchDependencies={[selectedOption, ...(onFetchExtraDependencies || [])]} + {...(rest.noDataMessage + ? { + noDataMessage: + typeof rest.noDataMessage === 'function' + ? rest.noDataMessage( + selectedOption, + selectorOptions.find( + option => option.value === selectedOption, + ), + ) + : rest.noDataMessage, + } + : {})} + onFetchDependencies={[ + selectedOption, + ...(onFetchExtraDependencies || []), + ]} /> - ) -} \ No newline at end of file + ); +}; diff --git a/plugins/main/public/components/common/charts/visualizations/legend.scss b/plugins/main/public/components/common/charts/visualizations/legend.scss index 99e97df2f2..a3a9b27787 100644 --- a/plugins/main/public/components/common/charts/visualizations/legend.scss +++ b/plugins/main/public/components/common/charts/visualizations/legend.scss @@ -1,7 +1,20 @@ +.chart-legend { + width: 60vw; + overflow: auto; + scrollbar-width: none; + @media (min-width: 1024px) { + width: 160px; + } +} + .chart-legend > li { - margin-top: 0px; + margin-top: 5px !important; +} + +.chart-legend > li svg { + margin-right: 6px; } .chart-legend > li > * { - padding: 0px; + padding: 0px; } diff --git a/plugins/main/public/components/common/charts/visualizations/legend.tsx b/plugins/main/public/components/common/charts/visualizations/legend.tsx index b36270bdb3..f3a2c6a6e8 100644 --- a/plugins/main/public/components/common/charts/visualizations/legend.tsx +++ b/plugins/main/public/components/common/charts/visualizations/legend.tsx @@ -1,32 +1,41 @@ -import React from "react"; -import { EuiIcon } from "@elastic/eui"; -import { EuiListGroup } from "@elastic/eui"; +import React from 'react'; +import { EuiIcon } from '@elastic/eui'; +import { EuiListGroup } from '@elastic/eui'; import './legend.scss'; type ChartLegendProps = { data: { - label: string - value: any - color: string - labelColor: string - }[] -} + label: string; + value: any; + color: string; + labelColor: string; + }[]; +}; /** * Create the legend to use with charts in visualizations. */ export function ChartLegend({ data }: ChartLegendProps) { - const list = data.map(({label, labelColor, value, ...rest}, idx) => ({ - label:
{`${label} (${value})`}
, - icon: , - ...rest + const list = data.map(({ label, labelColor, value, ...rest }, idx) => ({ + label: ( +
{`${label} (${value})`}
+ ), + icon: , + ...rest, })); return ( + flush + /> ); -} \ No newline at end of file +} diff --git a/plugins/main/public/components/common/charts/visualizations/visualizations.scss b/plugins/main/public/components/common/charts/visualizations/visualizations.scss new file mode 100644 index 0000000000..e24c29c81d --- /dev/null +++ b/plugins/main/public/components/common/charts/visualizations/visualizations.scss @@ -0,0 +1,19 @@ +.wazuh-visualization-layout { + @media (max-width: 767px) { + height: auto !important; + } +} + +.wazuh-visualization-chart { + align-items: flex-end; + padding-right: 1em; + min-height: 150px; + @media (max-width: 767px) { + align-items: center; + } +} + +.wazuh-visualization-legend { + height: 100%; + padding-left: 1em; +} 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 c7ce052e16..70315ae2fc 100644 --- a/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx +++ b/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { escapeRegExp } from 'lodash'; import { i18n } from '@osd/i18n'; import { FieldIcon } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; -import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; const COLLAPSE_LINE_LENGTH = 350; const DOT_PREFIX_RE = /(.).+?\./g; @@ -134,15 +134,10 @@ const DocViewer = (props: tDocViewerProps) => { {...fieldIconProps} /> - - - {displayName} - + + + {displayName} +
@@ -156,7 +151,7 @@ const DocViewer = (props: tDocViewerProps) => { */ // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: value as string }} - style={{ overflowY: 'auto' }} + style={{ overflowY: 'auto', wordBreak: 'break-all' }} /> diff --git a/plugins/main/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap b/plugins/main/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap index 9a0cb7b29a..b50866b573 100644 --- a/plugins/main/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap +++ b/plugins/main/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap @@ -57,7 +57,7 @@ exports[`ErrorBoundary component renders correctly to match the snapshot 1`] = ` size="xxl" type="faceSad" > - - + > + + + + > + +
@@ -357,7 +361,11 @@ exports[`[component] InputForm Renders correctly to match the snapshot: Input: s viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" - /> + > + +
@@ -396,7 +404,11 @@ exports[`[component] InputForm Renders correctly to match the snapshot: Input: s viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" - /> + > + + + > + + diff --git a/plugins/main/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap b/plugins/main/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap index 1a84b4ea0f..509fa987ee 100644 --- a/plugins/main/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap +++ b/plugins/main/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap @@ -62,7 +62,7 @@ exports[`withErrorBoundary hoc implementation renders correctly to match the sna size="xxl" type="faceSad" > - - + > + + + WrappedComponent => props => { - return condition(props) ? : -} \ No newline at end of file +export const withGuard = + (condition: (props: any) => boolean, ComponentFulfillsCondition) => + WrappedComponent => + props => { + return condition(props) ? ( + + ) : ( + + ); + }; + +export const withGuardAsync = + ( + condition: (props: any) => { ok: boolean; data: any }, + ComponentFulfillsCondition: React.JSX.Element, + ComponentLoadingResolution: null | React.JSX.Element = null, + ) => + WrappedComponent => + props => { + const [loading, setLoading] = useState(true); + const [fulfillsCondition, setFulfillsCondition] = useState({ + ok: false, + data: {}, + }); + + const execCondition = async () => { + try { + setLoading(true); + setFulfillsCondition({ ok: false, data: {} }); + setFulfillsCondition( + await condition({ ...props, check: execCondition }), + ); + } catch (error) { + setFulfillsCondition({ ok: false, data: { error } }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + execCondition(); + }, []); + + if (loading) { + return ComponentLoadingResolution ? ( + + ) : null; + } + + return fulfillsCondition.ok ? ( + + ) : ( + + ); + }; diff --git a/plugins/main/public/components/common/hooks/index.ts b/plugins/main/public/components/common/hooks/index.ts index a4cd7dbf91..e3ce7584c7 100644 --- a/plugins/main/public/components/common/hooks/index.ts +++ b/plugins/main/public/components/common/hooks/index.ts @@ -28,4 +28,3 @@ export * from './use_async_action_run_on_start'; export { useEsSearch } from './use-es-search'; export { useValueSuggestion, IValueSuggestion } from './use-value-suggestion'; export * from './use-state-storage'; -export * from './useDockedSideNav'; diff --git a/plugins/main/public/components/common/hooks/useDockedSideNav.tsx b/plugins/main/public/components/common/hooks/useDockedSideNav.tsx deleted file mode 100644 index 489536b4d6..0000000000 --- a/plugins/main/public/components/common/hooks/useDockedSideNav.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getChrome } from '../../../kibana-services'; - -export const useDockedSideNav = () => { - const [sideNavDocked, setSideNavDocked] = useState(false); - - useEffect(() => { - const isNavDrawerSubscription = getChrome() - .getIsNavDrawerLocked$() - .subscribe((value: boolean) => { - setSideNavDocked(value); - }); - - return () => { - isNavDrawerSubscription.unsubscribe(); - }; - }, []); - - return sideNavDocked; -}; diff --git a/plugins/main/public/components/common/loading/loading-spinner-data-source.tsx b/plugins/main/public/components/common/loading/loading-spinner-data-source.tsx new file mode 100644 index 0000000000..36c4b49930 --- /dev/null +++ b/plugins/main/public/components/common/loading/loading-spinner-data-source.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + EuiTitle, + EuiPanel, + EuiEmptyPrompt, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { IntlProvider } from 'react-intl'; + +export function LoadingSpinnerDataSource() { + return ( + + + } + title={ + +

+ +

+
+ } + /> +
+
+ ); +} 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 deleted file mode 100644 index e5e5f5fc97..0000000000 --- a/plugins/main/public/components/common/modules/events-enhance-discover-fields.ts +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import { FlyoutDetail } from '../../../components/agents/fim/inventory/flyout'; -import { FlyoutTechnique } from '../../overview/mitre/components/techniques/components/flyout-technique'; -import { AppNavigate } from '../../../react-services/app-navigate'; -import { getCore } from '../../../kibana-services'; - -// Field to add to elements enchanced -const CUSTOM_ATTRIBUTE_ENHANCED_DISCOVER_FIELD = - 'data-wazuh-discover-field-enhanced'; - -type TGetFlyoutProps = (content: string, rowData, options) => any; - -// Set attributes (as object) in a HTML element -const addAttributesToElement = (element, attributes: any, ...options) => { - Object.keys(attributes).forEach(attribute => { - const attributeValue: - | ((...options) => string | undefined) - | string - | undefined = attributes[attribute]; - const attributeValueResult = - typeof attributeValue === 'function' - ? attributeValue(...options) - : attributeValue; - if (attributeValueResult) { - element.setAttribute(attribute, attributeValueResult); - } - }); -}; - -// Enhance field: create an anchor HTML element that redirect to some URL -const createElementFieldURLRedirection = - (attributes?: any) => (content: string, rowData, element, options) => { - const container = document.createElement('a'); - addAttributesToElement(container, attributes, content, rowData, options); - container.textContent = content; - return container.getAttribute('href') ? container : undefined; - }; - -// Enhance field: create an anchor HTML element that open a flyout React component -const createElementFieldOpenFlyout = - (FlyoutComponent, getFlyoutProps: TGetFlyoutProps) => - (content: string, rowData, element, options) => { - const container = document.createElement('a'); - container.onclick = () => { - options.setFlyout({ - component: FlyoutComponent, - props: getFlyoutProps(content, rowData, options), - }); - }; - container.textContent = content; - return container; - }; - -// Enhance field: create a div HTML element with anchor HTML elements in the same cell to open a flyout React component with different data -const createElementFieldOpenFlyoutMultiple = - (FlyoutComponent, getFlyoutProps: TGetFlyoutProps, containerOptions) => - (content: string, rowData, element, options) => { - const container = document.createElement(containerOptions.element || 'div'); - const formattedContent = content.match(containerOptions.contentRegex); - if (!formattedContent) { - return; - } - const createElementFieldOpenFlyoutCreator = createElementFieldOpenFlyout( - FlyoutComponent, - getFlyoutProps, - ); - formattedContent.forEach((item, itemIndex) => { - const itemElement = createElementFieldOpenFlyoutCreator( - item, - rowData, - element, - options, - ); - container.appendChild(itemElement); - if (itemIndex < formattedContent.length - 1) { - const separatorElement = document.createElement( - containerOptions.separatorElement || 'span', - ); - separatorElement.textContent = containerOptions.separator || ', '; - container.appendChild(separatorElement); - } - }); - return container; - }; - -// Returns the style attribute value as string -const styleObjectToString = (obj: any) => - Object.keys(obj) - .map(key => `${key}: ${obj[key]}`) - .join('; '); - -// Styles for external links (as object) -const attributesExternalLink = { - target: '__blank', - rel: 'noreferrer', -}; - -// Define button styles -const buttonStyles = attributesExternalLink; - -export const EventsEnhanceDiscoverCell = { - 'rule.id': createElementFieldURLRedirection({ - ...buttonStyles, - href: content => - getCore().application.getUrlForApp('rules', { - path: `#/manager/rules?tab=rules&redirectRule=${content}`, - }), - }), - 'agent.id': createElementFieldURLRedirection({ - ...buttonStyles, - href: content => - content !== '000' - ? getCore().application.getUrlForApp('endpoints-summary', { - path: `#/agents?tab=welcome&agent=${content}`, - }) - : undefined, - }), - 'agent.name': createElementFieldURLRedirection({ - ...buttonStyles, - href: (content, rowData) => { - const agentId = (((rowData || {})._source || {}).agent || {}).id; - return agentId !== '000' - ? getCore().application.getUrlForApp('endpoints-summary', { - path: `#/agents?tab=welcome&agent=${agentId}`, - }) - : undefined; - }, - }), - 'syscheck.path': createElementFieldOpenFlyout( - FlyoutDetail, - (content, rowData, options) => ({ - fileName: content, - agentId: (((rowData || {})._source || {}).agent || {}).id, - type: 'file', - view: 'events', - closeFlyout: options.closeFlyout, - }), - ), - 'rule.mitre.id': createElementFieldOpenFlyoutMultiple( - FlyoutTechnique, - (content, rowData, options) => ({ - openDiscover(e, techniqueID) { - AppNavigate.navigateToModule(e, 'overview', { - tab: 'mitre', - tabView: 'discover', - filters: { 'rule.mitre.id': techniqueID }, - }); - }, - openDashboard(e, techniqueID) { - AppNavigate.navigateToModule(e, 'overview', { - tab: 'mitre', - tabView: 'dashboard', - filters: { 'rule.mitre.id': techniqueID }, - }); - }, - onChangeFlyout(isFlyoutVisible) { - isFlyoutVisible ? null : options.closeFlyout(); - }, - currentTechnique: content, - }), - { - contentRegex: /(T\d+\.?(\d+)?)/g, - element: 'span', - }, - ), - 'syscheck.value_name': (content, rowData, element, options) => { - if (content) return; - const container = document.createElement('span'); - container.insertAdjacentHTML( - 'beforeend', - '', - ); - container.insertAdjacentHTML( - 'beforeend', - 'Empty field', - ); - return container; - }, -}; - -// Method to enhance a cell of discover table -export const enhanceDiscoverEventsCell = ( - field, - content, - rowData, - element, - options, -) => { - if ( - !EventsEnhanceDiscoverCell[field] || - !element || - element.attributes[CUSTOM_ATTRIBUTE_ENHANCED_DISCOVER_FIELD] - ) { - return; - } - const elementCellEnhanced = EventsEnhanceDiscoverCell[field]( - content, - rowData, - element, - options, - ); - if (elementCellEnhanced) { - elementCellEnhanced.setAttribute( - CUSTOM_ATTRIBUTE_ENHANCED_DISCOVER_FIELD, - '', - ); // Set a custom attribute to indentify that element was enhanced - element.replaceWith(elementCellEnhanced); - } -}; diff --git a/plugins/main/public/components/common/modules/events-selected-fields.js b/plugins/main/public/components/common/modules/events-selected-fields.js deleted file mode 100644 index ee685c42e7..0000000000 --- a/plugins/main/public/components/common/modules/events-selected-fields.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Wazuh app - Simple description for each App tabs - * 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. - */ -export const EventsSelectedFiles = { - fim: [ - 'agent.name', - 'syscheck.path', - 'syscheck.event', - 'rule.description', - 'rule.level', - 'rule.id' - ], - mitre: [ - 'agent.name', - 'rule.mitre.id', - 'rule.mitre.tactic', - 'rule.description', - 'rule.level', - 'rule.id' - ], - sca: [ - 'data.sca.check.title', - 'data.sca.check.file', - 'data.sca.check.result', - 'data.sca.policy', - ], - general: [ - 'agent.name', - 'rule.description', - 'rule.level', - 'rule.id', - ], - aws: [ - 'data.aws.source', - 'rule.description', - 'rule.level', - 'rule.id', - ], - gcp: [ - 'agent.name', - 'data.gcp.jsonPayload.vmInstanceName', - 'data.gcp.resource.labels.location', - 'data.gcp.resource.labels.project_id', - 'data.gcp.resource.type', - 'data.gcp.severity', - ], - pm: [ - 'agent.name', - 'data.title', - 'rule.description', - 'rule.level', - 'rule.id', - ], - audit: [ - 'agent.name', - 'data.audit.command', - 'data.audit.pid', - 'rule.description', - 'rule.level', - 'rule.id', - ], - oscap: [ - 'agent.name', - 'data.oscap.check.title', - 'data.oscap.check.description', - 'data.oscap.check.result', - 'data.oscap.check.severity', - ], - ciscat: [ - 'agent.name', - 'data.cis.benchmark', - 'data.cis.group', - 'data.cis.pass', - 'data.cis.fail', - 'data.cis.unknown', - 'data.cis.result', - ], - vuls: [ - 'agent.name', - 'data.vulnerability.package.name', - 'data.vulnerability.cve', - 'data.vulnerability.severity', - 'data.vulnerability.status' - ], - virustotal: [ - 'agent.name', - 'data.virustotal.source.file', - 'data.virustotal.permalink', - 'data.virustotal.malicious', - 'data.virustotal.positives', - 'data.virustotal.total', - ], - osquery: [ - 'agent.name', - 'data.osquery.name', - 'data.osquery.pack', - 'data.osquery.action', - 'data.osquery.subquery', - ], - docker: [ - 'agent.name', - 'data.docker.from', - 'data.docker.Type', - 'data.docker.Action', - 'rule.description', - 'rule.level', - ], - pci: [ - 'agent.name', - 'rule.pci_dss', - 'rule.description', - 'rule.level', - 'rule.id' - ], - gdpr: [ - 'agent.name', - 'rule.gdpr', - 'rule.description', - 'rule.level', - 'rule.id' - ], - hipaa: [ - 'agent.name', - 'rule.hipaa', - 'rule.description', - 'rule.level', - 'rule.id' - ], - nist: [ - 'agent.name', - 'rule.nist_800_53', - 'rule.description', - 'rule.level', - 'rule.id' - ], - tsc: [ - 'agent.name', - 'rule.tsc', - 'rule.description', - 'rule.level', - 'rule.id' - ], - office: [ - 'data.office365.Subscription', - 'data.office365.Operation', - 'data.office365.UserId', - 'data.office365.ClientIP', - 'rule.level', - 'rule.id' - ], - github: [ - 'agent.id', - 'data.github.repo', - 'data.github.actor', - 'data.github.org', - 'rule.description', - 'rule.level', - 'rule.id' - ], - -}; diff --git a/plugins/main/public/components/common/modules/events.tsx b/plugins/main/public/components/common/modules/events.tsx deleted file mode 100644 index 0005eeb9d6..0000000000 --- a/plugins/main/public/components/common/modules/events.tsx +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import React, { Component, Fragment } from 'react'; -import { getAngularModule, getToasts } from '../../../kibana-services'; -import { EventsSelectedFiles } from './events-selected-fields'; -import { ModulesHelper } from './modules-helper'; -import store from '../../../redux/store'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiOverlayMask, - EuiOutsideClickDetector, -} from '@elastic/eui'; -import { PatternHandler } from '../../../react-services/pattern-handler'; -import { enhanceDiscoverEventsCell } from './events-enhance-discover-fields'; -import { toMountPoint } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; -import { withAgentSupportModule, withModuleTabLoader } from '../hocs'; -import { compose } from 'redux'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; - -export const Events = compose( - withAgentSupportModule, - withModuleTabLoader -)( - class Events extends Component { - intervalCheckExistsDiscoverTableTime: number = 200; - isMount: boolean; - hasRefreshedKnownFields: boolean; - isRefreshing: boolean; - state: { - flyout: false | { component: any; props: any }; - discoverRowsData: any[]; - }; - constructor(props) { - super(props); - this.isMount = true; - this.hasRefreshedKnownFields = false; - this.isRefreshing = false; - this.state = { - flyout: false, - discoverRowsData: [], - }; - } - - async componentDidMount() { - document.body.scrollTop = 0; // For Safari - document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera - const app = getAngularModule(); - this.$rootScope = app.$injector.get('$rootScope'); - this.$rootScope.showModuleEvents = this.props.section; - const scope = await ModulesHelper.getDiscoverScope(); - if (this.isMount) { - this.$rootScope.moduleDiscoverReady = true; - this.$rootScope.$applyAsync(); - const fields = [...EventsSelectedFiles[this.props.section]]; - const index = fields.indexOf('agent.name'); - if (index > -1 && store.getState().appStateReducers.currentAgentData.id) { - //if an agent is pinned we don't show the agent.name column - fields.splice(index, 1); - } - if (fields) { - scope.state.columns = fields; - scope.addColumn(false); - scope.removeColumn(false); - } - this.fetchWatch = scope.$watchCollection('fetchStatus', (fetchStatus) => { - if (scope.fetchStatus === 'complete') { - setTimeout(() => { - ModulesHelper.cleanAvailableFields(); - }, 1000); - // Check the discover table is in the DOM and enhance the initial table cells - this.intervalCheckExistsDiscoverTable = setInterval(() => { - const discoverTableTBody = document.querySelector('.kbn-table tbody'); - if (discoverTableTBody) { - const options = { setFlyout: this.setFlyout, closeFlyout: this.closeFlyout }; - this.enhanceDiscoverTableCurrentRows(this.state.discoverRowsData, options, true); - this.enhanceDiscoverTableAddObservers(options); - clearInterval(this.intervalCheckExistsDiscoverTable); - } - }, this.intervalCheckExistsDiscoverTableTime); - } - this.setState({ discoverRowsData: scope.rows }); - }); - } - } - - componentWillUnmount() { - this.isMount = false; - if (this.fetchWatch) this.fetchWatch(); - this.$rootScope.showModuleEvents = false; - this.$rootScope.moduleDiscoverReady = false; - this.$rootScope.$applyAsync(); - this.discoverTableRowsObserver && this.discoverTableRowsObserver.disconnect(); - this.discoverTableColumnsObserver && this.discoverTableColumnsObserver.disconnect(); - this.intervalCheckExistsDiscoverTableTime && - clearInterval(this.intervalCheckExistsDiscoverTable); - } - - enhanceDiscoverTableAddObservers = (options) => { - // Scrolling table observer, when load more events - this.discoverTableRowsObserver = new MutationObserver((mutationsList) => { - mutationsList.forEach((mutation) => { - if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes[0]) { - this.enhanceDiscoverTableScrolling( - mutation.addedNodes[0], - this.state.discoverRowsData, - options - ); - } - }); - }); - const discoverTableTBody = document.querySelector('.kbn-table tbody'); - this.discoverTableRowsObserver.observe(discoverTableTBody, { childList: true }); - - // Add observer when add or remove table header column - this.discoverTableColumnsObserver = new MutationObserver((mutationsList) => { - mutationsList.forEach((mutation) => { - if (mutation.type === 'childList') { - this.enhanceDiscoverTableCurrentRows(this.state.discoverRowsData, options); - } - }); - }); - const discoverTableElement = document.querySelector('.kbn-table').parentElement.parentElement - .parentElement; - this.discoverTableColumnsObserver.observe(discoverTableElement, { childList: true }); - }; - - enhanceDiscoverTableCurrentRows = (discoverRowsData, options, addObserverDetails = false) => { - // Get table headers - const discoverTableHeaders = document.querySelectorAll(`.kbn-table thead th`); - // Get table rows - const discoverTableRows = document.querySelectorAll(`.kbn-table tbody tr.kbnDocTable__row`); - - discoverTableRows.forEach((row, rowIndex) => { - // Enhance each cell of table rows - discoverTableHeaders.forEach((header, headerIndex) => { - const cell = row.querySelector(`td:nth-child(${headerIndex + 1}) div`); - if (cell) { - enhanceDiscoverEventsCell( - header.textContent, - cell.textContent, - discoverRowsData[rowIndex], - cell, - options - ); - } - }); - // Add observer to row details - if (addObserverDetails) { - const rowDetails = row.nextElementSibling; - this.enhanceDiscoverTableRowDetailsAddObserver(rowDetails, discoverRowsData, options); - } - }); - }; - - checkDiscoverTableDetailsMutation(element, mutation, discoverRowsData, options) { - const rowTable = element.previousElementSibling; - const discoverTableRows = document.querySelectorAll(`.kbn-table tbody tr.kbnDocTable__row`); - const rowIndex = Array.from(discoverTableRows).indexOf(rowTable); - const rowDetailsFields = mutation.addedNodes[0].querySelectorAll('.kbnDocViewer__field'); - let hasUnknownFields = false; - if (rowDetailsFields) { - rowDetailsFields.forEach(async (rowDetailField, i) => { - //check for unknown fields until 1 unknown field is found - if (!hasUnknownFields && this.checkUnknownFields(rowDetailField)) - hasUnknownFields = true; - const fieldName = rowDetailField.childNodes[0].childNodes[1].textContent || ''; - const fieldCell = - rowDetailField.parentNode.childNodes && - rowDetailField.parentNode.childNodes[2].childNodes[0]; - if (!fieldCell) { - return; - } - enhanceDiscoverEventsCell( - fieldName, - (fieldCell || {}).textContent || '', - discoverRowsData[rowIndex], - fieldCell, - options - ); - }); - if (hasUnknownFields) { - this.refreshKnownFields(); - } - } - } - - checkUnknownFields(rowDetailField) { - const fieldCell = - rowDetailField.parentNode.childNodes && rowDetailField.parentNode.childNodes[2]; - return (fieldCell.querySelector('svg[data-test-subj="noMappingWarning"]')) - } - - refreshKnownFields = async () => { - if (!this.hasRefreshedKnownFields) { - try { - this.hasRefreshedKnownFields = true; - this.isRefreshing = true; - await PatternHandler.refreshIndexPattern(); - this.isRefreshing = false; - this.reloadToast(); - } catch (error) { - this.isRefreshing = false; - throw error; - } - } else if (this.isRefreshing) { - await new Promise((r) => setTimeout(r, 150)); - await this.refreshKnownFields(); - } - }; - - enhanceDiscoverTableRowDetailsAddObserver(element, discoverRowsData, options) { - // Open for first time the row details - const observer = new MutationObserver((mutationsList) => { - mutationsList.forEach((mutation) => { - if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes[0]) { - this.checkDiscoverTableDetailsMutation(element, mutation, discoverRowsData, options); - // Add observer when switch between the tabs of Table and JSON - new MutationObserver((mutationList) => { - if ( - mutation.addedNodes[0] - .querySelector('div[role=tabpanel]') - .getAttribute('aria-labelledby') === 'kbn_doc_viewer_tab_0' - ) { - this.checkDiscoverTableDetailsMutation( - element, - mutation, - discoverRowsData, - options - ); - } - }).observe(mutation.addedNodes[0].querySelector('div[role=tabpanel]'), { - attributes: true, - }); - } - }); - }); - observer.observe(element, { childList: true }); - } - - enhanceDiscoverTableScrolling = (mutationElement, discoverRowsData, options) => { - // Get table headers - const discoverTableHeaders = document.querySelectorAll(`.kbn-table thead th`); - // Get table rows - const discoverTableRows = document.querySelectorAll(`.kbn-table tbody tr.kbnDocTable__row`); - try { - const rowIndex = Array.from(discoverTableRows).indexOf(mutationElement); - if (rowIndex !== -1) { - // It is a discover table row - discoverTableHeaders.forEach((header, headerIndex) => { - const cell = mutationElement.querySelector(`td:nth-child(${headerIndex + 1}) div`); - if (!cell) { - return; - } - enhanceDiscoverEventsCell( - header.textContent, - cell.textContent, - discoverRowsData[rowIndex], - cell, - options - ); - }); - } else { - // It is a details table row - this.enhanceDiscoverTableRowDetailsAddObserver( - mutationElement, - discoverRowsData, - options - ); - } - } catch (error) { - const options = { - context: `${Events.name}.hideCreateCustomLabel`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - }; - - setFlyout = (flyout) => { - this.setState({ flyout }); - }; - - closeFlyout = () => { - this.setState({ flyout: false }); - }; - - reloadToast = () => { - const toastLifeTimeMs = 300000; - getToasts().add({ - color: 'success', - title: 'The index pattern was refreshed successfully.', - text: toMountPoint( - - - There were some unknown fields for the current index pattern. You need to refresh - the page to apply the changes. - - - window.location.reload()} size="s"> - Reload page - - - - ), - toastLifeTimeMs, - }); - }; - - errorToast = (error) => { - getToasts().add({ - color: 'danger', - title: 'The index pattern could not be refreshed.', - text: - 'There are some unknown fields for the current index pattern. The index pattern fields need to be refreshed.', - }); - }; - - render() { - const { flyout } = this.state; - const FlyoutComponent = (flyout || {}).component; - return ( - - {flyout && ( - - )} - - ); - } - } -); diff --git a/plugins/main/public/components/common/modules/index.ts b/plugins/main/public/components/common/modules/index.ts index 1c8866e5ba..4809b5207c 100644 --- a/plugins/main/public/components/common/modules/index.ts +++ b/plugins/main/public/components/common/modules/index.ts @@ -11,5 +11,4 @@ */ export * from './dashboard'; -export * from './events'; export * from './modules-helper.js'; diff --git a/plugins/main/public/components/common/modules/modules-defaults.js b/plugins/main/public/components/common/modules/modules-defaults.js index 41f5391fe3..7b1705b0e4 100644 --- a/plugins/main/public/components/common/modules/modules-defaults.js +++ b/plugins/main/public/components/common/modules/modules-defaults.js @@ -10,7 +10,6 @@ * Find more information about this on the LICENSE file. */ import { Dashboard } from './dashboard'; -import { Events } from './events'; import { MainSca } from '../../agents/sca'; import { MainMitre } from './main-mitre'; import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; @@ -27,9 +26,23 @@ import { vulnerabilitiesColumns } from '../../overview/vulnerabilities/events/vu import { DashboardFim } from '../../overview/fim/dashboard/dashboard'; import { InventoryFim } from '../../overview/fim/inventory/inventory'; import React from 'react'; +import { dockerColumns } from '../../overview/docker/events/docker-columns'; +import { googleCloudColumns } from '../../overview/google-cloud/events/google-cloud-columns'; +import { amazonWebServicesColumns } from '../../overview/amazon-web-services/events/amazon-web-services-columns'; import { office365Columns } from '../../overview/office-panel/events/office-365-columns'; import { fileIntegrityMonitoringColumns } from '../../overview/fim/events/file-integrity-monitoring-columns'; import { configurationAssessmentColumns } from '../../agents/sca/events/configuration-assessment-columns'; +import { pciColumns } from '../../overview/pci/events/pci-columns'; +import { hipaaColumns } from '../../overview/hipaa/events/hipaa-columns'; +import { nistColumns } from '../../overview/nist/events/nist-columns'; +import { gdprColumns } from '../../overview/gdpr/events/gdpr-columns'; +import { tscColumns } from '../../overview/tsc/events/tsc-columns'; +import { githubColumns } from '../../overview/github-panel/events/github-columns'; +import { mitreAttackColumns } from '../../overview/mitre/events/mitre-attack-columns'; +import { virustotalColumns } from '../../overview/virustotal/events/virustotal-columns'; +import { malwareDetectionColumns } from '../../overview/malware-detection/events/malware-detection-columns'; +import { WAZUH_VULNERABILITIES_PATTERN } from '../../../../common/constants'; +import { withVulnerabilitiesStateDataSource } from '../../overview/vulnerabilities/common/hocs/validate-vulnerabilities-states-index-pattern'; const DashboardTab = { id: 'dashboard', @@ -51,14 +64,7 @@ const renderDiscoverTab = (indexName = DEFAULT_INDEX_PATTERN, columns) => { }; }; -const EventsTab = { - id: 'events', - name: 'Events', - buttons: [ButtonModuleExploreAgent], - component: Events, -}; - -const RegulatoryComplianceTabs = [ +const RegulatoryComplianceTabs = columns => [ DashboardTab, { id: 'inventory', @@ -66,7 +72,7 @@ const RegulatoryComplianceTabs = [ buttons: [ButtonModuleExploreAgent], component: ComplianceTable, }, - EventsTab, + renderDiscoverTab(DEFAULT_INDEX_PATTERN, columns), ]; export const ModulesDefaults = { @@ -99,22 +105,27 @@ export const ModulesDefaults = { }, aws: { init: 'dashboard', - tabs: [DashboardTab, EventsTab], + tabs: [ + DashboardTab, + renderDiscoverTab(DEFAULT_INDEX_PATTERN, amazonWebServicesColumns), + ], availableFor: ['manager', 'agent'], }, gcp: { init: 'dashboard', - tabs: [DashboardTab, EventsTab], + tabs: [ + DashboardTab, + renderDiscoverTab(DEFAULT_INDEX_PATTERN, googleCloudColumns), + ], availableFor: ['manager', 'agent'], }, + // This module is Malware Detection. Ref: https://github.com/wazuh/wazuh-dashboard-plugins/issues/5893 pm: { init: 'dashboard', - tabs: [DashboardTab, EventsTab], - availableFor: ['manager', 'agent'], - }, - audit: { - init: 'dashboard', - tabs: [DashboardTab, EventsTab], + tabs: [ + DashboardTab, + renderDiscoverTab(DEFAULT_INDEX_PATTERN, malwareDetectionColumns), + ], availableFor: ['manager', 'agent'], }, sca: { @@ -174,15 +185,10 @@ export const ModulesDefaults = { buttons: [ButtonModuleExploreAgent], component: GitHubPanel, }, - EventsTab, + renderDiscoverTab(DEFAULT_INDEX_PATTERN, githubColumns), ], availableFor: ['manager', 'agent'], }, - ciscat: { - init: 'dashboard', - tabs: [DashboardTab, EventsTab], - availableFor: ['manager', 'agent'], - }, vuls: { init: 'dashboard', tabs: [ @@ -190,17 +196,33 @@ export const ModulesDefaults = { id: 'dashboard', name: 'Dashboard', component: DashboardVuls, - buttons: [ButtonModuleExploreAgent], + /* For ButtonModuleExploreAgent to insert correctly according to the module's index pattern, the moduleIndexPatternTitle parameter is added. By default it applies the index patternt wazuh-alerts-* */ + buttons: [ + ({ ...props }) => ( + + ), + ], }, { id: 'inventory', name: 'Inventory', component: InventoryVuls, - buttons: [ButtonModuleExploreAgent], + /* For ButtonModuleExploreAgent to insert correctly according to the module's index pattern, the moduleIndexPatternTitle parameter is added. By default it applies the index patternt wazuh-alerts-* */ + buttons: [ + ({ ...props }) => ( + + ), + ], }, { ...renderDiscoverTab(ALERTS_INDEX_PATTERN, vulnerabilitiesColumns), - component: withModuleNotForAgent(() => ( + component: withVulnerabilitiesStateDataSource(() => ( { }); it('should return the same filters and apply them to the filter manager when are received by props', async () => { + const exampleIndexPatternId = 'wazuh-index-pattern'; const defaultFilters: Filter[] = [ { query: 'something to filter', @@ -183,6 +184,7 @@ describe('[hook] useSearchBarConfiguration', () => { .mockReturnValue(defaultFilters); const { result, waitForNextUpdate } = renderHook(() => useSearchBar({ + defaultIndexPatternID: exampleIndexPatternId, filters: defaultFilters, }), ); 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 40a7488210..5f88d36919 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 @@ -10,10 +10,10 @@ import { } 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'; +import { + AUTHORIZED_AGENTS, + DATA_SOURCE_FILTER_CONTROLLED_EXCLUDE_SERVER, +} from '../../../../common/constants'; // Input - types type tUseSearchBarCustomInputs = { @@ -23,6 +23,17 @@ type tUseSearchBarCustomInputs = { payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean, ) => void; + onMount?: ( + filterManager: FilterManager, + defaultIndexPatternID: string, + ) => void; + onUpdate?: (filters: Filter[], filterManager: FilterManager) => void; + onUnMount?: ( + previousFilters: Filter[], + toIndexPattern: string | null, + filterManager: FilterManager, + defaultIndexPatternID: string, + ) => void; }; type tUseSearchBarProps = Partial & tUseSearchBarCustomInputs; @@ -36,12 +47,14 @@ type tUserSearchBarResponse = { * @param props * @returns */ -const useSearchBar = (props?: tUseSearchBarProps): tUserSearchBarResponse => { +const useSearchBarConfiguration = ( + 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 filters = props?.filters ? props.filters : filterManager.getFilters(); const [query, setQuery] = props?.query ? useState(props?.query) : useQueryManager(); @@ -52,43 +65,26 @@ const useSearchBar = (props?: tUseSearchBarProps): tUserSearchBarResponse => { useState(); useEffect(() => { - const prevPattern = - AppState.getCurrentPattern() || getSettingDefaultValue('pattern'); - if (filters && filters.length > 0) { - sessionStorage.setItem( - SESSION_STORAGE_FILTERS_NAME, - JSON.stringify( - updatePrevFilters(filters, props?.defaultIndexPatternID), - ), - ); - } - sessionStorage.setItem(SESSION_STORAGE_PREV_FILTER_NAME, prevPattern); - AppState.setCurrentPattern(props?.defaultIndexPatternID); initSearchBar(); - - /** - * 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 () => { + /* Upon unmount, the previous filters are restored */ 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, - prevStoragePattern ?? prevPattern, - ); - filterManager.setFilters(cleanedFilters); - sessionStorage.removeItem(SESSION_STORAGE_FILTERS_NAME); + if (props?.onUnMount) { + props.onUnMount( + previousFilters, + prevStoragePattern, + filterManager, + props?.defaultIndexPatternID, + ); + } } }; }, []); @@ -100,8 +96,11 @@ const useSearchBar = (props?: tUseSearchBarProps): tUserSearchBarResponse => { setIsLoading(true); const indexPattern = await getIndexPattern(props?.defaultIndexPatternID); setIndexPatternSelected(indexPattern); - const initialFilters = props?.filters ?? filters; - filterManager.setFilters(initialFilters); + if (props?.onMount) { + props.onMount(filterManager, props?.defaultIndexPatternID); + } else { + filterManager.setFilters(filters); + } setIsLoading(false); }; @@ -126,199 +125,34 @@ const useSearchBar = (props?: tUseSearchBarProps): tUserSearchBarResponse => { } }; - 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 - * and filters those filters that are not related to the default index pattern - * @returns - */ - const getFilters = () => { - const originalFilters = filterManager ? filterManager.getFilters() : []; - 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. - * This cleanup adjusts the index pattern of a pinned agent, if applicable. - * @param previousFilters - * @returns - */ - 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 && // 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; - }; - /** * Search bar properties necessary to render and initialize the osd search bar component */ const searchBarProps: Partial = { isLoading, ...(indexPatternSelected && { indexPatterns: [indexPatternSelected] }), // indexPattern cannot be empty or empty [] - filters: getFilters(), + filters: filters + .filter( + (filter: Filter) => + ![ + AUTHORIZED_AGENTS, + DATA_SOURCE_FILTER_CONTROLLED_EXCLUDE_SERVER, + ].includes(filter?.meta?.controlledBy), // remove auto loaded agent.id filters + ) + .sort((a: Filter, b: Filter) => { + return a?.$state?.isImplicit && !(a?.meta?.key === 'agent.id') + ? -1 + : b?.$state?.isImplicit + ? 1 + : -1; + }), query, timeHistory, dateRangeFrom: timeFilter.from, dateRangeTo: timeFilter.to, onFiltersUpdated: (filters: Filter[]) => { - const storagePreviousFilters = sessionStorage.getItem( - SESSION_STORAGE_FILTERS_NAME, - ); - /** - * If there are persisted filters, it is necessary to add them when - * updating the filters in the filterManager - */ - if (storagePreviousFilters) { - const previousFilters = JSON.parse(storagePreviousFilters); - const cleanedFilters = cleanFilters( - previousFilters, - props?.defaultIndexPatternID, - ); - filterManager.setFilters([...cleanedFilters, ...filters]); - - props?.onFiltersUpdated && - props?.onFiltersUpdated([...cleanedFilters, ...filters]); + if (props?.onUpdate) { + props.onUpdate(filters, filterManager, props?.onFiltersUpdated); } else { filterManager.setFilters(filters); props?.onFiltersUpdated && props?.onFiltersUpdated(filters); @@ -343,4 +177,4 @@ const useSearchBar = (props?: tUseSearchBarProps): tUserSearchBarResponse => { }; }; -export default useSearchBar; +export default useSearchBarConfiguration; diff --git a/plugins/main/public/components/common/tables/__snapshots__/table-default.test.tsx.snap b/plugins/main/public/components/common/tables/__snapshots__/table-default.test.tsx.snap index c5da18140d..fd8283c553 100644 --- a/plugins/main/public/components/common/tables/__snapshots__/table-default.test.tsx.snap +++ b/plugins/main/public/components/common/tables/__snapshots__/table-default.test.tsx.snap @@ -226,7 +226,7 @@ exports[`Table Default component renders correctly to match the snapshot 1`] = ` size="s" type="arrowDown" > - - + > + + + - - + > + + +
- -
- -

- Table - - - - -

-
-
-
- -
- -
- - - -
-
- -
- +
+ +
+ +
+ +

+ Table + + + + +

+
+
+
+
+
+
+ +
-
- - + +
+
+
+
+
+ + +
+ + +
+
+
-
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" +
+ + +
-
- - - -
+ + + + + + + + + + + + Sorting + + + + + + +
+
+
+
+
- +
- +
- -
- -
- - - - - - - - - - + +
- -
- + + + + + - - + - - - - + + - - + - - - - + + - - + - - - - + + - - + + + + + + + + + + + + - - - - - - - - - - - - - -
-
- - +
- + - + - +
- - Name - - - - - - - - -
-
- - No items found - -
-
-
+ + No items found + +
+ + + + + + + + +
+
+ +
-
- - + +
+ `; diff --git a/plugins/main/public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap b/plugins/main/public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap index a0061544cc..7db15c6903 100644 --- a/plugins/main/public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap +++ b/plugins/main/public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap @@ -44,7 +44,7 @@ exports[`Export Table Csv component renders correctly to match the snapshot 1`] size="m" type="importAction" > - - + > + + + ({ const [refresh, setRefresh] = useState(Date.now()); const isMounted = useRef(false); + const tableRef = useRef(); const searchBarWQLOptions = useMemo( () => ({ @@ -177,6 +178,10 @@ export function TableWithSearchBar({ (async () => { try { setLoading(true); + + //Reset the table selection in case is enabled + tableRef.current.setSelection([]); + const { items, totalItems } = await onSearch( endpoint, filters, @@ -254,6 +259,7 @@ export function TableWithSearchBar({ /> ({ ...rest }), )} diff --git a/plugins/main/public/components/common/tables/table-wz-api.tsx b/plugins/main/public/components/common/tables/table-wz-api.tsx index fc11c05c42..43b83e72de 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.tsx @@ -18,7 +18,6 @@ import { EuiFlexItem, EuiText, EuiButtonEmpty, - EuiSpacer, EuiToolTip, EuiIcon, EuiCheckboxGroup, @@ -27,9 +26,6 @@ import { TableWithSearchBar } from './table-with-search-bar'; import { TableDefault } from './table-default'; import { WzRequest } from '../../../react-services/wz-request'; import { ExportTableCsv } from './components/export-table-csv'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; import { useStateStorage } from '../hooks'; /** @@ -50,8 +46,12 @@ export function TableWzAPI({ actionButtons, ...rest }: { - actionButtons?: ReactNode | ReactNode[]; + actionButtons?: + | ReactNode + | ReactNode[] + | (({ filters }: { filters }) => ReactNode); title?: string; + addOnTitle?: ReactNode; description?: string; downloadCsv?: boolean | string; searchTable?: boolean; @@ -61,15 +61,20 @@ export function TableWzAPI({ showReload?: boolean; searchBarProps?: any; reload?: boolean; + onDataChange?: Function; }) { const [totalItems, setTotalItems] = useState(0); const [filters, setFilters] = useState({}); const [isLoading, setIsLoading] = useState(false); + const onFiltersChange = filters => typeof rest.onFiltersChange === 'function' ? rest.onFiltersChange(filters) : null; + const onDataChange = data => + typeof rest.onDataChange === 'function' ? rest.onDataChange(data) : null; + /** * Changing the reloadFootprint timestamp will trigger reloading the table */ @@ -112,10 +117,15 @@ export function TableWzAPI({ ).data; setIsLoading(false); setTotalItems(totalItems); - return { + + const result = { items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, totalItems, }; + + onDataChange(result); + + return result; } catch (error) { setIsLoading(false); setTotalItems(0); @@ -132,19 +142,23 @@ export function TableWzAPI({ }, []); - const renderActionButtons = ( - <> - {Array.isArray(actionButtons) - ? actionButtons.map((button, key) => ( - - {button} - - )) - : typeof actionButtons === 'object' && ( - {actionButtons} - )} - - ); + const renderActionButtons = filters => { + if (Array.isArray(actionButtons)) { + return actionButtons.map((button, key) => ( + + {button} + + )); + } + + if (typeof actionButtons === 'object') { + return {actionButtons}; + } + + if (typeof actionButtons === 'function') { + return actionButtons({ filters: getFilters(filters) }); + } + }; /** * Generate a new reload footprint @@ -167,28 +181,34 @@ export function TableWzAPI({ const header = ( <> - - - {rest.title && ( - -

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

-
- )} - {rest.description && ( - {rest.description} - )} -
+ - + + + {rest.title && ( + +

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

+
+ )} +
+ {rest.addOnTitle ? ( + + {rest.addOnTitle} + + ) : null} +
+
+ + {/* Render optional custom action button */} - {renderActionButtons} + {renderActionButtons(filters)} {/* Render optional reload button */} {rest.showReload && ReloadButton} {/* Render optional export to CSV button */} @@ -266,11 +286,15 @@ export function TableWzAPI({ ); return ( - <> - {header} - {rest.description && } - {table} - + + {header} + {rest.description && ( + + {rest.description} + + )} + {table} + ); } diff --git a/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx b/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx index 8b72bb01e4..0eec91a272 100644 --- a/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx +++ b/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx @@ -37,7 +37,7 @@ import useSearchBar from '../search-bar/use-search-bar'; import { search } from '../search-bar'; import { getPlugins } from '../../../kibana-services'; import { histogramChartInput } from './config/histogram-chart'; -import { useDockedSideNav } from '../hooks/useDockedSideNav'; +import { getWazuhCorePlugin } from '../../../kibana-services'; const DashboardByRenderer = getPlugins().dashboard.DashboardContainerByValueRenderer; import './discover.scss'; @@ -60,7 +60,7 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { ); const [isSearching, setIsSearching] = useState(false); const [isExporting, setIsExporting] = useState(false); - const sideNavDocked = useDockedSideNav(); + const sideNavDocked = getWazuhCorePlugin().hooks.useDockedSideNav(); const onClickInspectDoc = useMemo( () => (index: number) => { diff --git a/plugins/main/public/components/endpoints-summary/dashboard/components/donut-card.test.tsx b/plugins/main/public/components/endpoints-summary/dashboard/components/donut-card.test.tsx new file mode 100644 index 0000000000..55c8033f29 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/dashboard/components/donut-card.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { render, act, fireEvent } from '@testing-library/react'; +import DonutCard from './donut-card'; +import '@testing-library/jest-dom/extend-expect'; + +/* It is necessary to mock the ResizeObserver class because it is used in the useChartDimensions hook in one of the DonutChart subcomponents */ +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} +global.ResizeObserver = ResizeObserver; + +jest.mock('../../../common/hooks/useApiService', () => ({ + __esModule: true, + useApiService: jest.fn(), +})); + +describe('DonutCard', () => { + const mockLoading = false; + const mockData = [ + { + status: 'active', + label: 'Active', + value: 1, + color: '#007871', + }, + { + status: 'disconnected', + label: 'Disconnected', + value: 0, + color: '#BD271E', + }, + { + status: 'pending', + label: 'Pending', + value: 0, + color: '#FEC514', + }, + { + status: 'never_connected', + label: 'Never connected', + value: 0, + color: '#646A77', + }, + ]; + const mockGetInfo = jest.fn().mockResolvedValue(mockData); + const useApiServiceMock = jest.fn(() => [mockLoading, mockData]); + const mockGetInfoNoData = jest.fn().mockResolvedValue([]); + const useApiServiceMockNoData = jest.fn(() => [mockLoading, []]); + + it('renders with data', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMock; + + await act(async () => { + const { getByText } = render( + , + ); + + expect(getByText('Component title example')).toBeInTheDocument(); + expect(getByText('Component description example')).toBeInTheDocument(); + expect(getByText('Component betaBadgeLabel example')).toBeInTheDocument(); + mockData.forEach(element => { + expect(getByText(`${element.label} (${element.value})`)).toBeInTheDocument(); + }); + }); + }); + + it('handles click on data', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMock; + + const handleClick = jest.fn(); + const firstMockData = mockData[0]; + + await act(async () => { + const { getByText } = render( + + ); + + fireEvent.click(getByText(`${firstMockData.label} (${firstMockData.value})`)); + + expect(handleClick).toHaveBeenCalledTimes(1); + + expect(handleClick).toHaveBeenCalledWith(firstMockData); + }); + }); + + it('show noDataTitle and noDataMessage when no data', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMockNoData; + + await act(async () => { + const { getByText } = render( + + ); + + expect(getByText('Component no data title example message')).toBeInTheDocument(); + expect(getByText('Component no data example message')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/dashboard/components/donut-card.tsx b/plugins/main/public/components/endpoints-summary/dashboard/components/donut-card.tsx new file mode 100644 index 0000000000..f06b7c2fed --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/dashboard/components/donut-card.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiCard } from '@elastic/eui'; +import { useApiService } from '../../../common/hooks/useApiService'; +import { VisualizationBasic } from '../../../common/charts/visualizations/basic'; + +interface AgentsByStatusCardProps { + title?: string; + description?: string; + betaBadgeLabel?: string; + noDataTitle?: string; + noDataMessage?: string; + getInfo: () => Promise; + onClickLabel?: (status: any) => void; + [key: string]: any; +} + +const DonutCard = ({ + title = '', + description = '', + betaBadgeLabel, + noDataTitle = 'No results', + noDataMessage = 'No results were found', + getInfo, + onClickLabel, + ...props +}: AgentsByStatusCardProps) => { + const [loading, data] = useApiService(getInfo, undefined); + + const handleClick = (item: any) => { + if (onClickLabel) { + onClickLabel(item); + } + }; + + return ( + + + + ({ + ...item, + onClick: () => handleClick(item), + }))} + noDataTitle={noDataTitle} + noDataMessage={noDataMessage} + /> + + + + ); +}; + +export default DonutCard; diff --git a/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.scss b/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.scss new file mode 100644 index 0000000000..65ab2e1b83 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.scss @@ -0,0 +1,22 @@ +.wazuh-outdated-agents-panel { + display: flex; + align-items: center; + justify-content: center; + margin: auto; + flex-direction: column; + cursor: pointer; +} + +.wazuh-outdated-metric .euiTitle { + font-size: 4.5rem; + line-height: inherit; +} + +.wazuh-outdated-metric small { + font-size: 1.5rem; +} + +.wazuh-outdated-icon { + width: 3.5rem; + height: 3.5rem; +} diff --git a/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.test.tsx b/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.test.tsx new file mode 100644 index 0000000000..5a66a651e1 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.test.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import OutdatedAgentsCard from './outdated-agents-card'; +import '@testing-library/jest-dom/extend-expect'; +import { mount } from 'enzyme'; +import { EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; + +jest.mock('../../../common/hooks/useApiService', () => ({ + __esModule: true, + useApiService: jest.fn(), +})); + +describe('OutdatedAgentsCard', () => { + const awaitForMyComponent = async (wrapper: any) => { + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + wrapper.update(); + }); + }; + + const mockLoading = false; + const mockDataNoOutdatedAgents = []; + const useApiServiceMockNoOutdatedAgent = jest.fn(() => [mockLoading, mockDataNoOutdatedAgents]); + const mockDataOutdatedAgents = [ + { + version: "Wazuh v3.0.0", + id: "003", + name: "main_database" + }, + { + version: "Wazuh v3.0.0", + id: "004", + name: "dmz002" + } +]; + const useApiServiceMockOutdatedAgent = jest.fn(() => [mockLoading, mockDataOutdatedAgents]); + + const handleClick = jest.fn(); + + it('renders with not outdated agents', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMockNoOutdatedAgent; + + await act(async () => { + const { getByTestId } = render( + + ); + + const outdatedAgentsNumberElement = getByTestId('wazuh-endpoints-summary-outdated-agents-number') + expect(outdatedAgentsNumberElement).toHaveClass('euiTextColor euiTextColor--success'); + expect(outdatedAgentsNumberElement.textContent).toBe(`${mockDataNoOutdatedAgents.length}`); + }); + }); + + it('renders with outdated agents', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMockOutdatedAgent; + + await act(async () => { + const { getByTestId } = render( + + ); + + const outdatedAgentsNumberElement = getByTestId('wazuh-endpoints-summary-outdated-agents-number') + expect(outdatedAgentsNumberElement).toHaveClass('euiTextColor euiTextColor--warning'); + expect(outdatedAgentsNumberElement.textContent).toBe(`${mockDataOutdatedAgents.length}`); + }); + }); + + it('renders popover on click with outdated agents', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMockOutdatedAgent; + + const wrapper = await mount( + , + ); + + await awaitForMyComponent(wrapper); + + expect(wrapper.find('.wazuh-outdated-agents-panel').exists()).toBeTruthy(); + expect(wrapper.find(EuiButtonEmpty).exists()).not.toBeTruthy(); + + wrapper.find('.wazuh-outdated-agents-panel').simulate('click'); + expect(wrapper.find(EuiButtonEmpty).exists()).toBeTruthy(); + }); + + it('handles click with correct data', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMockOutdatedAgent; + + const wrapper = await mount( + , + ); + + await awaitForMyComponent(wrapper); + + expect(wrapper.find('.wazuh-outdated-agents-panel').exists()).toBeTruthy(); + expect(wrapper.find(EuiButtonEmpty).exists()).not.toBeTruthy(); + + wrapper.find('.wazuh-outdated-agents-panel').simulate('click'); + expect(wrapper.find(EuiButtonEmpty).exists()).toBeTruthy(); + + wrapper.find(EuiButtonEmpty).simulate('click'); + expect(handleClick).toHaveBeenCalledTimes(1); + expect(handleClick).toHaveBeenCalledWith(mockDataOutdatedAgents); + }); + + it('EuiButtonEmpty filter must be disabled when no data', async () => { + require('../../../common/hooks/useApiService').useApiService = useApiServiceMockNoOutdatedAgent; + + const wrapper = await mount( + , + ); + + await awaitForMyComponent(wrapper); + + expect(wrapper.find('.wazuh-outdated-agents-panel').exists()).toBeTruthy(); + expect(wrapper.find(EuiButtonEmpty).exists()).not.toBeTruthy(); + + wrapper.find('.wazuh-outdated-agents-panel').simulate('click'); + expect(wrapper.find(EuiButtonEmpty).exists()).toBeTruthy(); + expect(wrapper.find(EuiButtonEmpty).prop('isDisabled')).toBe(true); + }); + + it('check documentation link to update agents', async () => { + const documentationLink = webDocumentationLink( + 'upgrade-guide/wazuh-agent/index.html', + ); + require('../../../common/hooks/useApiService').useApiService = useApiServiceMockNoOutdatedAgent; + + const wrapper = await mount( + , + ); + + await awaitForMyComponent(wrapper); + + expect(wrapper.find('.wazuh-outdated-agents-panel').exists()).toBeTruthy(); + expect(wrapper.find(EuiButtonEmpty).exists()).not.toBeTruthy(); + + wrapper.find('.wazuh-outdated-agents-panel').simulate('click'); + expect(wrapper.find(EuiLink).exists()).toBeTruthy(); + expect(wrapper.find(EuiLink).prop('href')).toBe(documentationLink); + }); + +}); diff --git a/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.tsx b/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.tsx new file mode 100644 index 0000000000..11e612f77f --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/dashboard/components/outdated-agents-card.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { + EuiFlexItem, + EuiCard, + EuiIcon, + EuiStat, + EuiTextColor, + EuiPopover, + EuiPopoverFooter, + EuiLink, + EuiButtonEmpty, +} from '@elastic/eui'; +import './outdated-agents-card.scss'; +import { getOutdatedAgents } from '../../services/get-outdated-agents'; +import { useApiService } from '../../../common/hooks/useApiService'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; + +interface OutdatedAgentsCardProps { + onClick?: (status: any) => void; + [key: string]: any; +} + +const OutdatedAgentsCard = ({ onClick, ...props }: OutdatedAgentsCardProps) => { + const [loading, data] = useApiService(getOutdatedAgents, undefined); + const outdatedAgents = data?.length; + const contentType = outdatedAgents > 0 ? 'warning' : 'success'; + const contentIcon = outdatedAgents > 0 ? 'alert' : 'check'; + const [showOutdatedAgents, setShowOutdatedAgents] = + React.useState(false); + + const onShowOutdatedAgents = () => setShowOutdatedAgents(!showOutdatedAgents); + const onHideOutdatedAgents = () => setShowOutdatedAgents(false); + + const handleClick = () => { + if (onClick) { + onClick(data); + setShowOutdatedAgents(false); + } + }; + + const renderMetric = () => { + return ( +
+ + + {outdatedAgents} + + } + description={ + + Agents + + } + titleColor='danger' + isLoading={loading} + titleSize='l' + textAlign='center' + reverse + /> +
+ ); + }; + + return ( + + + + 0)} + > + Filter outdated agents + + + + + How to update agents + + + + + + + ); +}; + +export default OutdatedAgentsCard; diff --git a/plugins/main/public/components/endpoints-summary/dashboard/endpoints-summary-dashboard.tsx b/plugins/main/public/components/endpoints-summary/dashboard/endpoints-summary-dashboard.tsx new file mode 100644 index 0000000000..b086886356 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/dashboard/endpoints-summary-dashboard.tsx @@ -0,0 +1,41 @@ +import React, { FC } from 'react'; +import { getAgentsByGroup } from '../services/get-agents-by-group'; +import { getAgentsByOs } from '../services/get-agents-by-os'; +import { getSummaryAgentsStatus } from '../services/get-summary-agents-status'; +import DonutCard from './components/donut-card'; +import OutdatedAgentsCard from './components/outdated-agents-card'; + +interface EndpointsSummaryDashboardProps { + filterAgentByStatus: (data: any) => void; + filterAgentByOS: (data: any) => void; + filterAgentByGroup: (data: any) => void; + filterByOutdatedAgent: (data: any) => void; +} + +export const EndpointsSummaryDashboard: FC = ({ + filterAgentByStatus, + filterAgentByOS, + filterAgentByGroup, + filterByOutdatedAgent, +}) => { + return ( +
+ + + + +
+ ); +}; diff --git a/plugins/main/public/components/endpoints-summary/endpoints-summary.scss b/plugins/main/public/components/endpoints-summary/endpoints-summary.scss index e420ca4e33..f9e50aef92 100644 --- a/plugins/main/public/components/endpoints-summary/endpoints-summary.scss +++ b/plugins/main/public/components/endpoints-summary/endpoints-summary.scss @@ -101,3 +101,22 @@ white-space: nowrap; } } + +.endpoints-summary-container-indicators { + width: 100%; + display: grid; + grid-template-columns: 1fr; + gap: 20px 10px; + min-height: 200px; + + @media (min-width: 1024px) { + grid-template-columns: 1fr 1fr; + } + + @media (min-width: 1440px) { + gap: 10px; + grid-template-columns: + minmax(375px, 1fr) minmax(375px, 1fr) minmax(375px, 1fr) + minmax(150px, 300px); + } +} diff --git a/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx b/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx index 494e2da137..dc416a3466 100644 --- a/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx +++ b/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx @@ -16,21 +16,16 @@ import { EuiPage, EuiFlexGroup, EuiFlexItem, - EuiStat, EuiSpacer, - EuiToolTip, - EuiCard, - EuiLink, - EuiText, + EuiFlexGrid, } from '@elastic/eui'; import { AgentsTable } from './table/agents-table'; -import { WzRequest } from '../../react-services/wz-request'; import WzReduxProvider from '../../redux/wz-redux-provider'; import { VisFactoryHandler } from '../../react-services/vis-factory-handler'; import { AppState } from '../../react-services/app-state'; import { FilterHandler } from '../../utils/filter-handler'; import { TabVisualizations } from '../../factories/tab-visualizations'; -import { WazuhConfig } from '../../react-services/wazuh-config.js'; +import { WazuhConfig } from '../../react-services/wazuh-config'; import { withReduxProvider, withGlobalBreadcrumb, @@ -38,22 +33,15 @@ import { withErrorBoundary, } from '../common/hocs'; import { compose } from 'redux'; -import { - UI_LOGGER_LEVELS, - UI_ORDER_AGENT_STATUS, -} from '../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../react-services/common-services'; -import { VisualizationBasic } from '../common/charts/visualizations/basic'; -import { - agentStatusColorByAgentStatus, - agentStatusLabelByAgentStatus, -} from '../../../common/services/wz_agent_status'; import { endpointSummary } from '../../utils/applications'; import { ShareAgent } from '../../factories/share-agent'; -import { getCore } from '../../kibana-services'; import './endpoints-summary.scss'; -import { RedirectAppLinks } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import OutdatedAgentsCard from './dashboard/components/outdated-agents-card'; +import DonutCard from './dashboard/components/donut-card'; +import { getSummaryAgentsStatus } from './services/get-summary-agents-status'; +import { getAgentsByOs } from './services/get-agents-by-os'; +import { getAgentsByGroup } from './services/get-agents-by-group'; +import { EndpointsSummaryDashboard } from './dashboard/endpoints-summary-dashboard'; export const EndpointsSummary = compose( withErrorBoundary, @@ -71,25 +59,18 @@ export const EndpointsSummary = compose( constructor() { super(); this.state = { - loadingSummary: true, - loadingLastRegisteredAgent: true, agentTableFilters: {}, - agentStatusSummary: [], - agentsActiveCoverage: undefined, }; this.wazuhConfig = new WazuhConfig(); - this.agentStatus = UI_ORDER_AGENT_STATUS.map(agentStatus => ({ - status: agentStatus, - label: agentStatusLabelByAgentStatus(agentStatus), - color: agentStatusColorByAgentStatus(agentStatus), - })); this.shareAgent = new ShareAgent(); + this.filterAgentByStatus = this.filterAgentByStatus.bind(this); + this.filterAgentByOS = this.filterAgentByOS.bind(this); + this.filterAgentByGroup = this.filterAgentByGroup.bind(this); + this.filterByOutdatedAgent = this.filterByOutdatedAgent.bind(this); } async componentDidMount() { this._isMount = true; - this.getSummary(); - this.fetchLastRegisteredAgent(); if (this.wazuhConfig.getConfig()['wazuh.monitoring.enabled']) { const tabVisualizations = new TabVisualizations(); tabVisualizations.removeAll(); @@ -118,91 +99,42 @@ export const EndpointsSummary = compose( }, {}); }; - async getSummary() { - try { - this.setState({ loadingSummary: true }); - - const { - data: { - data: { - connection: agentStatusSummary, - configuration: agentConfiguration, - }, - }, - } = await WzRequest.apiReq('GET', '/agents/summary/status', {}); - - const agentsActiveCoverage = ( - (agentStatusSummary?.active / agentStatusSummary?.total) * - 100 - ).toFixed(2); - - this.setState({ - loadingSummary: false, - agentStatusSummary, - agentsActiveCoverage: isNaN(agentsActiveCoverage) - ? 0 - : agentsActiveCoverage, - }); - } catch (error) { + filterAgentByStatus(item: any) { + this._isMount && this.setState({ - loadingSummary: false, - agentStatusSummary: [], - agentsActiveCoverage: undefined, + agentTableFilters: { q: `id!=000;status=${item.status}` }, }); - const options = { - context: `EndpointsSummary.getSummary`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Could not get agents summary`, - }, - }; - getErrorOrchestrator().handleError(options); - } } - async fetchLastRegisteredAgent() { - try { - this.setState({ loadingLastRegisteredAgent: true }); - const { - data: { - data: { - affected_items: [lastRegisteredAgent], - }, - }, - } = await WzRequest.apiReq('GET', '/agents', { - params: { limit: 1, sort: '-dateAdd', q: 'id!=000' }, - }); + filterAgentByOS(item: any) { + const query = + item.label === 'unknown' + ? 'id!=000;os.name=null' + : `id!=000;os.name~${item.label}`; + this._isMount && this.setState({ - loadingLastRegisteredAgent: false, - lastRegisteredAgent, + agentTableFilters: { q: query }, }); - } catch (error) { + } + + filterAgentByGroup(item: any) { + const query = + item.label === 'unknown' + ? 'id!=000;group=null' + : `id!=000;group=${item.label}`; + this._isMount && this.setState({ - loadingLastRegisteredAgent: false, + agentTableFilters: { q: query }, }); - const options = { - context: `EndpointsSummary.fetchLastRegisteredAgent`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `Could not get the last registered agent`, - }, - }; - getErrorOrchestrator().handleError(options); - } } - filterAgentByStatus(status) { + filterByOutdatedAgent(outdatedAgents: any) { + const ids: string = outdatedAgents + .map((agent: any) => `id=${agent.id}`) + .join(','); this._isMount && this.setState({ - agentTableFilters: { q: `id!=000;status=${status}` }, + agentTableFilters: { q: `id!=000;${ids}` }, }); } @@ -210,113 +142,12 @@ export const EndpointsSummary = compose( return ( - - - - - - ({ - label, - value: this.state.agentStatusSummary[status] || 0, - color, - onClick: () => this.filterAgentByStatus(status), - }), - )} - noDataTitle='No results' - noDataMessage='No results were found.' - /> - - - - - - - - {this.agentStatus.map(({ status, label, color }) => ( - - - this.filterAgentByStatus(status)} - style={{ cursor: 'pointer' }} - > - {this.state.agentStatusSummary[status]} - - - } - titleSize='s' - description={label} - titleColor={color} - className='white-space-nowrap' - /> - - ))} - - - - - - - - - - {this.state.lastRegisteredAgent?.name} - - - - ) : ( - - - ) - } - titleSize='s' - description='Last enrolled agent' - titleColor='primary' - /> - - - - - + diff --git a/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts b/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts new file mode 100644 index 0000000000..32afd959e7 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts @@ -0,0 +1,38 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useGetTotalAgents } from './agents'; +import { getAgentsService } from '../services'; + +jest.mock('../services', () => ({ + getAgentsService: jest.fn(), +})); + +describe('useGetTotalAgents hook', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch initial data without any error', async () => { + (getAgentsService as jest.Mock).mockReturnValue({ + total_affected_items: 3, + }); + + const { result, waitForNextUpdate } = renderHook(() => useGetTotalAgents()); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.totalAgents).toEqual(3); + expect(result.current.isLoading).toBeFalsy(); + }); + + it('should handle error while fetching data', async () => { + const mockErrorMessage = 'Some error occurred'; + (getAgentsService as jest.Mock).mockRejectedValue(mockErrorMessage); + + const { result, waitForNextUpdate } = renderHook(() => useGetTotalAgents()); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.error).toBe(mockErrorMessage); + expect(result.current.isLoading).toBeFalsy(); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/hooks/agents.ts b/plugins/main/public/components/endpoints-summary/hooks/agents.ts index facb9df07b..3c383516fa 100644 --- a/plugins/main/public/components/endpoints-summary/hooks/agents.ts +++ b/plugins/main/public/components/endpoints-summary/hooks/agents.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; -import { getTotalAgentsService } from '../services'; +import { getAgentsService } from '../services'; -export const useGetTotalAgents = () => { +export const useGetTotalAgents = (filters?: any) => { const [totalAgents, setTotalAgents] = useState(); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(); @@ -9,8 +9,11 @@ export const useGetTotalAgents = () => { const getTotalAgents = async () => { try { setIsLoading(true); - const totalAgents = await getTotalAgentsService(); - setTotalAgents(totalAgents); + const { total_affected_items } = await getAgentsService({ + filters, + limit: 1, + }); + setTotalAgents(total_affected_items); setError(undefined); } catch (error: any) { setError(error); diff --git a/plugins/main/public/components/endpoints-summary/hooks/groups.test.ts b/plugins/main/public/components/endpoints-summary/hooks/groups.test.ts new file mode 100644 index 0000000000..3dad828a43 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/hooks/groups.test.ts @@ -0,0 +1,44 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useGetGroups } from './groups'; +import { getGroupsService } from '../services'; + +jest.mock('../services', () => ({ + getGroupsService: jest.fn(), +})); + +describe('useGetGroups hook', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch initial data without any error', async () => { + (getGroupsService as jest.Mock).mockReturnValue({ + affected_items: [ + { name: 'group1' }, + { name: 'group2' }, + { name: 'group3' }, + ], + total_affected_items: 3, + }); + + const mockGroups = ['group1', 'group2', 'group3']; + const { result, waitForNextUpdate } = renderHook(() => useGetGroups()); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.groups).toEqual(mockGroups); + expect(result.current.isLoading).toBeFalsy(); + }); + + it('should handle error while fetching data', async () => { + const mockErrorMessage = 'Some error occurred'; + (getGroupsService as jest.Mock).mockRejectedValue(mockErrorMessage); + + const { result, waitForNextUpdate } = renderHook(() => useGetGroups()); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.error).toBe(mockErrorMessage); + expect(result.current.isLoading).toBeFalsy(); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/hooks/groups.ts b/plugins/main/public/components/endpoints-summary/hooks/groups.ts index 8c83e9dec3..4910e87ed8 100644 --- a/plugins/main/public/components/endpoints-summary/hooks/groups.ts +++ b/plugins/main/public/components/endpoints-summary/hooks/groups.ts @@ -9,7 +9,7 @@ export const useGetGroups = () => { const getGroups = async () => { try { setIsLoading(true); - const { affected_items } = await getGroupsService(); + const { affected_items } = await getGroupsService({}); const groups = affected_items.map(item => item.name); setGroups(groups); setError(undefined); diff --git a/plugins/main/public/components/endpoints-summary/hooks/index.ts b/plugins/main/public/components/endpoints-summary/hooks/index.ts index e1fdae97a1..063e5cc418 100644 --- a/plugins/main/public/components/endpoints-summary/hooks/index.ts +++ b/plugins/main/public/components/endpoints-summary/hooks/index.ts @@ -1 +1,2 @@ export { useGetTotalAgents } from './agents'; +export { useGetGroups } from './groups'; diff --git a/plugins/main/public/components/endpoints-summary/index.tsx b/plugins/main/public/components/endpoints-summary/index.tsx index 2d37a9c9d8..7b52d47946 100644 --- a/plugins/main/public/components/endpoints-summary/index.tsx +++ b/plugins/main/public/components/endpoints-summary/index.tsx @@ -25,7 +25,7 @@ export const MainEndpointsSummary = compose( withReduxProvider, withGlobalBreadcrumb([{ text: endpointSummary.breadcrumbLabel }]), )(() => { - const { isLoading, totalAgents, error } = useGetTotalAgents(); + const { isLoading, totalAgents, error } = useGetTotalAgents('id!=000'); if (error) { const options = { 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 45d29691ab..99a4f86f22 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 @@ -46,7 +46,10 @@ export const RegisterAgent = compose( withErrorBoundary, withReduxProvider, withGlobalBreadcrumb([ - { text: endpointSummary.title, href: `#${endpointSummary.redirectTo()}` }, + { + text: endpointSummary.breadcrumbLabel, + href: `#${endpointSummary.redirectTo()}`, + }, { text: 'Deploy new agent' }, ]), withUserAuthorizationPrompt([ diff --git a/plugins/main/public/components/endpoints-summary/services/add-agent-to-group.tsx b/plugins/main/public/components/endpoints-summary/services/add-agent-to-group.tsx index d32a186f14..eee4e590bb 100644 --- a/plugins/main/public/components/endpoints-summary/services/add-agent-to-group.tsx +++ b/plugins/main/public/components/endpoints-summary/services/add-agent-to-group.tsx @@ -1,4 +1,13 @@ +import IApiResponse from '../../../react-services/interfaces/api-response.interface'; import { WzRequest } from '../../../react-services/wz-request'; -export const addAgentToGroupService = async (agentId: string, group: string) => - await WzRequest.apiReq('PUT', `/agents/${agentId}/group/${group}`, {}); +export const addAgentToGroupService = async ({ + agentId, + groupId, +}: { + agentId: string; + groupId: string; +}) => + (await WzRequest.apiReq('PUT', `/agents/${agentId}/group/${groupId}`, { + wait_for_complete: true, + })) as IApiResponse; diff --git a/plugins/main/public/components/endpoints-summary/services/add-agents-to-group.tsx b/plugins/main/public/components/endpoints-summary/services/add-agents-to-group.tsx new file mode 100644 index 0000000000..5c563167f1 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/add-agents-to-group.tsx @@ -0,0 +1,9 @@ +import IApiResponse from '../../../react-services/interfaces/api-response.interface'; +import { paginatedAgentsGroupService } from './paginated-agents-group'; + +export const addAgentsToGroupService = async (parameters: { + agentIds: string[]; + groupId: string; + pageSize?: number; +}): Promise> => + await paginatedAgentsGroupService({ addOrRemove: 'add', ...parameters }); diff --git a/plugins/main/public/components/endpoints-summary/services/get-agents-by-group.test.ts b/plugins/main/public/components/endpoints-summary/services/get-agents-by-group.test.ts new file mode 100644 index 0000000000..4d025bbd67 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-agents-by-group.test.ts @@ -0,0 +1,92 @@ +import { getAgentsByGroup } from './get-agents-by-group'; +import { WzRequest } from '../../../react-services/wz-request'; +import { getColorPaletteByIndex } from './get-color-palette-by-index'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; + +jest.mock('../../../react-services/wz-request', () => ({ + WzRequest: { + apiReq: jest.fn(), + }, +})); + +jest.mock('./get-color-palette-by-index', () => ({ + getColorPaletteByIndex: jest.fn(), +})); + +jest.mock('../../../react-services/common-services', () => ({ + getErrorOrchestrator: jest.fn(), +})); + +describe('Get agents by group', () => { + it('should return grouped data', async () => { + const responseData = { + data: { + data: { + affected_items: [ + { + count: 2, + group: ['group1'], + }, + { + count: 1, + group: ['group2'], + }, + { + count: 4, + group: ['group1', 'group2'], + }, + ], + total_affected_items: 3, + total_failed_items: 0, + failed_items: [], + }, + message: 'All selected agents information was returned', + error: 0, + }, + }; + const expectedGroupedData = [ + { label: 'group1', value: 6, color: 'mockColor1' }, + { label: 'group2', value: 5, color: 'mockColor2' }, + ]; + + (WzRequest.apiReq as jest.Mock).mockResolvedValue(responseData); + + (getColorPaletteByIndex as jest.Mock).mockImplementation( + (index: number) => { + return `mockColor${index + 1}`; + }, + ); + + const groupedData = await getAgentsByGroup(); + + expect(groupedData).toEqual(expectedGroupedData); + }); + + it('should handle error', async () => { + const mockError = new Error('Mock error'); + + (WzRequest.apiReq as jest.Mock).mockRejectedValue(mockError); + + const mockHandleError = jest.fn(); + (getErrorOrchestrator as jest.Mock).mockReturnValue({ + handleError: mockHandleError, + }); + + const groupedData = await getAgentsByGroup(); + + expect(groupedData).toEqual([]); + expect(mockHandleError).toHaveBeenCalledWith({ + context: 'EndpointsSummary.getSummary', + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: mockError, + message: mockError.message || mockError, + title: 'Could not get agents summary', + }, + }); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/services/get-agents-by-group.ts b/plugins/main/public/components/endpoints-summary/services/get-agents-by-group.ts new file mode 100644 index 0000000000..7dda5067e9 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-agents-by-group.ts @@ -0,0 +1,75 @@ +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { WzRequest } from '../../../react-services/wz-request'; +import { getColorPaletteByIndex } from './get-color-palette-by-index'; + +interface AffectedItem { + count: number; + group?: string[]; +} + +interface AgentCountGroup { + label: string; + value: number; + color: string; +} + +export const getAgentsByGroup = async () => { + try { + const { + data: { + data: { affected_items }, + }, + }: any = await WzRequest.apiReq( + 'GET', + '/agents/stats/distinct?fields=group', + { + params: { q: 'id!=000' }, + }, + ); + const groupedData = getCountByGroup(affected_items); + return groupedData.slice(0, 5); + } catch (error) { + const options = { + context: `EndpointsSummary.getSummary`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Could not get agents summary`, + }, + }; + getErrorOrchestrator().handleError(options); + return []; + } +}; + +function getCountByGroup(data: AffectedItem[]): AgentCountGroup[] { + const countMap: Map = new Map(); + + data.forEach(item => { + if (item.group) { + item.group.forEach(group => { + if (countMap.has(group)) { + countMap.set(group, countMap.get(group)! + item.count); + } else { + countMap.set(group, item.count); + } + }); + } + }); + + const countArray = Array.from(countMap.entries()).map( + ([label, value], index: number) => { + return { + label, + value, + color: getColorPaletteByIndex(index), + }; + }, + ); + return countArray.sort((a, b) => b.value - a.value); +} diff --git a/plugins/main/public/components/endpoints-summary/services/get-agents-by-os.test.ts b/plugins/main/public/components/endpoints-summary/services/get-agents-by-os.test.ts new file mode 100644 index 0000000000..abca1d309a --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-agents-by-os.test.ts @@ -0,0 +1,79 @@ +import { getAgentsByOs } from './get-agents-by-os'; +import { WzRequest } from '../../../react-services/wz-request'; +import { getColorPaletteByIndex } from './get-color-palette-by-index'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; + +jest.mock('../../../react-services/wz-request', () => ({ + WzRequest: { + apiReq: jest.fn(), + }, +})); + +jest.mock('./get-color-palette-by-index', () => ({ + getColorPaletteByIndex: jest.fn(), +})); + +jest.mock('../../../react-services/common-services', () => ({ + getErrorOrchestrator: jest.fn(), +})); + +describe('getAgentsByOs', () => { + it('should return grouped data', async () => { + const responseData = { + data: { + data: { + affected_items: [ + { os: { platform: 'Windows' }, count: 3 }, + { os: { platform: 'Linux' }, count: 2 }, + { os: { platform: 'Mac' }, count: 1 }, + ], + }, + }, + }; + const expectedGroupedData = [ + { label: 'Windows', value: 3, color: 'mockColor1' }, + { label: 'Linux', value: 2, color: 'mockColor2' }, + { label: 'Mac', value: 1, color: 'mockColor3' }, + ]; + + (WzRequest.apiReq as jest.Mock).mockResolvedValue(responseData); + + (getColorPaletteByIndex as jest.Mock).mockImplementation( + (index: number) => { + return `mockColor${index + 1}`; + }, + ); + + const groupedData = await getAgentsByOs(); + + expect(groupedData).toEqual(expectedGroupedData); + }); + + it('should handle error', async () => { + const mockError = new Error('Mock error'); + + (WzRequest.apiReq as jest.Mock).mockRejectedValue(mockError); + + const mockHandleError = jest.fn(); + (getErrorOrchestrator as jest.Mock).mockReturnValue({ + handleError: mockHandleError, + }); + + const groupedData = await getAgentsByOs(); + + expect(groupedData).toEqual([]); + expect(mockHandleError).toHaveBeenCalledWith({ + context: 'EndpointsSummary.getSummary', + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: mockError, + message: mockError.message || mockError, + title: 'Could not get agents by OS', + }, + }); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/services/get-agents-by-os.ts b/plugins/main/public/components/endpoints-summary/services/get-agents-by-os.ts new file mode 100644 index 0000000000..de43ea8bec --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-agents-by-os.ts @@ -0,0 +1,46 @@ +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { WzRequest } from '../../../react-services/wz-request'; +import { getColorPaletteByIndex } from './get-color-palette-by-index'; + +export const getAgentsByOs = async () => { + const DEFAULT_COUNT = 1; + try { + const { + data: { + data: { affected_items }, + }, + }: any = await WzRequest.apiReq( + 'GET', + '/agents/stats/distinct?fields=os.platform', + { + params: { q: 'id!=000' }, + }, + ); + const groupedData: any[] = []; + affected_items?.forEach((item: any, index: number) => { + const itemOsName = item?.os?.platform ?? 'unknown'; + groupedData.push({ + label: itemOsName, + value: item.count ?? DEFAULT_COUNT, + color: getColorPaletteByIndex(index), + }); + }); + return groupedData.sort((a, b) => b.value - a.value).slice(0, 5); + } catch (error) { + const options = { + context: `EndpointsSummary.getSummary`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Could not get agents by OS`, + }, + }; + getErrorOrchestrator().handleError(options); + return []; + } +}; diff --git a/plugins/main/public/components/endpoints-summary/services/get-agents.test.tsx b/plugins/main/public/components/endpoints-summary/services/get-agents.test.tsx new file mode 100644 index 0000000000..2293b3373b --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-agents.test.tsx @@ -0,0 +1,85 @@ +import { getAgentsService } from './get-agents'; +import { WzRequest } from '../../../react-services/wz-request'; + +jest.mock('../../../react-services/wz-request', () => ({ + WzRequest: { + apiReq: jest.fn(), + }, +})); + +describe('getAgentsService', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should paginate agents and handle API responses correctly', async () => { + (WzRequest.apiReq as jest.Mock).mockImplementation( + async (method, endpoint, options) => { + if (options.params.offset === 0) { + return { + data: { + data: { + affected_items: [ + { id: '001', name: 'agent1' }, + { id: '002', name: 'agent2' }, + ], + total_affected_items: 3, + failed_items: [], + total_failed_items: 0, + }, + error: 0, + message: 'Success', + }, + }; + } else { + return { + data: { + data: { + affected_items: [{ id: '003', name: 'agent3' }], + total_affected_items: 3, + failed_items: [], + total_failed_items: 0, + }, + error: 0, + message: 'Success', + }, + }; + } + }, + ); + + const params = { + filters: {}, + pageSize: 2, + }; + + const result = await getAgentsService(params); + + expect(WzRequest.apiReq).toHaveBeenCalledWith('GET', '/agents', { + params: { + q: {}, + limit: 2, + offset: 0, + wait_for_complete: true, + }, + }); + + expect(WzRequest.apiReq).toHaveBeenCalledWith('GET', '/agents', { + params: { + q: {}, + limit: 2, + offset: 2, + wait_for_complete: true, + }, + }); + + expect(result).toEqual({ + affected_items: [ + { id: '001', name: 'agent1' }, + { id: '002', name: 'agent2' }, + { id: '003', name: 'agent3' }, + ], + total_affected_items: 3, + }); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/services/get-agents.tsx b/plugins/main/public/components/endpoints-summary/services/get-agents.tsx new file mode 100644 index 0000000000..ebe67f445e --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-agents.tsx @@ -0,0 +1,54 @@ +import IApiResponse from '../../../react-services/interfaces/api-response.interface'; +import { WzRequest } from '../../../react-services/wz-request'; +import { Agent } from '../types'; + +export const getAgentsService = async ({ + filters, + limit, + offset, + pageSize = 1000, +}: { + filters: any; + limit?: number; + offset?: number; + pageSize?: number; +}) => { + let queryOffset = offset ?? 0; + let queryLimit = limit && limit <= pageSize ? limit : pageSize; + let allAffectedItems: Agent[] = []; + let totalAffectedItems; + + do { + const { + data: { + data: { affected_items, total_affected_items }, + }, + } = (await WzRequest.apiReq('GET', '/agents', { + params: { + limit: queryLimit, + offset: queryOffset, + q: filters, + wait_for_complete: true, + }, + })) as IApiResponse; + + if (totalAffectedItems === undefined) { + totalAffectedItems = total_affected_items; + } + + allAffectedItems = allAffectedItems.concat(affected_items); + + queryOffset += queryLimit; + + const restItems = limit ? limit - allAffectedItems.length : pageSize; + queryLimit = restItems > pageSize ? pageSize : restItems; + } while ( + queryOffset < totalAffectedItems && + (!limit || allAffectedItems.length < limit) + ); + + return { + affected_items: allAffectedItems, + total_affected_items: totalAffectedItems, + }; +}; diff --git a/plugins/main/public/components/endpoints-summary/services/get-color-palette-by-index.tsx b/plugins/main/public/components/endpoints-summary/services/get-color-palette-by-index.tsx new file mode 100644 index 0000000000..9c5f651cfe --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-color-palette-by-index.tsx @@ -0,0 +1,12 @@ +import { euiPaletteColorBlind } from '@elastic/eui'; + +export function getColorPaletteByIndex(index: number) { + const colorPalette = euiPaletteColorBlind({ + rotations: 9, + direction: 'both', + order: 'middle-out', + }); + const validIndex = + index < colorPalette.length ? index : index - colorPalette.length; + return colorPalette[validIndex]; +} diff --git a/plugins/main/public/components/endpoints-summary/services/get-groups.test.tsx b/plugins/main/public/components/endpoints-summary/services/get-groups.test.tsx new file mode 100644 index 0000000000..f9f78d2a6f --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-groups.test.tsx @@ -0,0 +1,78 @@ +import { getGroupsService } from './get-groups'; +import { WzRequest } from '../../../react-services/wz-request'; + +jest.mock('../../../react-services/wz-request', () => ({ + WzRequest: { + apiReq: jest.fn(), + }, +})); + +describe('getGroupsService', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should paginate groups and handle API responses correctly', async () => { + (WzRequest.apiReq as jest.Mock).mockImplementation( + async (method, endpoint, options) => { + if (options.params.offset === 0) { + return { + data: { + data: { + affected_items: ['group1', 'group2'], + total_affected_items: 3, + failed_items: [], + total_failed_items: 0, + }, + error: 0, + message: 'Success', + }, + }; + } else { + return { + data: { + data: { + affected_items: ['group3'], + total_affected_items: 3, + failed_items: [], + total_failed_items: 0, + }, + error: 0, + message: 'Success', + }, + }; + } + }, + ); + + const params = { + filters: {}, + pageSize: 2, + }; + + const result = await getGroupsService(params); + + expect(WzRequest.apiReq).toHaveBeenCalledWith('GET', '/groups', { + params: { + q: {}, + limit: 2, + offset: 0, + wait_for_complete: true, + }, + }); + + expect(WzRequest.apiReq).toHaveBeenCalledWith('GET', '/groups', { + params: { + q: {}, + limit: 2, + offset: 2, + wait_for_complete: true, + }, + }); + + expect(result).toEqual({ + affected_items: ['group1', 'group2', 'group3'], + total_affected_items: 3, + }); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/services/get-groups.tsx b/plugins/main/public/components/endpoints-summary/services/get-groups.tsx index 1b463c163d..a565e1f3cb 100644 --- a/plugins/main/public/components/endpoints-summary/services/get-groups.tsx +++ b/plugins/main/public/components/endpoints-summary/services/get-groups.tsx @@ -1,8 +1,54 @@ +import IApiResponse from '../../../react-services/interfaces/api-response.interface'; import { WzRequest } from '../../../react-services/wz-request'; +import { Group } from '../types'; -export const getGroupsService = async () => { - const { - data: { data }, - } = await WzRequest.apiReq('GET', '/groups', {}); - return data; +export const getGroupsService = async ({ + filters, + limit, + offset, + pageSize = 1000, +}: { + filters: any; + limit?: number; + offset?: number; + pageSize?: number; +}) => { + let queryOffset = offset ?? 0; + let queryLimit = limit && limit <= pageSize ? limit : pageSize; + let allAffectedItems: Group[] = []; + let totalAffectedItems; + + do { + const { + data: { + data: { affected_items, total_affected_items }, + }, + } = (await WzRequest.apiReq('GET', '/groups', { + params: { + limit: queryLimit, + offset: queryOffset, + q: filters, + wait_for_complete: true, + }, + })) as IApiResponse; + + if (totalAffectedItems === undefined) { + totalAffectedItems = total_affected_items; + } + + allAffectedItems = allAffectedItems.concat(affected_items); + + queryOffset += queryLimit; + + const restItems = limit ? limit - allAffectedItems.length : pageSize; + queryLimit = restItems > pageSize ? pageSize : restItems; + } while ( + queryOffset < totalAffectedItems && + (!limit || allAffectedItems.length < limit) + ); + + return { + affected_items: allAffectedItems, + total_affected_items: totalAffectedItems, + }; }; diff --git a/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx b/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx new file mode 100644 index 0000000000..cd35c2d442 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx @@ -0,0 +1,10 @@ +import { WzRequest } from '../../../react-services/wz-request'; + +export const getOutdatedAgents = async () => { + const { + data: { + data: { affected_items }, + }, + } = await WzRequest.apiReq('GET', '/agents/outdated', {}); + return affected_items; +}; diff --git a/plugins/main/public/components/endpoints-summary/services/get-summary-agents-status.ts b/plugins/main/public/components/endpoints-summary/services/get-summary-agents-status.ts new file mode 100644 index 0000000000..b55863c462 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/get-summary-agents-status.ts @@ -0,0 +1,47 @@ +import { + UI_LOGGER_LEVELS, + UI_ORDER_AGENT_STATUS, +} from '../../../../common/constants'; +import { + agentStatusLabelByAgentStatus, + agentStatusColorByAgentStatus, +} from '../../../../common/services/wz_agent_status'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { WzRequest } from '../../../react-services/wz-request'; + +export const getSummaryAgentsStatus = async () => { + try { + const AGENT_STATUS = UI_ORDER_AGENT_STATUS.map(agentStatus => ({ + status: agentStatus, + label: agentStatusLabelByAgentStatus(agentStatus), + color: agentStatusColorByAgentStatus(agentStatus), + })); + const { + data: { + data: { connection: agentStatusSummary }, + }, + }: any = await WzRequest.apiReq('GET', '/agents/summary/status', {}); + + return AGENT_STATUS.map(({ label, status, color }) => ({ + status, + label: label, + value: agentStatusSummary[status] || 0, + color: color, + })); + } catch (error) { + const options = { + context: `EndpointsSummary.getSummary`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Could not get agents summary`, + }, + }; + getErrorOrchestrator().handleError(options); + return []; + } +}; diff --git a/plugins/main/public/components/endpoints-summary/services/get-total-agents.tsx b/plugins/main/public/components/endpoints-summary/services/get-total-agents.tsx deleted file mode 100644 index b75c59725e..0000000000 --- a/plugins/main/public/components/endpoints-summary/services/get-total-agents.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { WzRequest } from '../../../react-services/wz-request'; - -export const getTotalAgentsService = async () => { - const { - data: { - data: { total_affected_items }, - }, - } = await WzRequest.apiReq('GET', '/agents', { - params: { limit: 1, q: 'id!=000' }, - }); - return total_affected_items; -}; diff --git a/plugins/main/public/components/endpoints-summary/services/index.tsx b/plugins/main/public/components/endpoints-summary/services/index.tsx index 6615483443..a8d7711ff8 100644 --- a/plugins/main/public/components/endpoints-summary/services/index.tsx +++ b/plugins/main/public/components/endpoints-summary/services/index.tsx @@ -1,5 +1,6 @@ -export { getTotalAgentsService } from './get-total-agents'; -export { removeAgentFromGroupService } from './remove-agent-from-group'; +export { getAgentsService } from './get-agents'; export { removeAgentFromGroupsService } from './remove-agent-from-groups'; +export { removeAgentsFromGroupService } from './remove-agents-from-group'; export { addAgentToGroupService } from './add-agent-to-group'; +export { addAgentsToGroupService } from './add-agents-to-group'; export { getGroupsService } from './get-groups'; diff --git a/plugins/main/public/components/endpoints-summary/services/paginated-agents-group.test.tsx b/plugins/main/public/components/endpoints-summary/services/paginated-agents-group.test.tsx new file mode 100644 index 0000000000..e6f506dfe0 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/paginated-agents-group.test.tsx @@ -0,0 +1,205 @@ +import { paginatedAgentsGroupService } from './paginated-agents-group'; +import { WzRequest } from '../../../react-services/wz-request'; + +jest.mock('../../../react-services/wz-request', () => ({ + WzRequest: { + apiReq: jest.fn(), + }, +})); + +describe('paginatedAgentsGroupService', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should paginate agents and handle API responses correctly', async () => { + (WzRequest.apiReq as jest.Mock).mockImplementation( + async (method, endpoint, options) => { + if (options.params.agents_list === 'agent1,agent2') { + return { + data: { + data: { + affected_items: ['agent1', 'agent2'], + total_affected_items: 2, + failed_items: [], + total_failed_items: 0, + }, + error: 0, + message: 'Success', + }, + }; + } else { + return { + data: { + data: { + affected_items: ['agent3'], + total_affected_items: 1, + failed_items: [], + total_failed_items: 0, + }, + error: 0, + message: 'Success', + }, + }; + } + }, + ); + + const params = { + addOrRemove: 'add' as any, + agentIds: ['agent1', 'agent2', 'agent3'], + groupId: 'group1', + pageSize: 2, + }; + + const result = await paginatedAgentsGroupService(params); + + expect(WzRequest.apiReq).toHaveBeenCalledWith( + 'PUT', + '/agents/group', + { + params: { + group_id: 'group1', + agents_list: 'agent1,agent2', + wait_for_complete: true, + }, + }, + { returnOriginalResponse: true }, + ); + + expect(WzRequest.apiReq).toHaveBeenCalledWith( + 'PUT', + '/agents/group', + { + params: { + group_id: 'group1', + agents_list: 'agent3', + wait_for_complete: true, + }, + }, + { returnOriginalResponse: true }, + ); + + expect(result).toEqual({ + data: { + data: { + affected_items: ['agent1', 'agent2', 'agent3'], + total_affected_items: 3, + failed_items: [], + total_failed_items: 0, + }, + error: 0, + message: 'Success', + }, + }); + }); + + it('should paginate agents and handle API responses with failed items', async () => { + (WzRequest.apiReq as jest.Mock).mockImplementation( + async (method, endpoint, options) => { + if (options.params.agents_list === 'agent1,agent2') { + return { + data: { + data: { + affected_items: ['agent1'], + total_affected_items: 1, + failed_items: [ + { + error: { + code: '001', + message: 'agent error', + remediation: 'example remediation', + }, + id: ['agent2'], + }, + ], + total_failed_items: 1, + }, + error: 1, + message: 'agent2 error', + }, + }; + } else { + return { + data: { + data: { + affected_items: [], + total_affected_items: 0, + failed_items: [ + { + error: { + code: '001', + message: 'agent error', + remediation: 'example remediation', + }, + id: ['agent3'], + }, + ], + total_failed_items: 1, + }, + error: 1, + message: 'agent3 error', + }, + }; + } + }, + ); + + const params = { + addOrRemove: 'add' as any, + agentIds: ['agent1', 'agent2', 'agent3'], + groupId: 'group1', + pageSize: 2, + }; + + const result = await paginatedAgentsGroupService(params); + + expect(WzRequest.apiReq).toHaveBeenCalledWith( + 'PUT', + '/agents/group', + { + params: { + group_id: 'group1', + agents_list: 'agent1,agent2', + wait_for_complete: true, + }, + }, + { returnOriginalResponse: true }, + ); + + expect(WzRequest.apiReq).toHaveBeenCalledWith( + 'PUT', + '/agents/group', + { + params: { + group_id: 'group1', + agents_list: 'agent3', + wait_for_complete: true, + }, + }, + { returnOriginalResponse: true }, + ); + + expect(result).toEqual({ + data: { + data: { + affected_items: ['agent1'], + total_affected_items: 1, + failed_items: [ + { + error: { + code: '001', + message: 'agent error', + remediation: 'example remediation', + }, + id: ['agent2', 'agent3'], + }, + ], + total_failed_items: 2, + }, + error: 2, + message: 'agent2 error, agent3 error', + }, + }); + }); +}); diff --git a/plugins/main/public/components/endpoints-summary/services/paginated-agents-group.tsx b/plugins/main/public/components/endpoints-summary/services/paginated-agents-group.tsx new file mode 100644 index 0000000000..512098817c --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/paginated-agents-group.tsx @@ -0,0 +1,109 @@ +import IApiResponse from '../../../react-services/interfaces/api-response.interface'; +import { WzRequest } from '../../../react-services/wz-request'; + +export type ErrorAgent = { + error: { + code?: number; + message: string; + remediation?: string; + }; + id: string[]; +}; + +export const paginatedAgentsGroupService = async ({ + addOrRemove, + agentIds, + groupId, + pageSize = 1000, +}: { + addOrRemove: 'add' | 'remove'; + agentIds: string[]; + groupId: string; + pageSize?: number; +}): Promise> => { + let offset = 0; + let requestAgentIds: string[] = []; + let allAffectedItems: string[] = []; + let allFailedItems: ErrorAgent[] = []; + let totalAffectedItems = 0; + let totalFailedItems = 0; + let error = 0; + let message = ''; + + do { + requestAgentIds = agentIds.slice(offset, offset + pageSize); + + const { + data: { + data: { + affected_items: responseAffectedItems, + total_affected_items: responseTotalAffectedItems, + failed_items: responseFailedItems, + total_failed_items: responseTotalFailedItems, + }, + error: responseError, + message: responseMessage, + }, + } = (await WzRequest.apiReq( + addOrRemove === 'add' ? 'PUT' : 'DELETE', + `/agents/group`, + { + params: { + group_id: groupId, + agents_list: requestAgentIds.join(','), + wait_for_complete: true, + }, + }, + { returnOriginalResponse: true }, + )) as IApiResponse; + + error += responseError; + message = + offset === 0 + ? responseMessage + : message.includes(responseMessage) + ? message + : message + ', ' + responseMessage; + totalAffectedItems += responseTotalAffectedItems; + totalFailedItems += responseTotalFailedItems; + allAffectedItems = [...allAffectedItems, ...responseAffectedItems]; + + const notExistFailedItems = responseFailedItems.filter( + responseFailedItem => + !allFailedItems.find( + failedItem => failedItem.error.code === responseFailedItem.error.code, + ), + ); + + const mergeFailedItems = allFailedItems.map(failedItem => { + const responseFailedItemWithSameError = responseFailedItems.find( + responseFailedItem => + responseFailedItem.error.code === failedItem.error.code, + ); + + return { + ...failedItem, + id: responseFailedItemWithSameError + ? [...failedItem.id, ...responseFailedItemWithSameError.id] + : failedItem.id, + }; + }); + + allFailedItems = [...mergeFailedItems, ...notExistFailedItems]; + + offset += pageSize; + } while (offset < agentIds.length); + + return { + data: { + data: { + affected_items: allAffectedItems, + total_affected_items: totalAffectedItems, + failed_items: allFailedItems, + total_failed_items: totalFailedItems, + }, + error, + message, + }, + }; +}; diff --git a/plugins/main/public/components/endpoints-summary/services/remove-agent-from-group.tsx b/plugins/main/public/components/endpoints-summary/services/remove-agent-from-group.tsx deleted file mode 100644 index 633d49fe41..0000000000 --- a/plugins/main/public/components/endpoints-summary/services/remove-agent-from-group.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { WzRequest } from '../../../react-services/wz-request'; - -export const removeAgentFromGroupService = async ( - agentId: string, - group: string, -) => await WzRequest.apiReq('DELETE', `/agents/${agentId}/group/${group}`, {}); diff --git a/plugins/main/public/components/endpoints-summary/services/remove-agent-from-groups.tsx b/plugins/main/public/components/endpoints-summary/services/remove-agent-from-groups.tsx index 2784d026aa..2825670e82 100644 --- a/plugins/main/public/components/endpoints-summary/services/remove-agent-from-groups.tsx +++ b/plugins/main/public/components/endpoints-summary/services/remove-agent-from-groups.tsx @@ -1,11 +1,15 @@ import { WzRequest } from '../../../react-services/wz-request'; -export const removeAgentFromGroupsService = async ( - agentId: string, - groups: string[], -) => +export const removeAgentFromGroupsService = async ({ + agentId, + groupIds, +}: { + agentId: string; + groupIds: string[]; +}) => await WzRequest.apiReq('DELETE', `/agents/${agentId}/group`, { params: { - groups_list: groups.join(','), + groups_list: groupIds.join(','), + wait_for_complete: true, }, }); diff --git a/plugins/main/public/components/endpoints-summary/services/remove-agents-from-group.tsx b/plugins/main/public/components/endpoints-summary/services/remove-agents-from-group.tsx new file mode 100644 index 0000000000..a2000e9ba4 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/services/remove-agents-from-group.tsx @@ -0,0 +1,9 @@ +import IApiResponse from '../../../react-services/interfaces/api-response.interface'; +import { paginatedAgentsGroupService } from './paginated-agents-group'; + +export const removeAgentsFromGroupService = async (parameters: { + agentIds: string[]; + groupId: string; + pageSize?: number; +}): Promise> => + await paginatedAgentsGroupService({ addOrRemove: 'remove', ...parameters }); 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 6f16d19e0b..5a0be00f9f 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 @@ -12,229 +12,85 @@ exports[`AgentsTable component Renders correctly to match the snapshot 1`] = ` class="euiFlexItem" >
-
-

- Agents - - (0) - -

-
-
- -
-
- + Agents + + (0) + + +
+
- - - -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
+ + + + Deploy new agent + + +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - -
-
- - - - - +
-
- - +
- -
+ + + + + + +
+
+
+
+
- -
- - + + + + + + + + + +
+
+
+
+
- - - Actions - - -
+ +
+ +
+
- - No items found - +
+
+ +
+
+
-
-
-
-
-
-
-
-`; - -exports[`AgentsTable component Renders correctly to match the snapshot with custom columns 1`] = ` -
-
-
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + Actions + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+`; + +exports[`AgentsTable component Renders correctly to match the snapshot with custom columns 1`] = ` +
+
+
-
-

- Agents - - (0) - -

-
-
- -
-
- + Agents + + (0) + + +
+
- - - -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
+ Deploy new agent + + +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - -
-
- - - +
-
- - +
- -
+ + + + + + +
+
+
+
+
- -
+ +
+
+ +
+
+ + + + + + +
+
+
+
+
- - - Actions - - -
+ +
+ +
+
- - No items found - +
+
+ +
+
+
-
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + Actions + + +
+
+ + No items found + +
+
+
+
@@ -1038,250 +1275,86 @@ exports[`AgentsTable component Renders correctly to match the snapshot with no p class="euiFlexItem" >
-
-

- Agents - - (0) - -

-
-
- -
-
- + Agents + + (0) + + +
+
- - - -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
+ Deploy new agent + + +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - + +
-
- - - +
-
+
- -
+ + + + + + +
+
+
+
+
- -
+ +
+
+ +
+
+ + + + + + +
+
+
+
+
- -
- - - + + + + + + + + + + + + + + + + + + + - - - - - - + + + - - No items found - - - - - -
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + - Status + + Actions + - - - - - - Actions - - -
-
+
+
+ + No items found + +
+ +
+
+
diff --git a/plugins/main/public/components/endpoints-summary/table/actions/__snapshots__/edit-groups-modal.test.tsx.snap b/plugins/main/public/components/endpoints-summary/table/actions/__snapshots__/edit-groups-modal.test.tsx.snap new file mode 100644 index 0000000000..29c18c734e --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/table/actions/__snapshots__/edit-groups-modal.test.tsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EditAgentGroupsModal component should return the component 1`] = ` +